@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +11 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -31,16 +31,15 @@ describe("parseArgs", () => {
31
31
  expect(result.subcommand).toBe("status");
32
32
  });
33
33
 
34
- it("parses upgrade-pi subcommand (unified-bootstrap-install §8)", () => {
35
- const result = parseArgs(["upgrade-pi"]);
36
- expect(result.subcommand).toBe("upgrade-pi");
37
- });
38
-
39
- it("parses upgrade-pi with --port flag", () => {
40
- const result = parseArgs(["upgrade-pi", "--port", "9090"]);
41
- expect(result.subcommand).toBe("upgrade-pi");
42
- expect(result.flags.port).toBe(9090);
43
- });
34
+ // NOTE: `upgrade-pi` subcommand tests removed.
35
+ // The `upgrade-pi` subcommand was deliberately removed in change
36
+ // `eliminate-electron-runtime-install` (tasks 3.0.a + 3.5b, 2026-05-23)
37
+ // when bootstrap-install was deleted. `SUBCOMMANDS` is now
38
+ // `["start", "stop", "restart", "status"]`. The pi-core upgrade path
39
+ // survives via the `POST /api/pi-core/update` REST endpoint instead.
40
+ // These two tests were documented as deferred to a "Phase 3.9 sweep"
41
+ // in eliminate-electron-runtime-install/tasks.md task 5.9; this is
42
+ // that sweep.
44
43
 
