diff --git a/src/selfhost/config-lint.ts b/src/selfhost/config-lint.ts index 18730a8d4a..b1e78db458 100644 --- a/src/selfhost/config-lint.ts +++ b/src/selfhost/config-lint.ts @@ -69,7 +69,7 @@ const RETIRED_FIELD_MIGRATION_WARNINGS: Record = { blockedPaths: "blockedPaths is retired; use settings.hardGuardrailGlobs for path holds.", }; -function unknownTopLevelWarnings(text: string | null | undefined): string[] { +export function unknownTopLevelWarnings(text: string | null | undefined): string[] { const raw = text ?? ""; const trimmed = raw.trim(); if (!trimmed || isOversize(raw)) return []; diff --git a/src/services/focus-manifest-validation.ts b/src/services/focus-manifest-validation.ts index 86ca8be1dc..c1055585d0 100644 --- a/src/services/focus-manifest-validation.ts +++ b/src/services/focus-manifest-validation.ts @@ -11,6 +11,7 @@ import { type FocusManifest, type FocusManifestSource, } from "../signals/focus-manifest"; +import { unknownTopLevelWarnings } from "../selfhost/config-lint"; export type FocusManifestValidationStatus = "ok" | "warn" | "error"; @@ -28,7 +29,10 @@ export function buildFocusManifestValidation(input: { source?: FocusManifestSource | undefined; }): FocusManifestValidationResult { const manifest = parseFocusManifestContent(input.content, input.source ?? "repo_file"); - const warnings = [...manifest.warnings]; + // Warn on unrecognized top-level fields (e.g. a typo'd `gates:` instead of `gate:`), matching the + // selfhost config-lint validator — parseFocusManifestContent reads only known fields, so a mistyped + // block is otherwise silently dropped with no warning (#5929). + const warnings = [...manifest.warnings, ...unknownTopLevelWarnings(input.content)]; const normalized = focusManifestToNormalizedJson(manifest); return { present: manifest.present, diff --git a/test/unit/focus-manifest-validation.test.ts b/test/unit/focus-manifest-validation.test.ts index 08495829a5..fce6aa160d 100644 --- a/test/unit/focus-manifest-validation.test.ts +++ b/test/unit/focus-manifest-validation.test.ts @@ -129,4 +129,15 @@ maintainerRecap: expect(result.status).toBe("error"); expect(result.warnings.join(" ")).toMatch(/must be a mapping/i); }); + + it("warns on an unrecognized top-level field (e.g. a typo'd `gates:` for `gate:`), matching config-lint (#5929)", () => { + // A recognized field plus a typo'd block: previously the typo was silently dropped with status "ok". + const result = buildFocusManifestValidation({ content: "wantedPaths:\n - src/\ngates:\n enabled: true\n" }); + expect(result.status).toBe("warn"); + expect(result.warnings.join(" ")).toMatch(/unknown top-level field/i); + expect(result.warnings.join(" ")).toMatch(/gates/); + // A clean manifest still carries no unknown-field warning. + const clean = buildFocusManifestValidation({ content: "wantedPaths:\n - src/\n" }); + expect(clean.warnings.join(" ")).not.toMatch(/unknown top-level field/i); + }); });