From f527b49b9297878c635ca033c0d00aac42e0261b Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Mon, 6 Jul 2026 06:40:53 -0700
Subject: [PATCH] feat(review): add fix-handoff block renderer +
follow-up-issue MCP spec
Adds the render slice of #1962 (#2175): a boundary-safe, content-only
renderer that turns a review finding into a structured "apply this fix"
block for a contributor's own local agent to consume -- path/line/
severity/instruction plus an optional suggested change, a machine-
readable HTML-comment marker, and the shared no-cloud-write boundary
text. A finding with no commentable line still yields a valid path-only
block. No server-side write, no execution; pairs with the already-
shipped review.fixHandoff config/gate slice in src/review/fix-handoff.ts.
Also adds a follow-up-issue action spec builder (#2177) for deferred
review findings -- composes a bounded, public-safe title/body from a
single finding and delegates to the existing file_issue local-write
spec, so a maintainer can hand a contributor's agent "file this as a
tracked issue" without a new write path. Optional point-bearing label
support. Exposes a new gittensory_file_follow_up_issue MCP tool.
Documents the GITTENSORY_REVIEW_FIX_HANDOFF flag in src/env.d.ts and the
docs pages (a gap left by the config-slice PR).
Both slices are advisory/content-only and never touch gate disposition.
---
.../src/routes/docs.privacy-security.tsx | 1 +
apps/gittensory-ui/src/routes/docs.tuning.tsx | 5 +
src/env.d.ts | 6 +
src/mcp/local-write-tools.ts | 122 ++++++------------
src/mcp/server.ts | 19 +++
src/review/fix-handoff-render.ts | 70 ++++++++++
test/unit/fix-handoff-render.test.ts | 85 ++++++++++++
test/unit/local-write-tools.test.ts | 100 ++++++--------
test/unit/mcp-write-tools.test.ts | 21 +++
9 files changed, 286 insertions(+), 143 deletions(-)
create mode 100644 src/review/fix-handoff-render.ts
create mode 100644 test/unit/fix-handoff-render.test.ts
diff --git a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
index f58257f9fc..d926f5aed7 100644
--- a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
+++ b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
@@ -92,6 +92,7 @@ GITTENSORY_REVIEW_UNIFIED_COMMENT="true" # one in-place unified PR comme
GITTENSORY_REVIEW_ENRICHMENT="true" # external analyzer registry (REES) findings
GITTENSORY_REVIEW_INLINE_COMMENTS="true" # diff-anchored inline PR review comments
GITTENSORY_REVIEW_TEST_GENERATION="true" # boundary-safe test-gen action spec (contributor-run)
+GITTENSORY_REVIEW_FIX_HANDOFF="true" # machine-readable fix-handoff block (contributor-run)
GITTENSORY_REVIEW_PLANNER="true" # @gittensory plan on-demand implementation plan
GITTENSORY_REVIEW_SCREENSHOTS="true" # before/after visual capture for UI changes
diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx
index 7ee6984a8c..1412bc6415 100644
--- a/apps/gittensory-ui/src/routes/docs.tuning.tsx
+++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx
@@ -179,6 +179,11 @@ function Tuning() {
for the repo's detected framework; the contributor's own agent scaffolds and runs the
tests locally. Per-PR.
+
+ GITTENSORY_REVIEW_FIX_HANDOFF — renders a review finding as a structured,
+ machine-readable "apply this fix" block for the contributor's own local agent to consume —
+ content only, no server-side write, no execution. Per-PR.
+
GITTENSORY_REVIEW_PLANNER — enables @gittensory plan, an
on-demand structured implementation plan posted to the PR thread. Per-PR.
diff --git a/src/env.d.ts b/src/env.d.ts
index e913740daa..1d3151e66e 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -198,6 +198,12 @@ declare global {
* on the contributor's own machine (no source upload, no server-side write). Default OFF — unset/false
* keeps the review path byte-identical (no spec is ever built). */
GITTENSORY_REVIEW_TEST_GENERATION?: string;
+ /** Fix-handoff blocks (#2176, config slice of #1962): when truthy (AND the repo is in GITTENSORY_REVIEW_REPOS
+ * AND the repo's `.gittensory.yml` sets `review.fixHandoff: true`), a review finding is ALSO rendered as a
+ * structured, machine-readable "apply this fix" block (src/review/fix-handoff-render.ts) for the
+ * contributor's OWN local agent to consume — content only, no server-side write, no execution. Default
+ * OFF — unset/false keeps the review path byte-identical (no block is ever built). */
+ GITTENSORY_REVIEW_FIX_HANDOFF?: string;
/** Convergence (safety): when truthy, the ported safety scan runs in the review path — (1) untrusted PR
* title/body/diff is defanged (prompt-injection neutralized) before it reaches the AI reviewer, and (2)
* the PR diff is scanned for leaked secrets, surfacing a `secret_leak` blocker. Default OFF —
diff --git a/src/mcp/local-write-tools.ts b/src/mcp/local-write-tools.ts
index af584a0ff7..874ea50da8 100644
--- a/src/mcp/local-write-tools.ts
+++ b/src/mcp/local-write-tools.ts
@@ -43,91 +43,6 @@ export function buildFileIssueSpec(input: { repoFullName: string; title: string;
return spec("file_issue", "File a new issue.", { repoFullName: input.repoFullName, title: input.title, body: input.body, labels }, command);
}
-export type DeferredReviewFinding = {
- title: string;
- detail: string;
- path?: string | undefined;
- action?: string | undefined;
-};
-
-const FOLLOW_UP_ISSUE_TITLE_MAX = 120;
-const FOLLOW_UP_ISSUE_BODY_MAX = 4000;
-
-function stripFollowUpMarkers(value: string): string {
- return value.replace(//g, "").replace(/\r\n/g, "\n").trim();
-}
-
-function boundFollowUpLine(value: string, max: number): string {
- const cleaned = stripFollowUpMarkers(value).replace(/\s+/g, " ").trim();
- if (cleaned.length <= max) return cleaned;
- return `${cleaned.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
-}
-
-function boundFollowUpBody(value: string, max: number): string {
- const cleaned = stripFollowUpMarkers(value).trim();
- if (cleaned.length <= max) return cleaned;
- return `${cleaned.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
-}
-
-function composeFollowUpIssueTitle(finding: DeferredReviewFinding): string {
- const cleaned = stripFollowUpMarkers(finding.title);
- if (/^follow-up:/i.test(cleaned)) {
- return boundFollowUpLine(cleaned, FOLLOW_UP_ISSUE_TITLE_MAX);
- }
- const prefix = "Follow-up: ";
- return `${prefix}${boundFollowUpLine(cleaned, FOLLOW_UP_ISSUE_TITLE_MAX - prefix.length)}`;
-}
-
-function composeFollowUpIssueBody(input: { finding: DeferredReviewFinding; pullNumber?: number | undefined }): string {
- const lines: string[] = [];
- if (input.pullNumber !== undefined) lines.push(`Deferred from review on PR #${input.pullNumber}.`);
- if (input.finding.path) lines.push(`File: \`${input.finding.path}\``);
- lines.push("", boundFollowUpLine(input.finding.detail, FOLLOW_UP_ISSUE_BODY_MAX));
- if (input.finding.action) {
- lines.push("", "**Suggested next step**", boundFollowUpLine(input.finding.action, 500));
- }
- lines.push("", "_Filed locally from a deferred review finding — gittensory supplies content only._");
- return boundFollowUpBody(lines.join("\n"), FOLLOW_UP_ISSUE_BODY_MAX);
-}
-
-function sanitizeFollowUpFinding(finding: DeferredReviewFinding): Record {
- const sanitized: Record = {
- title: stripFollowUpMarkers(finding.title),
- detail: stripFollowUpMarkers(finding.detail),
- };
- if (finding.path) sanitized.path = finding.path;
- if (finding.action) sanitized.action = stripFollowUpMarkers(finding.action);
- return sanitized;
-}
-
-/** File a follow-up issue for a deferred review finding (#2177, #1962 slice). */
-export function buildFollowUpIssueSpec(input: {
- repoFullName: string;
- finding: DeferredReviewFinding;
- labels?: string[] | undefined;
- pullNumber?: number | undefined;
-}): LocalWriteActionSpec {
- const sanitizedFinding = sanitizeFollowUpFinding(input.finding);
- const title = composeFollowUpIssueTitle(input.finding);
- const body = composeFollowUpIssueBody({ finding: input.finding, pullNumber: input.pullNumber });
- const fileSpec = buildFileIssueSpec({
- repoFullName: input.repoFullName,
- title,
- body,
- labels: input.labels,
- });
- return {
- ...fileSpec,
- action: "follow_up_issue",
- description: `File a follow-up issue for a deferred review finding: ${title}`,
- inputs: {
- ...fileSpec.inputs,
- finding: sanitizedFinding,
- ...(input.pullNumber !== undefined ? { pullNumber: input.pullNumber } : {}),
- },
- };
-}
-
/** Add labels to an issue or PR (gh issue edit also targets PRs). */
export function buildApplyLabelsSpec(input: { repoFullName: string; number: number; labels: string[] }): LocalWriteActionSpec {
const labelArgs = input.labels.map((label) => ` --add-label ${sq(label)}`).join("");
@@ -181,3 +96,40 @@ export function buildTestGenSpec(input: {
command,
);
}
+
+// #2177 (follow-up-issue slice of #1962). Reuses buildFileIssueSpec's exact spec shape ("file_issue") — a
+// deferred review finding is just another issue-worth-filing content source, so there is no new spec verb or
+// no-cloud-write boundary here, only a deterministic title/body composer in front of the SAME builder.
+const FOLLOW_UP_ISSUE_TITLE_MAX = 200;
+const FOLLOW_UP_ISSUE_BODY_MAX = 4000;
+
+// Strip any machine-readable marker (e.g. fix-handoff's HTML comment marker, or a stray fenced block) before
+// the finding's text becomes issue content — a follow-up issue is read by a HUMAN triaging a backlog, not a
+// harness, so it should read as prose, not carry an internal marker meant for a different consumer.
+function stripMachineMarkers(text: string): string {
+ return text
+ .replace(//g, "")
+ .replace(/```[\s\S]*?```/g, "")
+ .replace(/\s+/g, " ")
+ .trim();
+}
+
+/** Build a LOCAL-execution spec to file a follow-up issue for a review finding a maintainer wants TRACKED
+ * rather than blocked on this PR. Composes a bounded, public-safe title/body from the finding and delegates to
+ * {@link buildFileIssueSpec}'s exact "file_issue" spec shape — no new write path. `label` is optional: when the
+ * caller supplies a point-bearing label (e.g. "gittensor:bug"), the follow-up carries it so the tracked issue
+ * is itself a scored, actionable contribution target; omitted ⇒ no labels at all (empty-label branch). */
+export function buildFollowUpIssueSpec(input: {
+ repoFullName: string;
+ path: string;
+ line?: number | undefined;
+ finding: string;
+ label?: string | null | undefined;
+}): LocalWriteActionSpec {
+ const location = input.line && input.line > 0 ? `${input.path}:${input.line}` : input.path;
+ const safeFinding = stripMachineMarkers(input.finding).slice(0, FOLLOW_UP_ISSUE_BODY_MAX);
+ const title = `Follow up: ${location}`.slice(0, FOLLOW_UP_ISSUE_TITLE_MAX);
+ const body = `Deferred review finding at \`${location}\`:\n\n${safeFinding}`.slice(0, FOLLOW_UP_ISSUE_BODY_MAX);
+ const labels = input.label ? [input.label] : [];
+ return buildFileIssueSpec({ repoFullName: input.repoFullName, title, body, labels });
+}
diff --git a/src/mcp/server.ts b/src/mcp/server.ts
index b25e22174e..4645910093 100644
--- a/src/mcp/server.ts
+++ b/src/mcp/server.ts
@@ -110,6 +110,7 @@ import {
buildCreateBranchSpec,
buildDeleteBranchSpec,
buildFileIssueSpec,
+ buildFollowUpIssueSpec,
buildOpenPrSpec,
buildPostEligibilityCommentSpec,
buildTestGenSpec,
@@ -325,6 +326,14 @@ const testGenShape = {
testDir: z.string().min(1).max(255).optional(),
criteria: z.array(z.string().min(1).max(300)).max(20).optional(),
};
+// #2177 (follow-up-issue slice of #1962): composes a file_issue spec from a single deferred review finding.
+const followUpIssueShape = {
+ repoFullName: z.string().min(3).max(SCENARIO_MAX_REPO_FULL_NAME_CHARS),
+ path: z.string().min(1).max(500),
+ line: z.number().int().positive().optional(),
+ finding: z.string().min(1).max(WRITE_TOOL_BODY_MAX),
+ label: z.string().min(1).max(100).optional(),
+};
const localWriteActionOutputSchema = {
action: z.string(),
description: z.string(),
@@ -1484,6 +1493,16 @@ export class GittensoryMcp {
},
async (input) => this.toolResult(this.localWriteSpec(buildTestGenSpec(input))),
);
+ server.registerTool(
+ "gittensory_file_follow_up_issue",
+ {
+ description:
+ "Build a LOCAL-execution spec to file a follow-up issue for a review finding a maintainer wants TRACKED rather than blocked on this PR. Composes a bounded, public-safe title/body from the finding (run it with your own gh creds; gittensory never performs the write).",
+ inputSchema: followUpIssueShape,
+ outputSchema: localWriteActionOutputSchema,
+ },
+ async (input) => this.toolResult(this.localWriteSpec(buildFollowUpIssueSpec(input))),
+ );
// #783 multi-step plan DAG — stateless: pass the plan back each call.
server.registerTool(
diff --git a/src/review/fix-handoff-render.ts b/src/review/fix-handoff-render.ts
new file mode 100644
index 0000000000..a01f01d8ac
--- /dev/null
+++ b/src/review/fix-handoff-render.ts
@@ -0,0 +1,70 @@
+// Fix-handoff block RENDERER (#2175, render slice of #1962 — the config/gate slice lives in
+// src/review/fix-handoff.ts's isFixHandoffEnabled/shouldEmitFixHandoff). Turns a single review finding into a
+// structured, machine-readable "apply this fix" block a CONTRIBUTOR'S OWN local coding agent can consume —
+// content only, no server-side write, no execution. Mirrors formatInlineBody's severity-label composition
+// (inline-comments.ts) and reuses the exact no-cloud-write boundary text every other local-execution artifact
+// carries (local-write-tools.ts's LOCAL_WRITE_BOUNDARY), so the guarantee reads identically everywhere
+// gittensory hands a contributor something to run themselves.
+//
+// The caller is responsible for gating emission via shouldEmitFixHandoff (fix-handoff.ts) BEFORE calling into
+// this module — this file is pure rendering, public-safe by construction: it only renders fields the caller
+// already produced through the public-safe filter (InlineFinding.body/suggestion are sanitized upstream by
+// composeInlineFindings before they ever reach here — this module adds no new free text of its own beyond the
+// fixed label/marker strings below).
+import { LOCAL_WRITE_BOUNDARY } from "../mcp/local-write-tools";
+import type { InlineFinding } from "../services/ai-review";
+
+/** A single finding rendered as a structured, LOCAL-execution fix-handoff block. `line` is `0` when the
+ * finding has no commentable diff line (mirrors the codebase's existing path-only sentinel — see
+ * `secretLeakFinding`/`scanDiffForSecretsWithLocations` in review/safety.ts, review/secrets-scan.ts) so the
+ * block still identifies WHERE to look, even path-only. */
+export type FixHandoffBlock = {
+ path: string;
+ line: number;
+ severity: "blocker" | "nit";
+ instruction: string;
+ suggestedChange?: string | undefined;
+ /** The rendered, machine-readable markdown block (fenced + an HTML comment marker a harness can grep for). */
+ body: string;
+ boundary: string;
+};
+
+/** The HTML comment marker prefixing every rendered block, so a contributor's own agent can reliably locate and
+ * parse fix-handoff blocks in a comment body without depending on markdown structure alone. */
+const FIX_HANDOFF_MARKER = "";
+
+/** PURE: build a single finding's fix-handoff block. Never throws; a finding whose `line` is not a positive
+ * integer (0, negative, non-finite — i.e. "no commentable line") still yields a valid PATH-ONLY block rather
+ * than being dropped, since the finding itself is still actionable context even without a line anchor. */
+export function buildFixHandoffBlock(finding: InlineFinding): FixHandoffBlock {
+ const hasLine = Number.isInteger(finding.line) && finding.line > 0;
+ const line = hasLine ? finding.line : 0;
+ const location = hasLine ? `${finding.path}:${line}` : `${finding.path} (no specific line)`;
+ const label = finding.severity === "blocker" ? "Blocker" : "Nit";
+ const suggestedChange = finding.suggestion?.trim() || undefined;
+ const suggestionBlock = suggestedChange ? `\n\nSuggested change:\n\`\`\`\n${suggestedChange}\n\`\`\`` : "";
+ const body = [
+ FIX_HANDOFF_MARKER,
+ `**Fix handoff — ${label} at \`${location}\`**`,
+ finding.body,
+ suggestionBlock,
+ `\n_${LOCAL_WRITE_BOUNDARY}_`,
+ ]
+ .filter((part) => part.length > 0)
+ .join("\n");
+ return {
+ path: finding.path,
+ line,
+ severity: finding.severity,
+ instruction: finding.body,
+ ...(suggestedChange !== undefined ? { suggestedChange } : {}),
+ body,
+ boundary: LOCAL_WRITE_BOUNDARY,
+ };
+}
+
+/** PURE: build a fix-handoff block for every finding in order. Empty in ⇒ empty out — no-op when there is
+ * nothing to hand off. */
+export function buildFixHandoffBlocks(findings: InlineFinding[]): FixHandoffBlock[] {
+ return findings.map((finding) => buildFixHandoffBlock(finding));
+}
diff --git a/test/unit/fix-handoff-render.test.ts b/test/unit/fix-handoff-render.test.ts
new file mode 100644
index 0000000000..5a2b37cc15
--- /dev/null
+++ b/test/unit/fix-handoff-render.test.ts
@@ -0,0 +1,85 @@
+import { describe, expect, it } from "vitest";
+import { buildFixHandoffBlock, buildFixHandoffBlocks } from "../../src/review/fix-handoff-render";
+import { LOCAL_WRITE_BOUNDARY } from "../../src/mcp/local-write-tools";
+import type { InlineFinding } from "../../src/services/ai-review";
+
+function finding(over: Partial = {}): InlineFinding {
+ return { path: "src/a.ts", line: 12, severity: "blocker", body: "Null check missing before dereference.", ...over };
+}
+
+describe("buildFixHandoffBlock (#2175)", () => {
+ it("renders a path:line location and blocker label", () => {
+ const block = buildFixHandoffBlock(finding({ line: 12, severity: "blocker" }));
+ expect(block).toMatchObject({ path: "src/a.ts", line: 12, severity: "blocker", instruction: "Null check missing before dereference." });
+ expect(block.body).toContain("src/a.ts:12");
+ expect(block.body).toContain("**Fix handoff — Blocker at `src/a.ts:12`**");
+ });
+
+ it("renders a nit label", () => {
+ const block = buildFixHandoffBlock(finding({ severity: "nit" }));
+ expect(block.body).toContain("Nit at");
+ });
+
+ it("includes the suggestedChange fenced block when present", () => {
+ const block = buildFixHandoffBlock(finding({ suggestion: "if (!value) return null;" }));
+ expect(block.suggestedChange).toBe("if (!value) return null;");
+ expect(block.body).toContain("Suggested change:");
+ expect(block.body).toContain("```\nif (!value) return null;\n```");
+ });
+
+ it("omits suggestedChange entirely when absent", () => {
+ const block = buildFixHandoffBlock(finding());
+ expect(block.suggestedChange).toBeUndefined();
+ expect(block.body).not.toContain("Suggested change:");
+ });
+
+ it("omits suggestedChange when the suggestion is whitespace-only", () => {
+ const block = buildFixHandoffBlock(finding({ suggestion: " " }));
+ expect(block.suggestedChange).toBeUndefined();
+ expect(block.body).not.toContain("Suggested change:");
+ });
+
+ it("yields a path-only block (line 0) when the finding has no commentable line (line <= 0)", () => {
+ const block = buildFixHandoffBlock(finding({ line: 0 }));
+ expect(block.line).toBe(0);
+ expect(block.body).toContain("src/a.ts (no specific line)");
+ expect(block.body).not.toContain("src/a.ts:0");
+ });
+
+ it("yields a path-only block when line is negative or non-finite (defensive)", () => {
+ expect(buildFixHandoffBlock(finding({ line: -1 })).line).toBe(0);
+ expect(buildFixHandoffBlock(finding({ line: Number.NaN })).line).toBe(0);
+ expect(buildFixHandoffBlock(finding({ line: 1.5 })).line).toBe(0); // non-integer isn't a valid diff line either
+ });
+
+ it("always includes the exact LOCAL_WRITE_BOUNDARY text (boundary-safe)", () => {
+ const block = buildFixHandoffBlock(finding());
+ expect(block.boundary).toBe(LOCAL_WRITE_BOUNDARY);
+ expect(block.body).toContain(LOCAL_WRITE_BOUNDARY);
+ });
+
+ it("includes the fix-handoff HTML comment marker so a harness can locate the block", () => {
+ const block = buildFixHandoffBlock(finding());
+ expect(block.body).toContain("");
+ });
+
+ it("carries the finding's body as the instruction verbatim (already public-safe upstream)", () => {
+ const block = buildFixHandoffBlock(finding({ body: "Add a guard for the empty-array case." }));
+ expect(block.instruction).toBe("Add a guard for the empty-array case.");
+ expect(block.body).toContain("Add a guard for the empty-array case.");
+ });
+});
+
+describe("buildFixHandoffBlocks (#2175)", () => {
+ it("maps every finding in order", () => {
+ const blocks = buildFixHandoffBlocks([finding({ path: "a.ts", line: 1 }), finding({ path: "b.ts", line: 2, severity: "nit" })]);
+ expect(blocks).toHaveLength(2);
+ expect(blocks[0]?.path).toBe("a.ts");
+ expect(blocks[1]?.path).toBe("b.ts");
+ expect(blocks[1]?.severity).toBe("nit");
+ });
+
+ it("returns an empty array for no findings (no-op)", () => {
+ expect(buildFixHandoffBlocks([])).toEqual([]);
+ });
+});
diff --git a/test/unit/local-write-tools.test.ts b/test/unit/local-write-tools.test.ts
index 6bd1940b67..e4d75456a1 100644
--- a/test/unit/local-write-tools.test.ts
+++ b/test/unit/local-write-tools.test.ts
@@ -94,72 +94,56 @@ describe("buildTestGenSpec (#2188)", () => {
});
});
-// #2177 (follow-up-issue action spec slice of #1962).
+// #2177 (follow-up-issue slice of #1962).
describe("buildFollowUpIssueSpec (#2177)", () => {
- it("builds a follow_up_issue spec with composed title/body, labels, and the local-execution boundary", () => {
- const s = buildFollowUpIssueSpec({
- repoFullName: "o/r",
- pullNumber: 42,
- labels: ["gittensor:bug"],
- finding: {
- title: "Handle null branch in widget loader",
- detail: "The loader never guards a null response.",
- path: "src/widget.ts",
- action: "Add a regression test for the null path.",
- },
- });
- expect(s.action).toBe("follow_up_issue");
+ it("delegates to the file_issue spec shape, composing a bounded title/body from the finding", () => {
+ const s = buildFollowUpIssueSpec({ repoFullName: "o/r", path: "src/a.ts", line: 42, finding: "Null check missing before dereference." });
+ expect(s.action).toBe("file_issue"); // reuses buildFileIssueSpec's exact spec shape — no new write path
expect(s.boundary).toBe(LOCAL_WRITE_BOUNDARY);
- expect(s.description).toContain("Follow-up: Handle null branch in widget loader");
- expect(s.command).toBe(
- "gh issue create --repo 'o/r' --title 'Follow-up: Handle null branch in widget loader' --body 'Deferred from review on PR #42.\nFile: `src/widget.ts`\n\nThe loader never guards a null response.\n\n**Suggested next step**\nAdd a regression test for the null path.\n\n_Filed locally from a deferred review finding — gittensory supplies content only._' --label 'gittensor:bug'",
- );
- expect(s.inputs).toMatchObject({ labels: ["gittensor:bug"], pullNumber: 42 });
- expect(s.inputs.finding).toEqual({
- title: "Handle null branch in widget loader",
- detail: "The loader never guards a null response.",
- path: "src/widget.ts",
- action: "Add a regression test for the null path.",
- });
+ expect(s.command).toContain("gh issue create");
+ expect(s.command).toContain("Follow up: src/a.ts:42");
+ expect(s.command).toContain("Null check missing before dereference.");
});
- it("omits labels and optional finding fields when absent, and strips HTML comment markers from the finding text", () => {
- const s = buildFollowUpIssueSpec({
- repoFullName: "o/r",
- finding: {
- title: "Follow-up: it's noisy",
- detail: "Detail stays public-safe.",
- },
- });
- expect(s.command).toContain("--title 'Follow-up: it'\\''s noisy'");
- expect(s.command).not.toContain("\n**Fix handoff — Blocker at `src/a.ts:42`**\nNull check missing.\n\n```suggestion\nif (!x) return null;\n```";
+ const s = buildFollowUpIssueSpec({ repoFullName: "o/r", path: "src/a.ts", line: 42, finding });
+ expect(s.command).not.toContain("