feat(FN-1729): merge fusion/fn-1729

This commit is contained in:
gsxdsm
2026-04-15 09:31:09 -07:00
parent dde7c3174e
commit 45c0630ead
3 changed files with 1214 additions and 0 deletions

View File

@@ -58,3 +58,103 @@ describe("settings key parity", () => {
expect(overlap).toEqual([]); expect(overlap).toEqual([]);
}); });
}); });
// ── Model Lane Key Parity Regression Tests (FN-1729) ────────────────────────
describe("model lane key parity regression (FN-1729)", () => {
// All model lane provider/modelId pairs that should exist
const allModelLanePairs = [
// Default baseline (global only)
{ provider: "defaultProvider", modelId: "defaultModelId", expectedScope: "global" },
// Fallback baseline (global only)
{ provider: "fallbackProvider", modelId: "fallbackModelId", expectedScope: "global" },
// Execution lane
{ provider: "executionProvider", modelId: "executionModelId", expectedScope: "project" },
{ provider: "executionGlobalProvider", modelId: "executionGlobalModelId", expectedScope: "global" },
// Planning lane
{ provider: "planningProvider", modelId: "planningModelId", expectedScope: "project" },
{ provider: "planningGlobalProvider", modelId: "planningGlobalModelId", expectedScope: "global" },
{ provider: "planningFallbackProvider", modelId: "planningFallbackModelId", expectedScope: "project" },
// Validator lane
{ provider: "validatorProvider", modelId: "validatorModelId", expectedScope: "project" },
{ provider: "validatorGlobalProvider", modelId: "validatorGlobalModelId", expectedScope: "global" },
{ provider: "validatorFallbackProvider", modelId: "validatorFallbackModelId", expectedScope: "project" },
// Summarizer lane
{ provider: "titleSummarizerProvider", modelId: "titleSummarizerModelId", expectedScope: "project" },
{ provider: "titleSummarizerGlobalProvider", modelId: "titleSummarizerGlobalModelId", expectedScope: "global" },
{ provider: "titleSummarizerFallbackProvider", modelId: "titleSummarizerFallbackModelId", expectedScope: "project" },
] as const;
it.each(allModelLanePairs)(
"$provider/$modelId is correctly classified as $expectedScope scope",
({ provider, modelId, expectedScope }) => {
if (expectedScope === "global") {
expect(isGlobalSettingsKey(provider)).toBe(true);
expect(isGlobalSettingsKey(modelId)).toBe(true);
expect(isProjectSettingsKey(provider)).toBe(false);
expect(isProjectSettingsKey(modelId)).toBe(false);
} else {
expect(isProjectSettingsKey(provider)).toBe(true);
expect(isProjectSettingsKey(modelId)).toBe(true);
expect(isGlobalSettingsKey(provider)).toBe(false);
expect(isGlobalSettingsKey(modelId)).toBe(false);
}
},
);
it("model lane keys appear in exactly one scope key list", () => {
const globalKeys = new Set(GLOBAL_SETTINGS_KEYS as readonly string[]);
const projectKeys = new Set(PROJECT_SETTINGS_KEYS as readonly string[]);
for (const { provider, modelId } of allModelLanePairs) {
const inGlobal = globalKeys.has(provider) && globalKeys.has(modelId);
const inProject = projectKeys.has(provider) && projectKeys.has(modelId);
// Each pair must appear in exactly one scope
expect(inGlobal || inProject).toBe(true);
expect(inGlobal && inProject).toBe(false);
}
});
it("all global model lane keys are in GLOBAL_SETTINGS_KEYS", () => {
const globalKeys = new Set(GLOBAL_SETTINGS_KEYS as readonly string[]);
const globalLanes = allModelLanePairs
.filter((p) => p.expectedScope === "global")
.flatMap((p) => [p.provider, p.modelId]);
for (const key of globalLanes) {
expect(globalKeys.has(key)).toBe(true);
}
});
it("all project model lane keys are in PROJECT_SETTINGS_KEYS", () => {
const projectKeys = new Set(PROJECT_SETTINGS_KEYS as readonly string[]);
const projectLanes = allModelLanePairs
.filter((p) => p.expectedScope === "project")
.flatMap((p) => [p.provider, p.modelId]);
for (const key of projectLanes) {
expect(projectKeys.has(key)).toBe(true);
}
});
it("default override keys are in project scope", () => {
expect(isProjectSettingsKey("defaultProviderOverride")).toBe(true);
expect(isProjectSettingsKey("defaultModelIdOverride")).toBe(true);
expect(isGlobalSettingsKey("defaultProviderOverride")).toBe(false);
expect(isGlobalSettingsKey("defaultModelIdOverride")).toBe(false);
});
it("no model lane provider exists without its corresponding modelId key", () => {
const allKeys = new Set([...GLOBAL_SETTINGS_KEYS, ...PROJECT_SETTINGS_KEYS]);
for (const { provider, modelId } of allModelLanePairs) {
// Both must exist or neither should exist
const hasProvider = allKeys.has(provider);
const hasModelId = allKeys.has(modelId);
expect(hasProvider).toBe(hasModelId);
}
});
});

