From 8fade18b985b6778f4913af22876baaa6acd255e Mon Sep 17 00:00:00 2001 From: Advait Paliwal Date: Wed, 15 Apr 2026 17:48:22 -0700 Subject: [PATCH] Add regression coverage for package update reliability --- tests/package-ops.test.ts | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/tests/package-ops.test.ts b/tests/package-ops.test.ts index 4727457..16a56c5 100644 --- a/tests/package-ops.test.ts +++ b/tests/package-ops.test.ts @@ -1,10 +1,10 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { appendFileSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; -import { installPackageSources, seedBundledWorkspacePackages } from "../src/pi/package-ops.js"; +import { installPackageSources, seedBundledWorkspacePackages, updateConfiguredPackages } from "../src/pi/package-ops.js"; function createBundledWorkspace(appRoot: string, packageNames: string[]): void { for (const packageName of packageNames) { @@ -18,6 +18,16 @@ function createBundledWorkspace(appRoot: string, packageNames: string[]): void { } } +function createInstalledGlobalPackage(homeRoot: string, packageName: string, version = "1.0.0"): void { + const packageDir = resolve(homeRoot, "npm-global", "lib", "node_modules", packageName); + mkdirSync(packageDir, { recursive: true }); + writeFileSync( + join(packageDir, "package.json"), + JSON.stringify({ name: packageName, version }, null, 2) + "\n", + "utf8", + ); +} + function writeSettings(agentDir: string, settings: Record): void { mkdirSync(agentDir, { recursive: true }); writeFileSync(resolve(agentDir, "settings.json"), JSON.stringify(settings, null, 2) + "\n", "utf8"); @@ -145,3 +155,101 @@ test("installPackageSources skips native packages on unsupported Node majors bef Object.defineProperty(process.versions, "node", { value: originalVersion, configurable: true }); } }); + +test("updateConfiguredPackages batches multiple npm updates into a single install per scope", async () => { + const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-")); + const workingDir = resolve(root, "project"); + const agentDir = resolve(root, "agent"); + const logPath = resolve(root, "npm-invocations.jsonl"); + mkdirSync(workingDir, { recursive: true }); + + const scriptPath = writeFakeNpmScript(root, [ + `import { appendFileSync } from "node:fs";`, + `import { resolve } from "node:path";`, + `const args = process.argv.slice(2);`, + `if (args.length === 2 && args[0] === "root" && args[1] === "-g") {`, + ` console.log(resolve(${JSON.stringify(root)}, "npm-global", "lib", "node_modules"));`, + ` process.exit(0);`, + `}`, + `appendFileSync(${JSON.stringify(logPath)}, JSON.stringify(args) + "\\n", "utf8");`, + "process.exit(0);", + ].join("\n")); + + writeSettings(agentDir, { + npmCommand: [process.execPath, scriptPath], + packages: ["npm:test-one", "npm:test-two"], + }); + createInstalledGlobalPackage(root, "test-one", "1.0.0"); + createInstalledGlobalPackage(root, "test-two", "1.0.0"); + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => ({ + ok: true, + json: async () => ({ version: "2.0.0" }), + })) as typeof fetch; + + try { + const result = await updateConfiguredPackages(workingDir, agentDir); + assert.deepEqual(result.skipped, []); + assert.deepEqual(result.updated.sort(), ["npm:test-one", "npm:test-two"]); + } finally { + globalThis.fetch = originalFetch; + } + + const invocations = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line) as string[]); + assert.equal(invocations.length, 1); + assert.ok(invocations[0]?.includes("install")); + assert.ok(invocations[0]?.includes("test-one@latest")); + assert.ok(invocations[0]?.includes("test-two@latest")); +}); + +test("updateConfiguredPackages skips native package updates on unsupported Node majors", async () => { + const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-")); + const workingDir = resolve(root, "project"); + const agentDir = resolve(root, "agent"); + const logPath = resolve(root, "npm-invocations.jsonl"); + mkdirSync(workingDir, { recursive: true }); + + const scriptPath = writeFakeNpmScript(root, [ + `import { appendFileSync } from "node:fs";`, + `import { resolve } from "node:path";`, + `const args = process.argv.slice(2);`, + `if (args.length === 2 && args[0] === "root" && args[1] === "-g") {`, + ` console.log(resolve(${JSON.stringify(root)}, "npm-global", "lib", "node_modules"));`, + ` process.exit(0);`, + `}`, + `appendFileSync(${JSON.stringify(logPath)}, JSON.stringify(args) + "\\n", "utf8");`, + "process.exit(0);", + ].join("\n")); + + writeSettings(agentDir, { + npmCommand: [process.execPath, scriptPath], + packages: ["npm:@kaiserlich-dev/pi-session-search", "npm:test-regular"], + }); + createInstalledGlobalPackage(root, "@kaiserlich-dev/pi-session-search", "1.0.0"); + createInstalledGlobalPackage(root, "test-regular", "1.0.0"); + + const originalFetch = globalThis.fetch; + const originalVersion = process.versions.node; + globalThis.fetch = (async () => ({ + ok: true, + json: async () => ({ version: "2.0.0" }), + })) as typeof fetch; + Object.defineProperty(process.versions, "node", { value: "25.0.0", configurable: true }); + + try { + const result = await updateConfiguredPackages(workingDir, agentDir); + assert.deepEqual(result.updated, ["npm:test-regular"]); + assert.deepEqual(result.skipped, ["npm:@kaiserlich-dev/pi-session-search"]); + } finally { + globalThis.fetch = originalFetch; + Object.defineProperty(process.versions, "node", { value: originalVersion, configurable: true }); + } + + const invocations = existsSync(logPath) + ? readFileSync(logPath, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as string[]) + : []; + assert.equal(invocations.length, 1); + assert.ok(invocations[0]?.includes("test-regular@latest")); + assert.ok(!invocations[0]?.some((entry) => entry.includes("pi-session-search"))); +});