/** * PR #974 round 6 (mrcfps): pin the split-start dev-flow auto-restart * sequence in `ensureDaemonGateForDesktop`. * * Background: `tools-dev start daemon` followed by `tools-dev start desktop` * was leaving the daemon ungated because `OD_REQUIRE_DESKTOP_AUTH=1` * is only injected when daemon and desktop spawn together. The * helper now introspects the running daemon's STATUS over IPC and * restarts it (and web, if running) before launching desktop main * when the daemon reports `desktopAuthGateActive: false`. * * These four cases pin the call sequence so a future refactor cannot * silently regress the gate. We record every call into an ordered * array of `{ kind, arg? }` records and assert the exact sequence — * the helper is a state machine, and the order of stop/start matters * (web stops BEFORE daemon so the proxy doesn't 502 against an * intermediate daemon down state). * * @see tools/dev/src/desktop-auth-gate.ts * @see https://github.com/nexu-io/open-design/pull/974 */ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { APP_KEYS, type DaemonStatusSnapshot, type WebStatusSnapshot } from "@open-design/sidecar-proto"; import { ensureDaemonGateForDesktop, type EnsureDaemonGateDeps, } from "../src/desktop-auth-gate.js"; type CallRecord = | { kind: "inspectDaemon" } | { kind: "inspectWeb" } | { kind: "stopApp"; app: typeof APP_KEYS.DAEMON | typeof APP_KEYS.WEB } | { kind: "startDaemonGated"; port: number | null; webPort: number | null } | { kind: "startWeb"; port: number | null } | { kind: "log"; msg: string }; function makeRecorder(setup: { daemon: DaemonStatusSnapshot | null; web: WebStatusSnapshot | null; }): { calls: CallRecord[]; deps: EnsureDaemonGateDeps } { const calls: CallRecord[] = []; const deps: EnsureDaemonGateDeps = { inspectDaemon: async () => { calls.push({ kind: "inspectDaemon" }); return setup.daemon; }, inspectWeb: async () => { calls.push({ kind: "inspectWeb" }); return setup.web; }, stopApp: async (app) => { calls.push({ kind: "stopApp", app }); }, startDaemonGated: async ({ port, webPort }) => { calls.push({ kind: "startDaemonGated", port, webPort }); }, startWeb: async ({ port }) => { calls.push({ kind: "startWeb", port }); }, log: (msg) => { calls.push({ kind: "log", msg }); }, }; return { calls, deps }; } function armed(): DaemonStatusSnapshot { return { desktopAuthGateActive: true, pid: 1234, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: "http://127.0.0.1:7456", }; } function ungated(): DaemonStatusSnapshot { return { desktopAuthGateActive: false, pid: 1234, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: "http://127.0.0.1:7456", }; } function webRunning(): WebStatusSnapshot { return { pid: 5678, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: "http://127.0.0.1:5173", }; } describe("ensureDaemonGateForDesktop", () => { it("is a no-op when no daemon is running (regular flow surfaces that)", async () => { const { calls, deps } = makeRecorder({ daemon: null, web: null }); await ensureDaemonGateForDesktop(deps); assert.deepEqual(calls, [{ kind: "inspectDaemon" }]); }); it("is a no-op when the running daemon already reports the gate active", async () => { // Bundled-targets path (`pnpm tools-dev`): trigger (A) in startApp // already armed the gate at first daemon spawn, so the helper just // confirms via STATUS and exits with no side effects. This case // protects against a regression that adds an unconditional restart. const { calls, deps } = makeRecorder({ daemon: armed(), web: webRunning() }); await ensureDaemonGateForDesktop(deps); assert.deepEqual(calls, [{ kind: "inspectDaemon" }]); }); it("restarts only the daemon when gate is inactive and web is NOT running", async () => { // Split-start `tools-dev start daemon` -> `tools-dev start desktop` // with no web sidecar in between. The helper logs once, stops the // daemon, restarts it gated, and does NOT touch web. Order matters: // log -> inspectWeb -> stopApp(daemon) -> startDaemonGated. const { calls, deps } = makeRecorder({ daemon: ungated(), web: null }); await ensureDaemonGateForDesktop(deps); assert.equal(calls.length, 5); assert.equal(calls[0].kind, "inspectDaemon"); assert.equal(calls[1].kind, "log"); assert.match((calls[1] as { msg: string }).msg, /restarting daemon/); assert.equal(calls[2].kind, "inspectWeb"); assert.deepEqual(calls[3], { kind: "stopApp", app: APP_KEYS.DAEMON }); assert.deepEqual(calls[4], { kind: "startDaemonGated", port: 7456, webPort: null }); // Defensive: ensure no web spawn happened. assert.equal(calls.find((c) => c.kind === "startWeb"), undefined); assert.equal(calls.find((c) => c.kind === "stopApp" && c.app === APP_KEYS.WEB), undefined); }); it("restarts daemon AND web in the right order when both are running and gate is inactive", async () => { // Split-start `start daemon` -> `start web` -> `start desktop`. // Order is critical: web stops FIRST (so its proxy doesn't 502 // against the down-then-up daemon during the restart window), // THEN daemon stops, THEN daemon respawns gated, THEN web restarts. // Inverting any pair would either leave a transient 502 or restart // web against the OLD daemon URL. const { calls, deps } = makeRecorder({ daemon: ungated(), web: webRunning() }); await ensureDaemonGateForDesktop(deps); assert.equal(calls.length, 7); assert.equal(calls[0].kind, "inspectDaemon"); assert.equal(calls[1].kind, "log"); assert.equal(calls[2].kind, "inspectWeb"); assert.deepEqual(calls[3], { kind: "stopApp", app: APP_KEYS.WEB }); assert.deepEqual(calls[4], { kind: "stopApp", app: APP_KEYS.DAEMON }); assert.deepEqual(calls[5], { kind: "startDaemonGated", port: 7456, webPort: 5173 }); assert.deepEqual(calls[6], { kind: "startWeb", port: 5173 }); }); // Round 7 (lefarcen P2): the hardening restart was passing the // current `start desktop` CLI options to startDaemonGated/startWeb, // which meant a daemon/web started with `--daemon-port 17456` could // silently drift to a random port during the restart. The helper now // extracts the running ports from the STATUS URLs and forwards them // explicitly. These three cases pin the new port-preservation contract. it("preserves the running daemon port across the hardening restart", async () => { const ungatedOnExplicitPort: DaemonStatusSnapshot = { desktopAuthGateActive: false, pid: 1234, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: "http://127.0.0.1:17456", }; const { calls, deps } = makeRecorder({ daemon: ungatedOnExplicitPort, web: null }); await ensureDaemonGateForDesktop(deps); const restart = calls.find((c) => c.kind === "startDaemonGated"); assert.notEqual(restart, undefined); assert.deepEqual(restart, { kind: "startDaemonGated", port: 17456, webPort: null }); }); it("preserves the running web port across the hardening restart when web is alive", async () => { const ungatedDaemon: DaemonStatusSnapshot = { desktopAuthGateActive: false, pid: 1234, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: "http://127.0.0.1:17456", }; const webOnExplicitPort: WebStatusSnapshot = { pid: 5678, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: "http://127.0.0.1:17573", }; const { calls, deps } = makeRecorder({ daemon: ungatedDaemon, web: webOnExplicitPort }); await ensureDaemonGateForDesktop(deps); const webRestart = calls.find((c) => c.kind === "startWeb"); assert.notEqual(webRestart, undefined); assert.deepEqual(webRestart, { kind: "startWeb", port: 17573 }); const daemonRestart = calls.find((c) => c.kind === "startDaemonGated"); assert.deepEqual(daemonRestart, { kind: "startDaemonGated", port: 17456, webPort: 17573 }); }); it("falls back to caller options (port:null) when the running URL has no port", async () => { // Defensive: a malformed or IPC-only status URL should yield null // so the caller's existing options govern the restart, rather than // shipping a meaningless "0" port that would force a random port. const ungatedNullishUrl: DaemonStatusSnapshot = { desktopAuthGateActive: false, pid: 1234, state: "running", updatedAt: "2026-05-09T03:00:00.000Z", url: null, }; const { calls, deps } = makeRecorder({ daemon: ungatedNullishUrl, web: null }); await ensureDaemonGateForDesktop(deps); const restart = calls.find((c) => c.kind === "startDaemonGated"); assert.deepEqual(restart, { kind: "startDaemonGated", port: null, webPort: null }); }); it("logs a single warn line that names both daemon and web in the restart message", async () => { // The user sees one line per `start desktop` invocation in the // ungated case, so it must be self-explanatory — naming both // affected services in one message lets them anticipate the // disruption without grepping for context. const { calls, deps } = makeRecorder({ daemon: ungated(), web: webRunning() }); await ensureDaemonGateForDesktop(deps); const logs = calls.filter((c): c is { kind: "log"; msg: string } => c.kind === "log"); assert.equal(logs.length, 1); assert.match(logs[0].msg, /tools-dev/); assert.match(logs[0].msg, /desktop-auth gate/); assert.match(logs[0].msg, /daemon/); assert.match(logs[0].msg, /web/); }); });