| 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 { |
| ACPX_LOCAL_INSTALL_COMMAND, |
| ACPX_PINNED_VERSION, |
| buildAcpxLocalInstallCommand, |
| } from "./config.js"; |
|
|
| const { resolveSpawnFailureMock, spawnAndCollectMock } = vi.hoisted(() => ({ |
| resolveSpawnFailureMock: vi.fn< |
| (error: unknown, cwd: string) => "missing-command" | "missing-cwd" | null |
| >(() => null), |
| spawnAndCollectMock: vi.fn(), |
| })); |
|
|
| vi.mock("./runtime-internals/process.js", () => ({ |
| resolveSpawnFailure: resolveSpawnFailureMock, |
| spawnAndCollect: spawnAndCollectMock, |
| })); |
|
|
| import { checkAcpxVersion, ensureAcpx } from "./ensure.js"; |
|
|
| describe("acpx ensure", () => { |
| const tempDirs: string[] = []; |
|
|
| beforeEach(() => { |
| resolveSpawnFailureMock.mockReset(); |
| resolveSpawnFailureMock.mockReturnValue(null); |
| spawnAndCollectMock.mockReset(); |
| }); |
|
|
| function makeTempAcpxInstall(version: string): string { |
| const root = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-ensure-test-")); |
| tempDirs.push(root); |
| const packageRoot = path.join(root, "node_modules", "acpx"); |
| fs.mkdirSync(path.join(packageRoot, "dist"), { recursive: true }); |
| fs.mkdirSync(path.join(root, "node_modules", ".bin"), { recursive: true }); |
| fs.writeFileSync( |
| path.join(packageRoot, "package.json"), |
| JSON.stringify({ name: "acpx", version }, null, 2), |
| "utf8", |
| ); |
| fs.writeFileSync(path.join(packageRoot, "dist", "cli.js"), "#!/usr/bin/env node\n", "utf8"); |
| const binPath = path.join(root, "node_modules", ".bin", "acpx"); |
| fs.symlinkSync(path.join(packageRoot, "dist", "cli.js"), binPath); |
| return binPath; |
| } |
|
|
| afterEach(() => { |
| for (const dir of tempDirs.splice(0)) { |
| fs.rmSync(dir, { recursive: true, force: true }); |
| } |
| }); |
|
|
| function mockEnsureInstallFlow() { |
| spawnAndCollectMock |
| .mockResolvedValueOnce({ |
| stdout: "acpx 0.0.9\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }) |
| .mockResolvedValueOnce({ |
| stdout: "added 1 package\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }) |
| .mockResolvedValueOnce({ |
| stdout: `acpx ${ACPX_PINNED_VERSION}\n`, |
| stderr: "", |
| code: 0, |
| error: null, |
| }); |
| } |
|
|
| function expectEnsureInstallCalls(stripProviderAuthEnvVars?: boolean) { |
| expect(spawnAndCollectMock.mock.calls[0]?.[0]).toMatchObject({ |
| command: "/plugin/node_modules/.bin/acpx", |
| args: ["--version"], |
| cwd: "/plugin", |
| stripProviderAuthEnvVars, |
| }); |
| expect(spawnAndCollectMock.mock.calls[1]?.[0]).toMatchObject({ |
| command: "npm", |
| args: ["install", "--omit=dev", "--no-save", `acpx@${ACPX_PINNED_VERSION}`], |
| cwd: "/plugin", |
| stripProviderAuthEnvVars, |
| }); |
| expect(spawnAndCollectMock.mock.calls[2]?.[0]).toMatchObject({ |
| command: "/plugin/node_modules/.bin/acpx", |
| args: ["--version"], |
| cwd: "/plugin", |
| stripProviderAuthEnvVars, |
| }); |
| } |
|
|
| it("accepts the pinned acpx version", async () => { |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: `acpx ${ACPX_PINNED_VERSION}\n`, |
| stderr: "", |
| code: 0, |
| error: null, |
| }); |
|
|
| const result = await checkAcpxVersion({ |
| command: "/plugin/node_modules/.bin/acpx", |
| cwd: "/plugin", |
| expectedVersion: ACPX_PINNED_VERSION, |
| }); |
|
|
| expect(result).toEqual({ |
| ok: true, |
| version: ACPX_PINNED_VERSION, |
| expectedVersion: ACPX_PINNED_VERSION, |
| }); |
| expect(spawnAndCollectMock).toHaveBeenCalledWith({ |
| command: "/plugin/node_modules/.bin/acpx", |
| args: ["--version"], |
| cwd: "/plugin", |
| stripProviderAuthEnvVars: undefined, |
| }); |
| }); |
|
|
| it("reports version mismatch", async () => { |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: "acpx 0.0.9\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }); |
|
|
| const result = await checkAcpxVersion({ |
| command: "/plugin/node_modules/.bin/acpx", |
| cwd: "/plugin", |
| expectedVersion: ACPX_PINNED_VERSION, |
| }); |
|
|
| expect(result).toMatchObject({ |
| ok: false, |
| reason: "version-mismatch", |
| expectedVersion: ACPX_PINNED_VERSION, |
| installedVersion: "0.0.9", |
| installCommand: ACPX_LOCAL_INSTALL_COMMAND, |
| }); |
| }); |
|
|
| it("falls back to package.json version when --version is unsupported", async () => { |
| const command = makeTempAcpxInstall(ACPX_PINNED_VERSION); |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: "", |
| stderr: "error: unknown option '--version'", |
| code: 2, |
| error: null, |
| }); |
|
|
| const result = await checkAcpxVersion({ |
| command, |
| cwd: path.dirname(path.dirname(command)), |
| expectedVersion: ACPX_PINNED_VERSION, |
| }); |
|
|
| expect(result).toEqual({ |
| ok: true, |
| version: ACPX_PINNED_VERSION, |
| expectedVersion: ACPX_PINNED_VERSION, |
| }); |
| }); |
|
|
| it("accepts command availability when expectedVersion is unset", async () => { |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: "Usage: acpx [options]\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }); |
|
|
| const result = await checkAcpxVersion({ |
| command: "/custom/acpx", |
| cwd: "/custom", |
| expectedVersion: undefined, |
| }); |
|
|
| expect(result).toEqual({ |
| ok: true, |
| version: "unknown", |
| expectedVersion: undefined, |
| }); |
| expect(spawnAndCollectMock).toHaveBeenCalledWith({ |
| command: "/custom/acpx", |
| args: ["--help"], |
| cwd: "/custom", |
| stripProviderAuthEnvVars: undefined, |
| }); |
| }); |
|
|
| it("forwards stripProviderAuthEnvVars to version checks", async () => { |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: "Usage: acpx [options]\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }); |
|
|
| await checkAcpxVersion({ |
| command: "/plugin/node_modules/.bin/acpx", |
| cwd: "/plugin", |
| expectedVersion: undefined, |
| stripProviderAuthEnvVars: true, |
| }); |
|
|
| expect(spawnAndCollectMock).toHaveBeenCalledWith({ |
| command: "/plugin/node_modules/.bin/acpx", |
| args: ["--help"], |
| cwd: "/plugin", |
| stripProviderAuthEnvVars: true, |
| }); |
| }); |
|
|
| it("installs and verifies pinned acpx when precheck fails", async () => { |
| mockEnsureInstallFlow(); |
|
|
| await ensureAcpx({ |
| command: "/plugin/node_modules/.bin/acpx", |
| pluginRoot: "/plugin", |
| expectedVersion: ACPX_PINNED_VERSION, |
| }); |
|
|
| expect(spawnAndCollectMock).toHaveBeenCalledTimes(3); |
| expectEnsureInstallCalls(); |
| }); |
|
|
| it("threads stripProviderAuthEnvVars through version probes and install", async () => { |
| mockEnsureInstallFlow(); |
|
|
| await ensureAcpx({ |
| command: "/plugin/node_modules/.bin/acpx", |
| pluginRoot: "/plugin", |
| expectedVersion: ACPX_PINNED_VERSION, |
| stripProviderAuthEnvVars: true, |
| }); |
|
|
| expectEnsureInstallCalls(true); |
| }); |
|
|
| it("fails with actionable error when npm install fails", async () => { |
| spawnAndCollectMock |
| .mockResolvedValueOnce({ |
| stdout: "acpx 0.0.9\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }) |
| .mockResolvedValueOnce({ |
| stdout: "", |
| stderr: "network down", |
| code: 1, |
| error: null, |
| }); |
|
|
| await expect( |
| ensureAcpx({ |
| command: "/plugin/node_modules/.bin/acpx", |
| pluginRoot: "/plugin", |
| expectedVersion: ACPX_PINNED_VERSION, |
| }), |
| ).rejects.toThrow("failed to install plugin-local acpx"); |
| }); |
|
|
| it("skips install path when allowInstall=false", async () => { |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: "", |
| stderr: "", |
| code: 0, |
| error: new Error("not found"), |
| }); |
| resolveSpawnFailureMock.mockReturnValue("missing-command"); |
|
|
| await expect( |
| ensureAcpx({ |
| command: "/custom/acpx", |
| pluginRoot: "/plugin", |
| expectedVersion: undefined, |
| allowInstall: false, |
| }), |
| ).rejects.toThrow("acpx command not found at /custom/acpx"); |
|
|
| expect(spawnAndCollectMock).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it("uses expectedVersion for install command metadata", async () => { |
| spawnAndCollectMock.mockResolvedValueOnce({ |
| stdout: "acpx 0.0.9\n", |
| stderr: "", |
| code: 0, |
| error: null, |
| }); |
|
|
| const result = await checkAcpxVersion({ |
| command: "/plugin/node_modules/.bin/acpx", |
| cwd: "/plugin", |
| expectedVersion: "0.2.0", |
| }); |
|
|
| expect(result).toMatchObject({ |
| ok: false, |
| installCommand: buildAcpxLocalInstallCommand("0.2.0"), |
| }); |
| }); |
| }); |
|
|