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.
55 lines
2.3 KiB
C#
55 lines
2.3 KiB
C#
using ClaudeDo.Data.Models;
|
|
|
|
namespace ClaudeDo.Data.Tests;
|
|
|
|
/// The window decides whether a handler session runs unattended, so its edges matter: an
|
|
/// off-by-one at midnight would leave an overnight window either always or never open.
|
|
public sealed class PowerUseWindowTests
|
|
{
|
|
private static TimeOnly T(string hhmm) => TimeOnly.ParseExact(hhmm, "HH:mm");
|
|
|
|
[Theory]
|
|
// Plain window, half-open: start included, end excluded.
|
|
[InlineData("09:00", "08:00", "17:00", true)]
|
|
[InlineData("08:00", "08:00", "17:00", true)]
|
|
[InlineData("17:00", "08:00", "17:00", false)]
|
|
[InlineData("07:59", "08:00", "17:00", false)]
|
|
[InlineData("23:30", "08:00", "17:00", false)]
|
|
// Wrap over midnight.
|
|
[InlineData("23:30", "22:00", "07:00", true)]
|
|
[InlineData("00:30", "22:00", "07:00", true)]
|
|
[InlineData("12:00", "22:00", "07:00", false)]
|
|
[InlineData("07:00", "22:00", "07:00", false)]
|
|
public void Contains_BothBoundsSet(string now, string start, string end, bool expected)
|
|
=> Assert.Equal(expected, PowerUseWindow.Contains(T(now), T(start), T(end)));
|
|
|
|
/// Only one bound is not a window — it must behave exactly like no window, or a half-filled
|
|
/// pair of boxes would silently switch PowerUse off for most of the day.
|
|
[Fact]
|
|
public void Contains_OnlyOneBoundSet_IsAlwaysOpen()
|
|
{
|
|
Assert.True(PowerUseWindow.Contains(T("12:00"), T("22:00"), null));
|
|
Assert.True(PowerUseWindow.Contains(T("12:00"), null, T("07:00")));
|
|
Assert.True(PowerUseWindow.Contains(T("12:00"), null, null));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("22:00", "22:00")]
|
|
[InlineData(" 07:30 ", "07:30")]
|
|
[InlineData("7:30", null)]
|
|
[InlineData("22:00:00", null)]
|
|
[InlineData("nonsense", null)]
|
|
[InlineData("", null)]
|
|
[InlineData(null, null)]
|
|
public void Normalize_KeepsOnlyExactHhMm(string? input, string? expected)
|
|
=> Assert.Equal(expected, PowerUseWindow.Normalize(input));
|
|
|
|
[Fact]
|
|
public void IsOpen_UsesTheTimeOfDayOfTheGivenInstant()
|
|
{
|
|
Assert.True(PowerUseWindow.IsOpen(new DateTime(2026, 9, 2, 23, 30, 0), "22:00", "07:00"));
|
|
Assert.False(PowerUseWindow.IsOpen(new DateTime(2026, 9, 2, 12, 0, 0), "22:00", "07:00"));
|
|
Assert.True(PowerUseWindow.IsOpen(new DateTime(2026, 9, 2, 12, 0, 0), null, null));
|
|
}
|
|
}
|