View File

@@ -501,4 +501,185 @@ describe("GlobalSettingsStore", () => {
expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro"); 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();
});
});
}); });

View File

@@ -991,6 +991,939 @@ describe("TaskStore", () => {
}); });
}); });
// ── Model Lane Persistence Regression Tests (FN-1729) ─────────────────────
describe("model lane persistence regression", () => {
// Table-driven test matrix: verifies all model lane fields persist correctly
// Fields are split by their correct scope (global or project)
const projectModelLanePairs = [
// Execution lane (project override)
{ provider: "executionProvider", modelId: "executionModelId" },
// Planning lane (project override + fallback)
{ provider: "planningProvider", modelId: "planningModelId" },
{ provider: "planningFallbackProvider", modelId: "planningFallbackModelId" },
// Validator lane (project override + fallback)
{ provider: "validatorProvider", modelId: "validatorModelId" },
{ provider: "validatorFallbackProvider", modelId: "validatorFallbackModelId" },
// Summarizer lane (project override + fallback)
{ provider: "titleSummarizerProvider", modelId: "titleSummarizerModelId" },
{ provider: "titleSummarizerFallbackProvider", modelId: "titleSummarizerFallbackModelId" },
// Default override (project-level override of global defaults)
{ provider: "defaultProviderOverride", modelId: "defaultModelIdOverride" },
] as const;
const globalModelLanePairs = [
// Default baseline
{ provider: "defaultProvider", modelId: "defaultModelId" },
// Fallback baseline
{ provider: "fallbackProvider", modelId: "fallbackModelId" },
// Execution lane (global baseline)
{ provider: "executionGlobalProvider", modelId: "executionGlobalModelId" },
// Planning lane (global baseline)
{ provider: "planningGlobalProvider", modelId: "planningGlobalModelId" },
// Validator lane (global baseline)
{ provider: "validatorGlobalProvider", modelId: "validatorGlobalModelId" },
// Summarizer lane (global baseline)
{ provider: "titleSummarizerGlobalProvider", modelId: "titleSummarizerGlobalModelId" },
] as const;
it.each(projectModelLanePairs)(
"persists project $provider/$modelId via updateSettings",
async ({ provider, modelId }) => {
const patch: Record<string, string> = {};
patch[provider] = "anthropic";
patch[modelId] = "claude-opus-4";
await store.updateSettings(patch);
const settings = await store.getSettings();
expect((settings as any)[provider]).toBe("anthropic");
expect((settings as any)[modelId]).toBe("claude-opus-4");
},
);
it.each(globalModelLanePairs)(
"persists global $provider/$modelId via updateGlobalSettings",
async ({ provider, modelId }) => {
const patch: Record<string, string> = {};
patch[provider] = "openai";
patch[modelId] = "gpt-4o";
await store.updateGlobalSettings(patch);
const settings = await store.getSettings();
expect((settings as any)[provider]).toBe("openai");
expect((settings as any)[modelId]).toBe("gpt-4o");
},
);
it.each(projectModelLanePairs)(
"project $provider/$modelId default to undefined",
async ({ provider, modelId }) => {
const settings = await store.getSettings();
expect((settings as any)[provider]).toBeUndefined();
expect((settings as any)[modelId]).toBeUndefined();
},
);
it.each(globalModelLanePairs)(
"global $provider/$modelId default to undefined",
async ({ provider, modelId }) => {
const settings = await store.getSettings();
expect((settings as any)[provider]).toBeUndefined();
expect((settings as any)[modelId]).toBeUndefined();
},
);
it.each(projectModelLanePairs)(
"project $provider/$modelId persist to project config file",
async ({ provider, modelId }) => {
const patch: Record<string, string> = {};
patch[provider] = "google";
patch[modelId] = "gemini-2.5-pro";
await store.updateSettings(patch);
const configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);
// Project fields should appear in project config
expect((config.settings as any)[provider]).toBe("google");
expect((config.settings as any)[modelId]).toBe("gemini-2.5-pro");
},
);
it.each(globalModelLanePairs)(
"global $provider/$modelId do NOT persist to project config file",
async ({ provider, modelId }) => {
const patch: Record<string, string> = {};
patch[provider] = "google";
patch[modelId] = "gemini-2.5-pro";
await store.updateGlobalSettings(patch);
const configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);
// Global fields should NOT appear in project config
expect((config.settings as any)[provider]).toBeUndefined();
expect((config.settings as any)[modelId]).toBeUndefined();
},
);
});
// ── Scope Separation Regression Tests (FN-1729) ───────────────────────────
describe("scope separation regression", () => {
it("getSettingsByScope: global lane keys never appear in project scope", async () => {
await store.updateGlobalSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4o",
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-haiku",
});
const { project } = await store.getSettingsByScope();
// All global lane keys must be absent from project scope
expect((project as any).executionGlobalProvider).toBeUndefined();
expect((project as any).executionGlobalModelId).toBeUndefined();
expect((project as any).planningGlobalProvider).toBeUndefined();
expect((project as any).planningGlobalModelId).toBeUndefined();
expect((project as any).validatorGlobalProvider).toBeUndefined();
expect((project as any).validatorGlobalModelId).toBeUndefined();
expect((project as any).titleSummarizerGlobalProvider).toBeUndefined();
expect((project as any).titleSummarizerGlobalModelId).toBeUndefined();
});
it("getSettingsByScope: project override keys never appear in global scope", async () => {
await store.updateSettings({
executionProvider: "anthropic",
executionModelId: "claude-opus-4",
planningProvider: "openai",
planningModelId: "gpt-4o",
planningFallbackProvider: "google",
planningFallbackModelId: "gemini-2.5-pro",
validatorProvider: "anthropic",
validatorModelId: "claude-sonnet-4-5",
validatorFallbackProvider: "openai",
validatorFallbackModelId: "gpt-4o-mini",
titleSummarizerProvider: "google",
titleSummarizerModelId: "gemini-2.5-pro",
titleSummarizerFallbackProvider: "anthropic",
titleSummarizerFallbackModelId: "claude-haiku",
});
const { global } = await store.getSettingsByScope();
// Project lane keys must be absent from global scope
expect((global as any).executionProvider).toBeUndefined();
expect((global as any).executionModelId).toBeUndefined();
expect((global as any).planningProvider).toBeUndefined();
expect((global as any).planningModelId).toBeUndefined();
expect((global as any).planningFallbackProvider).toBeUndefined();
expect((global as any).planningFallbackModelId).toBeUndefined();
expect((global as any).validatorProvider).toBeUndefined();
expect((global as any).validatorModelId).toBeUndefined();
expect((global as any).validatorFallbackProvider).toBeUndefined();
expect((global as any).validatorFallbackModelId).toBeUndefined();
expect((global as any).titleSummarizerProvider).toBeUndefined();
expect((global as any).titleSummarizerModelId).toBeUndefined();
expect((global as any).titleSummarizerFallbackProvider).toBeUndefined();
expect((global as any).titleSummarizerFallbackModelId).toBeUndefined();
});
it("getSettingsByScope: default baseline keys appear in global scope", async () => {
await store.updateGlobalSettings({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o",
});
const { global } = await store.getSettingsByScope();
expect(global.defaultProvider).toBe("anthropic");
expect(global.defaultModelId).toBe("claude-sonnet-4-5");
expect(global.fallbackProvider).toBe("openai");
expect(global.fallbackModelId).toBe("gpt-4o");
});
it("getSettingsByScope: default override keys appear in project scope", async () => {
await store.updateSettings({
defaultProviderOverride: "anthropic",
defaultModelIdOverride: "claude-opus-4",
});
const { project } = await store.getSettingsByScope();
expect(project.defaultProviderOverride).toBe("anthropic");
expect(project.defaultModelIdOverride).toBe("claude-opus-4");
});
it("getSettingsByScope: mixed global and project keys are separated correctly", async () => {
await store.updateGlobalSettings({
defaultProvider: "openai",
defaultModelId: "gpt-4o",
fallbackProvider: "google",
fallbackModelId: "gemini-2.5-pro",
planningGlobalProvider: "anthropic",
planningGlobalModelId: "claude-sonnet-4-5",
});
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-opus-4",
planningFallbackProvider: "openai",
planningFallbackModelId: "gpt-4o-mini",
executionProvider: "google",
executionModelId: "gemini-2.5-pro",
});
const { global, project } = await store.getSettingsByScope();
// Global scope
expect(global.defaultProvider).toBe("openai");
expect(global.defaultModelId).toBe("gpt-4o");
expect(global.fallbackProvider).toBe("google");
expect(global.fallbackModelId).toBe("gemini-2.5-pro");
expect(global.planningGlobalProvider).toBe("anthropic");
expect(global.planningGlobalModelId).toBe("claude-sonnet-4-5");
// Project scope
expect(project.planningProvider).toBe("anthropic");
expect(project.planningModelId).toBe("claude-opus-4");
expect(project.planningFallbackProvider).toBe("openai");
expect(project.planningFallbackModelId).toBe("gpt-4o-mini");
expect(project.executionProvider).toBe("google");
expect(project.executionModelId).toBe("gemini-2.5-pro");
// Verify no cross-contamination
expect((global as any).planningProvider).toBeUndefined();
expect((global as any).planningFallbackProvider).toBeUndefined();
expect((global as any).executionProvider).toBeUndefined();
expect((project as any).planningGlobalProvider).toBeUndefined();
expect((project as any).defaultProvider).toBeUndefined();
});
});
// ── Settings Parity Regression Tests (FN-1729) ────────────────────────────
describe("settings parity regression", () => {
it("getSettings() and getSettingsFast() return equivalent merged snapshots", async () => {
// Set up various settings across scopes
await store.updateGlobalSettings({
themeMode: "light",
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o",
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-opus-4",
});
await store.updateSettings({
maxConcurrent: 5,
autoMerge: false,
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
planningFallbackProvider: "openai",
planningFallbackModelId: "gpt-4o-mini",
executionProvider: "google",
executionModelId: "gemini-2.5-pro",
validatorProvider: "anthropic",
validatorModelId: "claude-haiku",
});
const fast = await store.getSettingsFast();
const regular = await store.getSettings();
// Verify parity for all model settings
expect(fast.defaultProvider).toBe(regular.defaultProvider);
expect(fast.defaultModelId).toBe(regular.defaultModelId);
expect(fast.fallbackProvider).toBe(regular.fallbackProvider);
expect(fast.fallbackModelId).toBe(regular.fallbackModelId);
expect(fast.planningGlobalProvider).toBe(regular.planningGlobalProvider);
expect(fast.planningGlobalModelId).toBe(regular.planningGlobalModelId);
expect(fast.planningProvider).toBe(regular.planningProvider);
expect(fast.planningModelId).toBe(regular.planningModelId);
expect(fast.planningFallbackProvider).toBe(regular.planningFallbackProvider);
expect(fast.planningFallbackModelId).toBe(regular.planningFallbackModelId);
expect(fast.executionGlobalProvider).toBe(regular.executionGlobalProvider);
expect(fast.executionGlobalModelId).toBe(regular.executionGlobalModelId);
expect(fast.executionProvider).toBe(regular.executionProvider);
expect(fast.executionModelId).toBe(regular.executionModelId);
expect(fast.validatorProvider).toBe(regular.validatorProvider);
expect(fast.validatorModelId).toBe(regular.validatorModelId);
expect(fast.validatorFallbackProvider).toBe(regular.validatorFallbackProvider);
expect(fast.validatorFallbackModelId).toBe(regular.validatorFallbackModelId);
expect(fast.titleSummarizerProvider).toBe(regular.titleSummarizerProvider);
expect(fast.titleSummarizerModelId).toBe(regular.titleSummarizerModelId);
expect(fast.titleSummarizerGlobalProvider).toBe(regular.titleSummarizerGlobalProvider);
expect(fast.titleSummarizerGlobalModelId).toBe(regular.titleSummarizerGlobalModelId);
expect(fast.titleSummarizerFallbackProvider).toBe(regular.titleSummarizerFallbackProvider);
expect(fast.titleSummarizerFallbackModelId).toBe(regular.titleSummarizerFallbackModelId);
// Also verify non-model settings parity
expect(fast.themeMode).toBe(regular.themeMode);
expect(fast.maxConcurrent).toBe(regular.maxConcurrent);
expect(fast.autoMerge).toBe(regular.autoMerge);
// The entire objects should be equal
expect(fast).toEqual(regular);
});
it("getSettings() and getSettingsFast() are equivalent on empty state", async () => {
const fast = await store.getSettingsFast();
const regular = await store.getSettings();
expect(fast).toEqual(regular);
});
it("getSettingsFast() includes all model lane fields", async () => {
await store.updateGlobalSettings({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o",
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-opus-4",
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4-turbo",
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-haiku",
});
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
planningFallbackProvider: "openai",
planningFallbackModelId: "gpt-4o-mini",
executionProvider: "google",
executionModelId: "gemini-2.5-pro",
validatorProvider: "anthropic",
validatorModelId: "claude-opus-4",
validatorFallbackProvider: "openai",
validatorFallbackModelId: "gpt-4o",
titleSummarizerProvider: "google",
titleSummarizerModelId: "gemini-2.5-pro",
titleSummarizerFallbackProvider: "anthropic",
titleSummarizerFallbackModelId: "claude-haiku",
});
const settings = await store.getSettingsFast();
// Verify all fields are present
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.planningGlobalProvider).toBe("google");
expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro");
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBe("claude-sonnet-4-5");
expect(settings.planningFallbackProvider).toBe("openai");
expect(settings.planningFallbackModelId).toBe("gpt-4o-mini");
expect(settings.executionGlobalProvider).toBe("anthropic");
expect(settings.executionGlobalModelId).toBe("claude-opus-4");
expect(settings.executionProvider).toBe("google");
expect(settings.executionModelId).toBe("gemini-2.5-pro");
expect(settings.validatorProvider).toBe("anthropic");
expect(settings.validatorModelId).toBe("claude-opus-4");
expect(settings.validatorFallbackProvider).toBe("openai");
expect(settings.validatorFallbackModelId).toBe("gpt-4o");
expect(settings.titleSummarizerProvider).toBe("google");
expect(settings.titleSummarizerModelId).toBe("gemini-2.5-pro");
expect(settings.titleSummarizerGlobalProvider).toBe("anthropic");
expect(settings.titleSummarizerGlobalModelId).toBe("claude-haiku");
expect(settings.titleSummarizerFallbackProvider).toBe("anthropic");
expect(settings.titleSummarizerFallbackModelId).toBe("claude-haiku");
});
});
// ── Model Precedence Regression Tests (FN-1729) ──────────────────────────
describe("model precedence regression", () => {
it("project override pair present → effective value uses project pair", async () => {
// Set global baseline
await store.updateGlobalSettings({
planningGlobalProvider: "anthropic",
planningGlobalModelId: "claude-sonnet-4-5",
});
// Set project override
await store.updateSettings({
planningProvider: "openai",
planningModelId: "gpt-4o",
});
const settings = await store.getSettings();
// Project pair should win
expect(settings.planningProvider).toBe("openai");
expect(settings.planningModelId).toBe("gpt-4o");
// Global should still be readable
expect(settings.planningGlobalProvider).toBe("anthropic");
expect(settings.planningGlobalModelId).toBe("claude-sonnet-4-5");
});
it("project override missing + global lane pair present → uses global lane pair", async () => {
// Set only global baseline
await store.updateGlobalSettings({
planningGlobalProvider: "anthropic",
planningGlobalModelId: "claude-sonnet-4-5",
});
const settings = await store.getSettings();
// Should fall back to global lane
expect(settings.planningProvider).toBeUndefined();
expect(settings.planningModelId).toBeUndefined();
expect(settings.planningGlobalProvider).toBe("anthropic");
expect(settings.planningGlobalModelId).toBe("claude-sonnet-4-5");
});
it("lane pair missing + default pair present → falls back to default pair", async () => {
// Set only default baseline (no planning lane at all)
await store.updateGlobalSettings({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
const settings = await store.getSettings();
// Planning lane should be empty
expect(settings.planningProvider).toBeUndefined();
expect(settings.planningModelId).toBeUndefined();
// Default baseline should be available
expect(settings.defaultProvider).toBe("anthropic");
expect(settings.defaultModelId).toBe("claude-sonnet-4-5");
});
it("execution lane precedence: project → global → default", async () => {
// Set default baseline
await store.updateGlobalSettings({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
// Set execution global lane
await store.updateGlobalSettings({
executionGlobalProvider: "google",
executionGlobalModelId: "gemini-2.5-pro",
});
// Set execution project override
await store.updateSettings({
executionProvider: "openai",
executionModelId: "gpt-4o",
});
const settings = await store.getSettings();
// Project override should win
expect(settings.executionProvider).toBe("openai");
expect(settings.executionModelId).toBe("gpt-4o");
// Global should still be accessible
expect(settings.executionGlobalProvider).toBe("google");
expect(settings.executionGlobalModelId).toBe("gemini-2.5-pro");
// Default should still be accessible
expect(settings.defaultProvider).toBe("anthropic");
expect(settings.defaultModelId).toBe("claude-sonnet-4-5");
});
it("all lanes simultaneously with different providers", async () => {
await store.updateGlobalSettings({
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",
});
await store.updateSettings({
executionProvider: "openai",
executionModelId: "gpt-4o-mini",
planningProvider: "google",
planningModelId: "gemini-2.5-flash",
planningFallbackProvider: "anthropic",
planningFallbackModelId: "claude-sonnet-4-5",
validatorProvider: "google",
validatorModelId: "gemini-2.5-pro",
validatorFallbackProvider: "anthropic",
validatorFallbackModelId: "claude-opus-4",
titleSummarizerProvider: "openai",
titleSummarizerModelId: "gpt-4o",
titleSummarizerFallbackProvider: "google",
titleSummarizerFallbackModelId: "gemini-2.5-flash",
});
const settings = await store.getSettings();
// All lanes should coexist independently
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.executionProvider).toBe("openai");
expect(settings.executionModelId).toBe("gpt-4o-mini");
expect(settings.executionGlobalProvider).toBe("google");
expect(settings.executionGlobalModelId).toBe("gemini-2.5-pro");
expect(settings.planningProvider).toBe("google");
expect(settings.planningModelId).toBe("gemini-2.5-flash");
expect(settings.planningGlobalProvider).toBe("anthropic");
expect(settings.planningGlobalModelId).toBe("claude-opus-4");
expect(settings.planningFallbackProvider).toBe("anthropic");
expect(settings.planningFallbackModelId).toBe("claude-sonnet-4-5");
expect(settings.validatorProvider).toBe("google");
expect(settings.validatorModelId).toBe("gemini-2.5-pro");
expect(settings.validatorGlobalProvider).toBe("openai");
expect(settings.validatorGlobalModelId).toBe("gpt-4-turbo");
expect(settings.validatorFallbackProvider).toBe("anthropic");
expect(settings.validatorFallbackModelId).toBe("claude-opus-4");
expect(settings.titleSummarizerProvider).toBe("openai");
expect(settings.titleSummarizerModelId).toBe("gpt-4o");
expect(settings.titleSummarizerGlobalProvider).toBe("anthropic");
expect(settings.titleSummarizerGlobalModelId).toBe("claude-haiku");
expect(settings.titleSummarizerFallbackProvider).toBe("google");
expect(settings.titleSummarizerFallbackModelId).toBe("gemini-2.5-flash");
});
});
// ── Compatibility & Null-Clear Regression Tests (FN-1729) ────────────────
describe("canonical vs legacy compatibility regression", () => {
it("canonical executionProvider + legacy planningProvider coexist without conflict", async () => {
// Set canonical execution lane (new FN-1710 format)
await store.updateGlobalSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
});
// Set legacy planning format
await store.updateSettings({
planningProvider: "openai",
planningModelId: "gpt-4o",
});
const settings = await store.getSettings();
// Both should coexist
expect(settings.executionGlobalProvider).toBe("anthropic");
expect(settings.executionGlobalModelId).toBe("claude-sonnet-4-5");
expect(settings.planningProvider).toBe("openai");
expect(settings.planningModelId).toBe("gpt-4o");
});
it("mixed legacy canonical shapes resolve deterministically", async () => {
// Legacy format
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
validatorProvider: "openai",
validatorModelId: "gpt-4o",
titleSummarizerProvider: "google",
titleSummarizerModelId: "gemini-2.5-pro",
});
// Canonical format
await store.updateSettings({
executionProvider: "anthropic",
executionModelId: "claude-opus-4",
executionGlobalProvider: undefined,
executionGlobalModelId: undefined,
});
// Also set global canonical fields
await store.updateGlobalSettings({
planningGlobalProvider: "anthropic",
planningGlobalModelId: "claude-haiku",
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4o-mini",
});
const settings = await store.getSettings();
// Legacy shapes preserved
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBe("claude-sonnet-4-5");
expect(settings.validatorProvider).toBe("openai");
expect(settings.validatorModelId).toBe("gpt-4o");
expect(settings.titleSummarizerProvider).toBe("google");
expect(settings.titleSummarizerModelId).toBe("gemini-2.5-pro");
// Canonical shapes preserved
expect(settings.executionProvider).toBe("anthropic");
expect(settings.executionModelId).toBe("claude-opus-4");
// Global canonical shapes preserved
expect(settings.planningGlobalProvider).toBe("anthropic");
expect(settings.planningGlobalModelId).toBe("claude-haiku");
expect(settings.validatorGlobalProvider).toBe("openai");
expect(settings.validatorGlobalModelId).toBe("gpt-4o-mini");
});
it("legacy format: planningProvider without planningModelId is valid partial pair", async () => {
await store.updateSettings({
planningProvider: "anthropic",
// planningModelId intentionally omitted
});
const settings = await store.getSettings();
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBeUndefined();
});
it("legacy format: validatorProvider without validatorModelId is valid partial pair", async () => {
await store.updateSettings({
validatorProvider: "openai",
// validatorModelId intentionally omitted
});
const settings = await store.getSettings();
expect(settings.validatorProvider).toBe("openai");
expect(settings.validatorModelId).toBeUndefined();
});
it("canonical format: executionProvider without executionModelId is valid partial pair", async () => {
await store.updateSettings({
executionProvider: "google",
// executionModelId intentionally omitted
});
const settings = await store.getSettings();
expect(settings.executionProvider).toBe("google");
expect(settings.executionModelId).toBeUndefined();
});
it("mixed: full pair + partial pair coexist in same lane", async () => {
// Set full planning pair
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
});
// Set partial validator pair (only provider)
await store.updateSettings({
validatorProvider: "openai",
// validatorModelId intentionally omitted
});
// Set full execution pair
await store.updateSettings({
executionProvider: "google",
executionModelId: "gemini-2.5-pro",
});
const settings = await store.getSettings();
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBe("claude-sonnet-4-5");
expect(settings.validatorProvider).toBe("openai");
expect(settings.validatorModelId).toBeUndefined();
expect(settings.executionProvider).toBe("google");
expect(settings.executionModelId).toBe("gemini-2.5-pro");
});
});
describe("null-as-delete regression for model settings", () => {
it("clears planningProvider/planningModelId with null via updateSettings", async () => {
// First set the values
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
});
let settings = await store.getSettings();
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBe("claude-sonnet-4-5");
// Clear with null
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningModelId: null });
settings = await store.getSettings();
expect(settings.planningProvider).toBeUndefined();
expect(settings.planningModelId).toBeUndefined();
});
it("clears validatorProvider/validatorModelId with null", async () => {
await store.updateSettings({
validatorProvider: "openai",
validatorModelId: "gpt-4o",
});
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ validatorProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ validatorModelId: null });
const settings = await store.getSettings();
expect(settings.validatorProvider).toBeUndefined();
expect(settings.validatorModelId).toBeUndefined();
});
it("clears executionProvider/executionModelId with null", async () => {
await store.updateSettings({
executionProvider: "google",
executionModelId: "gemini-2.5-pro",
});
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ executionProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ executionModelId: null });
const settings = await store.getSettings();
expect(settings.executionProvider).toBeUndefined();
expect(settings.executionModelId).toBeUndefined();
});
it("clears titleSummarizerProvider/titleSummarizerModelId with null", async () => {
await store.updateSettings({
titleSummarizerProvider: "anthropic",
titleSummarizerModelId: "claude-haiku",
});
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ titleSummarizerProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ titleSummarizerModelId: null });
const settings = await store.getSettings();
expect(settings.titleSummarizerProvider).toBeUndefined();
expect(settings.titleSummarizerModelId).toBeUndefined();
});
it("clears fallback model fields with null", async () => {
await store.updateSettings({
planningFallbackProvider: "anthropic",
planningFallbackModelId: "claude-sonnet-4-5",
validatorFallbackProvider: "openai",
validatorFallbackModelId: "gpt-4o",
titleSummarizerFallbackProvider: "google",
titleSummarizerFallbackModelId: "gemini-2.5-pro",
});
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningFallbackProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningFallbackModelId: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ validatorFallbackProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ validatorFallbackModelId: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ titleSummarizerFallbackProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ titleSummarizerFallbackModelId: null });
const settings = await store.getSettings();
expect(settings.planningFallbackProvider).toBeUndefined();
expect(settings.planningFallbackModelId).toBeUndefined();
expect(settings.validatorFallbackProvider).toBeUndefined();
expect(settings.validatorFallbackModelId).toBeUndefined();
expect(settings.titleSummarizerFallbackProvider).toBeUndefined();
expect(settings.titleSummarizerFallbackModelId).toBeUndefined();
});
it("clears global default model fields with null", async () => {
await store.updateGlobalSettings({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o",
});
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ defaultProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ defaultModelId: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ fallbackProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ fallbackModelId: null });
const settings = await store.getSettings();
expect(settings.defaultProvider).toBeUndefined();
expect(settings.defaultModelId).toBeUndefined();
expect(settings.fallbackProvider).toBeUndefined();
expect(settings.fallbackModelId).toBeUndefined();
});
it("clears global lane model fields with null", async () => {
await store.updateGlobalSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
planningGlobalProvider: "openai",
planningGlobalModelId: "gpt-4o",
validatorGlobalProvider: "google",
validatorGlobalModelId: "gemini-2.5-pro",
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-haiku",
});
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ executionGlobalProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ executionGlobalModelId: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ planningGlobalProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ planningGlobalModelId: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ validatorGlobalProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ validatorGlobalModelId: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ titleSummarizerGlobalProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateGlobalSettings({ titleSummarizerGlobalModelId: null });
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("null clear of one field in pair preserves the other", async () => {
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
});
// Clear only provider, keep modelId
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningProvider: null });
const settings = await store.getSettings();
expect(settings.planningProvider).toBeUndefined();
expect(settings.planningModelId).toBe("claude-sonnet-4-5"); // Preserved
});
it("cleared model settings fall back to undefined (not default values)", async () => {
// Set global defaults first
await store.updateGlobalSettings({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
// Set planning override
await store.updateSettings({
planningProvider: "openai",
planningModelId: "gpt-4o",
});
// Clear planning override
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningModelId: null });
const settings = await store.getSettings();
// Planning should be undefined (no fallback to default in settings layer)
expect(settings.planningProvider).toBeUndefined();
expect(settings.planningModelId).toBeUndefined();
// Default should still be accessible
expect(settings.defaultProvider).toBe("anthropic");
expect(settings.defaultModelId).toBe("claude-sonnet-4-5");
});
it("cleared model settings removed from persisted config", async () => {
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
});
// Verify persisted
let configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
let config = JSON.parse(configRaw);
expect((config.settings as any).planningProvider).toBe("anthropic");
// Clear with null
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ planningModelId: null });
// Verify removed from persisted config
configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
config = JSON.parse(configRaw);
expect((config.settings as any).planningProvider).toBeUndefined();
expect((config.settings as any).planningModelId).toBeUndefined();
});
});
// ── Global/Project Settings Merging ───────────────────────────── // ── Global/Project Settings Merging ─────────────────────────────
describe("global/project settings merging", () => { describe("global/project settings merging", () => {