@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1

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 (181) hide show
  1. package/AGENTS.md +339 -190
  2. package/README.md +50 -7
  3. package/docs/architecture.md +238 -23
  4. package/package.json +23 -13
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
  13. package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
  14. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  15. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  16. package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
  17. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  19. package/packages/extension/src/ask-user-tool.ts +1 -1
  20. package/packages/extension/src/bridge-context.ts +1 -1
  21. package/packages/extension/src/bridge.ts +169 -4
  22. package/packages/extension/src/command-handler.ts +65 -2
  23. package/packages/extension/src/flow-event-wiring.ts +1 -1
  24. package/packages/extension/src/markdown-image-inliner.ts +268 -0
  25. package/packages/extension/src/multiselect-list.ts +1 -1
  26. package/packages/extension/src/pi-env.d.ts +16 -9
  27. package/packages/extension/src/prompt-expander.ts +50 -2
  28. package/packages/extension/src/provider-register.ts +132 -8
  29. package/packages/extension/src/retry-tracker.ts +123 -0
  30. package/packages/extension/src/server-launcher.ts +18 -1
  31. package/packages/extension/src/session-sync.ts +15 -1
  32. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  33. package/packages/server/package.json +6 -6
  34. package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
  35. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
  36. package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
  37. package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
  38. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  39. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  40. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  41. package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
  42. package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
  43. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  44. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  45. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  46. package/packages/server/src/__tests__/directory-service.test.ts +1 -1
  47. package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
  48. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
  49. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  50. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  51. package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
  52. package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
  53. package/packages/server/src/__tests__/health-shape.test.ts +43 -0
  54. package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
  55. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  56. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
  57. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  58. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  59. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  60. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  61. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  62. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  63. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
  64. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  65. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  66. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  67. package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
  68. package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
  69. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  70. package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
  71. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
  72. package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
  74. package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
  75. package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
  76. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  77. package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
  78. package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
  79. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
  80. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  81. package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
  82. package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
  83. package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
  84. package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
  85. package/packages/server/src/bootstrap-install-from-list.ts +232 -0
  86. package/packages/server/src/bootstrap-state.ts +18 -0
  87. package/packages/server/src/browser-gateway.ts +70 -24
  88. package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
  89. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  90. package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
  91. package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
  92. package/packages/server/src/changelog-fs.ts +167 -0
  93. package/packages/server/src/changelog-parser.ts +321 -0
  94. package/packages/server/src/changelog-remote.ts +134 -0
  95. package/packages/server/src/cli.ts +23 -2
  96. package/packages/server/src/directory-service.ts +31 -0
  97. package/packages/server/src/event-wiring.ts +105 -5
  98. package/packages/server/src/headless-pid-registry.ts +54 -5
  99. package/packages/server/src/home-lock.d.ts +124 -0
  100. package/packages/server/src/home-lock.js +330 -0
  101. package/packages/server/src/home-lock.js.map +1 -0
  102. package/packages/server/src/idle-timer.ts +15 -1
  103. package/packages/server/src/pending-client-correlations.ts +73 -0
  104. package/packages/server/src/pending-fork-registry.ts +24 -12
  105. package/packages/server/src/pi-core-checker.ts +77 -17
  106. package/packages/server/src/pi-core-updater.ts +81 -15
  107. package/packages/server/src/pi-dev-version-check.ts +145 -0
  108. package/packages/server/src/pi-gateway.ts +10 -0
  109. package/packages/server/src/pi-version-skew.ts +12 -4
  110. package/packages/server/src/process-manager.ts +115 -21
  111. package/packages/server/src/provider-auth-handlers.ts +9 -0
  112. package/packages/server/src/provider-auth-storage.ts +83 -51
  113. package/packages/server/src/provider-catalogue-cache.ts +47 -0
  114. package/packages/server/src/routes/doctor-routes.ts +140 -0
  115. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  116. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  117. package/packages/server/src/routes/provider-auth-routes.ts +14 -1
  118. package/packages/server/src/routes/provider-routes.ts +4 -4
  119. package/packages/server/src/routes/system-routes.ts +38 -1
  120. package/packages/server/src/server.ts +85 -66
  121. package/packages/server/src/session-api.ts +54 -3
  122. package/packages/server/src/session-bootstrap.ts +27 -12
  123. package/packages/server/src/session-discovery.ts +11 -4
  124. package/packages/server/src/session-file-reader.ts +1 -1
  125. package/packages/server/src/session-scanner.ts +4 -2
  126. package/packages/server/src/spawn-failure-log.ts +130 -0
  127. package/packages/server/src/spawn-preflight.ts +82 -0
  128. package/packages/server/src/spawn-register-watchdog.ts +291 -0
  129. package/packages/server/src/spawn-token.ts +20 -0
  130. package/packages/server/src/terminal-manager.ts +12 -1
  131. package/packages/shared/package.json +1 -1
  132. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
  133. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  134. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  135. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  136. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  137. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  138. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
  139. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  140. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
  141. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
  142. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  143. package/packages/shared/src/__tests__/config.test.ts +48 -0
  144. package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
  145. package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
  146. package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
  147. package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
  148. package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
  149. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
  150. package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
  151. package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
  152. package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
  153. package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
  154. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
  155. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
  156. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  157. package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
  158. package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
  159. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  160. package/packages/shared/src/bootstrap-install.ts +197 -3
  161. package/packages/shared/src/browser-protocol.ts +155 -1
  162. package/packages/shared/src/changelog-types.ts +111 -0
  163. package/packages/shared/src/config.ts +15 -0
  164. package/packages/shared/src/dashboard-starter.ts +33 -0
  165. package/packages/shared/src/doctor-core.ts +821 -0
  166. package/packages/shared/src/index.ts +9 -0
  167. package/packages/shared/src/installable-list.ts +152 -0
  168. package/packages/shared/src/launch-source-flag.ts +14 -0
  169. package/packages/shared/src/launch-source-types.ts +18 -0
  170. package/packages/shared/src/openspec-activity-detector.ts +25 -7
  171. package/packages/shared/src/platform/detached-spawn.ts +13 -2
  172. package/packages/shared/src/platform/managed-node-path.ts +77 -0
  173. package/packages/shared/src/platform/node-spawn.ts +29 -21
  174. package/packages/shared/src/protocol.ts +54 -2
  175. package/packages/shared/src/resolve-jiti.ts +62 -9
  176. package/packages/shared/src/rest-api.ts +4 -0
  177. package/packages/shared/src/skill-block-parser.ts +115 -0
  178. package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
  179. package/packages/shared/src/tool-registry/definitions.ts +33 -8
  180. package/packages/shared/src/tool-registry/strategies.ts +42 -0
  181. package/packages/shared/src/types.ts +64 -0
