From 5474caa6504ae3f9a204c2a66632c6636f7dd1c7 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 3 Aug 2026 13:49:11 -0400 Subject: [PATCH 1/5] fix(advisor): preserve complete PR diffs --- test/pr-review-advisor-diff.test.ts | 74 +++++++++++++++++++++++++++++ tools/advisors/git.mts | 14 +++--- tools/pr-review-advisor/README.md | 4 +- tools/pr-review-advisor/analyze.mts | 4 +- 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 test/pr-review-advisor-diff.test.ts diff --git a/test/pr-review-advisor-diff.test.ts b/test/pr-review-advisor-diff.test.ts new file mode 100644 index 0000000000..c0d7de954a --- /dev/null +++ b/test/pr-review-advisor-diff.test.ts @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { getDiff } from "../tools/advisors/git.mts"; + +describe("PR review advisor diff", () => { + it("keeps content after 160,000 characters", () => { + const tmp = fs.mkdtempSync(path.join(tmpdir(), "nemoclaw-pr-advisor-diff-")); + const previousCwd = process.cwd(); + let diff = ""; + + try { + execFileSync("git", ["init", "--quiet"], { cwd: tmp }); + fs.writeFileSync(path.join(tmp, "review.txt"), "base\n"); + execFileSync("git", ["add", "review.txt"], { cwd: tmp }); + execFileSync( + "git", + [ + "-c", + "user.name=NemoClaw Test", + "-c", + "user.email=nemoclaw-test@example.com", + "-c", + "commit.gpgsign=false", + "commit", + "--quiet", + "-m", + "base", + ], + { cwd: tmp }, + ); + const base = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: tmp, + encoding: "utf8", + }).trim(); + + fs.writeFileSync( + path.join(tmp, "review.txt"), + `${"x".repeat(170_000)}\ncomplete-diff-tail\n`, + ); + execFileSync("git", ["add", "review.txt"], { cwd: tmp }); + execFileSync( + "git", + [ + "-c", + "user.name=NemoClaw Test", + "-c", + "user.email=nemoclaw-test@example.com", + "-c", + "commit.gpgsign=false", + "commit", + "--quiet", + "-m", + "head", + ], + { cwd: tmp }, + ); + + process.chdir(tmp); + diff = getDiff(base, "HEAD"); + } finally { + process.chdir(previousCwd); + fs.rmSync(tmp, { recursive: true, force: true }); + } + + expect(diff).toContain("complete-diff-tail"); + expect(diff).not.toContain("`; -} diff --git a/tools/pr-review-advisor/README.md b/tools/pr-review-advisor/README.md index ba65f84dac..14e4265e18 100644 --- a/tools/pr-review-advisor/README.md +++ b/tools/pr-review-advisor/README.md @@ -155,7 +155,7 @@ instead of failing closed without artifacts. - `prompts/00-system.md` — system prompt sent to the advisor. - `prompts/01-scope-risk-map-analysis.md` through `prompts/16-validate-synthesis-json.md` — seven alternating analysis/commit pairs followed by draft and validation synthesis turns in the same session, in execution order. -- `prompts/*.tool-results/` — bounded deterministic, domain-specific context payloads exposed as real tools after the matching user turn. The untrusted truncated diff appears only in the first turn, and repeated risk-plan projections use capped path samples. +- `prompts/*.tool-results/` — deterministic, domain-specific context payloads exposed as real tools after the matching user turn. The complete untrusted diff appears only in the first turn, and repeated risk-plan projections use capped path samples. - `turns/01-scope-risk-map-analysis.txt` through `turns/16-validate-synthesis-json.txt` — assistant output and completed/failed/timed-out status written as each turn settles. - `context/drift-context.json` — deterministic drift and overlap context. - `context/security-context.json` — deterministic security-risk context and the risk plan for the @@ -163,7 +163,7 @@ instead of failing closed without artifacts. - `context/validation-context.json` — deterministic acceptance, source-of-truth, static test-inventory, simplification-signal, and risk plan for the PR SHA, including the regression invariants reviewed for the PR. -- `context/pr.diff` — truncated PR diff used by the advisor. +- `context/pr.diff` — complete PR diff used by the advisor. - `pr-review-advisor-raw-output.txt` — raw multi-turn advisor transcript and diagnostics. - `pr-review-advisor-result.json` — normalized advisor result with findings projected from the canonical open ledger records, or execution metadata when analysis is unavailable. - `pr-review-advisor-final-result.json` — normalized canonical result used for comments. diff --git a/tools/pr-review-advisor/analyze.mts b/tools/pr-review-advisor/analyze.mts index f6b81bc153..9e5c1af693 100755 --- a/tools/pr-review-advisor/analyze.mts +++ b/tools/pr-review-advisor/analyze.mts @@ -360,7 +360,7 @@ async function main(): Promise { const schema = readJson>(schemaPath); const changedFiles = getChangedFiles(baseRef, headRef); const headSha = getHeadSha(headRef); - const diff = getDiff(baseRef, headRef, 160000); + const diff = getDiff(baseRef, headRef); const deterministic = await collectDeterministicContext({ baseRef, headRef, @@ -1808,7 +1808,7 @@ export function buildPromptTurns({ "pr_review_git_diff", diff || "", "diff", - "truncated git diff", + "complete git diff", ), ], prompt: `${stageAnalysisProtocol( From 9cd87c4d5de56461baa698019f3430c6552689ad Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 3 Aug 2026 14:06:05 -0400 Subject: [PATCH 2/5] fix(advisor): preserve early failure artifacts Signed-off-by: Julie Yaunches --- test/pr-review-advisor-diff.test.ts | 72 +++++++++++++++++++ ...r-review-advisor-workflow-boundary.test.ts | 40 ++++++++--- tools/pr-review-advisor/run-analysis.mts | 26 +++++-- 3 files changed, 122 insertions(+), 16 deletions(-) diff --git a/test/pr-review-advisor-diff.test.ts b/test/pr-review-advisor-diff.test.ts index c0d7de954a..4688f508f1 100644 --- a/test/pr-review-advisor-diff.test.ts +++ b/test/pr-review-advisor-diff.test.ts @@ -71,4 +71,76 @@ describe("PR review advisor diff", () => { expect(diff).toContain("complete-diff-tail"); expect(diff).not.toContain(" { + const tmp = fs.mkdtempSync(path.join(tmpdir(), "nemoclaw-pr-advisor-diff-")); + const previousCwd = process.cwd(); + let diff = ""; + + try { + execFileSync("git", ["init", "--quiet"], { cwd: tmp }); + fs.writeFileSync(path.join(tmp, "base.txt"), "base\n"); + execFileSync("git", ["add", "base.txt"], { cwd: tmp }); + commit(tmp, "base"); + const base = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: tmp, + encoding: "utf8", + }).trim(); + + execFileSync("git", ["checkout", "--orphan", "unrelated", "--quiet"], { cwd: tmp }); + execFileSync("git", ["rm", "-rf", ".", "--quiet"], { cwd: tmp }); + fs.writeFileSync(path.join(tmp, "head.txt"), "fallback-tail\n"); + execFileSync("git", ["add", "head.txt"], { cwd: tmp }); + commit(tmp, "head"); + + process.chdir(tmp); + diff = getDiff(base, "HEAD"); + } finally { + process.chdir(previousCwd); + fs.rmSync(tmp, { recursive: true, force: true }); + } + + expect(diff).toContain("fallback-tail"); + }); + + it("fails when neither diff form can resolve the requested ref", () => { + const tmp = fs.mkdtempSync(path.join(tmpdir(), "nemoclaw-pr-advisor-diff-")); + const previousCwd = process.cwd(); + + try { + execFileSync("git", ["init", "--quiet"], { cwd: tmp }); + fs.writeFileSync(path.join(tmp, "review.txt"), "base\n"); + execFileSync("git", ["add", "review.txt"], { cwd: tmp }); + commit(tmp, "base"); + const base = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: tmp, + encoding: "utf8", + }).trim(); + + process.chdir(tmp); + expect(() => getDiff(base, "missing-ref")).toThrow("failed to read complete diff"); + } finally { + process.chdir(previousCwd); + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); }); + +function commit(cwd: string, message: string): void { + execFileSync( + "git", + [ + "-c", + "user.name=NemoClaw Test", + "-c", + "user.email=nemoclaw-test@example.com", + "-c", + "commit.gpgsign=false", + "commit", + "--quiet", + "-m", + message, + ], + { cwd }, + ); +} diff --git a/test/pr-review-advisor-workflow-boundary.test.ts b/test/pr-review-advisor-workflow-boundary.test.ts index 03c2cd53d8..894c09d39c 100644 --- a/test/pr-review-advisor-workflow-boundary.test.ts +++ b/test/pr-review-advisor-workflow-boundary.test.ts @@ -893,17 +893,39 @@ process.exitCode = valid ? 0 : 1;`, } }); - it("fails the supported advisor lane when analyze exits non-zero", () => { - const input = advisorAnalysisInput(); + it("writes failure artifacts when analysis exits before producing artifacts", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pr-review-advisor-failure-")); + const input = advisorAnalysisInput({ outDir: path.join(tmp, "artifacts") }); const analyzePath = path.join(input.advisorDir, "tools", "pr-review-advisor", "analyze.mts"); - expect(() => - runPrReviewAdvisorAnalysis(input, { - fileExists: (file) => file === analyzePath, - readText: supportedAdvisorReadText(input), - runNode: () => 17, - }), - ).toThrow("analyze.mts exited with status 17"); + try { + expect(() => + runPrReviewAdvisorAnalysis(input, { + fileExists: (file) => file === analyzePath, + readText: supportedAdvisorReadText(input), + runGit: () => HEAD_SHA, + runNode: () => 17, + }), + ).toThrow("analyze.mts exited with status 17"); + + const result = JSON.parse( + fs.readFileSync(path.join(input.outDir, "pr-review-advisor-result.json"), "utf8"), + ); + const finalResult = JSON.parse( + fs.readFileSync(path.join(input.outDir, "pr-review-advisor-final-result.json"), "utf8"), + ); + expect(result).toMatchObject({ + failed: true, + reason: "analyze.mts exited with status 17", + }); + expect(finalResult).toMatchObject({ + headSha: HEAD_SHA, + summary: { recommendation: "info_only", confidence: "low" }, + reviewCompleteness: { requiresHumanReview: true }, + }); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } }); it("runs analyze in unavailable-result mode when the trusted checkout lacks model support", () => { diff --git a/tools/pr-review-advisor/run-analysis.mts b/tools/pr-review-advisor/run-analysis.mts index efd840b952..866236e500 100755 --- a/tools/pr-review-advisor/run-analysis.mts +++ b/tools/pr-review-advisor/run-analysis.mts @@ -62,9 +62,10 @@ function defaultInput(env = process.env): RunAnalysisInput { }; } -function writeBootstrapUnavailableResult( +function writeBootstrapResult( input: RunAnalysisInput, reason: string, + failed: boolean, options: Required>, ): void { options.mkdir(input.outDir); @@ -83,7 +84,9 @@ function writeBootstrapUnavailableResult( summary: { recommendation: "info_only", confidence: "low", - oneLine: `PR review advisor skipped: ${reason}`, + oneLine: failed + ? `PR review advisor failed: ${reason}` + : `PR review advisor skipped: ${reason}`, }, findings: [], terminologyReview: { @@ -96,7 +99,9 @@ function writeBootstrapUnavailableResult( { category: "Holistic Security Posture", verdict: "warning", - justification: "Advisor bootstrap skip; human review required.", + justification: failed + ? "Advisor bootstrap failed; human review required." + : "Advisor bootstrap skipped; human review required.", }, ], sourceOfTruthReview: [], @@ -124,7 +129,7 @@ function writeBootstrapUnavailableResult( }; options.writeFile( path.join(input.outDir, "pr-review-advisor-result.json"), - `${JSON.stringify({ skipped: true, reason }, null, 2)}\n`, + `${JSON.stringify(failed ? { failed: true, reason } : { skipped: true, reason }, null, 2)}\n`, ); options.writeFile( path.join(input.outDir, "pr-review-advisor-final-result.json"), @@ -132,7 +137,7 @@ function writeBootstrapUnavailableResult( ); options.writeFile( path.join(input.outDir, "pr-review-advisor-summary.md"), - `# ${input.title}\n\nAdvisor analysis skipped.\n\nReason: ${reason}\n`, + `# ${input.title}\n\nAdvisor analysis ${failed ? "failed" : "skipped"}.\n\nReason: ${reason}\n`, ); } @@ -203,9 +208,10 @@ export function runPrReviewAdvisorAnalysis( if (!fileExists(analyzePath)) { console.log("Skipping PR review advisor: trusted base checkout does not contain analyze.mts"); - writeBootstrapUnavailableResult( + writeBootstrapResult( input, "Trusted base checkout does not contain tools/pr-review-advisor/analyze.mts; advisor will run after the implementation lands on the base branch.", + false, { mkdir, writeFile, @@ -271,7 +277,13 @@ export function runPrReviewAdvisorAnalysis( appendEnv("PR_REVIEW_ADVISOR_SUPPORTED", "1"); const code = runNode(analyzePath, analysisArgs, inheritedEnv, input.advisorWorkdir); - if (code !== 0) throw new RunAnalysisError(`analyze.mts exited with status ${code}`); + if (code !== 0) { + const reason = `analyze.mts exited with status ${code}`; + if (!fileExists(path.join(input.outDir, "pr-review-advisor-final-result.json"))) { + writeBootstrapResult(input, reason, true, { mkdir, writeFile, runGit }); + } + throw new RunAnalysisError(reason); + } } function main(): void { From e16ad993c35e5138ad20177dd8cab9f30596f6ad Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 3 Aug 2026 14:21:08 -0400 Subject: [PATCH 3/5] fix(advisor): preserve partial failure artifacts Signed-off-by: Julie Yaunches --- ...r-review-advisor-workflow-boundary.test.ts | 30 +++++++++++++++++++ tools/pr-review-advisor/run-analysis.mts | 15 +++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/test/pr-review-advisor-workflow-boundary.test.ts b/test/pr-review-advisor-workflow-boundary.test.ts index 894c09d39c..eff62804f9 100644 --- a/test/pr-review-advisor-workflow-boundary.test.ts +++ b/test/pr-review-advisor-workflow-boundary.test.ts @@ -928,6 +928,36 @@ process.exitCode = valid ? 0 : 1;`, } }); + it("preserves partial artifacts while completing an early analysis failure", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pr-review-advisor-failure-")); + const input = advisorAnalysisInput({ outDir: path.join(tmp, "artifacts") }); + const analyzePath = path.join(input.advisorDir, "tools", "pr-review-advisor", "analyze.mts"); + const resultPath = path.join(input.outDir, "pr-review-advisor-result.json"); + const partialResult = '{"failed":true,"partial":true}\n'; + + try { + fs.mkdirSync(input.outDir, { recursive: true }); + fs.writeFileSync(resultPath, partialResult); + + expect(() => + runPrReviewAdvisorAnalysis(input, { + fileExists: (file) => file === analyzePath || fs.existsSync(file), + readText: supportedAdvisorReadText(input), + runGit: () => HEAD_SHA, + runNode: () => 17, + }), + ).toThrow("analyze.mts exited with status 17"); + + expect(fs.readFileSync(resultPath, "utf8")).toBe(partialResult); + expect(fs.existsSync(path.join(input.outDir, "pr-review-advisor-final-result.json"))).toBe( + true, + ); + expect(fs.existsSync(path.join(input.outDir, "pr-review-advisor-summary.md"))).toBe(true); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + it("runs analyze in unavailable-result mode when the trusted checkout lacks model support", () => { const appendedEnv: Array<[string, string]> = []; const runCalls: Array<{ diff --git a/tools/pr-review-advisor/run-analysis.mts b/tools/pr-review-advisor/run-analysis.mts index 866236e500..4021242fba 100755 --- a/tools/pr-review-advisor/run-analysis.mts +++ b/tools/pr-review-advisor/run-analysis.mts @@ -280,7 +280,20 @@ export function runPrReviewAdvisorAnalysis( if (code !== 0) { const reason = `analyze.mts exited with status ${code}`; if (!fileExists(path.join(input.outDir, "pr-review-advisor-final-result.json"))) { - writeBootstrapResult(input, reason, true, { mkdir, writeFile, runGit }); + try { + const writeMissingFile = (file: string, text: string): void => { + if (!fileExists(file)) writeFile(file, text); + }; + writeBootstrapResult(input, reason, true, { + mkdir, + writeFile: writeMissingFile, + runGit, + }); + } catch (error) { + console.error( + `Could not complete missing PR review advisor failure artifacts: ${error instanceof Error ? error.message : String(error)}`, + ); + } } throw new RunAnalysisError(reason); } From 0ba5ed4aa49711bb2374f634424b6039ada4a406 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 3 Aug 2026 14:29:00 -0400 Subject: [PATCH 4/5] fix(advisor): write pre-session failure artifacts Signed-off-by: Julie Yaunches --- test/pr-review-advisor-diff.test.ts | 39 ++++++++++++- tools/pr-review-advisor/analyze.mts | 90 +++++++++++++++++++++++++---- 2 files changed, 117 insertions(+), 12 deletions(-) diff --git a/test/pr-review-advisor-diff.test.ts b/test/pr-review-advisor-diff.test.ts index 4688f508f1..9cddec2fcb 100644 --- a/test/pr-review-advisor-diff.test.ts +++ b/test/pr-review-advisor-diff.test.ts @@ -1,13 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import fs from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { getDiff } from "../tools/advisors/git.mts"; +const ROOT = path.resolve(import.meta.dirname, ".."); + describe("PR review advisor diff", () => { it("keeps content after 160,000 characters", () => { const tmp = fs.mkdtempSync(path.join(tmpdir(), "nemoclaw-pr-advisor-diff-")); @@ -124,6 +126,41 @@ describe("PR review advisor diff", () => { fs.rmSync(tmp, { recursive: true, force: true }); } }); + + it("writes failure artifacts when trusted Git inputs are unavailable", () => { + const tmp = fs.mkdtempSync(path.join(tmpdir(), "nemoclaw-pr-advisor-diff-")); + const result = spawnSync( + process.execPath, + [ + "--experimental-strip-types", + path.join(ROOT, "tools/pr-review-advisor/analyze.mts"), + "--base", + "missing-ref", + "--head", + "HEAD", + "--schema", + path.join(ROOT, "tools/pr-review-advisor/schema.json"), + "--out-dir", + tmp, + ], + { cwd: ROOT, encoding: "utf8" }, + ); + + try { + expect(result.status).toBe(1); + expect( + JSON.parse(fs.readFileSync(path.join(tmp, "pr-review-advisor-result.json"), "utf8")), + ).toMatchObject({ failed: true }); + expect( + JSON.parse(fs.readFileSync(path.join(tmp, "pr-review-advisor-final-result.json"), "utf8")), + ).toMatchObject({ + headSha: expect.stringMatching(/^[0-9a-f]{40}$/u), + reviewCompleteness: { requiresHumanReview: true }, + }); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); }); function commit(cwd: string, message: string): void { diff --git a/tools/pr-review-advisor/analyze.mts b/tools/pr-review-advisor/analyze.mts index 9e5c1af693..e7bd26a168 100755 --- a/tools/pr-review-advisor/analyze.mts +++ b/tools/pr-review-advisor/analyze.mts @@ -300,6 +300,45 @@ export type DeterministicReviewContext = { github: GitHubReviewContext | null; }; +function preSessionFailureMetadata({ + baseRef, + headRef, + headSha, + changedFiles, + reason, +}: { + baseRef: string; + headRef: string; + headSha: string; + changedFiles: string[]; + reason: string; +}): ReviewMetadata { + return { + baseRef, + headRef, + headSha, + changedFiles, + deterministic: { + diffStat: "", + commits: [], + riskyAreas: [], + riskPlan: buildRiskPlan({ headSha, changedFiles }), + testDepth: { verdict: "unknown", rationale: reason, suggestedTests: [] }, + staticTestInventory: { + changedTestFiles: [], + nearbyTestNames: [], + candidateExistingCoverage: [], + }, + simplificationSignals: [], + workflowSignals: [], + localizedPatchSignals: [], + driftEvidence: [], + previousAdvisorReview: null, + github: null, + }, + }; +} + export type StaticTestInventory = { changedTestFiles: string[]; nearbyTestNames: string[]; @@ -357,17 +396,46 @@ async function main(): Promise { logProgress( `Starting PR review advisor analysis: base=${baseRef} head=${headRef} outDir=${outDir}`, ); - const schema = readJson>(schemaPath); - const changedFiles = getChangedFiles(baseRef, headRef); - const headSha = getHeadSha(headRef); - const diff = getDiff(baseRef, headRef); - const deterministic = await collectDeterministicContext({ - baseRef, - headRef, - headSha, - changedFiles, - diff, - }); + let schema: Record; + let changedFiles: string[] = []; + let headSha = ""; + let diff: string; + let deterministic: DeterministicReviewContext; + try { + schema = readJson>(schemaPath); + changedFiles = getChangedFiles(baseRef, headRef); + headSha = getHeadSha(headRef); + diff = getDiff(baseRef, headRef); + deterministic = await collectDeterministicContext({ + baseRef, + headRef, + headSha, + changedFiles, + diff, + }); + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + if (!headSha) { + try { + headSha = getHeadSha(headRef); + } catch { + headSha = "unavailable"; + } + } + try { + writeUnavailableArtifacts( + artifacts, + preSessionFailureMetadata({ baseRef, headRef, headSha, changedFiles, reason }), + reason, + true, + ); + } catch (artifactError) { + console.error( + `Could not write PR review advisor pre-session failure artifacts: ${artifactError instanceof Error ? artifactError.message : String(artifactError)}`, + ); + } + throw error; + } // GitHub context is fully materialized before the model session starts. Keep // repository credentials out of the environment inherited by read-only tools. delete process.env.GH_TOKEN; From 72b61de2cb58bfa5c8b9edbe461983783bdb81f6 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 3 Aug 2026 14:40:09 -0400 Subject: [PATCH 5/5] test(advisor): secure partial artifact fixture Signed-off-by: Julie Yaunches --- test/pr-review-advisor-workflow-boundary.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pr-review-advisor-workflow-boundary.test.ts b/test/pr-review-advisor-workflow-boundary.test.ts index eff62804f9..501f29bf67 100644 --- a/test/pr-review-advisor-workflow-boundary.test.ts +++ b/test/pr-review-advisor-workflow-boundary.test.ts @@ -937,7 +937,7 @@ process.exitCode = valid ? 0 : 1;`, try { fs.mkdirSync(input.outDir, { recursive: true }); - fs.writeFileSync(resultPath, partialResult); + fs.writeFileSync(resultPath, partialResult, { flag: "wx", mode: 0o600 }); expect(() => runPrReviewAdvisorAnalysis(input, {