Skip to content

Commit 75b49c0

Browse files
committed
test(release): cover handoff summary behavior
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
1 parent 1b36f70 commit 75b49c0

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

test/handoff-summary.test.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { execFileSync, spawnSync } from "node:child_process";
5+
import fs from "node:fs";
6+
import os from "node:os";
7+
import path from "node:path";
8+
9+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
10+
11+
import {
12+
buildHandoffSummary,
13+
renderHandoffMarkdown,
14+
} from "../.agents/skills/nemoclaw-maintainer-day/scripts/handoff-summary.ts";
15+
16+
const SCRIPT = path.join(
17+
process.cwd(),
18+
".agents",
19+
"skills",
20+
"nemoclaw-maintainer-day",
21+
"scripts",
22+
"handoff-summary.ts",
23+
);
24+
25+
function git(cwd: string, ...args: string[]): string {
26+
return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
27+
}
28+
29+
function runCli(cwd: string, ...args: string[]) {
30+
return spawnSync(
31+
process.execPath,
32+
["--experimental-strip-types", "--no-warnings", SCRIPT, ...args],
33+
{ cwd, encoding: "utf8" },
34+
);
35+
}
36+
37+
function expectMarkdownList(markdown: string, values: string[]): void {
38+
for (const value of values) expect(markdown).toContain(`- ${value}`);
39+
}
40+
41+
describe("release handoff summary", () => {
42+
it("classifies the release range and renders its QA focus", () => {
43+
const previous = "1".repeat(40);
44+
const candidate = "2".repeat(40);
45+
const results = new Map([
46+
[`rev-parse ${candidate}^{commit}`, candidate],
47+
[`merge-base ${previous} ${candidate}`, previous],
48+
[`rev-list --count ${previous}..${candidate}`, "2"],
49+
[
50+
`diff --name-only ${previous}..${candidate}`,
51+
[
52+
"install.sh",
53+
"src/lib/onboard/machine/runner.ts",
54+
"nemoclaw/src/blueprint/ssrf.ts",
55+
".github/workflows/e2e.yaml",
56+
"src/lib/inference/client.ts",
57+
"docs/changelog/2026-08-17.mdx",
58+
].join("\n"),
59+
],
60+
]);
61+
const command = (_command: string, args: string[]): string => {
62+
const operation = args.join(" ");
63+
expect(results.has(operation), operation).toBe(true);
64+
return results.get(operation)!;
65+
};
66+
67+
const summary = buildHandoffSummary(
68+
{
69+
previousTag: "v1.2.2",
70+
previousTagCommit: previous,
71+
targetVersion: "v1.2.3",
72+
candidateCommit: candidate,
73+
},
74+
command,
75+
);
76+
77+
expect(summary).toEqual({
78+
previousTag: "v1.2.2",
79+
previousTagCommit: previous,
80+
targetVersion: "v1.2.3",
81+
candidateCommit: candidate,
82+
commitCount: 2,
83+
riskyFileCount: 5,
84+
riskyAreas: [
85+
"Installer / bootstrap",
86+
"Onboarding / host glue",
87+
"Sandbox / policy / SSRF",
88+
"Workflow / enforcement",
89+
"Credentials / inference",
90+
],
91+
suggestedTestFocus: [
92+
"Fresh install and upgrade paths",
93+
"Onboarding wizard and sandbox creation",
94+
"Policy enforcement, network egress, and SSRF protections",
95+
"CI checks, pre-commit hooks, and DCO declarations",
96+
"Credential storage and inference provider routing",
97+
],
98+
});
99+
100+
const markdown = renderHandoffMarkdown(summary);
101+
expect(markdown).toContain(`- Candidate: \`${candidate}\``);
102+
expect(markdown).toContain("- Risky files detected: 5");
103+
expectMarkdownList(markdown, summary.riskyAreas);
104+
expectMarkdownList(markdown, summary.suggestedTestFocus);
105+
});
106+
});
107+
108+
describe("release handoff summary CLI", () => {
109+
let repo: string;
110+
let plan: string;
111+
112+
beforeAll(() => {
113+
repo = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-handoff-summary-"));
114+
git(repo, "init", "--quiet");
115+
git(repo, "config", "user.name", "NemoClaw Test");
116+
git(repo, "config", "user.email", "nemoclaw-test@example.com");
117+
git(repo, "config", "commit.gpgsign", "false");
118+
119+
const fixture = path.join(repo, "release-input.txt");
120+
fs.writeFileSync(fixture, "previous\n");
121+
git(repo, "add", "release-input.txt");
122+
git(repo, "commit", "--quiet", "-m", "test: previous release");
123+
const previous = git(repo, "rev-parse", "HEAD");
124+
125+
fs.appendFileSync(fixture, "candidate\n");
126+
git(repo, "commit", "--quiet", "-am", "test: release candidate");
127+
const candidate = git(repo, "rev-parse", "HEAD");
128+
129+
plan = path.join(repo, "plan.json");
130+
fs.writeFileSync(
131+
plan,
132+
JSON.stringify({
133+
nextTag: "v1.2.3",
134+
originMainCommit: candidate,
135+
originMainHeadline: "test: release candidate",
136+
previousTag: "v1.2.2",
137+
previousTagCommit: previous,
138+
previousTagObject: previous,
139+
}),
140+
);
141+
});
142+
143+
afterAll(() => {
144+
fs.rmSync(repo, { recursive: true, force: true });
145+
});
146+
147+
it("writes a release brief for a valid plan and output path", () => {
148+
const output = path.join(repo, "brief", "valid.md");
149+
const result = runCli(repo, "--plan", plan, "--output", output);
150+
151+
expect(result.status).toBe(0);
152+
expect(result.stderr).toBe("");
153+
expect(result.stdout.trim()).toBe(`Release brief written: ${output}`);
154+
const brief = fs.readFileSync(output, "utf8");
155+
expect(brief).toContain("# NemoClaw v1.2.3 release brief");
156+
expect(brief).toContain("- Commits: 1");
157+
});
158+
159+
it("rejects an invocation without an output path", () => {
160+
const output = path.join(repo, "brief", "missing-output.md");
161+
const result = runCli(repo, "--plan", plan);
162+
163+
expect(result.status).toBe(1);
164+
expect(result.stdout).toBe("");
165+
expect(result.stderr).toContain("usage: handoff-summary.ts --plan PATH --output PATH");
166+
expect(fs.existsSync(output)).toBe(false);
167+
});
168+
169+
it("refuses to overwrite an existing release brief", () => {
170+
const output = path.join(repo, "brief", "existing.md");
171+
fs.mkdirSync(path.dirname(output), { recursive: true });
172+
fs.writeFileSync(output, "existing release brief\n");
173+
174+
const result = runCli(repo, "--plan", plan, "--output", output);
175+
176+
expect(result.status).toBe(1);
177+
expect(result.stdout).toBe("");
178+
expect(result.stderr).toContain("handoff-summary: EEXIST");
179+
expect(fs.readFileSync(output, "utf8")).toBe("existing release brief\n");
180+
});
181+
});

0 commit comments

Comments
 (0)