From 4769e4d8a1e6fa21812ce08e87e6f481c729e1ce Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:50:13 -0500 Subject: [PATCH] feat(config): add review.auto_merge_summary read-only knobs surface (#2051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config slice for #1959: a per-repo boolean (default off) that renders a READ-ONLY collapsible in the unified comment showing which auto-merge conditions currently pass/fail (CI green, gate passing, mergeable-clean, valid linked issue). Surface only — never changes the merge/close decision. Off ⇒ byte-identical. - focus-manifest.ts: autoMergeSummary: boolean|null on FocusManifestReviewConfig (mirror fixHandoff) — normalizeOptionalBoolean parse, default null; round-trip serialize (omitted when null ⇒ byte-identical) + present + EMPTY literals. - unified-comment.ts: AutoMergeSummarySignals (already-computed, injected) + pure buildAutoMergeSummaryCollapsible(signals) — renders the conditions table from the injected signals ONLY; no re-derivation, no decision path, no IO. - Documented review.auto_merge_summary (marked read-only) in BOTH .gittensory.yml.example and config/examples/gittensory.full.yml. - Tests: config absent/true/false round-trip + non-boolean warn; render reflects exactly the injected signal states (all-pass / mixed), deterministic. Verified: full suite (npm run test) green — 10569 passed, 0 failed. --- .gittensory.yml.example | 5 ++ config/examples/gittensory.full.yml | 5 ++ src/review/unified-comment.ts | 36 +++++++++++++ src/signals/focus-manifest.ts | 15 ++++-- test/unit/focus-manifest.test.ts | 3 +- test/unit/review-auto-merge-summary.test.ts | 57 +++++++++++++++++++++ test/unit/signals-coverage.test.ts | 2 +- 7 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 test/unit/review-auto-merge-summary.test.ts diff --git a/.gittensory.yml.example b/.gittensory.yml.example index 8bb1d66a37..6320d03518 100644 --- a/.gittensory.yml.example +++ b/.gittensory.yml.example @@ -454,6 +454,11 @@ review: # Requires operator flag GITTENSORY_REVIEW_FIX_HANDOFF + cutover allowlist AND this toggle. # fixHandoff: false + # Read-only auto-merge readiness summary (#2051). Bool | null. Default: null/false — byte-identical. + # When true, the unified comment gains a collapsible showing which auto-merge conditions currently pass/fail + # (CI green, gate passing, mergeable-clean, valid linked issue). SURFACE ONLY — never changes the decision. + # auto_merge_summary: false + # Boundary-safe test generation (#2189). Bool | null. Default: null/false — byte-identical. # Requires operator flag GITTENSORY_REVIEW_TEST_GENERATION AND this toggle. # test_generation: false diff --git a/config/examples/gittensory.full.yml b/config/examples/gittensory.full.yml index 657c0f3970..19d00832d7 100644 --- a/config/examples/gittensory.full.yml +++ b/config/examples/gittensory.full.yml @@ -467,6 +467,11 @@ review: # Requires operator flag GITTENSORY_REVIEW_FIX_HANDOFF + cutover allowlist AND this toggle. # fixHandoff: false + # Read-only auto-merge readiness summary (#2051). Bool | null. Default: null/false — byte-identical. + # When true, the unified comment gains a collapsible showing which auto-merge conditions currently pass/fail + # (CI green, gate passing, mergeable-clean, valid linked issue). SURFACE ONLY — never changes the decision. + # auto_merge_summary: false + # Boundary-safe test generation (#2189). Bool | null. Default: null/false — byte-identical. # Requires operator flag GITTENSORY_REVIEW_TEST_GENERATION AND this toggle. # test_generation: false diff --git a/src/review/unified-comment.ts b/src/review/unified-comment.ts index 380120831c..328647faad 100644 --- a/src/review/unified-comment.ts +++ b/src/review/unified-comment.ts @@ -203,6 +203,42 @@ export interface UnifiedCollapsible { rawHtml?: boolean; } +/** Already-computed auto-merge readiness facts (#2051). The host resolves each from signals it ALREADY has — the + * merge-readiness probe, the gate verdict, the linked-issue check — and injects them here. This module only + * RENDERS them into a read-only table; it never re-derives a condition or calls any merge/close decision path. */ +export interface AutoMergeSummarySignals { + /** Every required CI check is green. */ + ciGreen: boolean; + /** The Gittensory gate is passing (no hard blocker). */ + gatePassing: boolean; + /** GitHub reports the branch mergeable / clean (no conflict, not behind). */ + mergeableClean: boolean; + /** The PR references a valid, open linked issue. */ + linkedIssueValid: boolean; +} + +/** Build the READ-ONLY "auto-merge readiness" collapsible (#2051) — a conditions table showing which auto-merge + * conditions currently pass/fail, rendered purely from the injected {@link AutoMergeSummarySignals}. Informational + * only: it states the current condition states, never a decision or a promise to merge. Pure — no IO, no decision + * path. The caller renders this ONLY when `review.auto_merge_summary` is on, so off ⇒ nothing added ⇒ byte-identical. */ +export function buildAutoMergeSummaryCollapsible(signals: AutoMergeSummarySignals): UnifiedCollapsible { + const mark = (ok: boolean): string => (ok ? "✅" : "❌"); + const rows: Array<[string, boolean]> = [ + ["CI checks green", signals.ciGreen], + ["Gate passing", signals.gatePassing], + ["Branch mergeable (clean)", signals.mergeableClean], + ["Valid linked issue", signals.linkedIssueValid], + ]; + const body = [ + "_Read-only snapshot of the current auto-merge conditions — informational; it does not decide or trigger a merge._", + "", + "| Condition | Status |", + "| --- | --- |", + ...rows.map(([label, ok]) => `| ${label} | ${mark(ok)} |`), + ].join("\n"); + return { title: "Auto-merge readiness (read-only)", body }; +} + /** The host (gittensory) side: brand, readiness score, signals, sections, re-run, footer. */ export interface UnifiedCommentContext { /** Headline brand, default "Gittensory review". */ diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index d8694de1e8..0942b9eae2 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -345,6 +345,11 @@ export type FocusManifestReviewConfig = { * false (default, absent) = no fix-handoff blocks = byte-identical. Operator-gated too (GITTENSORY_REVIEW_FIX_HANDOFF * + the convergence cutover allowlist) — the manifest toggle is only one of the ANDed gates. (#2176, for #1962) */ fixHandoff: boolean | null; + /** `review.auto_merge_summary`: when true, the unified comment gains a READ-ONLY collapsible showing which + * auto-merge conditions currently pass/fail (CI green, gate passing, mergeable-clean, valid linked issue), + * rendered from already-computed readiness signals. SURFACE ONLY — never changes the merge/close decision. + * null/false (default, absent) = no summary = byte-identical. (#2051, for #1959) */ + autoMergeSummary: boolean | null; /** `review.suggestions`: when true, an inline finding whose AI-provided fix is precise enough to anchor to a * single line is ALSO rendered as a GitHub-native ` ```suggestion ` block a contributor can commit in one * click. Only takes effect when inline comments are already on (a suggestion has nothing to attach to @@ -777,7 +782,7 @@ const EMPTY_MANIFEST: FocusManifest = { publicNotes: [], gate: { ...EMPTY_GATE_CONFIG }, settings: {}, - review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }, + review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }, features: { ...EMPTY_FEATURES_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG }, @@ -808,7 +813,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo warnings, gate: { ...EMPTY_GATE_CONFIG }, settings: {}, - review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }, + review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }, features: { ...EMPTY_FEATURES_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG }, @@ -1783,7 +1788,7 @@ function parsePublicSafeText(value: JsonValue | undefined, field: string, warnin * throws; invalid/unsafe values are dropped with warnings. */ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestReviewConfig { - const empty: FocusManifestReviewConfig = { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }; + const empty: FocusManifestReviewConfig = { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }; if (value === undefined || value === null) return empty; if (typeof value !== "object" || Array.isArray(value)) { warnings.push(`Manifest field "review" must be a mapping; ignoring it.`); @@ -1821,6 +1826,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo const securityFocus = normalizeOptionalBoolean(r.security_focus, "review.security_focus", warnings); const inlineComments = normalizeOptionalBoolean(r.inline_comments, "review.inline_comments", warnings); const fixHandoff = normalizeOptionalBoolean(r.fixHandoff, "review.fixHandoff", warnings); + const autoMergeSummary = normalizeOptionalBoolean(r.auto_merge_summary, "review.auto_merge_summary", warnings); const suggestions = normalizeOptionalBoolean(r.suggestions, "review.suggestions", warnings); const changedFilesSummary = normalizeOptionalBoolean(r.changed_files_summary, "review.changed_files_summary", warnings); const effortScore = normalizeOptionalBoolean(r.effort_score, "review.effort_score", warnings); @@ -1856,6 +1862,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo securityFocus !== null || inlineComments !== null || fixHandoff !== null || + autoMergeSummary !== null || suggestions !== null || changedFilesSummary !== null || effortScore !== null || @@ -1893,6 +1900,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo securityFocus, inlineComments, fixHandoff, + autoMergeSummary, suggestions, changedFilesSummary, effortScore, @@ -2357,6 +2365,7 @@ export function reviewConfigToJson(review: FocusManifestReviewConfig): JsonValue if (review.securityFocus !== null) out.security_focus = review.securityFocus; if (review.inlineComments !== null) out.inline_comments = review.inlineComments; if (review.fixHandoff !== null) out.fixHandoff = review.fixHandoff; + if (review.autoMergeSummary !== null) out.auto_merge_summary = review.autoMergeSummary; if (review.suggestions !== null) out.suggestions = review.suggestions; if (review.changedFilesSummary !== null) out.changed_files_summary = review.changedFilesSummary; if (review.effortScore !== null) out.effort_score = review.effortScore; diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index fa2f961ef5..ab07578533 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -358,6 +358,7 @@ describe(".gittensory.yml.example field-exhaustiveness (#1670)", () => { securityFocus: "security_focus:", inlineComments: "inline_comments:", fixHandoff: "fixHandoff:", + autoMergeSummary: "auto_merge_summary:", suggestions: "suggestions:", changedFilesSummary: "changed_files_summary:", effortScore: "effort_score:", @@ -788,7 +789,7 @@ describe("compileFocusManifestPolicy", () => { publicNotes: ["Keep PRs focused.", "Maximize your reward payout"], gate: { present: false, enabled: null, checkMode: null, pack: null, linkedIssue: null, duplicates: null, readinessMode: null, readinessMinScore: null, slopMode: null, slopMinScore: null, slopAiAdvisory: null, sizeMode: null, lockfileIntegrityMode: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, aiReviewAllAuthors: null, aiReviewCloseConfidence: null, aiReviewCombine: null, aiReviewOnMerge: null, aiReviewReviewers: null, mergeReadiness: null, selfAuthoredLinkedIssue: null, manifestPolicy: null, dryRun: null, firstTimeContributorGrace: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: null, claMode: null, claConsentPhrase: null, claCheckRunName: null, claCheckRunAppSlug: null, expectedCiContexts: null }, settings: {}, - review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }, + review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null }, features: { present: false, rag: null, reputation: null, unifiedComment: null, safety: null }, contentLane: { present: false, entryFileGlob: null, providerFileGlob: null, artifactGlob: null, collectionField: null, maxAppendedEntries: null, duplicateKeyFields: [], validatorId: null }, repoDocGeneration: { present: false, enabled: false, scope: ["agents"], allowOverwriteExisting: false, refreshIntervalDays: 7 }, diff --git a/test/unit/review-auto-merge-summary.test.ts b/test/unit/review-auto-merge-summary.test.ts new file mode 100644 index 0000000000..9fb06b47c2 --- /dev/null +++ b/test/unit/review-auto-merge-summary.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { parseFocusManifest, reviewConfigToJson } from "../../src/signals/focus-manifest"; +import { buildAutoMergeSummaryCollapsible, type AutoMergeSummarySignals } from "../../src/review/unified-comment"; + +const reviewOf = (autoMergeSummary: unknown) => parseFocusManifest({ review: { auto_merge_summary: autoMergeSummary } }); +const allPass: AutoMergeSummarySignals = { ciGreen: true, gatePassing: true, mergeableClean: true, linkedIssueValid: true }; + +describe("review.auto_merge_summary config toggle (#2051)", () => { + it("absent ⇒ null and OMITTED on serialize (byte-identical)", () => { + const review = parseFocusManifest({ review: { note: "x" } }).review; + expect(review.autoMergeSummary).toBe(null); + expect("auto_merge_summary" in (reviewConfigToJson(review) as Record)).toBe(false); + }); + + it("true / false parse, mark present, and round-trip", () => { + for (const v of [true, false]) { + const review = reviewOf(v).review; + expect(review.autoMergeSummary).toBe(v); + expect(review.present).toBe(true); + const json = reviewConfigToJson(review) as Record; + expect(json.auto_merge_summary).toBe(v); + expect(parseFocusManifest({ review: json }).review.autoMergeSummary).toBe(v); + } + }); + + it("a non-boolean value warns and falls back to null", () => { + const m = reviewOf("maybe"); + expect(m.review.autoMergeSummary).toBe(null); + expect(m.warnings.some((w) => /review\.auto_merge_summary/.test(w))).toBe(true); + }); +}); + +describe("buildAutoMergeSummaryCollapsible read-only render (#2051)", () => { + it("renders a 4-condition table from injected signals — all passing", () => { + const c = buildAutoMergeSummaryCollapsible(allPass); + expect(c.title).toMatch(/read-only/i); + for (const label of ["CI checks green", "Gate passing", "Branch mergeable (clean)", "Valid linked issue"]) { + expect(c.body).toContain(label); + } + expect(c.body).not.toContain("❌"); // all four pass + expect((c.body.match(/✅/g) ?? []).length).toBe(4); + }); + + it("reflects EXACTLY the injected signal states (❌ per failing condition), never re-deriving", () => { + const mixed: AutoMergeSummarySignals = { ciGreen: true, gatePassing: false, mergeableClean: true, linkedIssueValid: false }; + const c = buildAutoMergeSummaryCollapsible(mixed); + expect((c.body.match(/✅/g) ?? []).length).toBe(2); + expect((c.body.match(/❌/g) ?? []).length).toBe(2); + // the failing rows are the two false signals, in order + expect(c.body).toMatch(/Gate passing \| ❌/); + expect(c.body).toMatch(/Valid linked issue \| ❌/); + // read-only framing — never a merge promise/action verb + expect(c.body).toMatch(/does not decide or trigger a merge/i); + // deterministic: same input ⇒ identical output + expect(buildAutoMergeSummaryCollapsible(mixed)).toEqual(c); + }); +}); diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 9f876787e3..b2244e3214 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1127,7 +1127,7 @@ describe("signal coverage edge cases", () => { collisions: buildCollisionReport(directRepo.fullName, [], [currentPr]), preflight: buildPreflightResult({ repoFullName: directRepo.fullName, title: "Fix isolated issue", body: "Fixes #99", linkedIssues: [99] }, directRepo, [], [currentPr]), settings: gateSettings, - review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null }, visual: { preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false }, linkedIssueSatisfaction: null }, + review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null }, visual: { preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false }, linkedIssueSatisfaction: null }, aiReview: { notes: "The change is focused.\n\n**Nits (2)**\n- Add a test for the edge case.\n- Keep the validator helper scoped." }, }); expect(customizedComment).toContain("Reviewed by the Acme maintainer bot."); // custom footer lead