diff --git a/tests/package-ops.test.ts b/tests/package-ops.test.ts index 2e94350..4727457 100644 --- a/tests/package-ops.test.ts +++ b/tests/package-ops.test.ts @@ -4,7 +4,7 @@ import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, writeFileS import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; -import { seedBundledWorkspacePackages } from "../src/pi/package-ops.js"; +import { installPackageSources, seedBundledWorkspacePackages } from "../src/pi/package-ops.js"; function createBundledWorkspace(appRoot: string, packageNames: string[]): void { for (const packageName of packageNames) { @@ -18,6 +18,17 @@ function createBundledWorkspace(appRoot: string, packageNames: string[]): void { } } +function writeSettings(agentDir: string, settings: Record): void { + mkdirSync(agentDir, { recursive: true }); + writeFileSync(resolve(agentDir, "settings.json"), JSON.stringify(settings, null, 2) + "\n", "utf8"); +} + +function writeFakeNpmScript(root: string, body: string): string { + const scriptPath = resolve(root, "fake-npm.mjs"); + writeFileSync(scriptPath, body, "utf8"); + return scriptPath; +} + test("seedBundledWorkspacePackages links bundled packages into the Feynman npm prefix", () => { const appRoot = mkdtempSync(join(tmpdir(), "feynman-bundle-")); const homeRoot = mkdtempSync(join(tmpdir(), "feynman-home-")); @@ -54,3 +65,83 @@ test("seedBundledWorkspacePackages preserves existing installed packages", () => assert.equal(readFileSync(resolve(existingPackageDir, "package.json"), "utf8"), '{"name":"pi-subagents","version":"user"}\n'); assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), false); }); + +test("installPackageSources filters noisy npm chatter but preserves meaningful output", async () => { + const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-")); + const workingDir = resolve(root, "project"); + const agentDir = resolve(root, "agent"); + mkdirSync(workingDir, { recursive: true }); + + const scriptPath = writeFakeNpmScript(root, [ + `console.log("npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead");`, + 'console.log("changed 343 packages in 9s");', + 'console.log("59 packages are looking for funding");', + 'console.log("run `npm fund` for details");', + 'console.error("visible stderr line");', + 'console.log("visible stdout line");', + "process.exit(0);", + ].join("\n")); + + writeSettings(agentDir, { + npmCommand: [process.execPath, scriptPath], + }); + + let stdout = ""; + let stderr = ""; + const originalStdoutWrite = process.stdout.write.bind(process.stdout); + const originalStderrWrite = process.stderr.write.bind(process.stderr); + (process.stdout.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => { + stdout += chunk.toString(); + return true; + }) as typeof process.stdout.write; + (process.stderr.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => { + stderr += chunk.toString(); + return true; + }) as typeof process.stderr.write; + + try { + const result = await installPackageSources(workingDir, agentDir, ["npm:test-visible-package"]); + assert.deepEqual(result.installed, ["npm:test-visible-package"]); + assert.deepEqual(result.skipped, []); + } finally { + process.stdout.write = originalStdoutWrite; + process.stderr.write = originalStderrWrite; + } + + const combined = `${stdout}\n${stderr}`; + assert.match(combined, /visible stdout line/); + assert.match(combined, /visible stderr line/); + assert.doesNotMatch(combined, /node-domexception/); + assert.doesNotMatch(combined, /changed 343 packages/); + assert.doesNotMatch(combined, /packages are looking for funding/); + assert.doesNotMatch(combined, /npm fund/); +}); + +test("installPackageSources skips native packages on unsupported Node majors before invoking npm", async () => { + const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-")); + const workingDir = resolve(root, "project"); + const agentDir = resolve(root, "agent"); + const markerPath = resolve(root, "npm-invoked.txt"); + mkdirSync(workingDir, { recursive: true }); + + const scriptPath = writeFakeNpmScript(root, [ + `import { writeFileSync } from "node:fs";`, + `writeFileSync(${JSON.stringify(markerPath)}, "invoked\\n", "utf8");`, + "process.exit(0);", + ].join("\n")); + + writeSettings(agentDir, { + npmCommand: [process.execPath, scriptPath], + }); + + const originalVersion = process.versions.node; + Object.defineProperty(process.versions, "node", { value: "25.0.0", configurable: true }); + try { + const result = await installPackageSources(workingDir, agentDir, ["npm:@kaiserlich-dev/pi-session-search"]); + assert.deepEqual(result.installed, []); + assert.deepEqual(result.skipped, ["npm:@kaiserlich-dev/pi-session-search"]); + assert.equal(existsSync(markerPath), false); + } finally { + Object.defineProperty(process.versions, "node", { value: originalVersion, configurable: true }); + } +});