From a5989d6a8574c28c8eae248af107046008892fd3 Mon Sep 17 00:00:00 2001 From: brokemac79 Date: Sat, 27 Jun 2026 22:40:47 +0100 Subject: [PATCH 1/2] perf: reduce github comment round trips --- src/clawsweeper.ts | 68 ++++++-- src/repair/apply-result.ts | 110 +++++++++++- test/apply-pr-duplicate-proof.test.ts | 7 +- test/clawsweeper.test.ts | 230 ++++++++++++++++++++++++++ test/helpers.ts | 37 ++++- test/repair/apply-result.test.ts | 178 +++++++++++++++++--- 6 files changed, 585 insertions(+), 45 deletions(-) diff --git a/src/clawsweeper.ts b/src/clawsweeper.ts index b517182c52..86ee18419d 100644 --- a/src/clawsweeper.ts +++ b/src/clawsweeper.ts @@ -13348,11 +13348,12 @@ function coveringPrCloseCoveragePullRequestView( ): PrCloseCoverageProofPullRequestView { const pull = asRecord(ghJson(["api", `repos/${targetRepo()}/pulls/${number}`])); const issue = asRecord(ghJson(["api", `repos/${targetRepo()}/issues/${number}`])); - const commentsWindow = ghPagedContextWindow( - `repos/${targetRepo()}/issues/${number}/comments`, - numberOrUndefined(issue.comments), - 40, - ); + const commentsPath = `repos/${targetRepo()}/issues/${number}/comments`; + const commentsCount = numberOrUndefined(issue.comments); + const commentsWindow = + commentsCount === undefined + ? ghPagedLinkHeaderContextWindow(commentsPath, 40) + : ghPagedContextWindow(commentsPath, commentsCount, 40); const filteredComments = filterReviewContextComments(commentsWindow.items, number); return { number, @@ -13409,13 +13410,21 @@ function prCloseCoverageProofGateResult(options: { if (candidateRefs.length === 0) return null; const source = sourcePrCloseCoveragePullRequestView(options.item, options.context); + const coveringViews = new Map(); + const coveringView = (number: number): PrCloseCoverageProofPullRequestView => { + const cached = coveringViews.get(number); + if (cached) return cached; + const view = coveringPrCloseCoveragePullRequestView(number); + coveringViews.set(number, view); + return view; + }; let firstKeepOpenBlock: PrCloseCoverageProofGateBlock | null = null; let checkedPullRequestCandidate = false; for (const candidateRef of candidateRefs) { const linkedNumber = candidateRef.number; let covering: PrCloseCoverageProofPullRequestView; try { - covering = coveringPrCloseCoveragePullRequestView(linkedNumber); + covering = coveringView(linkedNumber); } catch (error) { if (candidateRef.kind !== "pull_url" && shorthandRefIsIssue(linkedNumber)) continue; return { @@ -15369,6 +15378,19 @@ function issueReviewComment( return codexComments.find(canPatchReviewComment) ?? codexComments[0]; } +function issueReviewCommentWithBody( + number: number, + body: string, +): Record | undefined { + const expected = body.trim(); + if (!expected) return undefined; + const comments = ghPaged(`repos/${targetRepo()}/issues/${number}/comments`).map( + asRecord, + ); + const exactComments = comments.filter((candidate) => commentBody(candidate)?.trim() === expected); + return exactComments.find(canPatchReviewComment) ?? exactComments[0]; +} + function commentUpdatedAt(comment: Record | undefined): string | undefined { const updatedAt = comment?.updated_at; if (typeof updatedAt === "string") return updatedAt; @@ -15518,26 +15540,50 @@ function upsertReviewComment( const markedBody = markedReviewCommentBody(number, body); const id = commentId(existing); const payload = writeCommentPayload(number, markedBody); + let args: string[]; if (id !== null && canPatchReviewComment(existing)) { - ghWithRetry([ + args = [ "api", `repos/${targetRepo()}/issues/comments/${id}`, "--method", "PATCH", "--input", payload, - ]); + ]; } else { - ghWithRetry([ + args = [ "api", `repos/${targetRepo()}/issues/${number}/comments`, "--method", "POST", "--input", payload, - ]); + ]; + } + const response = ghWithRetry(args); + const written = reviewCommentFromMutationResponse(response, args); + if (written) return written; + const fallback = issueReviewCommentWithBody(number, markedBody); + if (fallback) return fallback; + throw new Error( + `GitHub comment mutation for #${number} did not return or expose the synced review comment`, + ); +} + +function reviewCommentFromMutationResponse( + response: string, + args: readonly string[], +): Record | undefined { + if (!response.trim()) return undefined; + try { + const comment = asRecord(parseGhJson(response, args)); + if (commentId(comment) !== null || commentUrl(comment) || typeof comment.body === "string") { + return comment; + } + } catch { + return undefined; } - return issueReviewComment(number, [markedBody]); + return undefined; } function issueCommentWithMarker( diff --git a/src/repair/apply-result.ts b/src/repair/apply-result.ts index 75a776979c..0336ad2447 100644 --- a/src/repair/apply-result.ts +++ b/src/repair/apply-result.ts @@ -1262,9 +1262,21 @@ function fetchPrCloseCoverageProofCommentWindow( ): { comments: JsonValue[]; total: number } { const apiPath = `repos/${repo}/issues/${number}/comments`; const total = nonNegativeIntegerFromUnknown(commentsCount); - if (total === null || total <= limit) { - const comments = ghPaged(apiPath); - return { comments, total: total ?? comments.length }; + if (total === null) { + return fetchPrCloseCoverageProofCommentWindowWithoutCount(apiPath, limit); + } + if (total === 0 || limit <= 0) { + return { comments: [], total }; + } + if (total <= limit) { + return { + comments: fetchPrCloseCoverageProofCommentPage( + apiPath, + Math.min(total, GITHUB_MAX_PAGE_SIZE), + 1, + ), + total, + }; } if (total <= GITHUB_MAX_PAGE_SIZE) { @@ -1281,6 +1293,57 @@ function fetchPrCloseCoverageProofCommentWindow( return { comments: [...first, ...last], total }; } +function fetchPrCloseCoverageProofCommentWindowWithoutCount( + apiPath: string, + limit: number, +): { comments: JsonValue[]; total: number } { + const boundedLimit = Math.max(0, Math.floor(limit)); + if (boundedLimit <= 0) return { comments: [], total: 0 }; + const first = fetchPrCloseCoverageProofCommentPageWithHeaders(apiPath, GITHUB_MAX_PAGE_SIZE, 1); + const lastPage = + first.lastPageNumber ?? (first.comments.length < GITHUB_MAX_PAGE_SIZE ? 1 : null); + if (lastPage === null) { + const comments = ghPaged(apiPath); + return { comments, total: comments.length }; + } + if (lastPage <= 1) { + return { + comments: selectPrCloseCoverageProofCommentWindow(first.comments, boundedLimit), + total: first.comments.length, + }; + } + + const last = fetchPrCloseCoverageProofCommentPageWithHeaders( + apiPath, + GITHUB_MAX_PAGE_SIZE, + lastPage, + ).comments; + const total = Math.max(0, (lastPage - 1) * GITHUB_MAX_PAGE_SIZE + last.length); + const keepStart = Math.floor(boundedLimit / 2); + const keepEnd = Math.max(0, boundedLimit - keepStart); + const head = first.comments.slice(0, keepStart); + let tailSource = last; + if (last.length < keepEnd && lastPage > 1) { + const previous = + lastPage - 1 === 1 + ? first.comments + : fetchPrCloseCoverageProofCommentPage(apiPath, GITHUB_MAX_PAGE_SIZE, lastPage - 1); + tailSource = [...previous, ...last]; + } + return { comments: [...head, ...tailSource.slice(-keepEnd)], total }; +} + +function selectPrCloseCoverageProofCommentWindow( + comments: JsonValue[], + limit: number, +): JsonValue[] { + const boundedLimit = Math.max(0, Math.floor(limit)); + if (comments.length <= boundedLimit) return comments; + const keepStart = Math.floor(boundedLimit / 2); + const keepEnd = Math.max(0, boundedLimit - keepStart); + return [...comments.slice(0, keepStart), ...comments.slice(-keepEnd)]; +} + function fetchLastPrCloseCoverageProofComments( apiPath: string, total: number, @@ -1313,6 +1376,47 @@ function fetchPrCloseCoverageProofCommentPage( return Array.isArray(entries) ? entries : []; } +function fetchPrCloseCoverageProofCommentPageWithHeaders( + apiPath: string, + perPage: number, + page: number, +): { comments: JsonValue[]; lastPageNumber: number | null } { + const limitedPath = githubLimitedPagePath(apiPath, perPage, page); + const output = ghWithRetry(["api", "-i", limitedPath]); + const { body, headers } = splitGithubResponse(output); + const entries = JSON.parse(body || "[]") as JsonValue; + return { + comments: Array.isArray(entries) ? entries : [], + lastPageNumber: githubLastPageNumber(headers), + }; +} + +function splitGithubResponse(output: string): { headers: string; body: string } { + const normalized = output.replace(/\r\n/g, "\n"); + const separator = normalized.lastIndexOf("\n\n"); + if (separator < 0) return { headers: "", body: normalized }; + return { + headers: normalized.slice(0, separator), + body: normalized.slice(separator + 2), + }; +} + +function githubLastPageNumber(headers: string): number | null { + for (const line of headers.split("\n")) { + const delimiter = line.indexOf(":"); + if (delimiter <= 0) continue; + if (line.slice(0, delimiter).trim().toLowerCase() !== "link") continue; + for (const part of line.slice(delimiter + 1).split(",")) { + if (!part.includes('rel="last"')) continue; + const page = part.match(/[?&]page=(\d+)/)?.[1]; + if (!page) continue; + const value = Number(page); + if (Number.isSafeInteger(value) && value > 0) return value; + } + } + return null; +} + function compactPrCloseCoverageProofCommentWindow( comments: JsonValue[], total: number, diff --git a/test/apply-pr-duplicate-proof.test.ts b/test/apply-pr-duplicate-proof.test.ts index c9fd37293e..380aebe576 100644 --- a/test/apply-pr-duplicate-proof.test.ts +++ b/test/apply-pr-duplicate-proof.test.ts @@ -336,15 +336,12 @@ test("apply-decisions keeps existing duplicate PR close proposals open when cove retryReport.some((entry) => entry.action === "closed"), false, ); - assert.equal( - retryReport.find((entry) => entry.number === 348)?.action, - "review_comment_synced", - ); + assert.equal(retryReport.find((entry) => entry.number === 348)?.action, "kept_open"); assert.equal( retryReport.some((entry) => /proof should not rerun/.test(entry.reason)), false, ); - assert.match(readFileSync(commentWriteLogPath, "utf8"), /issues\/comments\/9348/); + assert.equal(readFileSync(commentWriteLogPath, "utf8"), ""); assert.equal(existsSync(join(closedDir, "348.md")), false); } finally { rmSync(root, { recursive: true, force: true }); diff --git a/test/clawsweeper.test.ts b/test/clawsweeper.test.ts index fa6cd298e4..97e7b10b40 100644 --- a/test/clawsweeper.test.ts +++ b/test/clawsweeper.test.ts @@ -420,6 +420,13 @@ if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { calls.some((args) => args.some((arg) => arg.includes("/issues/322"))), false, ); + const reviewCommentListFetches = calls.filter( + (args) => + args[0] === "api" && + (args[1] ?? "").includes("/issues/321/comments") && + args.includes("--paginate"), + ); + assert.equal(reviewCommentListFetches.length, 1); assert.deepEqual(JSON.parse(readFileSync(reportPath, "utf8")), [ { number: 321, @@ -444,6 +451,229 @@ if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { } }); +test("apply-decisions falls back to comment lookup after malformed mutation response", () => { + const root = mkdtempSync(tmpPrefix); + try { + const itemsDir = join(root, "items"); + const closedDir = join(root, "closed"); + const plansDir = join(root, "plans"); + const reportPath = join(root, "apply-report.json"); + const logPath = join(root, "gh.log"); + const patchedBodyPath = join(root, "patched-comment-body.txt"); + mkdirSync(itemsDir, { recursive: true }); + mkdirSync(plansDir, { recursive: true }); + + const report = workPlanCandidateReport({ + number: 321, + reviewed_at: "2026-05-01T00:05:00Z", + item_snapshot_hash: "reviewed-snapshot-321", + item_updated_at: "2026-05-01T00:00:00Z", + triage_priority: "P1", + }); + writeFileSync(join(itemsDir, "321.md"), report, "utf8"); + const placeholder = renderReviewStartStatusComment({ + number: 321, + kind: "issue", + title: "Render work plans", + }); + + const ghMock = ` +const { appendFileSync, existsSync, readFileSync, writeFileSync } = require("fs"); +const logPath = ${JSON.stringify(logPath)}; +const patchedBodyPath = ${JSON.stringify(patchedBodyPath)}; +const placeholder = ${JSON.stringify(placeholder)}; +const rawArgs = process.argv.slice(2); +const args = rawArgs[0] === "--repo" ? rawArgs.slice(2) : rawArgs; +appendFileSync(logPath, JSON.stringify(args) + "\\n"); +const path = args.includes("-i") ? args[args.indexOf("-i") + 1] : args[1] || ""; +const commentMatch = path.match(/\\/issues\\/(\\d+)\\/comments(?:\\?|$)/); +const issueMatch = path.match(/\\/issues\\/(\\d+)$/); +if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { + const inputPath = args[args.indexOf("--input") + 1]; + const body = JSON.parse(readFileSync(inputPath, "utf8")).body; + writeFileSync(patchedBodyPath, body, "utf8"); + appendFileSync(logPath, JSON.stringify(["comment-patch", body]) + "\\n"); + process.stdout.write("{not-json"); +} else if (args[0] === "api" && commentMatch) { + const body = existsSync(patchedBodyPath) ? readFileSync(patchedBodyPath, "utf8") : placeholder; + console.log(JSON.stringify([[{ + id: 9321, + html_url: "https://github.com/openclaw/clawsweeper/issues/321#issuecomment-9321", + created_at: "2026-05-01T00:01:00Z", + updated_at: "2026-05-01T00:06:00Z", + user: { login: "clawsweeper[bot]" }, + body + }]])); +} else if (args[0] === "api" && /\\/issues\\/321\\/timeline/.test(path)) { + console.log(JSON.stringify([{ + id: 1, + event: "commented", + created_at: "2026-05-01T00:01:00Z", + actor: { login: "clawsweeper[bot]" } + }])); +} else if (args[0] === "api" && issueMatch) { + console.log(JSON.stringify({ + number: 321, + title: "Render work plans", + html_url: "https://github.com/openclaw/clawsweeper/issues/321", + created_at: "2026-05-01T00:00:00Z", + updated_at: "2026-05-01T00:01:01Z", + closed_at: null, + state: "open", + locked: false, + active_lock_reason: null, + author_association: "CONTRIBUTOR", + user: { login: "reporter" }, + labels: [], + comments: 1, + pull_request: null + })); +} else if (args[0] === "issue" && args[1] === "view") { + console.log(JSON.stringify({ closedByPullRequestsReferences: [] })); +} else if (args[0] === "label" && args[1] === "create") { + console.log(""); +} else if (args[0] === "issue" && args[1] === "edit") { + console.log(""); +} else { + console.error("unexpected gh args", JSON.stringify(args)); + process.exit(1); +} +`; + withMockGh(root, ghMock, () => { + runApplyDecisionsForTest({ itemsDir, closedDir, plansDir, reportPath }); + }); + + const calls = readFileSync(logPath, "utf8") + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as string[]); + const reviewCommentListFetches = calls.filter( + (args) => + args[0] === "api" && + (args[1] ?? "").includes("/issues/321/comments") && + args.includes("--paginate"), + ); + assert.equal(reviewCommentListFetches.length, 2); + assert.deepEqual(JSON.parse(readFileSync(reportPath, "utf8")), [ + { + number: 321, + action: "review_comment_synced", + reason: "updated durable Codex review comment", + }, + ]); + assert.match(readFileSync(join(itemsDir, "321.md"), "utf8"), /^review_comment_synced_at: /m); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("apply-decisions rejects stale fallback after malformed mutation response", () => { + const root = mkdtempSync(tmpPrefix); + try { + const itemsDir = join(root, "items"); + const closedDir = join(root, "closed"); + const plansDir = join(root, "plans"); + const reportPath = join(root, "apply-report.json"); + const logPath = join(root, "gh.log"); + mkdirSync(itemsDir, { recursive: true }); + mkdirSync(plansDir, { recursive: true }); + + const report = workPlanCandidateReport({ + number: 321, + reviewed_at: "2026-05-01T00:05:00Z", + item_snapshot_hash: "reviewed-snapshot-321", + item_updated_at: "2026-05-01T00:00:00Z", + triage_priority: "P1", + }); + writeFileSync(join(itemsDir, "321.md"), report, "utf8"); + const placeholder = renderReviewStartStatusComment({ + number: 321, + kind: "issue", + title: "Render work plans", + }); + + const ghMock = ` +const { appendFileSync, readFileSync } = require("fs"); +const logPath = ${JSON.stringify(logPath)}; +const placeholder = ${JSON.stringify(placeholder)}; +const rawArgs = process.argv.slice(2); +const args = rawArgs[0] === "--repo" ? rawArgs.slice(2) : rawArgs; +appendFileSync(logPath, JSON.stringify(args) + "\\n"); +const path = args.includes("-i") ? args[args.indexOf("-i") + 1] : args[1] || ""; +const commentMatch = path.match(/\\/issues\\/(\\d+)\\/comments(?:\\?|$)/); +const issueMatch = path.match(/\\/issues\\/(\\d+)$/); +if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { + const inputPath = args[args.indexOf("--input") + 1]; + const body = JSON.parse(readFileSync(inputPath, "utf8")).body; + appendFileSync(logPath, JSON.stringify(["comment-patch", body]) + "\\n"); + process.stdout.write("{not-json"); +} else if (args[0] === "api" && commentMatch) { + console.log(JSON.stringify([[{ + id: 9321, + html_url: "https://github.com/openclaw/clawsweeper/issues/321#issuecomment-9321", + created_at: "2026-05-01T00:01:00Z", + updated_at: "2026-05-01T00:01:00Z", + user: { login: "clawsweeper[bot]" }, + body: placeholder + }]])); +} else if (args[0] === "api" && /\\/issues\\/321\\/timeline/.test(path)) { + console.log(JSON.stringify([{ + id: 1, + event: "commented", + created_at: "2026-05-01T00:01:00Z", + actor: { login: "clawsweeper[bot]" } + }])); +} else if (args[0] === "api" && issueMatch) { + console.log(JSON.stringify({ + number: 321, + title: "Render work plans", + html_url: "https://github.com/openclaw/clawsweeper/issues/321", + created_at: "2026-05-01T00:00:00Z", + updated_at: "2026-05-01T00:01:01Z", + closed_at: null, + state: "open", + locked: false, + active_lock_reason: null, + author_association: "CONTRIBUTOR", + user: { login: "reporter" }, + labels: [], + comments: 1, + pull_request: null + })); +} else if (args[0] === "issue" && args[1] === "view") { + console.log(JSON.stringify({ closedByPullRequestsReferences: [] })); +} else if (args[0] === "label" && args[1] === "create") { + console.log(""); +} else if (args[0] === "issue" && args[1] === "edit") { + console.log(""); +} else { + console.error("unexpected gh args", JSON.stringify(args)); + process.exit(1); +} +`; + assert.throws( + () => { + withMockGh(root, ghMock, () => { + runApplyDecisionsForTest({ itemsDir, closedDir, plansDir, reportPath }); + }); + }, + (error) => { + assert.match(String(error), /did not return or expose the synced review comment/); + return true; + }, + ); + + assert.equal(existsSync(reportPath), false); + assert.doesNotMatch( + readFileSync(join(itemsDir, "321.md"), "utf8"), + /^review_comment_synced_at: /m, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("apply-decisions syncs labels when first review placeholder advanced issue updated_at", () => { const root = mkdtempSync(tmpPrefix); try { diff --git a/test/helpers.ts b/test/helpers.ts index d2b2c68c60..35472ca7a9 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -451,7 +451,8 @@ export function promotionGhMock(options: { const linkedPulls = options.linkedPulls ?? {}; const linkedIssues = options.linkedIssues ?? {}; return ` - const { appendFileSync, existsSync } = require("fs"); + const { appendFileSync, existsSync, readFileSync, writeFileSync } = require("fs"); + const { join } = require("path"); const rawArgs = process.argv.slice(2); const args = rawArgs[0] === "--repo" ? rawArgs.slice(2) : rawArgs; const path = args[1] || ""; @@ -466,6 +467,31 @@ export function promotionGhMock(options: { const commentWriteLogPath = ${JSON.stringify(options.commentWriteLogPath ?? "")}; const closeAppliedBodyLogPath = ${JSON.stringify(options.closeAppliedBodyLogPath ?? "")}; const number = ${options.number}; + const commentStatePath = join(__dirname, "..", "comment-state-" + number + ".json"); + const mutationComment = (id, body) => ({ + id, + html_url: "https://github.com/openclaw/openclaw/pull/" + number + "#issuecomment-" + id, + created_at: "2026-05-01T01:00:00Z", + updated_at: "2026-05-01T02:00:00Z", + user: { login: "clawsweeper[bot]" }, + body + }); + const writeMutationComment = () => { + const input = args[args.indexOf("--input") + 1]; + const body = JSON.parse(readFileSync(input, "utf8")).body; + const idMatch = path.match(/\\/issues\\/comments\\/(\\d+)$/); + const id = idMatch ? Number(idMatch[1]) : 9000 + number; + const comment = mutationComment(id, body); + writeFileSync(commentStatePath, JSON.stringify(comment), "utf8"); + return comment; + }; + const liveComments = () => { + if (!existsSync(commentStatePath)) return comments; + const written = JSON.parse(readFileSync(commentStatePath, "utf8")); + const existingIndex = comments.findIndex((comment) => comment && comment.id === written.id); + if (existingIndex < 0) return [...comments, written]; + return comments.map((comment, index) => index === existingIndex ? { ...comment, ...written } : comment); + }; const title = ${JSON.stringify(title)}; const labels = ${JSON.stringify(options.labels ?? ["status: 📣 needs proof"])}; const itemCreatedAt = ${JSON.stringify(itemCreatedAt)}; @@ -503,14 +529,15 @@ export function promotionGhMock(options: { if (commentWriteLogPath) appendFileSync(commentWriteLogPath, args.join(" ") + "\\n"); if (closeAppliedBodyLogPath) { const input = args[args.indexOf("--input") + 1]; - appendFileSync(closeAppliedBodyLogPath, JSON.parse(require("fs").readFileSync(input, "utf8")).body + "\\n---body---\\n"); + appendFileSync(closeAppliedBodyLogPath, JSON.parse(readFileSync(input, "utf8")).body + "\\n---body---\\n"); } - console.log(""); + console.log(JSON.stringify(writeMutationComment())); } else if (args[0] === "api" && new RegExp("/issues/comments/\\\\d+$").test(path) && args.includes("--method")) { if (commentWriteLogPath) appendFileSync(commentWriteLogPath, args.join(" ") + "\\n"); - console.log(""); + console.log(JSON.stringify(writeMutationComment())); } else if (args[0] === "api" && new RegExp("/issues/" + number + "/comments(?:\\\\?|$)").test(path)) { - console.log(JSON.stringify(slurp ? [comments] : comments)); + const currentComments = liveComments(); + console.log(JSON.stringify(slurp ? [currentComments] : currentComments)); } else if (args[0] === "api" && new RegExp("/issues/" + number + "/timeline(?:\\\\?|$)").test(path)) { console.log(JSON.stringify(slurp ? [timeline] : timeline)); } else if (args[0] === "api" && new RegExp("/issues/" + number + "$").test(path)) { diff --git a/test/repair/apply-result.test.ts b/test/repair/apply-result.test.ts index 5c64f27fcc..a11a0ec86e 100644 --- a/test/repair/apply-result.test.ts +++ b/test/repair/apply-result.test.ts @@ -545,6 +545,74 @@ test("repair apply includes recent covering PR comments in coverage proof", () = } }); +test("repair apply bounds covering PR comments when issue comment count is absent", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-apply-result-")); + try { + const paths = writeApplyFixture(tmp, { + action: "close_duplicate", + classification: "duplicate", + canonical: "#202", + }); + writeFakeGh(paths.binDir, { + issues: { + 101: issue({ number: 101, title: "Add config validation", pullRequest: true }), + 202: issue({ + number: 202, + title: "Rewrite config validation", + pullRequest: true, + labels: ["proof: sufficient"], + }), + }, + pulls: { + 101: pull({ number: 101, title: "Add config validation" }), + 202: pull({ number: 202, title: "Rewrite config validation" }), + }, + comments: { + 101: [comment("alice", "PR A keeps legacy config behavior intact.")], + 202: Array.from({ length: 160 }, (_, index) => + comment( + "bob", + index === 159 + ? "Recent discussion: PR B carries forward the legacy behavior." + : `Older discussion ${index}`, + ), + ), + }, + omitIssueCommentCounts: [202], + logPath: paths.ghLogPath, + }); + writeFakeCodex(paths.binDir); + + runApplyResult(paths, { + proofDecision: "covered", + expectedPromptIncludes: "Recent discussion: PR B carries forward the legacy behavior.", + }); + + const report = JSON.parse(fs.readFileSync(paths.reportPath, "utf8")); + assert.equal(report.actions[0].status, "executed"); + assert.equal(hasPrCloseCall(paths.ghLogPath), true); + const coveringCommentFetches = ghCalls(paths.ghLogPath).filter( + (call) => + call.args[0] === "api" && + call.args.some((arg) => arg.includes("/issues/202/comments")) && + !call.args.includes("--method"), + ); + assert.equal( + coveringCommentFetches.some((call) => call.args.includes("--slurp")), + false, + ); + assert.equal(coveringCommentFetches.length, 2); + assert.ok(coveringCommentFetches.every((call) => call.args.includes("-i"))); + assert.ok( + coveringCommentFetches.every((call) => + call.args.some((arg) => /[?&]per_page=100(?:&|$)/.test(arg)), + ), + ); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +}); + test("repair apply blocks PR close when open covering PR lacks positive proof", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-apply-result-")); try { @@ -1137,6 +1205,7 @@ type FakeGhData = { issues: Record>; pulls: Record>; comments: Record[]>; + omitIssueCommentCounts?: number[]; prViewFailure?: { number: number; message: string }; afterProofPath?: string; postProofIssues?: Record>; @@ -1229,42 +1298,77 @@ function runApplyResult( ) { const args = ["dist/repair/apply-result.js", paths.jobPath, paths.resultPath]; if (options.allowMissingUpdatedAt) args.push("--allow-missing-updated-at"); + const env = applyResultEnv(paths, options); execFileSync(process.execPath, args, { cwd: repoRoot, - env: { - ...process.env, - CLAWSWEEPER_ALLOW_EXECUTE: "1", - CLAWSWEEPER_ALLOWED_OWNER: "openclaw", - CLAWSWEEPER_MODEL: "model-test", - CLAWSWEEPER_PR_CLOSE_COVERAGE_PROOF_TIMEOUT_MS: "10000", - GH_TOKEN: "write-token", - PATH: `${paths.binDir}${path.delimiter}${process.env.PATH ?? ""}`, - PR_CLOSE_COVERAGE_PROOF_DECISION: options.proofDecision, - PR_CLOSE_COVERAGE_PROOF_EXPECT_PROMPT: options.expectedPromptIncludes ?? "", - PR_CLOSE_COVERAGE_PROOF_UNEXPECTED_PROMPT: options.unexpectedPromptIncludes ?? "", - PR_CLOSE_COVERAGE_PROOF_FAIL_IF_INVOKED: options.failIfProofRuns ? "1" : "", - PR_CLOSE_COVERAGE_PROOF_FAILURE_MESSAGE: options.proofFailureMessage ?? "", - PR_CLOSE_COVERAGE_PROOF_AFTER_PROOF_PATH: options.afterProofPath ?? "", - }, + env, stdio: "pipe", }); } +function applyResultEnv( + paths: ApplyFixturePaths, + options: Parameters[1], +): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { + ...process.env, + CLAWSWEEPER_ALLOW_EXECUTE: "1", + CLAWSWEEPER_ALLOWED_OWNER: "openclaw", + CLAWSWEEPER_MODEL: "model-test", + CLAWSWEEPER_PR_CLOSE_COVERAGE_PROOF_TIMEOUT_MS: "10000", + GH_TOKEN: "write-token", + PR_CLOSE_COVERAGE_PROOF_DECISION: options.proofDecision, + PR_CLOSE_COVERAGE_PROOF_EXPECT_PROMPT: options.expectedPromptIncludes ?? "", + PR_CLOSE_COVERAGE_PROOF_UNEXPECTED_PROMPT: options.unexpectedPromptIncludes ?? "", + PR_CLOSE_COVERAGE_PROOF_FAIL_IF_INVOKED: options.failIfProofRuns ? "1" : "", + PR_CLOSE_COVERAGE_PROOF_FAILURE_MESSAGE: options.proofFailureMessage ?? "", + PR_CLOSE_COVERAGE_PROOF_AFTER_PROOF_PATH: options.afterProofPath ?? "", + }; + const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH"; + const testPath = `${paths.binDir}${path.delimiter}${env[pathKey] ?? ""}`; + env[pathKey] = testPath; + env.PATH = testPath; + if (process.platform === "win32") { + env.NODE_OPTIONS = prependNodeRequireOption( + env.NODE_OPTIONS, + path.join(paths.binDir, "fake-gh-preload.cjs"), + ); + } + return env; +} + +function prependNodeRequireOption(existing: string | undefined, modulePath: string): string { + const normalizedPath = process.platform === "win32" ? modulePath.replace(/\\/g, "/") : modulePath; + const requireOption = `--require=${JSON.stringify(normalizedPath)}`; + return existing ? `${requireOption} ${existing}` : requireOption; +} + function writeFakeGh(binDir: string, data: FakeGhData) { + const ghScriptPath = path.join(binDir, process.platform === "win32" ? "gh.js" : "gh"); fs.writeFileSync( - path.join(binDir, "gh"), + ghScriptPath, `#!/usr/bin/env node - const fs = require("node:fs"); -const args = process.argv.slice(2); +const fs = require("node:fs"); +const path = require("node:path"); +const windowsPreloadedGh = path.basename(process.execPath).toLowerCase() === "gh.exe"; +const args = process.argv.slice(windowsPreloadedGh ? 1 : 2); +if (windowsPreloadedGh && args[0]) { + args[0] = path.basename(args[0]); +} const data = ${JSON.stringify(data)}; fs.appendFileSync(data.logPath, JSON.stringify({ args }) + "\\n"); +const includeHeaders = args[0] === "api" && args[1] === "-i"; function write(value) { process.stdout.write(JSON.stringify(value)); } +function writeWithHeaders(value, headers = []) { + process.stdout.write(["HTTP/2 200", ...headers, "", JSON.stringify(value)].join("\\r\\n")); +} + if (args[0] === "api") { - const apiPath = args[1] || ""; + const apiPath = includeHeaders ? args[2] || "" : args[1] || ""; const url = new URL(apiPath, "https://api.github.test/"); let match = url.pathname.match(/\\/issues\\/(\\d+)\\/comments$/); if (match) { @@ -1281,7 +1385,24 @@ if (args[0] === "api") { const perPage = Number(url.searchParams.get("per_page") || comments.length || 100); const page = Number(url.searchParams.get("page") || "1"); const start = Math.max(0, page - 1) * perPage; - write(comments.slice(start, start + perPage)); + const pageComments = comments.slice(start, start + perPage); + if (includeHeaders) { + const lastPage = Math.max(1, Math.ceil(comments.length / Math.max(1, perPage))); + const links = []; + if (page < lastPage) { + const next = new URL(url.toString()); + next.searchParams.set("page", String(page + 1)); + links.push("<" + next.toString() + ">; rel=\\"next\\""); + } + if (lastPage > 1) { + const last = new URL(url.toString()); + last.searchParams.set("page", String(lastPage)); + links.push("<" + last.toString() + ">; rel=\\"last\\""); + } + writeWithHeaders(pageComments, links.length ? ["link: " + links.join(", ")] : []); + } else { + write(pageComments); + } } process.exit(0); } @@ -1307,7 +1428,9 @@ if (args[0] === "api") { ...issue, ...(postProofIssue ? postProofIssue : {}), ...(postProofUpdatedAt ? { updated_at: postProofUpdatedAt } : {}), - comments: data.comments[number]?.length || 0, + ...((data.omitIssueCommentCounts || []).includes(number) + ? {} + : { comments: data.comments[number]?.length || 0 }), }); process.exit(0); } @@ -1377,6 +1500,19 @@ process.exit(1); `, { mode: 0o755 }, ); + if (process.platform === "win32") { + fs.copyFileSync(process.execPath, path.join(binDir, "gh.exe")); + fs.writeFileSync( + path.join(binDir, "fake-gh-preload.cjs"), + `const path = require("node:path"); +if (path.basename(process.execPath).toLowerCase() === "gh.exe") { + require(path.join(__dirname, "gh.js")); + process.exit(process.exitCode || 0); +} +`, + "utf8", + ); + } } function writeFakeCodex(binDir: string) { From 7aec7efb505afd1e56bbcbd5a1c1e789e281b8b7 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Tue, 30 Jun 2026 13:15:43 -0700 Subject: [PATCH 2/2] test: trim comment round trip coverage --- src/clawsweeper.ts | 2 +- src/repair/apply-result.ts | 13 +- test/clawsweeper.test.ts | 223 ------------------------------- test/repair/apply-result.test.ts | 139 +++---------------- 4 files changed, 19 insertions(+), 358 deletions(-) diff --git a/src/clawsweeper.ts b/src/clawsweeper.ts index 86ee18419d..06bf8406bc 100644 --- a/src/clawsweeper.ts +++ b/src/clawsweeper.ts @@ -15577,7 +15577,7 @@ function reviewCommentFromMutationResponse( if (!response.trim()) return undefined; try { const comment = asRecord(parseGhJson(response, args)); - if (commentId(comment) !== null || commentUrl(comment) || typeof comment.body === "string") { + if (commentId(comment) !== null || commentUrl(comment)) { return comment; } } catch { diff --git a/src/repair/apply-result.ts b/src/repair/apply-result.ts index 0336ad2447..c0b4cafcd5 100644 --- a/src/repair/apply-result.ts +++ b/src/repair/apply-result.ts @@ -1308,7 +1308,7 @@ function fetchPrCloseCoverageProofCommentWindowWithoutCount( } if (lastPage <= 1) { return { - comments: selectPrCloseCoverageProofCommentWindow(first.comments, boundedLimit), + comments: first.comments, total: first.comments.length, }; } @@ -1333,17 +1333,6 @@ function fetchPrCloseCoverageProofCommentWindowWithoutCount( return { comments: [...head, ...tailSource.slice(-keepEnd)], total }; } -function selectPrCloseCoverageProofCommentWindow( - comments: JsonValue[], - limit: number, -): JsonValue[] { - const boundedLimit = Math.max(0, Math.floor(limit)); - if (comments.length <= boundedLimit) return comments; - const keepStart = Math.floor(boundedLimit / 2); - const keepEnd = Math.max(0, boundedLimit - keepStart); - return [...comments.slice(0, keepStart), ...comments.slice(-keepEnd)]; -} - function fetchLastPrCloseCoverageProofComments( apiPath: string, total: number, diff --git a/test/clawsweeper.test.ts b/test/clawsweeper.test.ts index 97e7b10b40..d4e1542ad1 100644 --- a/test/clawsweeper.test.ts +++ b/test/clawsweeper.test.ts @@ -451,229 +451,6 @@ if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { } }); -test("apply-decisions falls back to comment lookup after malformed mutation response", () => { - const root = mkdtempSync(tmpPrefix); - try { - const itemsDir = join(root, "items"); - const closedDir = join(root, "closed"); - const plansDir = join(root, "plans"); - const reportPath = join(root, "apply-report.json"); - const logPath = join(root, "gh.log"); - const patchedBodyPath = join(root, "patched-comment-body.txt"); - mkdirSync(itemsDir, { recursive: true }); - mkdirSync(plansDir, { recursive: true }); - - const report = workPlanCandidateReport({ - number: 321, - reviewed_at: "2026-05-01T00:05:00Z", - item_snapshot_hash: "reviewed-snapshot-321", - item_updated_at: "2026-05-01T00:00:00Z", - triage_priority: "P1", - }); - writeFileSync(join(itemsDir, "321.md"), report, "utf8"); - const placeholder = renderReviewStartStatusComment({ - number: 321, - kind: "issue", - title: "Render work plans", - }); - - const ghMock = ` -const { appendFileSync, existsSync, readFileSync, writeFileSync } = require("fs"); -const logPath = ${JSON.stringify(logPath)}; -const patchedBodyPath = ${JSON.stringify(patchedBodyPath)}; -const placeholder = ${JSON.stringify(placeholder)}; -const rawArgs = process.argv.slice(2); -const args = rawArgs[0] === "--repo" ? rawArgs.slice(2) : rawArgs; -appendFileSync(logPath, JSON.stringify(args) + "\\n"); -const path = args.includes("-i") ? args[args.indexOf("-i") + 1] : args[1] || ""; -const commentMatch = path.match(/\\/issues\\/(\\d+)\\/comments(?:\\?|$)/); -const issueMatch = path.match(/\\/issues\\/(\\d+)$/); -if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { - const inputPath = args[args.indexOf("--input") + 1]; - const body = JSON.parse(readFileSync(inputPath, "utf8")).body; - writeFileSync(patchedBodyPath, body, "utf8"); - appendFileSync(logPath, JSON.stringify(["comment-patch", body]) + "\\n"); - process.stdout.write("{not-json"); -} else if (args[0] === "api" && commentMatch) { - const body = existsSync(patchedBodyPath) ? readFileSync(patchedBodyPath, "utf8") : placeholder; - console.log(JSON.stringify([[{ - id: 9321, - html_url: "https://github.com/openclaw/clawsweeper/issues/321#issuecomment-9321", - created_at: "2026-05-01T00:01:00Z", - updated_at: "2026-05-01T00:06:00Z", - user: { login: "clawsweeper[bot]" }, - body - }]])); -} else if (args[0] === "api" && /\\/issues\\/321\\/timeline/.test(path)) { - console.log(JSON.stringify([{ - id: 1, - event: "commented", - created_at: "2026-05-01T00:01:00Z", - actor: { login: "clawsweeper[bot]" } - }])); -} else if (args[0] === "api" && issueMatch) { - console.log(JSON.stringify({ - number: 321, - title: "Render work plans", - html_url: "https://github.com/openclaw/clawsweeper/issues/321", - created_at: "2026-05-01T00:00:00Z", - updated_at: "2026-05-01T00:01:01Z", - closed_at: null, - state: "open", - locked: false, - active_lock_reason: null, - author_association: "CONTRIBUTOR", - user: { login: "reporter" }, - labels: [], - comments: 1, - pull_request: null - })); -} else if (args[0] === "issue" && args[1] === "view") { - console.log(JSON.stringify({ closedByPullRequestsReferences: [] })); -} else if (args[0] === "label" && args[1] === "create") { - console.log(""); -} else if (args[0] === "issue" && args[1] === "edit") { - console.log(""); -} else { - console.error("unexpected gh args", JSON.stringify(args)); - process.exit(1); -} -`; - withMockGh(root, ghMock, () => { - runApplyDecisionsForTest({ itemsDir, closedDir, plansDir, reportPath }); - }); - - const calls = readFileSync(logPath, "utf8") - .trim() - .split("\n") - .filter(Boolean) - .map((line) => JSON.parse(line) as string[]); - const reviewCommentListFetches = calls.filter( - (args) => - args[0] === "api" && - (args[1] ?? "").includes("/issues/321/comments") && - args.includes("--paginate"), - ); - assert.equal(reviewCommentListFetches.length, 2); - assert.deepEqual(JSON.parse(readFileSync(reportPath, "utf8")), [ - { - number: 321, - action: "review_comment_synced", - reason: "updated durable Codex review comment", - }, - ]); - assert.match(readFileSync(join(itemsDir, "321.md"), "utf8"), /^review_comment_synced_at: /m); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("apply-decisions rejects stale fallback after malformed mutation response", () => { - const root = mkdtempSync(tmpPrefix); - try { - const itemsDir = join(root, "items"); - const closedDir = join(root, "closed"); - const plansDir = join(root, "plans"); - const reportPath = join(root, "apply-report.json"); - const logPath = join(root, "gh.log"); - mkdirSync(itemsDir, { recursive: true }); - mkdirSync(plansDir, { recursive: true }); - - const report = workPlanCandidateReport({ - number: 321, - reviewed_at: "2026-05-01T00:05:00Z", - item_snapshot_hash: "reviewed-snapshot-321", - item_updated_at: "2026-05-01T00:00:00Z", - triage_priority: "P1", - }); - writeFileSync(join(itemsDir, "321.md"), report, "utf8"); - const placeholder = renderReviewStartStatusComment({ - number: 321, - kind: "issue", - title: "Render work plans", - }); - - const ghMock = ` -const { appendFileSync, readFileSync } = require("fs"); -const logPath = ${JSON.stringify(logPath)}; -const placeholder = ${JSON.stringify(placeholder)}; -const rawArgs = process.argv.slice(2); -const args = rawArgs[0] === "--repo" ? rawArgs.slice(2) : rawArgs; -appendFileSync(logPath, JSON.stringify(args) + "\\n"); -const path = args.includes("-i") ? args[args.indexOf("-i") + 1] : args[1] || ""; -const commentMatch = path.match(/\\/issues\\/(\\d+)\\/comments(?:\\?|$)/); -const issueMatch = path.match(/\\/issues\\/(\\d+)$/); -if (args[0] === "api" && /\\/issues\\/comments\\/\\d+$/.test(path)) { - const inputPath = args[args.indexOf("--input") + 1]; - const body = JSON.parse(readFileSync(inputPath, "utf8")).body; - appendFileSync(logPath, JSON.stringify(["comment-patch", body]) + "\\n"); - process.stdout.write("{not-json"); -} else if (args[0] === "api" && commentMatch) { - console.log(JSON.stringify([[{ - id: 9321, - html_url: "https://github.com/openclaw/clawsweeper/issues/321#issuecomment-9321", - created_at: "2026-05-01T00:01:00Z", - updated_at: "2026-05-01T00:01:00Z", - user: { login: "clawsweeper[bot]" }, - body: placeholder - }]])); -} else if (args[0] === "api" && /\\/issues\\/321\\/timeline/.test(path)) { - console.log(JSON.stringify([{ - id: 1, - event: "commented", - created_at: "2026-05-01T00:01:00Z", - actor: { login: "clawsweeper[bot]" } - }])); -} else if (args[0] === "api" && issueMatch) { - console.log(JSON.stringify({ - number: 321, - title: "Render work plans", - html_url: "https://github.com/openclaw/clawsweeper/issues/321", - created_at: "2026-05-01T00:00:00Z", - updated_at: "2026-05-01T00:01:01Z", - closed_at: null, - state: "open", - locked: false, - active_lock_reason: null, - author_association: "CONTRIBUTOR", - user: { login: "reporter" }, - labels: [], - comments: 1, - pull_request: null - })); -} else if (args[0] === "issue" && args[1] === "view") { - console.log(JSON.stringify({ closedByPullRequestsReferences: [] })); -} else if (args[0] === "label" && args[1] === "create") { - console.log(""); -} else if (args[0] === "issue" && args[1] === "edit") { - console.log(""); -} else { - console.error("unexpected gh args", JSON.stringify(args)); - process.exit(1); -} -`; - assert.throws( - () => { - withMockGh(root, ghMock, () => { - runApplyDecisionsForTest({ itemsDir, closedDir, plansDir, reportPath }); - }); - }, - (error) => { - assert.match(String(error), /did not return or expose the synced review comment/); - return true; - }, - ); - - assert.equal(existsSync(reportPath), false); - assert.doesNotMatch( - readFileSync(join(itemsDir, "321.md"), "utf8"), - /^review_comment_synced_at: /m, - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - test("apply-decisions syncs labels when first review placeholder advanced issue updated_at", () => { const root = mkdtempSync(tmpPrefix); try { diff --git a/test/repair/apply-result.test.ts b/test/repair/apply-result.test.ts index a11a0ec86e..1ddaf26fa9 100644 --- a/test/repair/apply-result.test.ts +++ b/test/repair/apply-result.test.ts @@ -482,69 +482,6 @@ test("repair apply checks superseded candidate PR coverage before canonical issu } }); -test("repair apply includes recent covering PR comments in coverage proof", () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-apply-result-")); - try { - const paths = writeApplyFixture(tmp, { - action: "close_duplicate", - classification: "duplicate", - canonical: "#202", - }); - writeFakeGh(paths.binDir, { - issues: { - 101: issue({ number: 101, title: "Add config validation", pullRequest: true }), - 202: issue({ - number: 202, - title: "Rewrite config validation", - pullRequest: true, - labels: ["proof: sufficient"], - }), - }, - pulls: { - 101: pull({ number: 101, title: "Add config validation" }), - 202: pull({ number: 202, title: "Rewrite config validation" }), - }, - comments: { - 101: [comment("alice", "PR A keeps legacy config behavior intact.")], - 202: Array.from({ length: 160 }, (_, index) => - comment( - "bob", - index === 159 - ? "Recent discussion: PR B carries forward the legacy behavior." - : `Older discussion ${index}`, - ), - ), - }, - logPath: paths.ghLogPath, - }); - writeFakeCodex(paths.binDir); - - runApplyResult(paths, { - proofDecision: "covered", - expectedPromptIncludes: "Recent discussion: PR B carries forward the legacy behavior.", - }); - - const report = JSON.parse(fs.readFileSync(paths.reportPath, "utf8")); - assert.equal(report.actions[0].status, "executed"); - assert.equal(hasPrCloseCall(paths.ghLogPath), true); - const coveringCommentFetches = ghCalls(paths.ghLogPath).filter( - (call) => - call.args[0] === "api" && - call.args[1].includes("/issues/202/comments") && - !call.args.includes("--method"), - ); - assert.equal( - coveringCommentFetches.some((call) => call.args.includes("--slurp")), - false, - ); - assert.equal(coveringCommentFetches.length, 2); - assert.match(coveringCommentFetches[0].args[1], /[?&]per_page=25(?:&|$)/); - assert.match(coveringCommentFetches[1].args[1], /[?&]per_page=100(?:&|$)/); - } finally { - fs.rmSync(tmp, { recursive: true, force: true }); - } -}); - test("repair apply bounds covering PR comments when issue comment count is absent", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-apply-result-")); try { @@ -1298,63 +1235,34 @@ function runApplyResult( ) { const args = ["dist/repair/apply-result.js", paths.jobPath, paths.resultPath]; if (options.allowMissingUpdatedAt) args.push("--allow-missing-updated-at"); - const env = applyResultEnv(paths, options); execFileSync(process.execPath, args, { cwd: repoRoot, - env, + env: { + ...process.env, + CLAWSWEEPER_ALLOW_EXECUTE: "1", + CLAWSWEEPER_ALLOWED_OWNER: "openclaw", + CLAWSWEEPER_MODEL: "model-test", + CLAWSWEEPER_PR_CLOSE_COVERAGE_PROOF_TIMEOUT_MS: "10000", + GH_TOKEN: "write-token", + PATH: `${paths.binDir}${path.delimiter}${process.env.PATH ?? ""}`, + PR_CLOSE_COVERAGE_PROOF_DECISION: options.proofDecision, + PR_CLOSE_COVERAGE_PROOF_EXPECT_PROMPT: options.expectedPromptIncludes ?? "", + PR_CLOSE_COVERAGE_PROOF_UNEXPECTED_PROMPT: options.unexpectedPromptIncludes ?? "", + PR_CLOSE_COVERAGE_PROOF_FAIL_IF_INVOKED: options.failIfProofRuns ? "1" : "", + PR_CLOSE_COVERAGE_PROOF_FAILURE_MESSAGE: options.proofFailureMessage ?? "", + PR_CLOSE_COVERAGE_PROOF_AFTER_PROOF_PATH: options.afterProofPath ?? "", + }, stdio: "pipe", }); } -function applyResultEnv( - paths: ApplyFixturePaths, - options: Parameters[1], -): NodeJS.ProcessEnv { - const env: NodeJS.ProcessEnv = { - ...process.env, - CLAWSWEEPER_ALLOW_EXECUTE: "1", - CLAWSWEEPER_ALLOWED_OWNER: "openclaw", - CLAWSWEEPER_MODEL: "model-test", - CLAWSWEEPER_PR_CLOSE_COVERAGE_PROOF_TIMEOUT_MS: "10000", - GH_TOKEN: "write-token", - PR_CLOSE_COVERAGE_PROOF_DECISION: options.proofDecision, - PR_CLOSE_COVERAGE_PROOF_EXPECT_PROMPT: options.expectedPromptIncludes ?? "", - PR_CLOSE_COVERAGE_PROOF_UNEXPECTED_PROMPT: options.unexpectedPromptIncludes ?? "", - PR_CLOSE_COVERAGE_PROOF_FAIL_IF_INVOKED: options.failIfProofRuns ? "1" : "", - PR_CLOSE_COVERAGE_PROOF_FAILURE_MESSAGE: options.proofFailureMessage ?? "", - PR_CLOSE_COVERAGE_PROOF_AFTER_PROOF_PATH: options.afterProofPath ?? "", - }; - const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH"; - const testPath = `${paths.binDir}${path.delimiter}${env[pathKey] ?? ""}`; - env[pathKey] = testPath; - env.PATH = testPath; - if (process.platform === "win32") { - env.NODE_OPTIONS = prependNodeRequireOption( - env.NODE_OPTIONS, - path.join(paths.binDir, "fake-gh-preload.cjs"), - ); - } - return env; -} - -function prependNodeRequireOption(existing: string | undefined, modulePath: string): string { - const normalizedPath = process.platform === "win32" ? modulePath.replace(/\\/g, "/") : modulePath; - const requireOption = `--require=${JSON.stringify(normalizedPath)}`; - return existing ? `${requireOption} ${existing}` : requireOption; -} - function writeFakeGh(binDir: string, data: FakeGhData) { - const ghScriptPath = path.join(binDir, process.platform === "win32" ? "gh.js" : "gh"); fs.writeFileSync( - ghScriptPath, + path.join(binDir, "gh"), `#!/usr/bin/env node const fs = require("node:fs"); const path = require("node:path"); -const windowsPreloadedGh = path.basename(process.execPath).toLowerCase() === "gh.exe"; -const args = process.argv.slice(windowsPreloadedGh ? 1 : 2); -if (windowsPreloadedGh && args[0]) { - args[0] = path.basename(args[0]); -} +const args = process.argv.slice(2); const data = ${JSON.stringify(data)}; fs.appendFileSync(data.logPath, JSON.stringify({ args }) + "\\n"); const includeHeaders = args[0] === "api" && args[1] === "-i"; @@ -1500,19 +1408,6 @@ process.exit(1); `, { mode: 0o755 }, ); - if (process.platform === "win32") { - fs.copyFileSync(process.execPath, path.join(binDir, "gh.exe")); - fs.writeFileSync( - path.join(binDir, "fake-gh-preload.cjs"), - `const path = require("node:path"); -if (path.basename(process.execPath).toLowerCase() === "gh.exe") { - require(path.join(__dirname, "gh.js")); - process.exit(process.exitCode || 0); -} -`, - "utf8", - ); - } } function writeFakeCodex(binDir: string) {