import assert from "node:assert/strict"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { checkedSpawn } from "./audit.bench.mjs"; import { assertLatest, collectPackageVersions, compareVersions, createReport, extractCandidates, parseChangelog, resolvePiInstallation, } from "./audit.mjs"; const changelog = `# Changelog ## [2.0.0] - 2026-07-13 ### Added - Added harmless sparkle output. ### Changed - Renamed \`oldCall()\` to \`newCall()\`. ### Fixed - Removed the obsolete \`legacyOption\` request field. - Fixed ordinary rendering noise. ## [1.5.0] - 2026-07-01 ### Breaking Changes - Replace \`alpha()\` with \`beta()\`. ## [1.0.0] - 2026-06-01 ### Added - Initial release. `; test("parses release sections newest first", () => { assert.deepEqual( parseChangelog(changelog).map(({ version }) => version), ["2.0.0", "1.5.0", "1.0.0"], ); }); test("extracts migration sections and keyword bullets in the version interval", () => { assert.deepEqual( extractCandidates(changelog, { afterVersion: "1.5.0", throughVersion: "2.0.0", }).map(({ section, text }) => [section, text]), [ ["Changed", "Renamed `oldCall()` to `newCall()`."], ["Fixed", "Removed the obsolete `legacyOption` request field."], ], ); }); test("rejects changelogs without release sections", () => { assert.throws(() => parseChangelog("# Changelog\n\nNothing yet."), /release/); }); test("compares semantic versions numerically", () => { assert.equal(compareVersions("0.80.10", "0.80.6"), 1); assert.equal(compareVersions("0.80.6", "0.80.6"), 0); assert.equal(compareVersions("0.79.9", "0.80.1"), -1); }); test("fails when installed Pi is not latest", () => { assert.doesNotThrow(() => assertLatest("0.80.6", "0.80.6")); assert.throws(() => assertLatest("0.80.5", "0.80.6"), /not latest/); }); test("resolves the installed Pi package from launcher and CLI paths", (context) => { const directory = mkdtempSync(path.join(tmpdir(), "pi-modernize-")); context.after(() => rmSync(directory, { recursive: true, force: true })); const packageRoot = path.join( directory, "node_modules", "@earendil-works", "pi-coding-agent", ); const cli = path.join(packageRoot, "dist", "cli.js"); const changelog = path.join(packageRoot, "CHANGELOG.md"); const launcher = path.join(directory, "pi.CMD"); mkdirSync(path.dirname(cli), { recursive: true }); writeFileSync( path.join(packageRoot, "package.json"), JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: "0.85.0", bin: { pi: "dist/cli.js" }, }), ); writeFileSync(cli, ""); writeFileSync(changelog, ""); writeFileSync(launcher, ""); const expected = { cli, changelog, version: "0.85.0" }; assert.deepEqual(resolvePiInstallation(launcher), expected); assert.deepEqual(resolvePiInstallation(cli), expected); }); test("fails when no installed Pi package is near the launcher", (context) => { const directory = mkdtempSync(path.join(tmpdir(), "pi-modernize-")); context.after(() => rmSync(directory, { recursive: true, force: true })); const launcher = path.join(directory, "pi"); writeFileSync(launcher, ""); assert.throws( () => resolvePiInstallation(launcher), /installed @earendil-works\/pi-coding-agent package not found/, ); }); test("reports inherited-stdio child failures by exit code", () => { assert.throws( () => checkedSpawn(process.execPath, ["-e", "process.exit(7)"], { stdio: "inherit", }), /exited with code 7/, ); }); test("collects resolved earendil Pi package versions", () => { assert.deepEqual( collectPackageVersions({ packages: { "node_modules/@earendil-works/pi-ai": { version: "0.80.4" }, "extensions/x/node_modules/@earendil-works/pi-ai": { version: "0.80.4", }, "node_modules/@earendil-works/pi-coding-agent": { version: "0.80.6", }, "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { version: "0.1.7", }, "node_modules/vitest": { version: "4.1.9" }, }, }), [ { name: "@earendil-works/pi-ai", version: "0.80.4" }, { name: "@earendil-works/pi-coding-agent", version: "0.80.6" }, ], ); }); test("reports only evidence needed before tool-driven inspection", () => { assert.deepEqual( createReport({ piVersion: "2.0.0", candidatesAfterVersion: "1.5.0", localChangelogPath: "/pi/CHANGELOG.md", remoteChangelogUrl: "https://example.test/CHANGELOG.md", candidates: [{ version: "2.0.0", text: "Replace alpha." }], }), { schemaVersion: 2, status: "ok", piVersion: "2.0.0", candidatesAfterVersion: "1.5.0", changelogs: { installed: "/pi/CHANGELOG.md", remote: "https://example.test/CHANGELOG.md", }, candidates: [{ version: "2.0.0", text: "Replace alpha." }], }, ); });