Files
nextcraft/apps/cli/tests/toolchain.test.ts
T
CIAgent 1a88ab3199
Release / release (push) Successful in 3m45s
CI / python (push) Successful in 1m52s
CI / node (push) Successful in 1m0s
merge: milestone/v0.8-ship-anywhere -> main (v0.8 complete — Ship Anywhere)
The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).

---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
  covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
  partial: []
---/ci---
2026-09-29 16:04:40 +00:00

150 lines
6.0 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { discoverToolchain, type Toolchain } from "../src/lib/toolchain.ts";
import { testCtx } from "./helpers.ts";
import type { Ctx } from "../src/ctx.ts";
// v0.4.6 toolchain discovery: node/pnpm behind nvm/PNPM_HOME init files are
// invisible to non-interactive shells (the deployed host's real failure).
// These tests fake the filesystem layout via overridden exists().
function ctxWith(
env: Record<string, string | undefined>,
existsPaths: string[],
): Ctx & { out(): string } {
const base = testCtx({ env: { ...env } as Record<string, string | undefined> });
const set = new Set(existsPaths.map((p) => join(p)));
return {
...base,
exists: (p: string) => set.has(p),
} as unknown as Ctx & { out(): string };
}
function touch(root: string, rel: string): string {
const p = join(root, rel);
mkdirSync(join(p, ".."), { recursive: true });
writeFileSync(p, "x");
return p;
}
test("toolchain: pnpm on PATH is used as-is", () => {
const dir = mkdtempSync(join(tmpdir(), "nc-tc-"));
touch(dir, "bin/pnpm");
const tc = discoverToolchain(
ctxWith({ PATH: `${join(dir, "bin")}:/usr/bin` }, [join(dir, "bin/pnpm")]),
);
assert.equal(tc.pnpmSource, "PATH");
assert.equal(tc.pnpm, join(dir, "bin", "pnpm"));
assert.equal(tc.envPathPrefix, undefined);
rmSync(dir, { recursive: true, force: true });
});
test("toolchain: nvm layout discovered when off PATH (the host failure)", () => {
const home = mkdtempSync(join(tmpdir(), "nc-tc-nvm-"));
const nvmBin = join(home, ".nvm/versions/node/v24.21.0/bin");
mkdirSync(nvmBin, { recursive: true });
writeFileSync(join(nvmBin, "pnpm"), "x");
const nvmRoot = join(home, ".nvm", "versions", "node");
const existsPaths = [nvmRoot, nvmBin, join(nvmBin, "pnpm")];
// PATH has no pnpm anywhere
const tc = discoverToolchain(
ctxWith({ PATH: "/usr/bin:/bin", HOME: home }, existsPaths),
);
assert.equal(tc.pnpmSource, "nvm");
assert.ok(tc.pnpm?.includes("v24.21.0"), tc.pnpm);
assert.equal(tc.envPathPrefix, nvmBin);
rmSync(home, { recursive: true, force: true });
});
test("toolchain: PNPM_HOME bin discovered as fallback", () => {
const home = mkdtempSync(join(tmpdir(), "nc-tc-ph-"));
const bin = join(home, ".local/share/pnpm/bin");
mkdirSync(bin, { recursive: true });
writeFileSync(join(bin, "pnpm"), "x");
const tc = discoverToolchain(
ctxWith({ PATH: "/usr/bin:/bin", HOME: home }, [bin, join(bin, "pnpm"), home]),
);
assert.equal(tc.pnpmSource, "PNPM_HOME");
assert.equal(tc.envPathPrefix, bin);
rmSync(home, { recursive: true, force: true });
});
test("toolchain: split install (nvm node + PNPM_HOME pnpm) — prefix carries BOTH (host case)", () => {
const home = mkdtempSync(join(tmpdir(), "nc-tc-split-"));
const nvmBin = join(home, ".nvm/versions/node/v24.21.0/bin");
const pnpmBin = join(home, ".local/share/pnpm/bin");
mkdirSync(nvmBin, { recursive: true });
mkdirSync(pnpmBin, { recursive: true });
writeFileSync(join(nvmBin, "node"), "x"); // node lives in nvm
writeFileSync(join(pnpmBin, "pnpm"), "x"); // pnpm lives in PNPM_HOME
const nvmRoot = join(home, ".nvm", "versions", "node");
const tc = discoverToolchain(
ctxWith(
{ PATH: "/usr/bin:/bin", HOME: home },
[nvmRoot, nvmBin, join(nvmBin, "node"), pnpmBin, join(pnpmBin, "pnpm")],
),
);
assert.equal(tc.pnpmSource, "PNPM_HOME");
assert.equal(tc.pnpm, join(pnpmBin, "pnpm"));
// the prefix must contain pnpm's dir AND node's dir — pnpm children
// exec bare `node`, which must resolve to the nvm node (the exact
// production-host layout: pnpm via PNPM_HOME, node via nvm).
const prefix = (tc.envPathPrefix ?? "").split(":");
assert.ok(prefix.includes(pnpmBin), prefix.join(":"));
assert.ok(prefix.includes(nvmBin), prefix.join(":"));
rmSync(home, { recursive: true, force: true });
});
test("toolchain: newest nvm version wins when several installed", () => {
const home = mkdtempSync(join(tmpdir(), "nc-tc-nvm2-"));
const nvmRoot = join(home, ".nvm", "versions", "node");
const existsPaths: string[] = [nvmRoot];
for (const v of ["v18.20.1", "v24.21.0", "v20.10.0"]) {
const bin = join(home, `.nvm/versions/node/${v}/bin`);
mkdirSync(bin, { recursive: true });
writeFileSync(join(bin, "pnpm"), "x");
existsPaths.push(bin, join(bin, "pnpm"));
}
const tc = discoverToolchain(ctxWith({ PATH: "/usr/bin", HOME: home }, existsPaths));
assert.ok(tc.pnpm?.includes("v24.21.0"), `expected newest, got ${tc.pnpm}`);
rmSync(home, { recursive: true, force: true });
});
test("toolchain: none found → pnpm undefined, never throws", () => {
const tc = discoverToolchain(ctxWith({ PATH: "/usr/bin" }, []));
assert.equal(tc.pnpm, undefined);
assert.equal(tc.pnpmSource, "none");
});
test("toolchain: runPnpm prepends the discovered dir to child PATH", async () => {
const seen: Array<{ env?: Record<string, string | undefined> }> = [];
const ctx = ctxWith({ PATH: "/usr/bin" }, []);
const tc: Toolchain = {
pnpm: "/opt/found/pnpm",
pnpmSource: "nvm",
envPathPrefix: "/opt/found",
};
const spawn = async (
_cmd: string,
_args: string[],
opts: { env?: Record<string, string | undefined> },
) => {
seen.push({ env: opts.env });
return { code: 0, signal: null, timedOut: false, stdout: "ok", stderr: "" };
};
const { runPnpm } = await import("../src/lib/toolchain.ts");
await runPnpm({ ...ctx, spawn } as unknown as Ctx, tc, ["--version"], { capture: true });
assert.equal(seen[0]?.env?.PATH, "/opt/found:/usr/bin");
});
test("toolchain: runPnpm with no pnpm returns 127 without spawning", async () => {
const ctx = ctxWith({ PATH: "/usr/bin" }, []);
const tc: Toolchain = { pnpm: undefined, pnpmSource: "none", envPathPrefix: undefined };
const { runPnpm } = await import("../src/lib/toolchain.ts");
const r = await runPnpm(ctx, tc, ["install"]);
assert.equal(r.code, 127);
});