fix(FN-1278): enforce settings parity for keys and defaults

- Add missing GlobalSettings and ProjectSettings entries to key arrays for full runtime coverage
- Fill default settings gaps for newly listed global and project configuration fields
- Add compile-time parity assertions to fail typechecking when interface keys and arrays drift
- Add parity tests validating key coverage, default coverage, duplicate detection, and cross-scope overlap
This commit is contained in:
gsxdsm
2026-04-08 10:21:15 -07:00
parent 4c7db73ffe
commit e156975300
2 changed files with 190 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_GLOBAL_SETTINGS,
DEFAULT_PROJECT_SETTINGS,
GLOBAL_SETTINGS_KEYS,
PROJECT_SETTINGS_KEYS,
} from "../types.js";
import type { GlobalSettings, ProjectSettings } from "../types.js";
const GLOBAL_KEYS: (keyof GlobalSettings)[] = [
"themeMode",
"colorTheme",
"defaultProvider",
"defaultModelId",
"fallbackProvider",
"fallbackModelId",
"defaultThinkingLevel",
"ntfyEnabled",
"ntfyTopic",
"ntfyEvents",
"ntfyDashboardHost",
"defaultProjectId",
"setupComplete",
"favoriteProviders",
"favoriteModels",
"openrouterModelSync",
"modelOnboardingComplete",
];
const PROJECT_KEYS: (keyof ProjectSettings)[] = [
"globalPause",
"enginePaused",
"maxConcurrent",
"maxWorktrees",
"pollIntervalMs",
"groupOverlappingFiles",
"autoMerge",
"mergeStrategy",
"worktreeInitCommand",
"testCommand",
"buildCommand",
"recycleWorktrees",
"worktreeNaming",
"taskPrefix",
"includeTaskIdInCommit",
"planningProvider",
"planningModelId",
"planningFallbackProvider",
"planningFallbackModelId",
"validatorProvider",
"validatorModelId",
"validatorFallbackProvider",
"validatorFallbackModelId",
"modelPresets",
"autoSelectModelPreset",
"defaultPresetBySize",
"autoResolveConflicts",
"smartConflictResolution",
"strictScopeEnforcement",
"buildRetryCount",
"buildTimeoutMs",
"requirePlanApproval",
"taskStuckTimeoutMs",
"aiSessionTtlMs",
"aiSessionCleanupIntervalMs",
"autoUnpauseEnabled",
"autoUnpauseBaseDelayMs",
"autoUnpauseMaxDelayMs",
"maxStuckKills",
"maxSpawnedAgentsPerParent",
"maxSpawnedAgentsGlobal",
"maintenanceIntervalMs",
"autoUpdatePrStatus",
"autoCreatePr",
"autoBackupEnabled",
"autoBackupSchedule",
"autoBackupRetention",
"autoBackupDir",
"autoSummarizeTitles",
"titleSummarizerProvider",
"titleSummarizerModelId",
"titleSummarizerFallbackProvider",
"titleSummarizerFallbackModelId",
"scripts",
"setupScript",
"insightExtractionEnabled",
"insightExtractionSchedule",
"insightExtractionMinIntervalMs",
"memoryEnabled",
"tokenCap",
"runStepsInNewSessions",
"maxParallelSteps",
"missionStaleThresholdMs",
"missionMaxTaskRetries",
"missionHealthCheckIntervalMs",
"agentPrompts",
"reflectionEnabled",
"reflectionIntervalMs",
"reflectionAfterTask",
];
function assertExactKeyCoverage(scopeName: string, actual: readonly string[], expected: readonly string[]): void {
const uniqueActual = [...new Set(actual)];
const uniqueExpected = [...new Set(expected)];
const missing = uniqueExpected.filter((key) => !uniqueActual.includes(key));
const extra = uniqueActual.filter((key) => !uniqueExpected.includes(key));
const duplicates = actual.filter((key, index) => actual.indexOf(key) !== index);
if (missing.length > 0 || extra.length > 0 || duplicates.length > 0) {
throw new Error(
[
`${scopeName} parity mismatch`,
`Missing: ${missing.length ? missing.join(", ") : "(none)"}`,
`Extra: ${extra.length ? extra.join(", ") : "(none)"}`,
`Duplicates: ${duplicates.length ? [...new Set(duplicates)].join(", ") : "(none)"}`,
].join("\n"),
);
}
}
describe("settings key parity", () => {
it("GLOBAL_SETTINGS_KEYS covers all GlobalSettings keys", () => {
assertExactKeyCoverage("GLOBAL_SETTINGS_KEYS", GLOBAL_SETTINGS_KEYS as readonly string[], GLOBAL_KEYS as string[]);
});
it("PROJECT_SETTINGS_KEYS covers all ProjectSettings keys", () => {
assertExactKeyCoverage("PROJECT_SETTINGS_KEYS", PROJECT_SETTINGS_KEYS as readonly string[], PROJECT_KEYS as string[]);
});
it("DEFAULT_GLOBAL_SETTINGS covers all GlobalSettings keys", () => {
assertExactKeyCoverage(
"DEFAULT_GLOBAL_SETTINGS",
Object.keys(DEFAULT_GLOBAL_SETTINGS),
GLOBAL_KEYS as string[],
);
});
it("DEFAULT_PROJECT_SETTINGS covers all ProjectSettings keys", () => {
assertExactKeyCoverage(
"DEFAULT_PROJECT_SETTINGS",
Object.keys(DEFAULT_PROJECT_SETTINGS),
PROJECT_KEYS as string[],
);
});
it("No key appears in both GLOBAL_SETTINGS_KEYS and PROJECT_SETTINGS_KEYS", () => {
const projectKeySet = new Set(PROJECT_SETTINGS_KEYS as readonly string[]);
const overlap = (GLOBAL_SETTINGS_KEYS as readonly string[]).filter((key) => projectKeySet.has(key));
expect(overlap).toEqual([]);
});
});

