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:
@@ -0,0 +1,71 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { AppController } from "../src/main/app-controller";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||
import { createStoragePaths } from "../src/main/storage";
|
||||
import type { AppSettings } from "../src/shared/types";
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
app: { getPath: () => "C:\\MDD\\Test" },
|
||||
BrowserWindow: class {},
|
||||
clipboard: {},
|
||||
dialog: {},
|
||||
ipcMain: { handle: vi.fn(), on: vi.fn() },
|
||||
Menu: { buildFromTemplate: vi.fn(), setApplicationMenu: vi.fn() },
|
||||
safeStorage: { isEncryptionAvailable: () => false, encryptString: vi.fn(), decryptString: vi.fn() },
|
||||
shell: {},
|
||||
Tray: class {}
|
||||
}));
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function createController(settings: AppSettings): AppController {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-"));
|
||||
tempDirs.push(dir);
|
||||
const controller = Object.create(AppController.prototype) as any;
|
||||
controller.settings = settings;
|
||||
controller.storagePaths = createStoragePaths(dir);
|
||||
controller.manager = { setSettings: vi.fn() };
|
||||
controller.audit = vi.fn();
|
||||
controller.overlayLiveUsageCounters = vi.fn();
|
||||
controller.pruneRealDebridWebFallbacks = vi.fn();
|
||||
controller.realDebridWebFallbacks = new Map();
|
||||
return controller as AppController;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("AppController daily start settings", () => {
|
||||
it("clears a legacy one-time schedule when a new daily rule is saved", () => {
|
||||
configureCredentialProtector({
|
||||
isEncryptionAvailable: () => false,
|
||||
encryptString: (value) => Buffer.from(value, "utf8"),
|
||||
decryptString: (value) => Buffer.from(value).toString("utf8")
|
||||
});
|
||||
const controller = createController({
|
||||
...defaultSettings(),
|
||||
scheduledStartEpochMs: 1_800_000_000_000
|
||||
});
|
||||
|
||||
const updated = controller.updateSettings({
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 18 * 60 + 45,
|
||||
dailyStartFirstLocalDate: "2026-08-23"
|
||||
});
|
||||
|
||||
expect(updated.scheduledStartEpochMs).toBe(0);
|
||||
expect(updated).toMatchObject({
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 18 * 60 + 45,
|
||||
dailyStartFirstLocalDate: "2026-08-23"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import { afterAll, describe, expect, it, vi } from "vitest";
|
||||
import type { DailyStartSettings } from "../src/shared/types";
|
||||
import { DailyStartScheduler, nextDailyStartEpochMs, prepareDailyStartSettingsPatch, shouldDeferAutoResumeToDailyStart } from "../src/main/daily-start-scheduler";
|
||||
|
||||
const originalTimezone = process.env.TZ;
|
||||
process.env.TZ = "Europe/Berlin";
|
||||
|
||||
afterAll(() => {
|
||||
if (originalTimezone === undefined) {
|
||||
delete process.env.TZ;
|
||||
} else {
|
||||
process.env.TZ = originalTimezone;
|
||||
}
|
||||
});
|
||||
|
||||
function settings(overrides: Partial<DailyStartSettings> = {}): DailyStartSettings {
|
||||
return {
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 10 * 60,
|
||||
dailyStartFirstLocalDate: "2026-08-22",
|
||||
dailyStartLastHandledLocalDate: "",
|
||||
dailyStartPendingLocalDate: "",
|
||||
dailyStartLastOutcome: "",
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
class FakeDailyStartController {
|
||||
public settings: DailyStartSettings;
|
||||
public running = false;
|
||||
public paused = false;
|
||||
public canStart = true;
|
||||
public items: Record<string, { status: string }> = { queued: { status: "queued" } };
|
||||
public events: string[] = [];
|
||||
public start = vi.fn(async () => {
|
||||
this.events.push("start");
|
||||
});
|
||||
|
||||
public constructor(value: DailyStartSettings) {
|
||||
this.settings = value;
|
||||
}
|
||||
|
||||
public getSnapshot() {
|
||||
return {
|
||||
settings: this.settings,
|
||||
session: {
|
||||
running: this.running,
|
||||
paused: this.paused,
|
||||
items: this.items
|
||||
},
|
||||
canStart: this.canStart
|
||||
};
|
||||
}
|
||||
|
||||
public updateSettings(partial: Partial<DailyStartSettings>): DailyStartSettings {
|
||||
Object.assign(this.settings, partial);
|
||||
if (partial.dailyStartPendingLocalDate) {
|
||||
this.events.push(`pending:${partial.dailyStartPendingLocalDate}`);
|
||||
}
|
||||
if (partial.dailyStartLastHandledLocalDate) {
|
||||
this.events.push(`handled:${partial.dailyStartLastHandledLocalDate}`);
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
}
|
||||
|
||||
describe("daily start scheduler", () => {
|
||||
it("keeps today's future and past targets on the local calendar date", () => {
|
||||
const futureNow = new Date(2026, 7, 22, 9, 15).getTime();
|
||||
const pastNow = new Date(2026, 7, 22, 12, 30).getTime();
|
||||
const expected = new Date(2026, 7, 22, 10, 0, 0, 0).getTime();
|
||||
|
||||
expect(nextDailyStartEpochMs(settings(), futureNow)).toBe(expected);
|
||||
expect(nextDailyStartEpochMs(settings(), pastNow)).toBe(expected);
|
||||
});
|
||||
|
||||
it("uses tomorrow as the first eligible local date", () => {
|
||||
const now = new Date(2026, 7, 22, 8, 0).getTime();
|
||||
|
||||
expect(nextDailyStartEpochMs(settings({ dailyStartFirstLocalDate: "2026-08-23" }), now))
|
||||
.toBe(new Date(2026, 7, 23, 10, 0, 0, 0).getTime());
|
||||
});
|
||||
|
||||
it("dispatches once on each of five eligible local days", async () => {
|
||||
let now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
for (let day = 22; day <= 26; day += 1) {
|
||||
now = new Date(2026, 7, day, 10, 5).getTime();
|
||||
await scheduler.reconcile();
|
||||
}
|
||||
|
||||
expect(controller.start).toHaveBeenCalledTimes(5);
|
||||
expect(controller.settings.dailyStartLastHandledLocalDate).toBe("2026-08-26");
|
||||
});
|
||||
|
||||
it("persists pending before dispatch and coalesces simultaneous reconciles", async () => {
|
||||
const now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
await Promise.all([scheduler.reconcile(), scheduler.reconcile()]);
|
||||
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
expect(controller.events).toEqual([
|
||||
"pending:2026-08-22",
|
||||
"start",
|
||||
"handled:2026-08-22"
|
||||
]);
|
||||
expect(controller.settings.dailyStartPendingLocalDate).toBe("");
|
||||
expect(controller.settings.dailyStartLastOutcome).toBe("started");
|
||||
});
|
||||
|
||||
it("catches an overdue target after a clock jump without dispatching twice", async () => {
|
||||
let now = new Date(2026, 7, 22, 9, 55).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
expect(await scheduler.reconcile()).toBeNull();
|
||||
now = new Date(2026, 7, 22, 11, 10).getTime();
|
||||
expect(await scheduler.reconcile()).toBe("started");
|
||||
expect(await scheduler.reconcile()).toBeNull();
|
||||
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("treats repeated suspend and resume reconciles as one daily dispatch", async () => {
|
||||
const now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
await scheduler.reconcile();
|
||||
await scheduler.reconcile();
|
||||
await scheduler.reconcile();
|
||||
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("recovers a persisted pending receipt after restart and then deduplicates it", async () => {
|
||||
const now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings({ dailyStartPendingLocalDate: "2026-08-22" }));
|
||||
|
||||
await new DailyStartScheduler(controller, () => now).reconcile();
|
||||
await new DailyStartScheduler(controller, () => now).reconcile();
|
||||
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
expect(controller.settings.dailyStartLastHandledLocalDate).toBe("2026-08-22");
|
||||
expect(controller.settings.dailyStartPendingLocalDate).toBe("");
|
||||
});
|
||||
|
||||
it("marks an empty queue handled without calling start", async () => {
|
||||
const now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
controller.items = {};
|
||||
|
||||
expect(await new DailyStartScheduler(controller, () => now).reconcile()).toBe("empty_queue");
|
||||
expect(controller.start).not.toHaveBeenCalled();
|
||||
expect(controller.settings.dailyStartLastHandledLocalDate).toBe("2026-08-22");
|
||||
expect(controller.settings.dailyStartLastOutcome).toBe("empty_queue");
|
||||
});
|
||||
|
||||
it("keeps a missing-account occurrence pending and retries after account recovery", async () => {
|
||||
const now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
controller.canStart = false;
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
expect(await scheduler.reconcile()).toBe("missing_account");
|
||||
expect(controller.start).not.toHaveBeenCalled();
|
||||
expect(controller.settings.dailyStartPendingLocalDate).toBe("2026-08-22");
|
||||
expect(controller.settings.dailyStartLastHandledLocalDate).toBe("");
|
||||
|
||||
controller.canStart = true;
|
||||
expect(await scheduler.reconcile()).toBe("started");
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("treats running and paused sessions as already active", async () => {
|
||||
let now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
controller.running = true;
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
expect(await scheduler.reconcile()).toBe("already_active");
|
||||
controller.running = false;
|
||||
controller.paused = true;
|
||||
now = new Date(2026, 7, 23, 10, 5).getTime();
|
||||
expect(await scheduler.reconcile()).toBe("already_active");
|
||||
|
||||
expect(controller.start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("records a stale pending day as missed without disabling future days", async () => {
|
||||
const now = new Date(2026, 7, 23, 9, 0).getTime();
|
||||
const controller = new FakeDailyStartController(settings({ dailyStartPendingLocalDate: "2026-08-22" }));
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
expect(await scheduler.reconcile()).toBe("missed");
|
||||
expect(controller.settings.dailyStartEnabled).toBe(true);
|
||||
expect(controller.settings.dailyStartLastHandledLocalDate).toBe("2026-08-22");
|
||||
expect(controller.settings.dailyStartLastOutcome).toBe("missed");
|
||||
expect(nextDailyStartEpochMs(controller.settings, now)).toBe(new Date(2026, 7, 23, 10, 0).getTime());
|
||||
});
|
||||
|
||||
it("keeps start failures pending for a later retry", async () => {
|
||||
const now = new Date(2026, 7, 22, 10, 5).getTime();
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
controller.start.mockRejectedValueOnce(new Error("start rejected"));
|
||||
const scheduler = new DailyStartScheduler(controller, () => now);
|
||||
|
||||
await expect(scheduler.reconcile()).rejects.toThrow("start rejected");
|
||||
expect(controller.settings.dailyStartPendingLocalDate).toBe("2026-08-22");
|
||||
expect(controller.settings.dailyStartLastHandledLocalDate).toBe("");
|
||||
expect(controller.settings.dailyStartLastOutcome).toBe("start_failed");
|
||||
|
||||
await scheduler.reconcile();
|
||||
expect(controller.start).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("constructs each DST target from local calendar fields instead of adding 24 hours", () => {
|
||||
const beforeDst = settings({
|
||||
dailyStartMinuteOfDay: 2 * 60 + 30,
|
||||
dailyStartFirstLocalDate: "2026-03-29"
|
||||
});
|
||||
const firstNow = new Date(2026, 2, 28, 12, 0).getTime();
|
||||
const firstTarget = nextDailyStartEpochMs(beforeDst, firstNow);
|
||||
const nextTarget = nextDailyStartEpochMs({
|
||||
...beforeDst,
|
||||
dailyStartLastHandledLocalDate: "2026-03-29"
|
||||
}, new Date(2026, 2, 29, 12, 0).getTime());
|
||||
|
||||
expect(firstTarget).toBe(new Date(2026, 2, 29, 2, 30, 0, 0).getTime());
|
||||
expect(nextTarget).toBe(new Date(2026, 2, 30, 2, 30, 0, 0).getTime());
|
||||
expect(nextTarget - firstTarget).toBe(23 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("clears a legacy one-time target only when a new daily rule is saved", () => {
|
||||
expect(prepareDailyStartSettingsPatch({
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 600,
|
||||
dailyStartFirstLocalDate: "2026-08-22"
|
||||
})).toEqual({
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 600,
|
||||
dailyStartFirstLocalDate: "2026-08-22",
|
||||
scheduledStartEpochMs: 0
|
||||
});
|
||||
expect(prepareDailyStartSettingsPatch({ dailyStartPendingLocalDate: "2026-08-22" }))
|
||||
.toEqual({ dailyStartPendingLocalDate: "2026-08-22" });
|
||||
});
|
||||
|
||||
it("defers boot auto-resume for a future daily target unless the saved run was active", () => {
|
||||
const now = new Date(2026, 7, 22, 9, 0).getTime();
|
||||
const future = settings({ dailyStartMinuteOfDay: 10 * 60 });
|
||||
|
||||
expect(shouldDeferAutoResumeToDailyStart(future, false, now)).toBe(true);
|
||||
expect(shouldDeferAutoResumeToDailyStart(future, true, now)).toBe(false);
|
||||
expect(shouldDeferAutoResumeToDailyStart({ ...future, dailyStartEnabled: false }, false, now)).toBe(false);
|
||||
expect(shouldDeferAutoResumeToDailyStart({ ...future, dailyStartMinuteOfDay: 8 * 60 }, false, now)).toBe(false);
|
||||
});
|
||||
|
||||
it("reconciles at boot and at most sixty seconds after a target becomes due", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 7, 22, 9, 59, 30));
|
||||
try {
|
||||
const controller = new FakeDailyStartController(settings());
|
||||
const scheduler = new DailyStartScheduler(controller);
|
||||
|
||||
scheduler.begin();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(controller.start).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
|
||||
scheduler.end();
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
expect(controller.start).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
+43
-1
@@ -9,7 +9,7 @@ import { parseRealDebridApiAccounts, serializeRealDebridApiAccounts } from "../s
|
||||
import { AppSettings } from "../src/shared/types";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||
import { addHistoryEntryForRetention, createStoragePaths, emptySession, loadHistory, loadHistoryForRetention, loadSession, loadSettings, normalizeLoadedSession, normalizeSettings, removeHistoryEntries, resetHistoryForRetention, saveHistory, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "../src/main/storage";
|
||||
import { addHistoryEntryForRetention, createStoragePaths, emptySession, loadHistory, loadHistoryForRetention, loadSession, loadSessionWithStatus, loadSettings, normalizeLoadedSession, normalizeSettings, removeHistoryEntries, resetHistoryForRetention, saveHistory, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "../src/main/storage";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
type SettingsSaveMode = "sync" | "async";
|
||||
@@ -46,6 +46,48 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("settings storage", () => {
|
||||
it("defaults and normalizes persistent daily start calendar fields", () => {
|
||||
const defaults = defaultSettings();
|
||||
|
||||
expect(defaults.dailyStartEnabled).toBe(false);
|
||||
expect(defaults.dailyStartMinuteOfDay).toBe(0);
|
||||
expect(defaults.dailyStartFirstLocalDate).toBe("");
|
||||
expect(defaults.dailyStartLastHandledLocalDate).toBe("");
|
||||
expect(defaults.dailyStartPendingLocalDate).toBe("");
|
||||
expect(defaults.dailyStartLastOutcome).toBe("");
|
||||
|
||||
expect(normalizeSettings({
|
||||
...defaults,
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 1_500,
|
||||
dailyStartFirstLocalDate: "2026-02-29",
|
||||
dailyStartLastHandledLocalDate: "2026-08-21",
|
||||
dailyStartPendingLocalDate: "not-a-day",
|
||||
dailyStartLastOutcome: "unsupported"
|
||||
} as unknown as AppSettings)).toMatchObject({
|
||||
dailyStartEnabled: true,
|
||||
dailyStartMinuteOfDay: 1_439,
|
||||
dailyStartFirstLocalDate: "",
|
||||
dailyStartLastHandledLocalDate: "2026-08-21",
|
||||
dailyStartPendingLocalDate: "",
|
||||
dailyStartLastOutcome: ""
|
||||
});
|
||||
});
|
||||
|
||||
it("reports whether a loaded session was active before transient normalization", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||
tempDirs.push(dir);
|
||||
const paths = createStoragePaths(dir);
|
||||
const session = emptySession();
|
||||
session.running = true;
|
||||
saveSession(paths, session);
|
||||
|
||||
const loaded = loadSessionWithStatus(paths);
|
||||
|
||||
expect(loaded.wasRunning).toBe(true);
|
||||
expect(loaded.session.running).toBe(false);
|
||||
});
|
||||
|
||||
it("migrates legacy package success notifications without changing their delivery frequency", () => {
|
||||
expect(loadSettingsFrom({ notifyOnPackageCompleted: true }).notifyPackageSuccessMode).toBe("individual");
|
||||
expect(loadSettingsFrom({}).notifyPackageSuccessMode).toBe("digest");
|
||||
|
||||
@@ -262,11 +262,17 @@ function createSettings(): AppSettings {
|
||||
message: "API-Key aktiv",
|
||||
checkedAt: 1786312800000
|
||||
}
|
||||
},
|
||||
providerDailyUsageDay: "2026-08-10",
|
||||
scheduledStartEpochMs: 0
|
||||
};
|
||||
}
|
||||
},
|
||||
providerDailyUsageDay: "2026-08-10",
|
||||
dailyStartEnabled: false,
|
||||
dailyStartMinuteOfDay: 0,
|
||||
dailyStartFirstLocalDate: "",
|
||||
dailyStartLastHandledLocalDate: "",
|
||||
dailyStartPendingLocalDate: "",
|
||||
dailyStartLastOutcome: "",
|
||||
scheduledStartEpochMs: 0
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptySnapshot(): UiSnapshot {
|
||||
const renderer = createRendererState(createSettings());
|
||||
|
||||
Reference in New Issue
Block a user