|
| 1 | +import { test } from "node:test"; |
| 2 | +import assert from "node:assert/strict"; |
| 3 | +import { spawnSync } from "node:child_process"; |
| 4 | +import { mkdtemp, rm, symlink } from "node:fs/promises"; |
| 5 | +import http from "node:http"; |
| 6 | +import { tmpdir } from "node:os"; |
| 7 | +import path from "node:path"; |
| 8 | +import { fileURLToPath } from "node:url"; |
| 9 | +import { deskAlive } from "./cli.js"; |
| 10 | + |
| 11 | +// A desk lock can outlive its process (crash, SIGKILL) — deskAlive is the only thing standing |
| 12 | +// between "trust the lock" and "trust it only if the server answers" (see state.ts's stablePort |
| 13 | +// invariant notes). Cover both sides: a dead URL and a URL that actually answers. |
| 14 | + |
| 15 | +test("deskAlive resolves false for a URL nothing is listening on, within the abort budget", async () => { |
| 16 | + // Port 1 is a privileged, essentially-never-bound port — connection refused (or a hang the |
| 17 | + // ~1500ms internal abort catches) either way lands on false. We only assert the outcome and a |
| 18 | + // generous wall-clock ceiling, not the exact abort timing. |
| 19 | + const start = Date.now(); |
| 20 | + const alive = await deskAlive("http://127.0.0.1:1/"); |
| 21 | + assert.equal(alive, false); |
| 22 | + assert.ok(Date.now() - start < 5000, "resolves well within the ~1500ms abort budget plus slack"); |
| 23 | +}); |
| 24 | + |
| 25 | +test("deskAlive resolves true for a live server that answers", async () => { |
| 26 | + const server = http.createServer((_req, res) => { |
| 27 | + res.writeHead(200, { "content-type": "application/json" }); |
| 28 | + res.end("{}"); |
| 29 | + }); |
| 30 | + await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve)); |
| 31 | + try { |
| 32 | + const address = server.address(); |
| 33 | + const port = typeof address === "object" && address ? address.port : 0; |
| 34 | + const alive = await deskAlive(`http://127.0.0.1:${port}/`); |
| 35 | + assert.equal(alive, true); |
| 36 | + } finally { |
| 37 | + server.close(); |
| 38 | + } |
| 39 | +}); |
| 40 | + |
| 41 | +// Regression: npm installs the published bin as a SYMLINK (.bin/galley -> dist/cli.js). Node |
| 42 | +// resolves import.meta.url through the symlink to cli.ts's real path, but leaves |
| 43 | +// process.argv[1] as the symlink path — an entry-point guard that compares the two directly |
| 44 | +// (no realpath) never fires under a symlinked invocation, and the CLI silently no-ops (exits 0, |
| 45 | +// no output). Reproduce that exact shape here: symlink to the real src/cli.ts and run it through |
| 46 | +// `node --import tsx` the same way the built bin runs through node directly. |
| 47 | +test("running cli.ts through a symlink (npm bin shape) still runs main()", async () => { |
| 48 | + const dir = await mkdtemp(path.join(tmpdir(), "galley-cli-symlink-")); |
| 49 | + const link = path.join(dir, "galley"); |
| 50 | + const cliPath = fileURLToPath(new URL("./cli.ts", import.meta.url)); |
| 51 | + try { |
| 52 | + await symlink(cliPath, link); |
| 53 | + const result = spawnSync(process.execPath, ["--import", "tsx", link, "--help"], { |
| 54 | + encoding: "utf8", |
| 55 | + }); |
| 56 | + assert.equal(result.status, 0); |
| 57 | + assert.match(result.stdout, /galley — an integrated review environment/); |
| 58 | + } finally { |
| 59 | + await rm(dir, { recursive: true, force: true }); |
| 60 | + } |
| 61 | +}); |
0 commit comments