|
| 1 | +import assert from "node:assert/strict"; |
| 2 | +import { execFileSync, spawnSync } from "node:child_process"; |
| 3 | +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import { join } from "node:path"; |
| 6 | +import test from "node:test"; |
| 7 | + |
| 8 | +const source = readFileSync(new URL("../public/install.sh", import.meta.url), "utf8"); |
| 9 | +// Run the real payload installer without provisioning runtimes, shell rc files, |
| 10 | +// or the optional proxy. Downloads and npm are local fixtures; node/tar are real. |
| 11 | +const installer = source.slice(0, source.indexOf('\nCMD=')) + '\ninstall_cli\n'; |
| 12 | +const nestedPackage = true; |
| 13 | + |
| 14 | +function fixture(t, { existing = false, npmMode = "install" } = {}) { |
| 15 | + const dir = mkdtempSync(join(tmpdir(), "moshcode-dependencies-")); |
| 16 | + t.after(() => rmSync(dir, { recursive: true, force: true })); |
| 17 | + const home = join(dir, "install with spaces"); |
| 18 | + const pkg = nestedPackage ? join(home, "pkg") : home; |
| 19 | + const bin = join(dir, "fake-bin"); |
| 20 | + const release = join(dir, "moshcode-fixture"); |
| 21 | + mkdirSync(bin); |
| 22 | + mkdirSync(join(release, "bin"), { recursive: true }); |
| 23 | + writeFileSync(join(release, "package.json"), JSON.stringify({ |
| 24 | + name: "moshcode", version: "0.97.0", type: "module", |
| 25 | + dependencies: { "@profullstack/synconfig": "^0.1.1" }, |
| 26 | + })); |
| 27 | + writeFileSync(join(release, "bin/moshcode.mjs"), |
| 28 | + 'import { SNAPSHOT_VERSION } from "@profullstack/synconfig";\nconsole.log(`0.97.0 sync=${SNAPSHOT_VERSION}`);\n'); |
| 29 | + const archive = join(dir, "release.tar.gz"); |
| 30 | + execFileSync("tar", ["-czf", archive, "-C", dir, "moshcode-fixture"]); |
| 31 | + writeFileSync(join(bin, "curl"), '#!/bin/sh\ncat "$TEST_ARCHIVE"\n', { mode: 0o755 }); |
| 32 | + writeFileSync(join(bin, "npm"), `#!/bin/sh |
| 33 | +printf '%s\n' "$@" > "$TEST_NPM_ARGS" |
| 34 | +[ "$TEST_NPM_MODE" != fail ] || exit 42 |
| 35 | +[ "$TEST_NPM_MODE" != skip ] || exit 0 |
| 36 | +mkdir -p node_modules/@profullstack/synconfig |
| 37 | +printf '%s' '{"type":"module","exports":"./index.js"}' > node_modules/@profullstack/synconfig/package.json |
| 38 | +printf '%s' 'export const SNAPSHOT_VERSION = 1;' > node_modules/@profullstack/synconfig/index.js |
| 39 | +`, { mode: 0o755 }); |
| 40 | + if (existing) { |
| 41 | + mkdirSync(join(pkg, "bin"), { recursive: true }); |
| 42 | + writeFileSync(join(pkg, "bin/moshcode.mjs"), 'console.log("old working CLI");\n'); |
| 43 | + writeFileSync(join(pkg, "keep-until-success"), "previous installation"); |
| 44 | + } |
| 45 | + const npmArgs = join(dir, "npm-args"); |
| 46 | + const result = spawnSync("sh", ["-s"], { |
| 47 | + input: installer, |
| 48 | + encoding: "utf8", |
| 49 | + env: { |
| 50 | + ...process.env, |
| 51 | + PATH: `${bin}:${process.env.PATH}`, |
| 52 | + MOSHCODE_HOME: home, |
| 53 | + MOSHCODE_BIN: join(dir, "wrappers"), |
| 54 | + MOSHCODE_REF: "fixture", |
| 55 | + TEST_ARCHIVE: archive, |
| 56 | + TEST_NPM_ARGS: npmArgs, |
| 57 | + TEST_NPM_MODE: npmMode, |
| 58 | + TMPDIR: dir, |
| 59 | + NO_COLOR: "1", |
| 60 | + }, |
| 61 | + timeout: 15000, |
| 62 | + }); |
| 63 | + return { result, pkg, npmArgs }; |
| 64 | +} |
| 65 | + |
| 66 | +test("a fresh archive installs runtime dependencies before its first startup", (t) => { |
| 67 | + const { result, pkg, npmArgs } = fixture(t); |
| 68 | + assert.equal(result.status, 0, result.stdout + result.stderr); |
| 69 | + assert.equal(execFileSync(process.execPath, [join(pkg, "bin/moshcode.mjs"), "--version"], { encoding: "utf8" }).trim(), "0.97.0 sync=1"); |
| 70 | + const args = readFileSync(npmArgs, "utf8").trim().split("\n"); |
| 71 | + assert.equal(args[0], "install"); |
| 72 | + assert.ok(args.includes("--omit=dev")); |
| 73 | + assert.ok(args.includes("--ignore-scripts")); |
| 74 | + assert.ok(args.includes("--package-lock=false")); |
| 75 | +}); |
| 76 | + |
| 77 | +test("an update replaces the old payload only after dependencies and startup succeed", (t) => { |
| 78 | + const { result, pkg } = fixture(t, { existing: true }); |
| 79 | + assert.equal(result.status, 0, result.stdout + result.stderr); |
| 80 | + assert.equal(existsSync(join(pkg, "keep-until-success")), false); |
| 81 | + assert.match(execFileSync(process.execPath, [join(pkg, "bin/moshcode.mjs")], { encoding: "utf8" }), /0\.97\.0 sync=1/); |
| 82 | +}); |
| 83 | + |
| 84 | +for (const [npmMode, expected] of [["fail", /runtime dependency installation failed/], ["skip", /CLI startup check failed/]]) { |
| 85 | + test(`${npmMode === "fail" ? "a registry failure" : "an unresolved dependency after npm succeeds"} preserves the installed CLI`, (t) => { |
| 86 | + const { result, pkg } = fixture(t, { existing: true, npmMode }); |
| 87 | + assert.notEqual(result.status, 0); |
| 88 | + assert.match(result.stderr, expected); |
| 89 | + assert.match(result.stderr, /existing installation unchanged/); |
| 90 | + assert.equal(readFileSync(join(pkg, "keep-until-success"), "utf8"), "previous installation"); |
| 91 | + assert.equal(execFileSync(process.execPath, [join(pkg, "bin/moshcode.mjs")], { encoding: "utf8" }).trim(), "old working CLI"); |
| 92 | + }); |
| 93 | +} |
0 commit comments