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:
152
packages/core/src/__tests__/settings-parity.test.ts
Normal file
152
packages/core/src/__tests__/settings-parity.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user