From 022eaf385c542cfa6a402a2fc734ce1f0f70ec24 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 25 Aug 2026 08:29:40 +0200 Subject: [PATCH] feat(prime): add PrimeActionKind to the schedule entity --- .../PrimeScheduleEntityConfiguration.cs | 19 ++++ src/ClaudeDo.Data/Models/PrimeActionKind.cs | 12 +++ .../Models/PrimeScheduleEntity.cs | 1 + .../Repositories/PrimeScheduleRepository.cs | 1 + .../PrimeScheduleRepositoryTests.cs | 92 ++++++++----------- 5 files changed, 69 insertions(+), 56 deletions(-) create mode 100644 src/ClaudeDo.Data/Models/PrimeActionKind.cs diff --git a/src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs index d06f0306..7adb7740 100644 --- a/src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs @@ -1,11 +1,27 @@ using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace ClaudeDo.Data.Configuration; public class PrimeScheduleEntityConfiguration : IEntityTypeConfiguration { + private static string KindToString(PrimeActionKind v) + => v == PrimeActionKind.Ping ? "ping" + : v == PrimeActionKind.FillMyDay ? "fill_my_day" + : v == PrimeActionKind.Custom ? "custom" + : throw new ArgumentOutOfRangeException(nameof(v)); + + private static PrimeActionKind KindFromString(string v) + => v == "ping" ? PrimeActionKind.Ping + : v == "fill_my_day" ? PrimeActionKind.FillMyDay + : v == "custom" ? PrimeActionKind.Custom + : throw new ArgumentOutOfRangeException(nameof(v)); + + private static readonly ValueConverter KindConverter = + new(v => KindToString(v), v => KindFromString(v)); + public void Configure(EntityTypeBuilder builder) { builder.ToTable("prime_schedules"); @@ -15,6 +31,9 @@ public class PrimeScheduleEntityConfiguration : IEntityTypeConfiguration s.Days).HasColumnName("days_of_week") .IsRequired().HasDefaultValue(PrimeDays.Weekdays); + builder.Property(s => s.Kind).HasColumnName("action_kind") + .IsRequired().HasDefaultValue(PrimeActionKind.Ping) + .HasConversion(KindConverter); builder.Property(s => s.TimeOfDay).HasColumnName("time_of_day").IsRequired(); builder.Property(s => s.Enabled).HasColumnName("enabled").IsRequired().HasDefaultValue(true); builder.Property(s => s.LastRunAt).HasColumnName("last_run_at"); diff --git a/src/ClaudeDo.Data/Models/PrimeActionKind.cs b/src/ClaudeDo.Data/Models/PrimeActionKind.cs new file mode 100644 index 00000000..b0ea4a22 --- /dev/null +++ b/src/ClaudeDo.Data/Models/PrimeActionKind.cs @@ -0,0 +1,12 @@ +namespace ClaudeDo.Data.Models; + +/// What a Prime schedule actually does when it fires. +public enum PrimeActionKind +{ + /// One throwaway turn that only opens the usage window. Default for new schedules. + Ping, + /// Run the daily prep prompt and let Claude fill MyDay. + FillMyDay, + /// Run the schedule's own prompt with the ClaudeDo MCP tools available. + Custom +} diff --git a/src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs b/src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs index 061e87d8..e1905c48 100644 --- a/src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs +++ b/src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs @@ -4,6 +4,7 @@ public sealed class PrimeScheduleEntity { public Guid Id { get; set; } = Guid.NewGuid(); public PrimeDays Days { get; set; } = PrimeDays.Weekdays; + public PrimeActionKind Kind { get; set; } = PrimeActionKind.Ping; public TimeSpan TimeOfDay { get; set; } public bool Enabled { get; set; } = true; public DateTimeOffset? LastRunAt { get; set; } diff --git a/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs b/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs index 05d980f1..f2db177b 100644 --- a/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs +++ b/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs @@ -32,6 +32,7 @@ public sealed class PrimeScheduleRepository existing.TimeOfDay = entity.TimeOfDay; existing.Enabled = entity.Enabled; existing.PromptOverride = entity.PromptOverride; + existing.Kind = entity.Kind; } await _context.SaveChangesAsync(ct); } diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs index 5993d643..13fbcdc4 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs @@ -1,4 +1,3 @@ -// tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Tests.Infrastructure; @@ -10,72 +9,53 @@ public class PrimeScheduleRepositoryTests : IDisposable private readonly DbFixture _db = new(); public void Dispose() => _db.Dispose(); - [Fact] - public async Task Upsert_Then_List_RoundTrips() + private static PrimeScheduleEntity Row(PrimeActionKind kind) => new() { - var id = Guid.NewGuid(); - using (var ctx = _db.CreateContext()) - { - await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity - { - Id = id, - Days = PrimeDays.Weekdays, - TimeOfDay = new TimeSpan(7, 0, 0), - Enabled = true, - CreatedAt = DateTimeOffset.UtcNow, - }); - } + Id = Guid.NewGuid(), + Days = PrimeDays.Weekdays, + TimeOfDay = new TimeSpan(7, 0, 0), + Enabled = true, + Kind = kind, + }; - using var read = _db.CreateContext(); - var rows = await new PrimeScheduleRepository(read).ListAsync(); - Assert.Single(rows); - Assert.Equal(id, rows[0].Id); - Assert.Equal(new TimeSpan(7, 0, 0), rows[0].TimeOfDay); - Assert.Equal(PrimeDays.Weekdays, rows[0].Days); + [Fact] + public void Kind_defaults_to_ping_on_a_new_entity() + { + Assert.Equal(PrimeActionKind.Ping, new PrimeScheduleEntity().Kind); } [Fact] - public async Task UpdateLastRunAt_Persists() + public async Task Kind_round_trips_through_the_database() { - var id = Guid.NewGuid(); - var when = new DateTimeOffset(2026, 5, 5, 7, 1, 0, TimeSpan.FromHours(2)); - using (var ctx = _db.CreateContext()) - { - await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity - { - Id = id, - Days = PrimeDays.Weekdays, - TimeOfDay = new TimeSpan(7, 0, 0), - Enabled = true, - CreatedAt = DateTimeOffset.UtcNow, - }); - } - using (var ctx = _db.CreateContext()) - await new PrimeScheduleRepository(ctx).UpdateLastRunAsync(id, when); + using var ctx = _db.CreateContext(); + var repo = new PrimeScheduleRepository(ctx); + var row = Row(PrimeActionKind.Custom); - using var read = _db.CreateContext(); - var row = await new PrimeScheduleRepository(read).GetAsync(id); - Assert.NotNull(row); - Assert.Equal(when, row!.LastRunAt); + await repo.UpsertAsync(row); + var reloaded = await repo.GetAsync(row.Id); + + Assert.NotNull(reloaded); + Assert.Equal(PrimeActionKind.Custom, reloaded!.Kind); } [Fact] - public async Task Delete_Removes_Row() + public async Task UpsertAsync_updates_kind_on_an_existing_row() { - var id = Guid.NewGuid(); - using (var ctx = _db.CreateContext()) - await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity - { - Id = id, - Days = PrimeDays.All, - TimeOfDay = TimeSpan.Zero, - Enabled = true, - CreatedAt = DateTimeOffset.UtcNow, - }); - using (var ctx = _db.CreateContext()) - await new PrimeScheduleRepository(ctx).DeleteAsync(id); + using var ctx = _db.CreateContext(); + var repo = new PrimeScheduleRepository(ctx); + var row = Row(PrimeActionKind.Ping); + await repo.UpsertAsync(row); - using var read = _db.CreateContext(); - Assert.Empty(await new PrimeScheduleRepository(read).ListAsync()); + await repo.UpsertAsync(new PrimeScheduleEntity + { + Id = row.Id, + Days = row.Days, + TimeOfDay = row.TimeOfDay, + Enabled = row.Enabled, + Kind = PrimeActionKind.FillMyDay, + }); + + var reloaded = await repo.GetAsync(row.Id); + Assert.Equal(PrimeActionKind.FillMyDay, reloaded!.Kind); } }