@@ -7,8 +7,9 @@ source: managed
7
7
  path: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
8
8
  tried:
9
9
  override no override set
10
+ bare-import cannot resolve @earendil-works/pi-coding-agent/package.json
10
11
  bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
11
- bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
12
+ managed missing: <HOME>/.pi-dashboard/node_modules/@earendil-works/pi-coding-agent/dist/cli.js
12
13
  managed ok
13
14
  argv:
14
15
  - C:/Program Files/nodejs/node.exe
@@ -22,10 +23,11 @@ source: npm-global
22
23
  path: <NPM_ROOT>/@mariozechner/pi-coding-agent/dist/cli.js
23
24
  tried:
24
25
  override no override set
26
+ bare-import cannot resolve @earendil-works/pi-coding-agent/package.json
25
27
  bare-import cannot resolve @mariozechner/pi-coding-agent/package.json
26
- bare-import cannot resolve @oh-my-pi/pi-coding-agent/package.json
28
+ managed missing: <HOME>/.pi-dashboard/node_modules/@earendil-works/pi-coding-agent/dist/cli.js
27
29
  managed missing: <HOME>/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent/dist/cli.js
28
- managed missing: <HOME>/.pi-dashboard/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js
30
+ npm-global missing: <NPM_ROOT>/@earendil-works/pi-coding-agent/dist/cli.js
29
31
  npm-global ok
30
32
  argv:
31
33
  - C:/Program Files/nodejs/node.exe
@@ -39,6 +41,7 @@ source: system
39
41
  path: C:/Program Files/nodejs/node.exe
40
42
  tried:
41
43
  override no override set
44
+ managed missing: <HOME>/.pi-dashboard/node/node.exe
42
45
  managed missing: <HOME>/.pi-dashboard/node_modules/.bin/node.cmd
43
46
  where ok
44
47
  argv:
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * <root>/
5
5
  * node_modules/
