Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

230 lines
9.6 KiB
TypeScript

/**
* 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/);
});
});