feat(scheduler): persist recurring daily starts

Add a local-calendar daily start scheduler with pending receipts, per-day deduplication, DST-safe target calculation, missed-day recovery, and retryable account/start failure outcomes.

Persist and validate the daily rule, expose its next target to the renderer, preserve legacy one-time schedules until a daily rule is saved, and gate boot auto-resume on recorded active-run evidence.

Wire boot, settings, account, suspend, resume, interval, and shutdown lifecycle handling with focused RED/GREEN coverage for calendar, persistence, renderer, and controller boundaries.
This commit is contained in:
Sucukdeluxe
2026-08-22 09:36:45 +02:00
parent d7384c34d1
commit 4eb95c92b9
14 changed files with 843 additions and 71 deletions
+51 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { defaultSettings } from "../src/main/constants";
import { createRendererSettings } from "../src/main/renderer-state";
import { validateRendererSettingsUpdate } from "../src/main/renderer-settings";
@@ -73,4 +73,54 @@ describe("renderer settings validation", () => {
expect(projected.archivePasswordListConfigured).toBe(true);
expect(JSON.stringify(projected)).not.toContain(password);
});
it("projects daily start state and accepts only editable calendar controls", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 7, 22, 9, 0));
try {
const current = {
...defaultSettings(),
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23",
dailyStartLastHandledLocalDate: "2026-08-22",
dailyStartPendingLocalDate: "",
dailyStartLastOutcome: "started" as const
};
const projected = createRendererSettings(current);
expect(projected).toMatchObject({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23",
dailyStartLastHandledLocalDate: "2026-08-22",
dailyStartPendingLocalDate: "",
dailyStartLastOutcome: "started"
});
expect(projected.nextDailyStartEpochMs).toBe(new Date(2026, 7, 23, 18, 45, 0, 0).getTime());
expect(validateRendererSettingsUpdate({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 7 * 60 + 30,
dailyStartFirstLocalDate: "2026-08-24",
dailyStartLastHandledLocalDate: "2026-08-23",
dailyStartPendingLocalDate: "2026-08-24",
dailyStartLastOutcome: "missed",
nextDailyStartEpochMs: 123
}, current)).toEqual({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 7 * 60 + 30,
dailyStartFirstLocalDate: "2026-08-24"
});
} finally {
vi.useRealTimers();
}
});
it("rejects invalid editable daily calendar values at the renderer boundary", () => {
const current = defaultSettings();
expect(() => validateRendererSettingsUpdate({ dailyStartMinuteOfDay: 1_440 }, current)).toThrow("Settings-Payload ist ungültig");
expect(() => validateRendererSettingsUpdate({ dailyStartMinuteOfDay: 12.5 }, current)).toThrow("Settings-Payload ist ungültig");
expect(() => validateRendererSettingsUpdate({ dailyStartFirstLocalDate: "2026-02-29" }, current)).toThrow("Settings-Payload ist ungültig");
});
});