6
- * @mariozechner/pi-coding-agent/dist/cli.js
6
+ * @earendil-works/pi-coding-agent/dist/cli.js
7
7
  * openspec/dist/cli.js
8
8
  * tsx/dist/cli.mjs
9
9
  * packages/
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Regression: `resolveNpmArgv` from `bootstrap-install.ts` MUST consult
3
+ * the injected `ToolRegistry` for `npm` before falling back to the
4
+ * platform `npm` / `npm.cmd` PATH binary.
5
+ *
6
+ * The change `embed-managed-node-runtime` adds a managedRuntime
7
+ * strategy to the npm chain in the registry. If a future refactor
8
+ * accidentally bypasses the registry inside `resolveNpmArgv`, the
9
+ * managed Node runtime would stop being preferred for shared bootstrap
10
+ * spawns \u2014 the user-visible regression class this whole change exists
11
+ * to prevent.
12
+ *
13
+ * See change: embed-managed-node-runtime (task 4.2).
14
+ */
15
+ import { describe, expect, it } from "vitest";
16
+ import { resolveNpmArgv } from "../bootstrap-install.js";
17
+ import type {
18
+ Resolution,
19
+ ToolRegistry,
20
+ } from "../tool-registry/index.js";
21
+
22
+ function fakeRegistry(opts: {
23
+ hasNpm?: boolean;
24
+ resolveResult?: Partial<Resolution>;
25
+ }): ToolRegistry {
26
+ // Only the methods `resolveNpmArgv` actually calls.
27
+ return {
28
+ has: (name: string) => name === "npm" && (opts.hasNpm ?? true),
29
+ resolve: () =>
30
+ ({
31
+ name: "npm",
32
+ ok: true,
33
+ path: "/managed/node/bin/npm",
34
+ source: "managed",
35
+ tried: [],
36
+ resolvedAt: Date.now(),
37
+ ...opts.resolveResult,
38
+ }) as Resolution,
39
+ } as unknown as ToolRegistry;
40
+ }
41
+
42
+ describe("resolveNpmArgv", () => {
43
+ it("explicit npmArgv wins over registry", () => {
44
+ const argv = resolveNpmArgv({
45
+ npmArgv: ["/explicit/node", "/explicit/npm-cli.js"],
46
+ registry: fakeRegistry({}),
47
+ });
48
+ expect(argv).toEqual(["/explicit/node", "/explicit/npm-cli.js"]);
49
+ });
50
+
51
+ it("uses ToolRegistry.resolve('npm') when no explicit argv", () => {
52
+ const argv = resolveNpmArgv({
53
+ registry: fakeRegistry({
54
+ resolveResult: { ok: true, path: "/managed/node/bin/npm" } as Partial<Resolution>,
55
+ }),
56
+ });
57
+ expect(argv).toEqual(["/managed/node/bin/npm"]);
58
+ });
59
+
60
+ it("falls back to npm/npm.cmd on PATH when registry has no entry", () => {
61
+ const argv = resolveNpmArgv({
62
+ registry: {
63
+ has: () => false,
64
+ resolve: () => {
65
+ throw new Error("should not be called");
66
+ },
67
+ } as unknown as ToolRegistry,
68
+ });
69
+ expect(argv).toHaveLength(1);
70
+ expect(argv[0]).toMatch(/^npm(\.cmd)?$/);
71
+ });
72
+ });
@@ -12,8 +12,13 @@ import type {
12
12
  BrowserPromptDismissMessage,
13
13
  BrowserPromptCancelMessage,
14
14
  BrowserExtUiDecoratorMessage,
15
+ BrowserAssetRegisterMessage,
15
16
  } from "../browser-protocol.js";
16
- import type { ExtensionToServerMessage, ExtUiDecoratorMessage } from "../protocol.js";
17
+ import type {
18
+ ExtensionToServerMessage,
19
+ ExtUiDecoratorMessage,
20
+ AssetRegisterMessage,
21
+ } from "../protocol.js";
17
22
  import type { DecoratorDescriptor } from "../types.js";
18
23
 
19
24
  // Type-level assertion: if these types are NOT in the union, this will fail to compile.
@@ -26,6 +31,11 @@ type _PromptCancelInUnion = AssertExtends<BrowserPromptCancelMessage, ServerToBr
26
31
  // esbuild strips the switch arms in production builds.
27
32
  type _ExtUiDecoratorInExtensionUnion = AssertExtends<ExtUiDecoratorMessage, ExtensionToServerMessage>;
