import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { installCommand, resolveNpmInstallRequest } from "../commands/install.js"; import { uninstallCommand } from "../commands/uninstall.js"; import { readInstallManifest, resolveInstallStorePaths } from "../install-store.js"; import { resolveCliVersion } from "../version.js"; const ORIGINAL_ENV = { ...process.env }; describe("managed install commands", () => { let root: string; beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-install-command-")); process.env = { ...ORIGINAL_ENV, HOME: path.join(root, "home"), PAPERCLIP_HOME: path.join(root, "home", ".paperclip"), PATH: "/usr/bin:/bin", SHELL: "/bin/bash", }; fs.mkdirSync(process.env.HOME!, { recursive: true }); vi.spyOn(console, "log").mockImplementation(() => undefined); }); afterEach(() => { vi.restoreAllMocks(); process.env = { ...ORIGINAL_ENV }; fs.rmSync(root, { recursive: true, force: true }); }); it("selects stable, canary, and exact-version npm sources", () => { expect(resolveNpmInstallRequest({})).toEqual({ spec: "latest", channel: "latest" }); expect(resolveNpmInstallRequest({ canary: true })).toEqual({ spec: "canary", channel: "canary" }); expect(resolveNpmInstallRequest({ version: "2026.720.0" })).toEqual({ spec: "2026.720.0", channel: "pinned", }); expect(() => resolveNpmInstallRequest({ canary: true, version: "1.2.3" })).toThrow(); expect(() => resolveNpmInstallRequest({ version: "latest" })).toThrow(); }); it("installs through the shim, reports provenance, and uninstalls without deleting user data", async () => { const version = "2026.720.0"; const runCommand = vi.fn(async (file: string, args: string[], _options?: unknown) => { if (file === "npm" && args[0] === "view") return { stdout: JSON.stringify(version), stderr: "" }; if (file === "npm" && args[0] === "install") { const prefix = args[args.indexOf("--prefix") + 1]; const entrypoint = path.join(prefix, "node_modules", "paperclipai", "dist", "index.js"); fs.mkdirSync(path.dirname(entrypoint), { recursive: true }); fs.writeFileSync(entrypoint, "#!/usr/bin/env node\n"); return { stdout: "", stderr: "" }; } if (file === process.execPath && args.at(-1) === "--version") { return { stdout: `${version}\n`, stderr: "" }; } throw new Error(`Unexpected command: ${file} ${args.join(" ")}`); }); await installCommand({}, { runCommand, now: () => new Date("2026-07-22T18:00:00.000Z") }); const paths = resolveInstallStorePaths(); const manifest = readInstallManifest(paths); expect(manifest?.version).toBe(version); expect(manifest?.channel).toBe("latest"); expect(fs.realpathSync(paths.currentPath)).toBe(fs.realpathSync(manifest!.payloadPath)); expect(fs.existsSync(paths.shimPath)).toBe(true); const installCall = runCommand.mock.calls.find( ([file, args]) => file === "npm" && args[0] === "install", ); expect(installCall?.[1]).toContain("--@paperclipai:registry=https://registry.npmjs.org"); const installOptions = installCall?.[2] as { env?: NodeJS.ProcessEnv } | undefined; expect(installOptions?.env?.npm_config_userconfig).toContain(".npmrc-"); const entrypoint = path.join(manifest!.payloadPath, "node_modules", "paperclipai", "dist", "index.js"); expect(resolveCliVersion(entrypoint)).toContain(`managed npm latest; payload ${manifest!.payloadPath}`); const userData = path.join(process.env.PAPERCLIP_HOME!, "instances", "default", "keep.txt"); fs.mkdirSync(path.dirname(userData), { recursive: true }); fs.writeFileSync(userData, "keep"); await uninstallCommand(); expect(fs.existsSync(paths.cliRoot)).toBe(false); expect(fs.existsSync(paths.shimPath)).toBe(false); expect(fs.readFileSync(userData, "utf8")).toBe("keep"); }); });