import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { GlobalSettingsStore, defaultGlobalDir } from "./global-settings.js"; import { DEFAULT_GLOBAL_SETTINGS } from "./types.js"; import { readFile, rm, writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { mkdtempSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; function makeTmpDir(): string { return mkdtempSync(join(tmpdir(), "kb-global-settings-test-")); } /** Temporarily clear VITEST to test default GlobalSettingsStore resolution. */ async function withDefaultGlobalSettingsStore( fn: (store: GlobalSettingsStore) => Promise, ): Promise { const savedVitest = process.env.VITEST; delete process.env.VITEST; try { const store = new GlobalSettingsStore(); return await fn(store); } finally { if (savedVitest === undefined) { delete process.env.VITEST; } else { process.env.VITEST = savedVitest; } } } describe("GlobalSettingsStore", () => { let dir: string; let store: GlobalSettingsStore; const originalHome = process.env.HOME; beforeEach(() => { dir = makeTmpDir(); store = new GlobalSettingsStore(dir); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); if (originalHome === undefined) { delete process.env.HOME; } else { process.env.HOME = originalHome; } }); describe("init()", () => { it("creates the directory and settings.json if missing", async () => { const nested = join(dir, "nested", "deep"); const nestedStore = new GlobalSettingsStore(nested); const created = await nestedStore.init(); expect(created).toBe(true); expect(existsSync(join(nested, "settings.json"))).toBe(true); }); it("creates settings.json with defaults on first init", async () => { await store.init(); const raw = await readFile(join(dir, "settings.json"), "utf-8"); const parsed = JSON.parse(raw); expect(parsed.themeMode).toBe("dark"); expect(parsed.colorTheme).toBe("default"); expect(parsed.ntfyEnabled).toBe(false); }); it("returns false if settings.json already exists", async () => { await store.init(); // creates file const created = await store.init(); // second call expect(created).toBe(false); }); it("preserves existing settings on re-init", async () => { await store.init(); await store.updateSettings({ themeMode: "light" }); const created = await store.init(); expect(created).toBe(false); const settings = await store.getSettings(); expect(settings.themeMode).toBe("light"); }); it("adopts the legacy ~/.pi/kb directory when ~/.fusion does not exist", async () => { const homeDir = makeTmpDir(); process.env.HOME = homeDir; const legacyDir = join(homeDir, ".pi", "kb"); await mkdir(legacyDir, { recursive: true }); await writeFile( join(legacyDir, "settings.json"), JSON.stringify({ themeMode: "light" }), ); try { await withDefaultGlobalSettingsStore(async (defaultStore) => { await defaultStore.init(); expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json")); expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true); expect(existsSync(join(homeDir, ".pi", "kb"))).toBe(false); const settings = await defaultStore.getSettings(); expect(settings.themeMode).toBe("light"); }); } finally { await rm(homeDir, { recursive: true, force: true }); } }); it("adopts the legacy ~/.pi/fusion directory when ~/.fusion does not exist", async () => { const homeDir = makeTmpDir(); process.env.HOME = homeDir; // Create the legacy ~/.pi/fusion directory with settings const legacyDir = join(homeDir, ".pi", "fusion"); await mkdir(legacyDir, { recursive: true }); await writeFile( join(legacyDir, "settings.json"), JSON.stringify({ themeMode: "light" }), ); // Verify legacy exists and new does not expect(existsSync(join(homeDir, ".pi", "fusion", "settings.json"))).toBe(true); expect(existsSync(join(homeDir, ".fusion"))).toBe(false); try { await withDefaultGlobalSettingsStore(async (defaultStore) => { await defaultStore.init(); // Verify migration happened expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json")); expect(existsSync(join(homeDir, ".fusion", "settings.json"))).toBe(true); expect(existsSync(join(homeDir, ".pi", "fusion"))).toBe(false); // Verify settings were preserved const settings = await defaultStore.getSettings(); expect(settings.themeMode).toBe("light"); }); } finally { await rm(homeDir, { recursive: true, force: true }); } }); }); describe("getSettings()", () => { it("returns defaults when file does not exist", async () => { const settings = await store.getSettings(); expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS); }); it("returns persisted values merged with defaults", async () => { await mkdir(dir, { recursive: true }); await writeFile( join(dir, "settings.json"), JSON.stringify({ themeMode: "light", colorTheme: "ocean" }), ); const settings = await store.getSettings(); expect(settings.themeMode).toBe("light"); expect(settings.colorTheme).toBe("ocean"); // Defaults are filled in for missing fields expect(settings.ntfyEnabled).toBe(false); expect(settings.defaultProvider).toBeUndefined(); }); it("returns defaults on invalid JSON", async () => { await mkdir(dir, { recursive: true }); await writeFile(join(dir, "settings.json"), "not-json{{{"); const settings = await store.getSettings(); expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS); }); it("returns defaults when directory does not exist", async () => { const nonExistent = new GlobalSettingsStore(join(dir, "nope", "nada")); const settings = await nonExistent.getSettings(); expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS); }); }); describe("updateSettings()", () => { it("persists a partial update and returns merged settings", async () => { await store.init(); const updated = await store.updateSettings({ themeMode: "system" }); expect(updated.themeMode).toBe("system"); expect(updated.colorTheme).toBe("default"); // unchanged default // Verify persistence const raw = await readFile(join(dir, "settings.json"), "utf-8"); const parsed = JSON.parse(raw); expect(parsed.themeMode).toBe("system"); }); it("merges multiple updates without losing fields", async () => { await store.init(); await store.updateSettings({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" }); await store.updateSettings({ ntfyEnabled: true, ntfyTopic: "my-topic" }); const settings = await store.getSettings(); expect(settings.defaultProvider).toBe("anthropic"); expect(settings.defaultModelId).toBe("claude-sonnet-4-5"); expect(settings.ntfyEnabled).toBe(true); expect(settings.ntfyTopic).toBe("my-topic"); expect(settings.themeMode).toBe("dark"); // preserved default }); it("creates directory if missing", async () => { const nested = join(dir, "auto", "create"); const nestedStore = new GlobalSettingsStore(nested); await nestedStore.updateSettings({ themeMode: "light" }); expect(existsSync(join(nested, "settings.json"))).toBe(true); const settings = await nestedStore.getSettings(); expect(settings.themeMode).toBe("light"); }); it("can clear a field by setting it to null (null-as-delete semantics)", async () => { await store.init(); await store.updateSettings({ defaultProvider: "anthropic" }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ defaultProvider: null }); const settings = await store.getSettings(); expect(settings.defaultProvider).toBeUndefined(); }); it("clearing ntfyTopic with null removes it from disk and returns undefined", async () => { await store.init(); await store.updateSettings({ ntfyEnabled: true, ntfyTopic: "my-topic" }); // Verify it was persisted const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.ntfyTopic).toBe("my-topic"); // Clear the topic with null // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ ntfyTopic: null }); // Verify it was removed from disk const rawAfter = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(rawAfter.ntfyTopic).toBeUndefined(); // Verify getSettings returns undefined const settings = await store.getSettings(); expect(settings.ntfyTopic).toBeUndefined(); }); it("clearing ntfyDashboardHost with null removes it from disk", async () => { await store.init(); await store.updateSettings({ ntfyDashboardHost: "https://dashboard.example.com" }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ ntfyDashboardHost: null }); const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.ntfyDashboardHost).toBeUndefined(); const settings = await store.getSettings(); expect(settings.ntfyDashboardHost).toBeUndefined(); }); it("clearing ntfyEvents with null resets to default on read", async () => { await store.init(); await store.updateSettings({ ntfyEvents: ["in-review", "failed"] }); // Verify it was persisted with custom value const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.ntfyEvents).toEqual(["in-review", "failed"]); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ ntfyEvents: null }); // After clear, reading back gives the default value // (either undefined on disk with default applied, or default written directly) const settings = await store.getSettings(); expect(settings.ntfyEvents).toEqual(["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review"]); }); it("handles concurrent updates safely via locking", async () => { await store.init(); // Fire 10 concurrent updates const promises = Array.from({ length: 10 }, (_, i) => store.updateSettings({ ntfyTopic: `topic-${i}` }), ); await Promise.all(promises); // The final value should be one of the submitted values (last writer wins) const settings = await store.getSettings(); expect(settings.ntfyTopic).toMatch(/^topic-\d$/); }); }); describe("schema protection", () => { it("preserves unknown keys during updateSettings", async () => { await store.init(); // Simulate a setting that existed in an older schema version const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); raw.legacyCustomField = "preserve-me"; raw.anotherRemovedSetting = 42; await writeFile(join(dir, "settings.json"), JSON.stringify(raw, null, 2)); // Update a known field — unknown keys must survive the write cycle await store.updateSettings({ ntfyEnabled: true }); const ondisk = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(ondisk.legacyCustomField).toBe("preserve-me"); expect(ondisk.anotherRemovedSetting).toBe(42); expect(ondisk.ntfyEnabled).toBe(true); }); it("readRaw returns all keys including unknown ones", async () => { await store.init(); const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); raw.futureField = "hello"; await writeFile(join(dir, "settings.json"), JSON.stringify(raw, null, 2)); const result = await store.readRaw(); expect(result.futureField).toBe("hello"); expect(result.themeMode).toBe("dark"); }); it("readRaw returns empty object for missing file", async () => { const emptyStore = new GlobalSettingsStore(join(dir, "nonexistent")); const result = await emptyStore.readRaw(); expect(result).toEqual({}); }); it("preserves unknown keys across multiple save cycles", async () => { await store.init(); // Inject an unknown key const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); raw.removedFeatureHost = "http://example.com"; await writeFile(join(dir, "settings.json"), JSON.stringify(raw, null, 2)); // Multiple save cycles with different known fields await store.updateSettings({ ntfyEnabled: true }); await store.updateSettings({ ntfyTopic: "test-topic" }); await store.updateSettings({ themeMode: "light" }); const ondisk = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(ondisk.removedFeatureHost).toBe("http://example.com"); expect(ondisk.ntfyEnabled).toBe(true); expect(ondisk.ntfyTopic).toBe("test-topic"); expect(ondisk.themeMode).toBe("light"); }); }); describe("getSettingsPath()", () => { it("returns the path to settings.json", () => { const path = store.getSettingsPath(); expect(path).toBe(join(dir, "settings.json")); }); }); describe("atomic writes", () => { it("does not leave tmp files after a successful write", async () => { await store.init(); await store.updateSettings({ themeMode: "light" }); const tmpPath = join(dir, "settings.json.tmp"); expect(existsSync(tmpPath)).toBe(false); }); }); describe("in-memory cache", () => { it("returns cached result on second getSettings() call without reading disk", async () => { await store.init(); await writeFile(join(dir, "settings.json"), JSON.stringify({ themeMode: "light" })); // First call reads from disk const first = await store.getSettings(); expect(first.themeMode).toBe("light"); // Modify the file externally await writeFile(join(dir, "settings.json"), JSON.stringify({ themeMode: "dark" })); // Second call should return cached result (light), not the new disk value (dark) const second = await store.getSettings(); expect(second.themeMode).toBe("light"); }); it("updateSettings() updates the cache", async () => { await store.init(); // First call populates cache const first = await store.getSettings(); expect(first.themeMode).toBe("dark"); // Update settings await store.updateSettings({ themeMode: "light" }); // getSettings should return updated cached value const second = await store.getSettings(); expect(second.themeMode).toBe("light"); }); it("invalidateCache() forces re-read from disk", async () => { await store.init(); await writeFile(join(dir, "settings.json"), JSON.stringify({ themeMode: "light" })); // First call reads from disk const first = await store.getSettings(); expect(first.themeMode).toBe("light"); // Modify the file externally await writeFile(join(dir, "settings.json"), JSON.stringify({ themeMode: "system" })); // Without invalidation, should return cached value const cached = await store.getSettings(); expect(cached.themeMode).toBe("light"); // After invalidation, should re-read from disk store.invalidateCache(); const afterInvalidate = await store.getSettings(); expect(afterInvalidate.themeMode).toBe("system"); }); }); // ── Global Lane Model Settings (FN-1710) ────────────────────────── describe("global lane model settings", () => { it("all *Global* lane fields default to undefined", async () => { const settings = await store.getSettings(); expect(settings.executionGlobalProvider).toBeUndefined(); expect(settings.executionGlobalModelId).toBeUndefined(); expect(settings.planningGlobalProvider).toBeUndefined(); expect(settings.planningGlobalModelId).toBeUndefined(); expect(settings.validatorGlobalProvider).toBeUndefined(); expect(settings.validatorGlobalModelId).toBeUndefined(); expect(settings.titleSummarizerGlobalProvider).toBeUndefined(); expect(settings.titleSummarizerGlobalModelId).toBeUndefined(); }); it("persists executionGlobalProvider/executionGlobalModelId", async () => { await store.updateSettings({ executionGlobalProvider: "anthropic", executionGlobalModelId: "claude-sonnet-4-5", }); const settings = await store.getSettings(); expect(settings.executionGlobalProvider).toBe("anthropic"); expect(settings.executionGlobalModelId).toBe("claude-sonnet-4-5"); // Verify persistence const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.executionGlobalProvider).toBe("anthropic"); expect(raw.executionGlobalModelId).toBe("claude-sonnet-4-5"); }); it("persists planningGlobalProvider/planningGlobalModelId", async () => { await store.updateSettings({ planningGlobalProvider: "google", planningGlobalModelId: "gemini-2.5-pro", }); const settings = await store.getSettings(); expect(settings.planningGlobalProvider).toBe("google"); expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro"); }); it("persists validatorGlobalProvider/validatorGlobalModelId", async () => { await store.updateSettings({ validatorGlobalProvider: "openai", validatorGlobalModelId: "gpt-4o", }); const settings = await store.getSettings(); expect(settings.validatorGlobalProvider).toBe("openai"); expect(settings.validatorGlobalModelId).toBe("gpt-4o"); }); it("persists titleSummarizerGlobalProvider/titleSummarizerGlobalModelId", async () => { await store.updateSettings({ titleSummarizerGlobalProvider: "anthropic", titleSummarizerGlobalModelId: "claude-haiku", }); const settings = await store.getSettings(); expect(settings.titleSummarizerGlobalProvider).toBe("anthropic"); expect(settings.titleSummarizerGlobalModelId).toBe("claude-haiku"); }); it("persists all global lane fields together", async () => { await store.updateSettings({ executionGlobalProvider: "anthropic", executionGlobalModelId: "claude-opus-4", planningGlobalProvider: "google", planningGlobalModelId: "gemini-2.5-pro", validatorGlobalProvider: "openai", validatorGlobalModelId: "gpt-4-turbo", titleSummarizerGlobalProvider: "anthropic", titleSummarizerGlobalModelId: "claude-sonnet-4-5", }); const settings = await store.getSettings(); expect(settings.executionGlobalProvider).toBe("anthropic"); expect(settings.executionGlobalModelId).toBe("claude-opus-4"); expect(settings.planningGlobalProvider).toBe("google"); expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro"); expect(settings.validatorGlobalProvider).toBe("openai"); expect(settings.validatorGlobalModelId).toBe("gpt-4-turbo"); expect(settings.titleSummarizerGlobalProvider).toBe("anthropic"); expect(settings.titleSummarizerGlobalModelId).toBe("claude-sonnet-4-5"); }); it("can clear global lane fields with null", async () => { await store.updateSettings({ executionGlobalProvider: "anthropic", executionGlobalModelId: "claude-sonnet-4-5", }); // Clear them // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ executionGlobalProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ executionGlobalModelId: null }); const settings = await store.getSettings(); expect(settings.executionGlobalProvider).toBeUndefined(); expect(settings.executionGlobalModelId).toBeUndefined(); }); it("merges global lane fields without losing other fields", async () => { await store.updateSettings({ executionGlobalProvider: "anthropic", executionGlobalModelId: "claude-sonnet-4-5", }); await store.updateSettings({ planningGlobalProvider: "google", planningGlobalModelId: "gemini-2.5-pro", }); const settings = await store.getSettings(); // Both should be present expect(settings.executionGlobalProvider).toBe("anthropic"); expect(settings.executionGlobalModelId).toBe("claude-sonnet-4-5"); expect(settings.planningGlobalProvider).toBe("google"); expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro"); }); }); // ── Model Baseline/Fallback Round-Trip Regression (FN-1729) ────────────── describe("model baseline/fallback round-trip regression (FN-1729)", () => { it("defaultProvider/defaultModelId round-trips with defaults + persisted values", async () => { // Persist default baseline await store.updateSettings({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", }); let settings = await store.getSettings(); expect(settings.defaultProvider).toBe("anthropic"); expect(settings.defaultModelId).toBe("claude-sonnet-4-5"); // Verify persistence const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.defaultProvider).toBe("anthropic"); expect(raw.defaultModelId).toBe("claude-sonnet-4-5"); }); it("fallbackProvider/fallbackModelId round-trips with defaults + persisted values", async () => { await store.updateSettings({ fallbackProvider: "openai", fallbackModelId: "gpt-4o", }); let settings = await store.getSettings(); expect(settings.fallbackProvider).toBe("openai"); expect(settings.fallbackModelId).toBe("gpt-4o"); // Verify persistence const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.fallbackProvider).toBe("openai"); expect(raw.fallbackModelId).toBe("gpt-4o"); }); it("all global model lanes round-trip correctly", async () => { await store.updateSettings({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", fallbackProvider: "openai", fallbackModelId: "gpt-4o", executionGlobalProvider: "google", executionGlobalModelId: "gemini-2.5-pro", planningGlobalProvider: "anthropic", planningGlobalModelId: "claude-opus-4", validatorGlobalProvider: "openai", validatorGlobalModelId: "gpt-4-turbo", titleSummarizerGlobalProvider: "anthropic", titleSummarizerGlobalModelId: "claude-haiku", }); // Verify all values let settings = await store.getSettings(); expect(settings.defaultProvider).toBe("anthropic"); expect(settings.defaultModelId).toBe("claude-sonnet-4-5"); expect(settings.fallbackProvider).toBe("openai"); expect(settings.fallbackModelId).toBe("gpt-4o"); expect(settings.executionGlobalProvider).toBe("google"); expect(settings.executionGlobalModelId).toBe("gemini-2.5-pro"); expect(settings.planningGlobalProvider).toBe("anthropic"); expect(settings.planningGlobalModelId).toBe("claude-opus-4"); expect(settings.validatorGlobalProvider).toBe("openai"); expect(settings.validatorGlobalModelId).toBe("gpt-4-turbo"); expect(settings.titleSummarizerGlobalProvider).toBe("anthropic"); expect(settings.titleSummarizerGlobalModelId).toBe("claude-haiku"); // Verify persistence const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.defaultProvider).toBe("anthropic"); expect(raw.defaultModelId).toBe("claude-sonnet-4-5"); expect(raw.fallbackProvider).toBe("openai"); expect(raw.fallbackModelId).toBe("gpt-4o"); expect(raw.executionGlobalProvider).toBe("google"); expect(raw.executionGlobalModelId).toBe("gemini-2.5-pro"); expect(raw.planningGlobalProvider).toBe("anthropic"); expect(raw.planningGlobalModelId).toBe("claude-opus-4"); expect(raw.validatorGlobalProvider).toBe("openai"); expect(raw.validatorGlobalModelId).toBe("gpt-4-turbo"); expect(raw.titleSummarizerGlobalProvider).toBe("anthropic"); expect(raw.titleSummarizerGlobalModelId).toBe("claude-haiku"); // Clear and verify empty // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ defaultProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ defaultModelId: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ fallbackProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ fallbackModelId: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ executionGlobalProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ executionGlobalModelId: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ planningGlobalProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ planningGlobalModelId: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ validatorGlobalProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ validatorGlobalModelId: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ titleSummarizerGlobalProvider: null }); // @ts-expect-error - null is intentionally used to clear field (null-as-delete) await store.updateSettings({ titleSummarizerGlobalModelId: null }); settings = await store.getSettings(); expect(settings.defaultProvider).toBeUndefined(); expect(settings.defaultModelId).toBeUndefined(); expect(settings.fallbackProvider).toBeUndefined(); expect(settings.fallbackModelId).toBeUndefined(); expect(settings.executionGlobalProvider).toBeUndefined(); expect(settings.executionGlobalModelId).toBeUndefined(); expect(settings.planningGlobalProvider).toBeUndefined(); expect(settings.planningGlobalModelId).toBeUndefined(); expect(settings.validatorGlobalProvider).toBeUndefined(); expect(settings.validatorGlobalModelId).toBeUndefined(); expect(settings.titleSummarizerGlobalProvider).toBeUndefined(); expect(settings.titleSummarizerGlobalModelId).toBeUndefined(); // Verify cleared from persistence const rawAfter = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(rawAfter.defaultProvider).toBeUndefined(); expect(rawAfter.defaultModelId).toBeUndefined(); expect(rawAfter.fallbackProvider).toBeUndefined(); expect(rawAfter.fallbackModelId).toBeUndefined(); expect(rawAfter.executionGlobalProvider).toBeUndefined(); expect(rawAfter.executionGlobalModelId).toBeUndefined(); expect(rawAfter.planningGlobalProvider).toBeUndefined(); expect(rawAfter.planningGlobalModelId).toBeUndefined(); expect(rawAfter.validatorGlobalProvider).toBeUndefined(); expect(rawAfter.validatorGlobalModelId).toBeUndefined(); expect(rawAfter.titleSummarizerGlobalProvider).toBeUndefined(); expect(rawAfter.titleSummarizerGlobalModelId).toBeUndefined(); }); it("model baseline/fallback defaults do not persist until explicitly set", async () => { // Initialize the store to create settings file with defaults await store.init(); // Don't set any model settings const settings = await store.getSettings(); // All should be undefined (not default values) expect(settings.defaultProvider).toBeUndefined(); expect(settings.defaultModelId).toBeUndefined(); expect(settings.fallbackProvider).toBeUndefined(); expect(settings.fallbackModelId).toBeUndefined(); // Settings file should exist with defaults but no model fields persisted const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); // Only default theme fields should be present expect(raw.themeMode).toBe("dark"); expect(raw.colorTheme).toBe("default"); // Model fields should not be persisted expect(raw.defaultProvider).toBeUndefined(); expect(raw.defaultModelId).toBeUndefined(); expect(raw.fallbackProvider).toBeUndefined(); expect(raw.fallbackModelId).toBeUndefined(); }); it("partial provider without modelId is valid and persists correctly", async () => { // Set provider without modelId await store.updateSettings({ defaultProvider: "anthropic", // defaultModelId intentionally omitted }); const settings = await store.getSettings(); expect(settings.defaultProvider).toBe("anthropic"); expect(settings.defaultModelId).toBeUndefined(); // Verify partial pair persisted const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8")); expect(raw.defaultProvider).toBe("anthropic"); expect(raw.defaultModelId).toBeUndefined(); }); }); });