28
33
  type _ExtUiDecoratorInBrowserUnion = AssertExtends<BrowserExtUiDecoratorMessage, ServerToBrowserMessage>;
34
+ // chat-markdown-local-images-and-math: asset_register must live in BOTH the
35
+ // extension→server union (so the server's switch arm survives esbuild) AND
36
+ // the server→browser union (so the client's reducer arm survives esbuild).
37
+ type _AssetRegisterInExtensionUnion = AssertExtends<AssetRegisterMessage, ExtensionToServerMessage>;
38
+ type _AssetRegisterInBrowserUnion = AssertExtends<BrowserAssetRegisterMessage, ServerToBrowserMessage>;
29
39
 
30
40
  // Runtime verification that the type discriminants are reachable in a switch
31
41
  function extractPromptType(msg: ServerToBrowserMessage): string | null {
@@ -119,3 +129,39 @@ describe("ext_ui_decorator is a member of both protocol unions", () => {
119
129
  expect(msg.descriptor.kind).toBe("footer-segment");
120
130
  });
121
131
  });
132
+
133
+ // chat-markdown-local-images-and-math: asset_register switch-arm reachability.
134
+ function extractAssetHash(msg: ServerToBrowserMessage): string | null {
135
+ switch (msg.type) {
136
+ case "asset_register":
137
+ return msg.hash;
138
+ default:
139
+ return null;
140
+ }
141
+ }
142
+
143
+ describe("asset_register is a member of both protocol unions", () => {
144
+ it("server→browser asset_register is a valid discriminant", () => {
145
+ const msg: BrowserAssetRegisterMessage = {
146
+ type: "asset_register",
147
+ sessionId: "s1",
148
+ hash: "abc1234567890123",
149
+ mimeType: "image/png",
150
+ data: "iVBORw0KGgo=",
151
+ };
152
+ expect(extractAssetHash(msg)).toBe("abc1234567890123");
153
+ });
154
+
155
+ it("extension→server asset_register carries the same shape", () => {
156
+ const msg: AssetRegisterMessage = {
157
+ type: "asset_register",
158
+ sessionId: "s1",
159
+ hash: "abc1234567890123",
160
+ mimeType: "image/svg+xml",
161
+ data: "PHN2Zy8+",
162
+ };
163
+ expect(msg.type).toBe("asset_register");
164
+ expect(msg.hash).toBe("abc1234567890123");
165
+ expect(msg.mimeType).toBe("image/svg+xml");
166
+ });
167
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Type-only assertions for changelog-types. The runtime body is a
3
+ * single `expect(true).toBe(true)` — the real test is that the
4
+ * `const x: T = {...}` literal blocks compile under `tsc --noEmit`
5
+ * during the normal test run.
6
+ *
7
+ * See change: pi-update-whats-new-panel.
8
+ */
9
+ import { describe, it, expect } from "vitest";
10
+ import type {
11
+ ChangelogBullet,
12
+ ChangelogRelease,
13
+ ChangelogResponse,
14
+ } from "../changelog-types.js";
15
+
16
+ describe("changelog-types", () => {
17
+ it("ChangelogBullet accepts minimal and full shapes", () => {
18
+ const minimal: ChangelogBullet = { text: "fix a bug", issues: [] };
19
+ const full: ChangelogBullet = {
20
+ text: "fix a bug ([#3588](https://github.com/x/y/issues/3588))",
21
+ issues: [{ num: 3588, url: "https://github.com/x/y/issues/3588" }],
22
+ };
23
+ expect(minimal.text).toBe("fix a bug");
24
+ expect(full.issues[0].num).toBe(3588);
25
+ });
26
+
27
+ it("ChangelogRelease accepts a fully populated entry", () => {
28
+ const rel: ChangelogRelease = {
29
+ version: "0.70.0",
30
+ date: "2026-04-23",
31
+ breaking: [{ text: "...", issues: [] }],
32
+ features: [{ text: "...", issues: [] }],
33
+ changed: [],
34
+ fixed: [{ text: "...", issues: [] }],
35
+ raw: "## [0.70.0] - 2026-04-23\n\n…",
36
+ };
37
+ expect(rel.version).toBe("0.70.0");
38
+ });
39
+
40
+ it("ChangelogRelease tolerates null date", () => {
41
+ const rel: ChangelogRelease = {
42
+ version: "0.0.1",
43
+ date: null,
44
+ breaking: [],
45
+ features: [],
46
+ changed: [],
47
+ fixed: [],
48
+ raw: "",
49
+ };
50
+ expect(rel.date).toBeNull();
51
+ });
52
+
53
+ it("ChangelogResponse wraps a release list", () => {
54
+ const resp: ChangelogResponse = {
55
+ pkg: "@mariozechner/pi-coding-agent",
56
+ from: "0.62.0",
57
+ to: "0.70.0",
58
+ releases: [],
59
+ hasBreaking: false,
60
+ changelogUrl: null,
61
+ parsedAt: "2026-05-08T12:00:00.000Z",
62
+ };
63
+ expect(resp.releases).toHaveLength(0);
64
+ });
65
+
66
+ it("ChangelogResponse allows null changelogUrl", () => {
67
+ const resp: ChangelogResponse = {
68
+ pkg: "x",
69
+ from: "1.0.0",
70
+ to: "1.0.1",
71
+ releases: [],
72
+ hasBreaking: false,
73
+ changelogUrl: null,
74
+ parsedAt: new Date().toISOString(),
75
+ };
76
+ expect(resp.changelogUrl).toBeNull();
77
+ });
78
+ });
@@ -471,3 +471,51 @@ describe("loadConfig reattachPlacement", () => {
471
471
  expect(content.reattachPlacement).toBeUndefined();
472
472
  });
473
473
  });
