From dbf9bc603198fd40bf330a118132862644e465ff Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:39:42 -0700 Subject: [PATCH] fix(config): preserve review clearing overlays --- src/selfhost/private-config.ts | 10 +++++++--- test/unit/private-config.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/selfhost/private-config.ts b/src/selfhost/private-config.ts index dfb6779f1d..5abfb08ba8 100644 --- a/src/selfhost/private-config.ts +++ b/src/selfhost/private-config.ts @@ -126,6 +126,10 @@ function stripReviewKey(mapping: Record): Record): boolean { + return Object.prototype.hasOwnProperty.call(mapping, "review"); +} + function extractReviewMapping(mapping: Record): Record | null { const { review } = mapping; if (review === undefined || review === null) return null; @@ -207,10 +211,10 @@ function combineConfigLayersWithMeta( let mergedReview: unknown; for (const layer of parsedLayers) { - const review = extractReviewMapping(layer.mapping); - if (review === null) continue; + if (!hasReviewKey(layer.mapping)) continue; + const { review } = layer.mapping; mergedReview = mergedReview === undefined ? review : mergeConfigOverlay(mergedReview, review); - if (layer.kind === "shared" && layer.sourcePath) sharedConfigSource = layer.sourcePath; + if (layer.kind === "shared" && extractReviewMapping(layer.mapping) && layer.sourcePath) sharedConfigSource = layer.sourcePath; } if (mergedReview !== undefined) mergedBody.review = mergedReview; diff --git a/test/unit/private-config.test.ts b/test/unit/private-config.test.ts index c5316897e9..464bc0fe49 100644 --- a/test/unit/private-config.test.ts +++ b/test/unit/private-config.test.ts @@ -429,6 +429,33 @@ describe("makeLocalManifestReader — review.shared_config overlay (#2046)", () expect(manifest.review.securityFocus).toBe(true); }); + it("lets a higher-priority review null clear an inherited shared review block", async () => { + const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); + mkdirSync(join(dir, "_shared")); + writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n footer:\n text: shared footer\n tone: house-tone\n"); + mkdirSync(join(dir, "repo")); + writeFileSync(join(dir, "repo", ".gittensory.yml"), "review: null\n"); + const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); + const manifest = parseFocusManifestContent(loaded!.content!); + expect(manifest.review.present).toBe(false); + expect(manifest.review.footerText).toBeNull(); + expect(manifest.review.tone).toBeNull(); + }); + + it("lets a higher-priority non-mapping review value replace an inherited shared review block", async () => { + const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); + mkdirSync(join(dir, "_shared")); + writeFileSync(join(dir, "_shared", ".gittensory.yml"), "review:\n footer:\n text: shared footer\n tone: house-tone\n"); + mkdirSync(join(dir, "repo")); + writeFileSync(join(dir, "repo", ".gittensory.yml"), "review: false\n"); + const loaded = await readLocalManifestLoad(makeLocalManifestReader(dir)!, "owner/repo"); + const manifest = parseFocusManifestContent(loaded!.content!); + expect(manifest.review.present).toBe(false); + expect(manifest.review.footerText).toBeNull(); + expect(manifest.review.tone).toBeNull(); + expect(manifest.warnings).toContain('Manifest field "review" must be a mapping; ignoring it.'); + }); + it("sets sharedConfigSource when only the shared base is present for a repo", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-repo-config-")); mkdirSync(join(dir, "_shared"));