feat(usage): PowerUse-Zeitfenster + Handoff-Schwellwert als Settings
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.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Hub;
|
||||
|
||||
/// The PowerUse window and handoff threshold are edited on the usage monitor, but Settings→General
|
||||
/// rebuilds the whole DTO positionally on save. So the contract that matters is: whatever
|
||||
/// GetAppSettings hands out, feeding it straight back must change nothing.
|
||||
public sealed class PowerUseSettingsHubTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
|
||||
private WorkerHub CreateHub()
|
||||
{
|
||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||
var hub = new WorkerHub(
|
||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||
hub.Context = new FakeHubCallerContext();
|
||||
return hub;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppSettings_Defaults_HaveNoWindowAnd95Percent()
|
||||
{
|
||||
var settings = await CreateHub().GetAppSettings();
|
||||
|
||||
Assert.Null(settings.PowerUseWindowStart);
|
||||
Assert.Null(settings.PowerUseWindowEnd);
|
||||
Assert.Equal(95, settings.PowerUseHandoffPct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAppSettings_RoundTripsTheWindowAndThreshold()
|
||||
{
|
||||
var hub = CreateHub();
|
||||
|
||||
var before = await hub.GetAppSettings();
|
||||
await hub.UpdateAppSettings(before with
|
||||
{
|
||||
PowerUseEnabled = true,
|
||||
PowerUseWindowStart = "22:00",
|
||||
PowerUseWindowEnd = "07:00",
|
||||
PowerUseHandoffPct = 88,
|
||||
});
|
||||
var after = await hub.GetAppSettings();
|
||||
|
||||
Assert.True(after.PowerUseEnabled);
|
||||
Assert.Equal("22:00", after.PowerUseWindowStart);
|
||||
Assert.Equal("07:00", after.PowerUseWindowEnd);
|
||||
Assert.Equal(88, after.PowerUseHandoffPct);
|
||||
}
|
||||
|
||||
/// The Settings→General case: a save that touches none of the PowerUse fields must leave them
|
||||
/// exactly as they were.
|
||||
[Fact]
|
||||
public async Task UpdateAppSettings_UnchangedDto_LeavesPowerUseFieldsAlone()
|
||||
{
|
||||
var hub = CreateHub();
|
||||
await hub.UpdateAppSettings((await hub.GetAppSettings()) with
|
||||
{
|
||||
PowerUseEnabled = true,
|
||||
PowerUseWindowStart = "22:00",
|
||||
PowerUseWindowEnd = "07:00",
|
||||
PowerUseHandoffPct = 88,
|
||||
});
|
||||
|
||||
var stored = await hub.GetAppSettings();
|
||||
await hub.UpdateAppSettings(stored);
|
||||
var after = await hub.GetAppSettings();
|
||||
|
||||
Assert.Equal(stored.PowerUseEnabled, after.PowerUseEnabled);
|
||||
Assert.Equal(stored.PowerUseWindowStart, after.PowerUseWindowStart);
|
||||
Assert.Equal(stored.PowerUseWindowEnd, after.PowerUseWindowEnd);
|
||||
Assert.Equal(stored.PowerUseHandoffPct, after.PowerUseHandoffPct);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAppSettings_InvalidTimeAndOutOfRangePct_AreNormalized()
|
||||
{
|
||||
var hub = CreateHub();
|
||||
|
||||
await hub.UpdateAppSettings((await hub.GetAppSettings()) with
|
||||
{
|
||||
PowerUseWindowStart = "25:99",
|
||||
PowerUseWindowEnd = "07:00",
|
||||
PowerUseHandoffPct = 120,
|
||||
});
|
||||
var after = await hub.GetAppSettings();
|
||||
|
||||
Assert.Null(after.PowerUseWindowStart);
|
||||
Assert.Equal("07:00", after.PowerUseWindowEnd);
|
||||
Assert.Equal(99, after.PowerUseHandoffPct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user