From d81005b8f0cf0f6a312be43a9bfd3db1e6766a65 Mon Sep 17 00:00:00 2001 From: jaytbarimbao-collab <300663773+jaytbarimbao-collab@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:12:56 -0400 Subject: [PATCH] fix(config): warn on unknown top-level fields in the focus-manifest validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #5929. buildFocusManifestValidation (behind loopover_validate_config / POST /v1/validate/focus-manifest) read only known fields, so a typo'd top-level block (e.g. `gates:` for `gate:`) was silently dropped with status "ok" and zero warnings — unlike the selfhost config-lint validator, which flags it. Reuses config-lint's unknownTopLevelWarnings() (now exported) so both validators share one allowlist + retired-field handling, and adds a test. --- src/selfhost/config-lint.ts | 2 +- src/services/focus-manifest-validation.ts | 6 +++++- test/unit/focus-manifest-validation.test.ts | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) 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); + }); });