45
44
  it("parses subcommand with flags", () => {
46
45
  const result = parseArgs(["start", "--port", "3000", "--pi-port", "4000"]);
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Tests for `packages/server/bin/pi-dashboard.mjs` --version short-circuit.
3
+ *
4
+ * Bug B (see openspec/changes/fix-electron-cold-launch-probe-cascade):
5
+ * The wrapper previously resolved jiti BEFORE parsing argv, exiting 1
6
+ * with "cannot find jiti" even for metadata queries like --version.
7
+ * That killed `probeNpmGlobal` in launch-source.ts, which calls
8
+ * `pi-dashboard --version` and rejects null/empty/non-zero responses.
9
+ *
10
+ * Contract enforced by these tests:
11
+ * - `--version` / `-v` / `version` SHALL print sibling package.json's
12
+ * `version` to stdout and exit 0, EVEN when jiti is missing.
13
+ * - Any other subcommand (start, status, no args) SHALL preserve the
14
+ * pre-fix behaviour: exit 1 with "cannot find jiti" install hint
15
+ * when the wrapper can't resolve jiti.
16
+ * - When sibling package.json is unreadable / malformed, the short
17
+ * circuit MUST NOT silently succeed with an empty string — it
18
+ * SHALL fall through to the existing jiti-resolve path.
19
+ */
20
+ import { describe, it, expect, beforeAll } from "vitest";
21
+ import { spawnSync } from "node:child_process";
22
+ import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, cpSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import path from "node:path";
25
+ import url from "node:url";
26
+
27
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
28
+ const wrapperPath = path.resolve(here, "..", "..", "bin", "pi-dashboard.mjs");
29
+ const sibPkgJson = path.resolve(here, "..", "..", "package.json");
30
+
31
+ beforeAll(() => {
32
+ if (!existsSync(wrapperPath)) throw new Error(`Wrapper missing at ${wrapperPath}`);
33
+ if (!existsSync(sibPkgJson)) throw new Error(`Sibling package.json missing at ${sibPkgJson}`);
34
+ });
35
+
36
+ const expectedVersion = JSON.parse(readFileSync(sibPkgJson, "utf-8")).version as string;
37
+
38
+ /**
39
+ * Copy the wrapper + a chosen package.json into an isolated tmp dir with
40
+ * NO node_modules adjacency. createRequire from there cannot resolve
41
+ * jiti, so the jiti-miss path is exercised.
42
+ */
43
+ function makeIsolatedWrapper(pkgJsonContent: string | null): { wrapper: string; cleanup: () => void } {
44
+ const tmp = mkdtempSync(path.join(tmpdir(), "pi-dashboard-cli-version-"));
45
+ // Wrapper has to live at .../bin/pi-dashboard.mjs because it computes
46
+ // sibling package.json as `../package.json` relative to itself.
47
+ const binDir = path.join(tmp, "bin");
48
+ cpSync(path.dirname(wrapperPath), binDir, { recursive: true });
49
+ if (pkgJsonContent !== null) {
50
+ writeFileSync(path.join(tmp, "package.json"), pkgJsonContent);
51
+ }
52
+ const wrapper = path.join(binDir, "pi-dashboard.mjs");
53
+ return { wrapper, cleanup: () => rmSync(tmp, { recursive: true, force: true }) };
54
+ }
55
+
56
+ describe("bin/pi-dashboard.mjs --version short-circuit (Bug B)", () => {
57
+ it("(a) --version with no jiti reachable → exit 0, stdout = pkg.version", () => {
58
+ const fakePkg = JSON.stringify({ name: "pi-dashboard-test", version: "9.9.9-isolated" });
59
+ const { wrapper, cleanup } = makeIsolatedWrapper(fakePkg);
60
+ try {
61
+ const result = spawnSync(process.execPath, [wrapper, "--version"], {
62
+ encoding: "utf-8",
63
+ env: { ...process.env, NODE_PATH: "" },
64
+ timeout: 10_000,
65
+ });
66
+ expect(result.status).toBe(0);
67
+ expect(result.stdout.trim()).toBe("9.9.9-isolated");
68
+ expect(result.stderr).not.toContain("cannot find jiti");
69
+ } finally {
70
+ cleanup();
71
+ }
72
+ });
73
+
74
+ it("(a') -v shortform behaves identically", () => {
75
+ const fakePkg = JSON.stringify({ name: "pi-dashboard-test", version: "1.2.3-short" });
76
+ const { wrapper, cleanup } = makeIsolatedWrapper(fakePkg);
77
+ try {
78
+ const result = spawnSync(process.execPath, [wrapper, "-v"], {
79
+ encoding: "utf-8",
80
+ env: { ...process.env, NODE_PATH: "" },
81
+ timeout: 10_000,
82
+ });
83
+ expect(result.status).toBe(0);
84
+ expect(result.stdout.trim()).toBe("1.2.3-short");
85
+ } finally {
86
+ cleanup();
87
+ }
88
+ });
89
+
90
+ it("(b) start with no jiti reachable → exit 1, stderr contains install-hint", () => {
91
+ const fakePkg = JSON.stringify({ name: "pi-dashboard-test", version: "9.9.9-isolated" });
92
+ const { wrapper, cleanup } = makeIsolatedWrapper(fakePkg);
93
+ try {
94
+ const result = spawnSync(process.execPath, [wrapper, "start"], {
95
+ encoding: "utf-8",
96
+ env: { ...process.env, NODE_PATH: "" },
97
+ timeout: 10_000,
98
+ });
99
+ expect(result.status).toBe(1);
100
+ expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
101
+ expect(result.stderr).toContain("npm install -g @earendil-works/pi-coding-agent");
102
+ expect(result.stdout.trim()).not.toBe("9.9.9-isolated");
103
+ } finally {
104
+ cleanup();
105
+ }
106
+ });
107
+
108
+ it("(c) --version on healthy install → exit 0 without re-execing cli.ts", () => {
109
+ const result = spawnSync(process.execPath, [wrapperPath, "--version"], {
110
+ encoding: "utf-8",
111
+ timeout: 15_000,
112
+ });
113
+ expect(result.status).toBe(0);
114
+ expect(result.stdout.trim()).toBe(expectedVersion);
115
+ // No cli.ts startup banner — proves no re-exec.
116
+ expect(result.stdout).not.toMatch(/Dashboard server/i);
117
+ expect(result.stderr).not.toContain("cannot find jiti");
118
+ }, 30_000);
119
+
120
+ it("(d) --version with corrupt sibling package.json → falls through to jiti path", () => {
121
+ const { wrapper, cleanup } = makeIsolatedWrapper("{ this is not valid json");
122
+ try {
123
+ const result = spawnSync(process.execPath, [wrapper, "--version"], {
124
+ encoding: "utf-8",
125
+ env: { ...process.env, NODE_PATH: "" },
126
+ timeout: 10_000,
127
+ });
128
+ // Fall-through: jiti unreachable in tmp dir → legacy install-hint fires.
129
+ // Critically: NOT a silent exit 0 with empty version.
130
+ expect(result.status).toBe(1);
131
+ expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
132
+ } finally {
133
+ cleanup();
134
+ }
135
+ });
136
+
137
+ it("(d') --version with missing sibling package.json → falls through", () => {
138
+ const { wrapper, cleanup } = makeIsolatedWrapper(null);
139
+ try {
140
+ const result = spawnSync(process.execPath, [wrapper, "--version"], {
141
+ encoding: "utf-8",
142
+ env: { ...process.env, NODE_PATH: "" },
143
+ timeout: 10_000,
144
+ });
145
+ expect(result.status).toBe(1);
146
+ expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
147
+ } finally {
148
+ cleanup();
149
+ }
150
+ });
151
+ });
@@ -62,6 +62,15 @@ function makePrefs(pinnedDirs: string[] = []): PreferencesStore {
62
62
  pinDirectory: vi.fn(),
63
63
  unpinDirectory: vi.fn(),
64
64
  reorderPinnedDirs: vi.fn(),
65
+ getWorkspaces: vi.fn(() => []),
66
+ createWorkspace: vi.fn(() => null),
67
+ renameWorkspace: vi.fn(() => false),
68
+ deleteWorkspace: vi.fn(() => false),
69
+ setWorkspaceCollapsed: vi.fn(() => false),
70
+ addFolderToWorkspace: vi.fn(() => false),
71
+ removeFolderFromWorkspace: vi.fn(() => false),
72
+ reorderWorkspaceFolders: vi.fn(() => false),
73
+ reorderWorkspaces: vi.fn(() => false),
65
74
  flush: vi.fn(),
66
75
  dispose: vi.fn(),
67
76
  };
@@ -55,6 +55,15 @@ function createMockPreferencesStore(): PreferencesStore {
55
55
  pinDirectory: vi.fn(),
56
56
  unpinDirectory: vi.fn(),
57
57
  reorderPinnedDirs: vi.fn(),
58
+ getWorkspaces: vi.fn(() => []),
59
+ createWorkspace: vi.fn(() => null),
60
+ renameWorkspace: vi.fn(() => false),
61
+ deleteWorkspace: vi.fn(() => false),
62
+ setWorkspaceCollapsed: vi.fn(() => false),
63
+ addFolderToWorkspace: vi.fn(() => false),
64
+ removeFolderFromWorkspace: vi.fn(() => false),
65
+ reorderWorkspaceFolders: vi.fn(() => false),
66
+ reorderWorkspaces: vi.fn(() => false),
58
67
  flush: vi.fn(),
59
68
  dispose: vi.fn(),
60
69
  };
@@ -63,6 +63,15 @@ function createMockPreferencesStore(): PreferencesStore {
63
63
  pinDirectory: vi.fn(),
64
64
  unpinDirectory: vi.fn(),
65
65
  reorderPinnedDirs: vi.fn(),
66
+ getWorkspaces: vi.fn(() => []),
67
+ createWorkspace: vi.fn(() => null),
68
+ renameWorkspace: vi.fn(() => false),
69
+ deleteWorkspace: vi.fn(() => false),
70
+ setWorkspaceCollapsed: vi.fn(() => false),
71
+ addFolderToWorkspace: vi.fn(() => false),
72
+ removeFolderFromWorkspace: vi.fn(() => false),
73
+ reorderWorkspaceFolders: vi.fn(() => false),
74
+ reorderWorkspaces: vi.fn(() => false),
66
75
  flush: vi.fn(),
67
76
  dispose: vi.fn(),
68
77
  };
@@ -62,6 +62,15 @@ function createMockPreferencesStore(): PreferencesStore {
62
62
  pinDirectory: vi.fn(),
63
63
  unpinDirectory: vi.fn(),
64
64
  reorderPinnedDirs: vi.fn(),
65
+ getWorkspaces: vi.fn(() => []),
66
+ createWorkspace: vi.fn(() => null),
67
+ renameWorkspace: vi.fn(() => false),
68
+ deleteWorkspace: vi.fn(() => false),
69
+ setWorkspaceCollapsed: vi.fn(() => false),
70
+ addFolderToWorkspace: vi.fn(() => false),
71
+ removeFolderFromWorkspace: vi.fn(() => false),
72
+ reorderWorkspaceFolders: vi.fn(() => false),
73
+ reorderWorkspaces: vi.fn(() => false),
65
74
  flush: vi.fn(),
66
75
  dispose: vi.fn(),
67
76
  };
@@ -60,6 +60,15 @@ function createMockPreferencesStore(pinnedDirs: string[] = []): PreferencesStore
60
60
  pinDirectory: vi.fn(),
61
61
  unpinDirectory: vi.fn(),
62
62
  reorderPinnedDirs: vi.fn(),
63
+ getWorkspaces: vi.fn(() => []),
64
+ createWorkspace: vi.fn(() => null),
65
+ renameWorkspace: vi.fn(() => false),
66
+ deleteWorkspace: vi.fn(() => false),
67
+ setWorkspaceCollapsed: vi.fn(() => false),
68
+ addFolderToWorkspace: vi.fn(() => false),
69
+ removeFolderFromWorkspace: vi.fn(() => false),
70
+ reorderWorkspaceFolders: vi.fn(() => false),
71
+ reorderWorkspaces: vi.fn(() => false),
63
72
  flush: vi.fn(),
64
73
  dispose: vi.fn(),
65
74
  };
@@ -115,6 +115,59 @@ describe("/api/doctor", () => {
115
115
  }
116
116
  });