View File

@@ -1019,6 +1019,10 @@ export const DEFAULT_GLOBAL_SETTINGS: Required<Pick<GlobalSettings, "themeMode"
ntfyTopic: undefined,
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
ntfyDashboardHost: undefined,
defaultProjectId: undefined,
setupComplete: undefined,
favoriteProviders: undefined,
favoriteModels: undefined,
openrouterModelSync: true,
modelOnboardingComplete: undefined,
};
@@ -1034,6 +1038,8 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
autoMerge: true,
mergeStrategy: "direct",
worktreeInitCommand: undefined,
testCommand: undefined,
buildCommand: undefined,
recycleWorktrees: false,
worktreeNaming: "random",
taskPrefix: "FN",
@@ -1076,6 +1082,8 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
titleSummarizerModelId: undefined,
titleSummarizerFallbackProvider: undefined,
titleSummarizerFallbackModelId: undefined,
scripts: undefined,
setupScript: undefined,
insightExtractionEnabled: false,
insightExtractionSchedule: "0 2 * * *",
insightExtractionMinIntervalMs: 86_400_000,
@@ -1116,6 +1124,9 @@ export const GLOBAL_SETTINGS_KEYS: ReadonlyArray<keyof GlobalSettings> = [
"ntfyEvents",
"ntfyDashboardHost",
"defaultProjectId",
"setupComplete",
"favoriteProviders",
"favoriteModels",
"openrouterModelSync",
"modelOnboardingComplete",
] as const;
@@ -1150,8 +1161,14 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
"defaultPresetBySize",
"autoResolveConflicts",
"smartConflictResolution",
"strictScopeEnforcement",
"buildRetryCount",
"buildTimeoutMs",
"requirePlanApproval",
"taskStuckTimeoutMs",
"autoUnpauseEnabled",
"autoUnpauseBaseDelayMs",
"autoUnpauseMaxDelayMs",
"aiSessionTtlMs",
"aiSessionCleanupIntervalMs",
"maxStuckKills",
@@ -1166,6 +1183,8 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
"titleSummarizerModelId",
"titleSummarizerFallbackProvider",
"titleSummarizerFallbackModelId",
"scripts",
"setupScript",
"tokenCap",
"insightExtractionEnabled",
"insightExtractionSchedule",
@@ -1173,6 +1192,7 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
"memoryEnabled",
"maxSpawnedAgentsPerParent",
"maxSpawnedAgentsGlobal",
"maintenanceIntervalMs",
"runStepsInNewSessions",
"maxParallelSteps",
"missionStaleThresholdMs",
@@ -1184,6 +1204,24 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
"reflectionAfterTask",
] as const;
// ── Compile-time parity: ensures every interface key is listed exactly once ──
// If either assertion fails with "Type 'X' is not assignable to type 'Y'",
// a key was added to the interface without updating the corresponding array
// (or vice versa). Add or remove the key to fix.
type _GlobalKeysCheck = typeof GLOBAL_SETTINGS_KEYS[number] extends keyof GlobalSettings
? keyof GlobalSettings extends typeof GLOBAL_SETTINGS_KEYS[number]
? true
: never
: never;
const _globalParity: _GlobalKeysCheck = true as _GlobalKeysCheck;
type _ProjectKeysCheck = typeof PROJECT_SETTINGS_KEYS[number] extends keyof ProjectSettings
? keyof ProjectSettings extends typeof PROJECT_SETTINGS_KEYS[number]
? true
: never
: never;
const _projectParity: _ProjectKeysCheck = true as _ProjectKeysCheck;
export interface BoardConfig {
nextId: number;
settings?: Settings;