diff --git a/src/selfhost/config-lint.ts b/src/selfhost/config-lint.ts index 2ff0af08ef..c063745f8a 100644 --- a/src/selfhost/config-lint.ts +++ b/src/selfhost/config-lint.ts @@ -73,8 +73,13 @@ function unknownTopLevelWarnings(text: string | null | undefined): string[] { const parsed = parseTopLevelObject(trimmed); if (parsed === null) return []; const keys = Object.keys(parsed).filter((key) => !TOP_LEVEL_FIELD_SET.has(key)); - const retiredWarnings = keys.filter((key) => key in RETIRED_FIELD_MIGRATION_WARNINGS).map((key) => RETIRED_FIELD_MIGRATION_WARNINGS[key]!); - const unknown = keys.filter((key) => !(key in RETIRED_FIELD_MIGRATION_WARNINGS)).map(formatFieldName); + // `hasOwnProperty.call`, NOT `key in`: a manifest field named like an Object.prototype member + // (`constructor`, `toString`, `hasOwnProperty`, ...) would otherwise test true for the inherited + // property and resolve to the prototype's function instead of a real retired-field warning string, + // corrupting the string[] result and suppressing the genuine unknown-field warning. + const isRetired = (key: string): boolean => Object.prototype.hasOwnProperty.call(RETIRED_FIELD_MIGRATION_WARNINGS, key); + const retiredWarnings = keys.filter(isRetired).map((key) => RETIRED_FIELD_MIGRATION_WARNINGS[key]!); + const unknown = keys.filter((key) => !isRetired(key)).map(formatFieldName); return [ ...retiredWarnings, ...(unknown.length > 0 ? [`Manifest contains unknown top-level field${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`] : []), diff --git a/test/unit/selfhost-config-lint.test.ts b/test/unit/selfhost-config-lint.test.ts index 87c1d4d4cb..9ddb8297b8 100644 --- a/test/unit/selfhost-config-lint.test.ts +++ b/test/unit/selfhost-config-lint.test.ts @@ -159,6 +159,16 @@ unknownSecretKey: super-secret-value expect(JSON.stringify(result)).not.toContain("/tmp/private"); }); + it("treats a field named like an Object.prototype member as unknown, not retired", () => { + const result = lintManifestText("wantedPaths: [src/]\nconstructor: whatever\n"); + + expect(result.ok).toBe(false); + expect(result.recognizedFields).toEqual(["wantedPaths"]); + expect(result.warnings).toEqual(["Manifest contains unknown top-level field: constructor."]); + // Every warning must be a real string — a prototype-name collision must never leak a function. + expect(result.warnings.every((warning) => typeof warning === "string")).toBe(true); + }); + it("uses singular wording for one unknown top-level field", () => { const result = lintManifestText("wantedPaths: [src/]\nunknownSecretKey: super-secret-value\n");