117
117
 
118
+ it("probeServer reads process state, never spawns a subprocess", async () => {
119
+ // Set the env vars the new probeServer reads directly.
120
+ const prev = {
121
+ DASHBOARD_STARTER: process.env.DASHBOARD_STARTER,
122
+ NODE_ENV: process.env.NODE_ENV,
123
+ DASHBOARD_INSTALLABLE_TOTAL: process.env.DASHBOARD_INSTALLABLE_TOTAL,
124
+ DASHBOARD_INSTALLABLE_INSTALLED: process.env.DASHBOARD_INSTALLABLE_INSTALLED,
125
+ };
126
+ process.env.DASHBOARD_STARTER = "Electron";
127
+ process.env.NODE_ENV = "production";
128
+ process.env.DASHBOARD_INSTALLABLE_TOTAL = "3";
129
+ process.env.DASHBOARD_INSTALLABLE_INSTALLED = "3";
130
+
131
+ try {
132
+ // Inject a deps override that captures what probeServer returns
133
+ // by building default deps and calling probeServer directly.
134
+ const { buildDefaultDepsForTest } = await import("../routes/doctor-routes.js") as {
135
+ buildDefaultDepsForTest?: () => SharedChecksDeps;
136
+ };
137
+
138
+ // We don't export buildDefaultDeps directly, so instead assert via
139
+ // the route: if the self-curl deadlock were present it would time out;
140
+ // instead the route must complete quickly (< 500 ms).
141
+ app = await makeApp();
142
+ const start = Date.now();
143
+ const res = await app.inject({ method: "GET", url: "/api/doctor" });
144
+ const elapsed = Date.now() - start;
145
+
146
+ expect(res.statusCode).toBe(200);
147
+ // Must complete well under the old 3 s curl timeout. The full
148
+ // doctor run includes binary-detection checks that can take ~1 s
149
+ // on slow CI; we just assert no self-curl deadlock (< 3 s).
150
+ expect(elapsed).toBeLessThan(3000);
151
+
152
+ // The server check row should say "running" / "ok" since we are
153
+ // processing this request inside the running server.
154
+ const body = res.json() as DoctorReport;
155
+ const serverRow = body.checks.find((c) => c.name === "Dashboard server");
156
+ expect(serverRow).toBeDefined();
157
+ expect(serverRow?.status).toBe("ok");
158
+ } finally {
159
+ // Restore env
160
+ if (prev.DASHBOARD_STARTER === undefined) delete process.env.DASHBOARD_STARTER;
161
+ else process.env.DASHBOARD_STARTER = prev.DASHBOARD_STARTER;
162
+ if (prev.NODE_ENV === undefined) delete process.env.NODE_ENV;
163
+ else process.env.NODE_ENV = prev.NODE_ENV;
164
+ if (prev.DASHBOARD_INSTALLABLE_TOTAL === undefined) delete process.env.DASHBOARD_INSTALLABLE_TOTAL;
165
+ else process.env.DASHBOARD_INSTALLABLE_TOTAL = prev.DASHBOARD_INSTALLABLE_TOTAL;
166
+ if (prev.DASHBOARD_INSTALLABLE_INSTALLED === undefined) delete process.env.DASHBOARD_INSTALLABLE_INSTALLED;
167
+ else process.env.DASHBOARD_INSTALLABLE_INSTALLED = prev.DASHBOARD_INSTALLABLE_INSTALLED;
168
+ }
169
+ });
170
+
118
171
  it("returns 200 with a single fallback row when buildDeps throws", async () => {
119
172
  app = await makeApp(() => {
120
173
  throw new Error("boom — deps unavailable");
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Tests for the `queue_update` extension-to-server message handling.
3
+ * Validates that the server caches Session.pendingQueues wholesale and
4
+ * broadcasts session_updated to subscribers.
5
+ * See change: add-followup-edit-and-steer-cancel.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import { WebSocket } from "ws";
9
+ import { mkdtempSync, writeFileSync } from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ import { createServer, type DashboardServer } from "../server.js";
13
+
14
+ const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
15
+
16
+ describe("event-wiring: queue_update caches Session.pendingQueues and broadcasts", () => {
17
+ let server: DashboardServer;
18
+ let piPort: number;
19
+ let browserPort: number;
20
+ let testPort = 19800;
21
+
22
+ beforeEach(async () => {
23
+ testPort += 2;
24
+ browserPort = testPort;
25
+ piPort = testPort + 1;
26
+ server = await createServer({
27
+ port: browserPort,
28
+ piPort,
29
+ dev: true,
30
+ autoShutdown: false,
31
+ shutdownIdleSeconds: 999,
32
+ tunnel: false,
33
+ editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
34
+ });
35
+ await server.start();
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await server.stop();
40
+ });
41
+
42
+ it("wholesale replaces Session.pendingQueues on each queue_update event", async () => {
43
+ const { sessionManager } = server;
44
+ const SID = "queue-test-sess";
45
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-queue-test-"));
46
+ const sessionFile = path.join(tmpDir, "s.jsonl");
47
+ writeFileSync(sessionFile, "");
48
+
49
+ const bridgeWs = new WebSocket(`ws://localhost:${piPort}`);
50
+ await new Promise<void>((resolve, reject) => {
51
+ bridgeWs.on("error", reject);
52
+ bridgeWs.on("open", () => {
53
+ bridgeWs.send(JSON.stringify({
54
+ type: "session_register",
55
+ sessionId: SID,
56
+ cwd: tmpDir,
57
+ source: "cli",
58
+ sessionFile,
59
+ }));
60
+ bridgeWs.send(JSON.stringify({ type: "replay_complete", sessionId: SID }));
61
+ resolve();
62
+ });
63
+ });
64
+ await wait(80);
65
+
66
+ // Initial state: queues start empty after register.
67
+ expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: [], followUp: [] });
68
+
69
+ // 1. Bridge emits queue_update with steering only
70
+ bridgeWs.send(JSON.stringify({
71
+ type: "queue_update",
72
+ sessionId: SID,
73
+ steering: ["first"],
74
+ followUp: [],
75
+ }));
76
+ await wait(60);
77
+ expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: ["first"], followUp: [] });
78
+
79
+ // 2. Bridge emits queue_update with both queues populated — wholesale replace
80
+ bridgeWs.send(JSON.stringify({
81
+ type: "queue_update",
82
+ sessionId: SID,
83
+ steering: ["alpha", "beta"],
84
+ followUp: ["wrap up"],
85
+ }));
86
+ await wait(60);
87
+ expect(sessionManager.get(SID)?.pendingQueues).toEqual({
88
+ steering: ["alpha", "beta"],
89
+ followUp: ["wrap up"],
90
+ });
91
+
92
+ // 3. Bridge emits empty snapshot (drain finished or clear ran)
93
+ bridgeWs.send(JSON.stringify({
94
+ type: "queue_update",
95
+ sessionId: SID,
96
+ steering: [],
97
+ followUp: [],
98
+ }));
99
+ await wait(60);
100
+ expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: [], followUp: [] });
101
+
102
+ bridgeWs.close();
103
+ });
104
+
105
+ it("resets Session.pendingQueues to empty on session re-register", async () => {
106
+ const { sessionManager } = server;
107
+ const SID = "queue-rereg-sess";
108
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-queue-rereg-"));
109
+ const sessionFile = path.join(tmpDir, "s.jsonl");
110
+ writeFileSync(sessionFile, "");
111
+
112
+ // First bridge connects and populates the queues.
113
+ const ws1 = new WebSocket(`ws://localhost:${piPort}`);
114
+ await new Promise<void>((resolve) => {
115
+ ws1.on("open", () => {
116
+ ws1.send(JSON.stringify({
117
+ type: "session_register",
118
+ sessionId: SID,
119
+ cwd: tmpDir,
120
+ source: "cli",
121
+ sessionFile,
122
+ }));
123
+ ws1.send(JSON.stringify({ type: "replay_complete", sessionId: SID }));
124
+ ws1.send(JSON.stringify({
125
+ type: "queue_update",
126
+ sessionId: SID,
127
+ steering: ["a", "b"],
128
+ followUp: ["c"],
129
+ }));
130
+ setTimeout(resolve, 100);
131
+ });
132
+ });
133
+ expect(sessionManager.get(SID)?.pendingQueues?.steering).toHaveLength(2);
134
+ expect(sessionManager.get(SID)?.pendingQueues?.followUp).toHaveLength(1);
135
+ ws1.close();
136
+ await wait(80);
137
+
138
+ // Second bridge re-registers same sessionId — pendingQueues MUST reset.
139
+ const ws2 = new WebSocket(`ws://localhost:${piPort}`);
140
+ await new Promise<void>((resolve) => {
141
+ ws2.on("open", () => {
142
+ ws2.send(JSON.stringify({
143
+ type: "session_register",
144
+ sessionId: SID,
145
+ cwd: tmpDir,
146
+ source: "cli",
147
+ sessionFile,
148
+ }));
149
+ ws2.send(JSON.stringify({ type: "replay_complete", sessionId: SID }));
150
+ setTimeout(resolve, 100);
151
+ });
152
+ });
153
+ expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: [], followUp: [] });
154
+ ws2.close();
155
+ });
156
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Regression test: when a new session registers for a cwd that has a
3
+ * pending auto-resume, `resuming: false` must be set on the OLD session
4
+ * (pendingResume.oldSessionId), not the newly-registered one.
5
+ *
6
+ * Root cause: the original code used `sessionId` (new session) instead
7
+ * of `pendingResume.oldSessionId`, making the update a no-op on the new
8
+ * session and leaving the old session permanently stuck at `resuming: true`.
9
+ * `consume()` also cancels the 30s timeout, so `onTimeout` never fired.
10
+ *
11
+ * See change: fix-electron-server-launch-node-bin (resume stuck bug).
12
+ */
13
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
14
+ import { WebSocket } from "ws";
15
+ import { mkdtempSync, writeFileSync } from "node:fs";
16
+ import path from "node:path";
17
+ import os from "node:os";
18
+ import { createServer, type DashboardServer } from "../server.js";
19
+
20
+ const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
21
+
22
+ describe("event-wiring: pending-resume clears old session resuming flag", () => {
23
+ let server: DashboardServer;
24
+ let piPort: number;
25
+ let browserPort: number;
26
+ let testPort = 19700;
27
+
28
+ beforeEach(async () => {
29
+ testPort += 2;
30
+ browserPort = testPort;
31
+ piPort = testPort + 1;
32
+ server = await createServer({
33
+ port: browserPort,
34
+ piPort,
35
+ dev: true,
36
+ autoShutdown: false,
37
+ shutdownIdleSeconds: 999,
38
+ tunnel: false,
39
+ editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
40
+ });
41
+ await server.start();
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await server.stop();
46
+ });
47
+
48
+ it("clears resuming on oldSessionId (not newSessionId) when resumed session registers", async () => {
49
+ const { sessionManager, browserGateway } = server;
50
+
51
+ const OLD_SESSION = "old-session-aaa";
52
+ const NEW_SESSION = "new-session-bbb";
53
+ // Use a real temp dir so meta-persistence doesn't fail on mkdir.
54
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-resume-test-"));
55
+ const CWD = tmpDir;
56
+ const sessionFile = path.join(tmpDir, "old.jsonl");
57
+ writeFileSync(sessionFile, ""); // create empty file
58
+
59
+ // 1. Seed the old session as ended with resuming:true.
60
+ // register() sets status:"active", so follow up with update() to ended+resuming.
61
+ sessionManager.register({ id: OLD_SESSION, cwd: CWD, sessionFile, source: "terminal" });
62
+ sessionManager.update(OLD_SESSION, { status: "ended", resuming: true });
63
+ expect(sessionManager.get(OLD_SESSION)?.resuming).toBe(true);
64
+
65
+ // 2. Record a pending resume for the cwd (mirrors handleSendPrompt auto-resume path)
66
+ browserGateway.pendingResumeRegistry.record(CWD, {
67
+ text: "continue from here",
68
+ oldSessionId: OLD_SESSION,
69
+ sessionFile,
70
+ });
71
+
72
+ // 3. A new session with the same cwd registers (the resumed pi process connecting back)
73
+ const newBridgeWs = new WebSocket(`ws://localhost:${piPort}`);
74
+ await new Promise<void>((resolve, reject) => {
75
+ newBridgeWs.on("error", reject);
76
+ newBridgeWs.on("open", () => {
77
+ newBridgeWs.send(JSON.stringify({
78
+ type: "session_register",
79
+ sessionId: NEW_SESSION,
80
+ cwd: CWD,
81
+ source: "cli",
82
+ }));
83
+ newBridgeWs.send(JSON.stringify({ type: "replay_complete", sessionId: NEW_SESSION }));
84
+ resolve();
85
+ });
86
+ });
87
+ await wait(200); // let event-wiring process
88
+
89
+ // 4. OLD session's resuming flag must now be false
90
+ expect(
91
+ sessionManager.get(OLD_SESSION)?.resuming,
92
+ `Old session ${OLD_SESSION} should have resuming:false after new session registered`,
93
+ ).toBe(false);
94
+
95
+ // 5. NEW session must not have a spurious resuming:false (it never had resuming:true)
96
+ // New session's resuming should be undefined (never set).
97
+ const newResuming = sessionManager.get(NEW_SESSION)?.resuming;
98
+ expect(
99
+ newResuming,
100
+ `New session ${NEW_SESSION} should not have resuming set; got ${newResuming}`,
101
+ ).toBeFalsy();
102
+
103
+ newBridgeWs.close();
104
+ });
105
+ });
@@ -1,29 +1,37 @@
1
1
  /**
2
- * Tests for /api/health response shape after Phase A additions.
2
+ * Tests for /api/health response shape.
3
3
  *
4
4
  * Asserts:
5
5
  * - `pid` field is present (regression pin).
6
- * - `starter` field is present, defaults to "Standalone".
6
+ * - `launchSource` field is present and reflects DASHBOARD_STARTER.
7
7
  *
8
- * Note: the "Standalone default for missing DASHBOARD_STARTER" case is
9
- * also covered exhaustively in packages/shared/src/__tests__/dashboard-starter.test.ts.
10
- * This test pins the contract at the HTTP layer so a refactor cannot silently
11
- * drop either field from the health response.
8
+ * `launchSource` replaces the legacy `starter` field per change:
9
+ * eliminate-electron-runtime-install (task 3.2). It is the single source
10
+ * of truth for arm-aware client gating (e.g. hiding pi-core update UI
11
+ * under Electron, since bundled node_modules/ is read-only there).
12
12
  */
