Drei neue app_settings-Spalten (Migration AddPowerUseWindow): power_use_window_start/end (HH:mm, lokal, nullable) und power_use_handoff_pct (NOT NULL DEFAULT 95, geclampt 50-99). Die Fensterlogik liegt als reine Funktion in PowerUseWindow (Contains/Normalize/IsOpen): beide Grenzen leer oder nur eine gesetzt = rund um die Uhr, Ende vor Start wrappt ueber Mitternacht, Grenzen halboffen. Geprueft wird in ComposeHandlerPromptAsync, also pro Phase zum Zeitpunkt des Prompt-Baus - die Uhr ist per Func<DateTime> injizierbar. Bedienung im Usage-Monitor neben dem PowerUse-Toggle (Commit auf Blur/Enter, ein Write fuer alle vier Felder); in Settings->General unsichtbar, aber load->save durchgereicht. Nebenbefund, gleicher Pfad: AppSettingsRepository.UpdateAsync kopiert Feld fuer Feld und hat ModelPresets, PowerUseEnabled und ListHandlerMaxHandoffs nie geschrieben - jeder Save dieser drei war still wirkungslos. Mit ergaenzt und per Round-trip-Test gepinnt.
151 lines
5.7 KiB
C#
151 lines
5.7 KiB
C#
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Tests;
|
|
|
|
public sealed class PowerUseTests : IDisposable
|
|
{
|
|
private readonly string _dbPath;
|
|
private readonly ClaudeDoDbContext _ctx;
|
|
|
|
private const string ListId = "l1";
|
|
|
|
public PowerUseTests()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_poweruse_{Guid.NewGuid():N}.db");
|
|
var options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
|
.UseSqlite($"Data Source={_dbPath}")
|
|
.Options;
|
|
_ctx = new ClaudeDoDbContext(options);
|
|
_ctx.Database.EnsureCreated();
|
|
|
|
_ctx.Lists.Add(new ListEntity { Id = ListId, Name = "Test", CreatedAt = DateTime.UtcNow });
|
|
_ctx.SaveChanges();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_ctx.Dispose();
|
|
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
|
try { File.Delete(_dbPath + suffix); } catch { }
|
|
}
|
|
|
|
private ClaudeDoDbContext Reload() => new(
|
|
new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
|
.UseSqlite(_ctx.Database.GetConnectionString()).Options);
|
|
|
|
[Fact]
|
|
public async Task ListConfig_PowerUseAllowed_DefaultsToFalse()
|
|
{
|
|
await new ListRepository(_ctx).SetConfigAsync(new ListConfigEntity { ListId = ListId, Model = "sonnet" });
|
|
|
|
using var reload = Reload();
|
|
var cfg = await new ListRepository(reload).GetConfigAsync(ListId);
|
|
Assert.False(cfg!.PowerUseAllowed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListConfig_PowerUseAllowed_RoundtripsTrue()
|
|
{
|
|
await new ListRepository(_ctx).SetConfigAsync(new ListConfigEntity { ListId = ListId, PowerUseAllowed = true });
|
|
|
|
using var reload = Reload();
|
|
var cfg = await new ListRepository(reload).GetConfigAsync(ListId);
|
|
Assert.True(cfg!.PowerUseAllowed);
|
|
}
|
|
|
|
/// SetConfigAsync copies the incoming entity field by field onto the existing row. A field
|
|
/// forgotten in that copy block reads as "silently reset on the next unrelated save", which is
|
|
/// exactly how the ticket-project link broke before. Pin it for PowerUseAllowed.
|
|
[Fact]
|
|
public async Task ListConfig_UpdatingAnotherField_DoesNotResetPowerUseAllowed()
|
|
{
|
|
var repo = new ListRepository(_ctx);
|
|
await repo.SetConfigAsync(new ListConfigEntity { ListId = ListId, PowerUseAllowed = true });
|
|
await repo.SetConfigAsync(new ListConfigEntity { ListId = ListId, PowerUseAllowed = true, VerifyCommand = "dotnet build" });
|
|
|
|
using var reload = Reload();
|
|
var cfg = await new ListRepository(reload).GetConfigAsync(ListId);
|
|
Assert.True(cfg!.PowerUseAllowed);
|
|
Assert.Equal("dotnet build", cfg.VerifyCommand);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AppSettings_PowerUseDefaults_AreOffAndFourHandoffs()
|
|
{
|
|
var settings = await new AppSettingsRepository(_ctx).GetAsync();
|
|
|
|
Assert.False(settings.PowerUseEnabled);
|
|
Assert.Equal(4, settings.ListHandlerMaxHandoffs);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AppSettings_PowerUseFields_Roundtrip()
|
|
{
|
|
await new AppSettingsRepository(_ctx).GetAsync();
|
|
var row = await _ctx.AppSettings.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
|
|
row.PowerUseEnabled = true;
|
|
row.ListHandlerMaxHandoffs = 40;
|
|
await _ctx.SaveChangesAsync();
|
|
|
|
using var reload = Reload();
|
|
var settings = await new AppSettingsRepository(reload).GetAsync();
|
|
Assert.True(settings.PowerUseEnabled);
|
|
Assert.Equal(40, settings.ListHandlerMaxHandoffs);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AppSettings_WindowDefaults_AreEmptyAnd95Percent()
|
|
{
|
|
var settings = await new AppSettingsRepository(_ctx).GetAsync();
|
|
|
|
Assert.Null(settings.PowerUseWindowStart);
|
|
Assert.Null(settings.PowerUseWindowEnd);
|
|
Assert.Equal(95, settings.PowerUseHandoffPct);
|
|
}
|
|
|
|
/// UpdateAsync copies the incoming entity field by field onto the singleton row — a field
|
|
/// missing from that block never persists, however correctly the hub filled the entity. That
|
|
/// was true for PowerUseEnabled, ListHandlerMaxHandoffs and ModelPresets until 2026-09-02, so
|
|
/// pin the whole PowerUse group through the real write path.
|
|
[Fact]
|
|
public async Task AppSettings_UpdateAsync_PersistsThePowerUseGroup()
|
|
{
|
|
var repo = new AppSettingsRepository(_ctx);
|
|
var current = await repo.GetAsync();
|
|
current.PowerUseEnabled = true;
|
|
current.PowerUseWindowStart = "22:00";
|
|
current.PowerUseWindowEnd = "07:00";
|
|
current.PowerUseHandoffPct = 88;
|
|
current.ListHandlerMaxHandoffs = 40;
|
|
await repo.UpdateAsync(current);
|
|
|
|
using var reload = Reload();
|
|
var settings = await new AppSettingsRepository(reload).GetAsync();
|
|
Assert.True(settings.PowerUseEnabled);
|
|
Assert.Equal("22:00", settings.PowerUseWindowStart);
|
|
Assert.Equal("07:00", settings.PowerUseWindowEnd);
|
|
Assert.Equal(88, settings.PowerUseHandoffPct);
|
|
Assert.Equal(40, settings.ListHandlerMaxHandoffs);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AppSettings_UpdateAsync_NormalizesAndClamps()
|
|
{
|
|
var repo = new AppSettingsRepository(_ctx);
|
|
var current = await repo.GetAsync();
|
|
current.PowerUseWindowStart = "nonsense";
|
|
current.PowerUseWindowEnd = " 07:00 ";
|
|
current.PowerUseHandoffPct = 5;
|
|
await repo.UpdateAsync(current);
|
|
|
|
using var reload = Reload();
|
|
var settings = await new AppSettingsRepository(reload).GetAsync();
|
|
Assert.Null(settings.PowerUseWindowStart);
|
|
Assert.Equal("07:00", settings.PowerUseWindowEnd);
|
|
Assert.Equal(50, settings.PowerUseHandoffPct);
|
|
}
|
|
}
|