474
+
475
+ describe("loadConfig spawnRegisterTimeoutMs", () => {
476
+ let testDir: string;
477
+ let configFile: string;
478
+ let origHome: string;
479
+
480
+ beforeEach(() => {
481
+ testDir = path.join(os.tmpdir(), `test-config-srt-${Date.now()}`);
482
+ fs.mkdirSync(path.join(testDir, ".pi", "dashboard"), { recursive: true });
483
+ configFile = path.join(testDir, ".pi", "dashboard", "config.json");
484
+ origHome = process.env.HOME!;
485
+ process.env.HOME = testDir;
486
+ });
487
+
488
+ afterEach(() => {
489
+ process.env.HOME = origHome;
490
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
491
+ });
492
+
493
+ it("defaults to 30000 when field is omitted", () => {
494
+ expect(loadConfig().spawnRegisterTimeoutMs).toBe(30000);
495
+ });
496
+
497
+ it("preserves in-range value", () => {
498
+ fs.writeFileSync(configFile, JSON.stringify({ spawnRegisterTimeoutMs: 45000 }));
499
+ expect(loadConfig().spawnRegisterTimeoutMs).toBe(45000);
500
+ });
501
+
502
+ it("clamps below-range value to 5000", () => {
503
+ fs.writeFileSync(configFile, JSON.stringify({ spawnRegisterTimeoutMs: 1000 }));
504
+ expect(loadConfig().spawnRegisterTimeoutMs).toBe(5000);
505
+ });
506
+
507
+ it("clamps above-range value to 120000", () => {
508
+ fs.writeFileSync(configFile, JSON.stringify({ spawnRegisterTimeoutMs: 999999 }));
509
+ expect(loadConfig().spawnRegisterTimeoutMs).toBe(120000);
510
+ });
511
+
512
+ it("falls back to default for non-number string", () => {
513
+ fs.writeFileSync(configFile, JSON.stringify({ spawnRegisterTimeoutMs: "thirty" }));
514
+ expect(loadConfig().spawnRegisterTimeoutMs).toBe(30000);
515
+ });
516
+
517
+ it("falls back to default for null", () => {
518
+ fs.writeFileSync(configFile, JSON.stringify({ spawnRegisterTimeoutMs: null }));
519
+ expect(loadConfig().spawnRegisterTimeoutMs).toBe(30000);
520
+ });
521
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { parseDashboardStarter } from "../dashboard-starter.js";
3
+
4
+ describe("parseDashboardStarter", () => {
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ it("returns Standalone when env is empty object", () => {
10
+ expect(parseDashboardStarter({})).toBe("Standalone");
11
+ });
12
+
13
+ it("returns Standalone when DASHBOARD_STARTER is undefined", () => {
14
+ expect(parseDashboardStarter({ DASHBOARD_STARTER: undefined })).toBe("Standalone");
15
+ });
16
+
17
+ it("returns Standalone when DASHBOARD_STARTER is empty string", () => {
18
+ expect(parseDashboardStarter({ DASHBOARD_STARTER: "" })).toBe("Standalone");
19
+ });
20
+
21
+ it("returns Bridge for valid value", () => {
22
+ expect(parseDashboardStarter({ DASHBOARD_STARTER: "Bridge" })).toBe("Bridge");
23
+ });
24
+
25
+ it("returns Standalone for valid value", () => {
26
+ expect(parseDashboardStarter({ DASHBOARD_STARTER: "Standalone" })).toBe("Standalone");
27
+ });
28
+
29
+ it("returns Electron for valid value", () => {
30
+ expect(parseDashboardStarter({ DASHBOARD_STARTER: "Electron" })).toBe("Electron");
31
+ });
32
+
33
+ it("returns Standalone and warns on invalid value", () => {
34
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
35
+ const result = parseDashboardStarter({ DASHBOARD_STARTER: "bogus" });
36
+ expect(result).toBe("Standalone");
37
+ expect(warnSpy).toHaveBeenCalledOnce();
38
+ expect(warnSpy.mock.calls[0]![0]).toContain("bogus");
39
+ });
40
+ });
@@ -61,6 +61,30 @@ describe("spawnDetached", () => {
61
61
  rmSync(path.dirname(logPath), { recursive: true, force: true });
62
62
  });
63
63
 
64
+ // See change: fix-electron-extracted-jiti-and-stdio-capture.
65
+ it("redirects BOTH stdout and stderr to logFd when provided", async () => {
66
+ const logPath = tmpLog();
67
+ const fd = openSync(logPath, "a");
68
+ try {
69
+ const r = await spawnDetached({
70
+ cmd: process.execPath,
71
+ args: [
72
+ "-e",
73
+ "console.log('STDOUT-LINE'); process.stderr.write('STDERR-LINE'); setTimeout(() => process.exit(0), 100)",
74
+ ],
75
+ logFd: fd,
76
+ });
77
+ expect(r.ok).toBe(true);
78
+ await new Promise((res) => r.process!.once("exit", res));
79
+ } finally {
80
+ try { closeSync(fd); } catch { /* ignore */ }
81
+ }
82
+ const content = readFileSync(logPath, "utf-8");
83
+ expect(content).toContain("STDOUT-LINE");
84
+ expect(content).toContain("STDERR-LINE");
85
+ rmSync(path.dirname(logPath), { recursive: true, force: true });
86
+ });
87
+
64
88
  it("does not keep parent event loop alive (unref)", async () => {
65
89
  // Can only check behaviour indirectly: the returned pid/process exist
66
90
  // and the child is running detached. Lifecycle survival is covered by
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Doctor core — section assignment, suggestion taxonomy, and the
3
+ * Decision-8 lint (every non-ok check has non-empty
4
+ * message/detail/suggestion). See change: doctor-rich-output.
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+ SECTION_OF,
9
+ SUGGESTIONS,
10
+ stampSectionsAndSuggestions,
11
+ type DoctorCheck,
12
+ type DoctorStatus,
13
+ } from "../doctor-core.js";
14
+
15
+ const ALL_CHECK_NAMES = Object.keys(SECTION_OF);
16
+
17
+ describe("SECTION_OF", () => {
18
+ it("maps every canonical check name to one of the five sections", () => {
19
+ const allowed = new Set(["runtime", "pi-tooling", "server", "setup", "diagnostics"]);
20
+ for (const name of ALL_CHECK_NAMES) {
21
+ expect(allowed.has(SECTION_OF[name])).toBe(true);
22
+ }
23
+ });
24
+
25
+ it("covers all five sections (none empty)", () => {
26
+ const sections = new Set(Object.values(SECTION_OF));
27
+ for (const s of ["runtime", "pi-tooling", "server", "setup", "diagnostics"]) {
28
+ expect(sections.has(s as never)).toBe(true);
29
+ }
30
+ });
31
+ });
32
+
33
+ describe("SUGGESTIONS", () => {
34
+ it("returns undefined for status=ok across every check name", () => {
35
+ for (const name of ALL_CHECK_NAMES) {
36
+ const fn = SUGGESTIONS[name];
37
+ expect(fn).toBeDefined();
38
+ expect(fn?.("ok")).toBeUndefined();
39
+ }
40
+ });
41
+
42
+ it("returns a non-empty string for status=error or warning when defined", () => {
43
+ for (const name of ALL_CHECK_NAMES) {
44
+ const fn = SUGGESTIONS[name];
45
+ // Electron is the only one that returns undefined even for non-ok
46
+ // (because today it never fails). Skip it.
47
+ if (name === "Electron") continue;
48
+ const w = fn?.("warning");
49
+ const e = fn?.("error");
50
+ expect(typeof w === "string" && w.length > 0).toBe(true);
51
+ expect(typeof e === "string" && e.length > 0).toBe(true);
52
+ }
53
+ });
54
+
55
+ it("constrains suggestion text to the allowed Markdown subset", () => {
56
+ // Allowed: **bold**, single-backtick code, [text](url). Disallow: tables,
57
+ // headings, fenced blocks, raw HTML.
58
+ for (const name of ALL_CHECK_NAMES) {
59
+ const fn = SUGGESTIONS[name];
60
+ const candidates: (string | undefined)[] = [
61
+ fn?.("warning"),
62
+ fn?.("error"),
63
+ fn?.("error", undefined, "not-found"),
64
+ fn?.("error", undefined, "permission-denied"),
65
+ fn?.("error", undefined, "timeout"),
66
+ fn?.("error", undefined, "non-zero-exit"),
67
+ ];
68
+ for (const s of candidates) {
69
+ if (!s) continue;
70
+ // No fenced code blocks.
71
+ expect(/```/.test(s)).toBe(false);
72
+ // No headings at line start.
73
+ expect(/^#{1,6}\s/m.test(s)).toBe(false);
74
+ // No raw HTML tags (closing, self-closing, or with attributes).
75
+ // Plain `<placeholder>` text is allowed (used as prose).
76
+ expect(/<\/[a-zA-Z]|<[a-zA-Z][^>]*\s+[^>]+>|<[a-zA-Z][^>]*\/>/.test(s)).toBe(false);
77
+ // Triple-asterisk or underline for bold not allowed.
78
+ expect(/\*\*\*|___/.test(s)).toBe(false);
79
+ }
80
+ }
81
+ });
82
+ });
83
+
84
+ describe("stampSectionsAndSuggestions (Decision 8 lint)", () => {
85
+ it("stamps section + suggestion on non-ok rows by name", () => {
86
+ const checks: DoctorCheck[] = [
87
+ { name: "pi CLI", section: undefined as unknown as never, status: "error", message: "Not found", detail: "Searched PATH" },
88
+ { name: "System Node.js", section: undefined as unknown as never, status: "ok", message: "v22 at /usr/bin/node" },
89
+ ];
90
+ const out = stampSectionsAndSuggestions(checks);
91
+ expect(out[0].section).toBe("pi-tooling");
92
+ expect(out[0].suggestion).toBeDefined();
93
+ expect(out[1].section).toBe("runtime");
94
+ expect(out[1].suggestion).toBeUndefined();
95
+ });
96
+
97
+ it("every non-ok row produced through stamping has non-empty message + detail + suggestion", () => {
98
+ const statuses: DoctorStatus[] = ["warning", "error"];
99
+ for (const name of ALL_CHECK_NAMES) {
100
+ // Electron suggestion is always undefined (decision-by-design); skip.
101
+ if (name === "Electron") continue;
102
+ for (const status of statuses) {
103
+ const checks: DoctorCheck[] = [
104
+ {
105
+ name,
106
+ section: undefined as unknown as never,
107
+ status,
108
+ message: "synthetic message",
109
+ detail: "synthetic detail",
110
+ },
111
+ ];
112
+ const [stamped] = stampSectionsAndSuggestions(checks);
113
+ expect(stamped.message.length).toBeGreaterThan(0);
114
+ expect((stamped.detail ?? "").length).toBeGreaterThan(0);
115
+ expect((stamped.suggestion ?? "").length).toBeGreaterThan(0);
116
+ }
117
+ }
118
+ });
119
+
120
+ it("does not overwrite an existing suggestion", () => {
121
+ const checks: DoctorCheck[] = [
122
+ {
123
+ name: "pi CLI",
124
+ section: "pi-tooling",
125
+ status: "error",
126
+ message: "x",
127
+ detail: "y",
128
+ suggestion: "custom",
129
+ },
130
+ ];
131
+ const out = stampSectionsAndSuggestions(checks);
132
+ expect(out[0].suggestion).toBe("custom");
133
+ });
134
+ });