13
- import { describe, it, expect, afterEach } from "vitest";
13
+ import { describe, it, expect, afterEach, beforeEach } from "vitest";
14
14
  import { createTestServer, type TestServerHandle } from "../test-support/test-server.js";
15
15
 
16
16
  let handle: TestServerHandle | undefined;
17
+ let savedStarter: string | undefined;
18
+
19
+ describe("GET /api/health — shape", () => {
20
+ beforeEach(() => {
21
+ savedStarter = process.env.DASHBOARD_STARTER;
22
+ });
17
23
 
18
- describe("GET /api/health — Phase A shape", () => {
19
24
  afterEach(async () => {
20
25
  if (handle) {
21
26
  try { await handle.stop(); } catch { /* already stopped */ }
22
27
  handle = undefined;
23
28
  }
29
+ if (savedStarter === undefined) delete process.env.DASHBOARD_STARTER;
30
+ else process.env.DASHBOARD_STARTER = savedStarter;
24
31
  });
25
32
 
26
33
  it("includes pid field (regression pin)", async () => {
34
+ delete process.env.DASHBOARD_STARTER;
27
35
  handle = await createTestServer();
28
36
  const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
29
37
  expect(res.status).toBe(200);
@@ -32,12 +40,27 @@ describe("GET /api/health — Phase A shape", () => {
32
40
  expect(body.pid).toBe(process.pid);
33
41
  });
34
42
 
35
- it("includes starter field defaulting to Standalone", async () => {
43
+ it("launchSource defaults to 'standalone' when DASHBOARD_STARTER unset", async () => {
44
+ delete process.env.DASHBOARD_STARTER;
45
+ handle = await createTestServer();
46
+ const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
47
+ const body = await res.json() as Record<string, unknown>;
48
+ expect(body.launchSource).toBe("standalone");
49
+ });
50
+
51
+ it("launchSource is 'electron' when DASHBOARD_STARTER=Electron", async () => {
52
+ process.env.DASHBOARD_STARTER = "Electron";
53
+ handle = await createTestServer();
54
+ const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
55
+ const body = await res.json() as Record<string, unknown>;
56
+ expect(body.launchSource).toBe("electron");
57
+ });
58
+
59
+ it("launchSource is 'bridge' when DASHBOARD_STARTER=Bridge", async () => {
60
+ process.env.DASHBOARD_STARTER = "Bridge";
36
61
  handle = await createTestServer();
37
62
  const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
38
- expect(res.status).toBe(200);
39
63
  const body = await res.json() as Record<string, unknown>;
40
- // When bootstrapState has no starter set, defaults to "Standalone".
41
- expect(body.starter).toBe("Standalone");
64
+ expect(body.launchSource).toBe("bridge");
42
65
  });
43
66
  });