@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
@@ -35,11 +35,11 @@ const FAKE_MANIFEST: readonly RecommendedExtension[] = [
35
35
  toolsRegistered: [],
36
36
  },
37
37
  {
38
- id: "@tintinweb/pi-subagents",
39
- source: "npm:@tintinweb/pi-subagents",
40
- displayName: "@tintinweb/pi-subagents",
38
+ id: "pi-dashboard-subagents",
39
+ source: "npm:pi-dashboard-subagents",
40
+ displayName: "pi-dashboard-subagents",
41
41
  fallbackDescription: "Sub-agents for pi.",
42
- status: "strongly-suggested",
42
+ status: "optional",
43
43
  unlocks: [],
44
44
  toolsRegistered: [],
45
45
  },
@@ -49,7 +49,7 @@ describe("extractBasenameFromSource", () => {
49
49
  it("strips npm: prefix and version pin", () => {
50
50
  expect(extractBasenameFromSource("npm:pi-agent-browser")).toBe("pi-agent-browser");
51
51
  expect(extractBasenameFromSource("npm:pi-agent-browser@1.2.3")).toBe("pi-agent-browser");
52
- expect(extractBasenameFromSource("npm:@tintinweb/pi-subagents")).toBe("@tintinweb/pi-subagents");
52
+ expect(extractBasenameFromSource("npm:@scope/example-pkg")).toBe("@scope/example-pkg"); // generic scoped-name parsing
53
53
  });
54
54
 
55
55
  it("strips .git suffix from git URLs", () => {
@@ -70,8 +70,8 @@ describe("extractBasenameFromSource", () => {
70
70
  describe("matchRecommendedEntry", () => {
71
71
  it("matches by exact source", () => {
72
72
  expect(
73
- matchRecommendedEntry("npm:@tintinweb/pi-subagents", FAKE_MANIFEST)?.id,
74
- ).toBe("@tintinweb/pi-subagents");
73
+ matchRecommendedEntry("npm:pi-dashboard-subagents", FAKE_MANIFEST)?.id,
74
+ ).toBe("pi-dashboard-subagents");
75
75
  });
76
76
 
77
77
  it("matches git source regardless of trailing slash / case", () => {
@@ -115,21 +115,21 @@ describe("enrichInstalledRow", () => {
115
115
 
116
116
  it("enriches a recommended npm row with displayName and description from manifest", () => {
117
117
  const row: RawInstalledRow = {
118
- source: "npm:@tintinweb/pi-subagents",
118
+ source: "npm:pi-dashboard-subagents",
119
119
  scope: "user",
120
120
  filtered: false,
121
121
  installedPath: "/fake/path",
122
122
  };
123
123
  const out = enrichInstalledRow(row, {
124
124
  ...baseDeps,
125
- readMeta: () => ({ version: "0.6.1", description: "Live npm desc" }),
125
+ readMeta: () => ({ version: "0.1.1", description: "Live npm desc" }),
126
126
  existsFn: () => false,
127
127
  resourcesPath: "/res",
128
128
  });
129
- expect(out.displayName).toBe("@tintinweb/pi-subagents");
129
+ expect(out.displayName).toBe("pi-dashboard-subagents");
130
130
  // Recommended manifest description wins over package.json description.
131
131
  expect(out.description).toBe("Sub-agents for pi.");
132
- expect(out.version).toBe("0.6.1");
132
+ expect(out.version).toBe("0.1.1");
133
133
  expect(out.isRecommended).toBe(true);
134
134
  expect(out.isBundled).toBe(false);
135
135
  });
@@ -173,7 +173,7 @@ describe("enrichInstalledRow", () => {
173
173
 
174
174
  it("handles missing installedPath silently", () => {
175
175
  const row: RawInstalledRow = {
176
- source: "npm:@tintinweb/pi-subagents",
176
+ source: "npm:pi-dashboard-subagents",
177
177
  scope: "user",
178
178
  filtered: false,
179
179
  };
@@ -17,13 +17,10 @@ describe("isActivityEvent", () => {
17
17
  "agent_start",
18
18
  "agent_end",
19
19
  "bash_output",
20
- "flow_started",
21
- "flow_complete",
22
- "flow_agent_started",
23
- "flow_agent_complete",
24
- "architect_started",
25
- "architect_complete",
26
- "architect_cancelled",
20
+ // Flow / architect events removed: per change
21
+ // pluginize-flows-via-registry the shell carries no flow
22
+ // knowledge. lastActivityAt for flow-only sessions is bumped
23
+ // by the constituent tool/agent events the flow generates.
27
24
  ];
28
25
 
29
26
  for (const t of included) {
@@ -112,14 +112,18 @@ describe("package-routes", () => {
112
112
 
113
113
  it("matches a row to RECOMMENDED_EXTENSIONS by source", async () => {
114
114
  wrapper.listInstalled.mockReturnValueOnce([
115
- { source: "npm:@tintinweb/pi-subagents", scope: "user", filtered: false },
115
+ {
116
+ source: "https://github.com/BlackBeltTechnology/pi-dashboard-subagents.git",
117
+ scope: "user",
118
+ filtered: false,
119
+ },
116
120
  ]);
117
121
  const res = await app.inject({ method: "GET", url: "/api/packages/installed?scope=global" });
118
122
  const body = JSON.parse(res.body);
119
123
  const row = body.data[0];
120
124
  expect(row.isRecommended).toBe(true);
121
125
  // displayName comes from the recommended manifest.
122
- expect(row.displayName).toBe("@tintinweb/pi-subagents");
126
+ expect(row.displayName).toBe("pi-dashboard-subagents");
123
127
  });
124
128
 
125
129
  it("missing installedPath does not break enrichment", async () => {
@@ -168,19 +168,16 @@ describe("pi-changelog-routes", () => {
168
168
  expect(res.statusCode).toBe(400);
169
169
  });
170
170
 
171
- it("returns 503 when bootstrap is not ready", async () => {
172
- await app.close();
173
- bootstrapState.get = () => ({ status: "installing" as const });
174
- app = Fastify({ logger: false });
175
- registerPiChangelogRoutes(app, { bootstrapState });
176
- await app.ready();
177
- const res = await app.inject({
178
- method: "GET",
179
- url: "/api/pi-core/changelog?pkg=@mariozechner/pi-coding-agent&from=0.68.0&to=0.70.0",
180
- });
181
- expect(res.statusCode).toBe(503);
182
- expect(res.json().bootstrap).toBe("installing");
183
- });
171
+ // NOTE: "returns 503 when bootstrap is not ready" test removed.
172
+ // The bootstrap gate on this route was deliberately removed in change
173
+ // `eliminate-electron-runtime-install` (task 3.5, 2026-05-23). The
174
+ // route file's own docstring confirms it: "Bootstrap gate removed
175
+ // under change: eliminate-electron-runtime-install (task 3.5)". The
176
+ // `PiChangelogRouteDeps` interface comment also says the field was
177
+ // removed; the route is unconditionally available. This test was
178
+ // documented as deferred to a "Phase 3.9 sweep" in
179
+ // eliminate-electron-runtime-install/tasks.md task 5.9; this is that
180
+ // sweep.
184
181
 
185
182
  it("returns no releases when from === to", async () => {
186
183
  makeManagedPkg("@mariozechner/pi-coding-agent", {
@@ -20,7 +20,7 @@ describe("PiCoreChecker._internal.looksLikePiEcosystem", () => {
20
20
  });
21
21
 
22
22
  it("rejects scoped pi-* packages that are NOT in the whitelist", () => {
23
- expect(_internal.looksLikePiEcosystem("@tintinweb/pi-subagents")).toBe(false);
23
+ expect(_internal.looksLikePiEcosystem("@scope/pi-fake")).toBe(false);
24
24
  expect(_internal.looksLikePiEcosystem("@benvargas/pi-claude-code-use")).toBe(false);
25
25
  });
26
26
 
@@ -105,7 +105,7 @@ describe("PiCoreChecker.getStatus", () => {
105
105
  dependencies: {
106
106
  "pi-agent-browser": { version: "0.1.0" },
107
107
  "pi-web-access": { version: "0.10.6" },
108
- "@tintinweb/pi-subagents": { version: "0.6.1" },
108
+ "pi-dashboard-subagents": { version: "0.1.1" },
109
109
  },
110
110
  }),
111
111
  fetchLatest: async () => null,
@@ -15,13 +15,14 @@ import {
15
15
  readPiCompatibility,
16
16
  readCurrentPiVersion,
17
17
  computeCompatibility,
18
- _resetVersionSkewCache,
19
18
  } from "../pi-version-skew.js";
20
19
  import type { ToolRegistry, Resolution } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
21
20
 
22
21
  describe("pi-version-skew", () => {
23
22
  beforeEach(() => {
24
- _resetVersionSkewCache();
23
+ // Cache (formerly `_resetVersionSkewCache`) removed under change:
24
+ // eliminate-electron-runtime-install (task 3.6) along with
25
+ // updateBootstrapCompatibility. Tests now exercise pure helpers only.
25
26
  });
26
27
 
27
28
  describe("parseVersion", () => {
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Route tests for `GET /api/plugins` and `POST /api/plugins/:id/toggle`.
3
+ *
4
+ * See change: add-plugin-activation-ui.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
7
+ import Fastify, { type FastifyInstance } from "fastify";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { registerPluginActivationRoutes } from "../routes/plugin-activation-routes.js";
12
+ import {
13
+ clearDiscoveryCache,
14
+ clearStatusStore,
15
+ discoverPlugins,
16
+ getPluginStatusStore,
17
+ } from "@blackbelt-technology/dashboard-plugin-runtime/server";
18
+
19
+ const HOME_OVERRIDE = path.join(os.tmpdir(), "pi-dashboard-activation-test-" + process.pid);
20
+
21
+ function makeRepoRootWithPlugin(id: string, displayName: string, opts: { enabledInConfig?: boolean } = {}) {
22
+ const repoRoot = path.join(os.tmpdir(), `activation-routes-test-${id}-${Date.now()}`);
23
+ const pkgDir = path.join(repoRoot, "packages", id);
24
+ fs.mkdirSync(pkgDir, { recursive: true });
25
+ fs.writeFileSync(
26
+ path.join(pkgDir, "package.json"),
27
+ JSON.stringify({
28
+ name: `@test/${id}`,
29
+ version: "0.0.0",
30
+ "pi-dashboard-plugin": { id, displayName, claims: [] },
31
+ }),
32
+ );
33
+
34
+ if (opts.enabledInConfig !== undefined) {
35
+ fs.mkdirSync(path.join(HOME_OVERRIDE, ".pi", "dashboard"), { recursive: true });
36
+ fs.writeFileSync(
37
+ path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"),
38
+ JSON.stringify({ plugins: { [id]: { enabled: opts.enabledInConfig } } }, null, 2),
39
+ );
40
+ }
41
+ return repoRoot;
42
+ }
43
+
44
+ function makeRepoRootWithPluginPair(
45
+ rootSuffix: string,
46
+ a: { id: string; displayName: string; enabledInConfig?: boolean },
47
+ b: { id: string; displayName: string; dependsOn: string[]; enabledInConfig?: boolean },
48
+ ) {
49
+ const repoRoot = path.join(os.tmpdir(), `activation-routes-test-${rootSuffix}-${Date.now()}`);
50
+ for (const p of [a, b]) {
51
+ const pkgDir = path.join(repoRoot, "packages", p.id);
52
+ fs.mkdirSync(pkgDir, { recursive: true });
53
+ const manifest: Record<string, unknown> = { id: p.id, displayName: p.displayName, claims: [] };
54
+ if ("dependsOn" in p && p.dependsOn) manifest.dependsOn = p.dependsOn;
55
+ fs.writeFileSync(
56
+ path.join(pkgDir, "package.json"),
57
+ JSON.stringify({
58
+ name: `@test/${p.id}`,
59
+ version: "0.0.0",
60
+ "pi-dashboard-plugin": manifest,
61
+ }),
62
+ );
63
+ }
64
+ const cfgPlugins: Record<string, { enabled: boolean }> = {};
65
+ if (a.enabledInConfig !== undefined) cfgPlugins[a.id] = { enabled: a.enabledInConfig };
66
+ if (b.enabledInConfig !== undefined) cfgPlugins[b.id] = { enabled: b.enabledInConfig };
67
+ if (Object.keys(cfgPlugins).length > 0) {
68
+ fs.mkdirSync(path.join(HOME_OVERRIDE, ".pi", "dashboard"), { recursive: true });
69
+ fs.writeFileSync(
70
+ path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"),
71
+ JSON.stringify({ plugins: cfgPlugins }, null, 2),
72
+ );
73
+ }
74
+ return repoRoot;
75
+ }
76
+
77
+ let originalHome: string | undefined;
78
+ async function makeApp(repoRoot: string, broadcasts: unknown[] = []): Promise<FastifyInstance> {
79
+ const app = Fastify({ logger: false });
80
+ registerPluginActivationRoutes(app, {
81
+ networkGuard: async () => undefined,
82
+ broadcast: (m) => broadcasts.push(m),
83
+ repoRoot,
84
+ });
85
+ await app.ready();
86
+ return app;
87
+ }
88
+
89
+ describe("/api/plugins", () => {
90
+ let app: FastifyInstance;
91
+ beforeEach(() => {
92
+ clearDiscoveryCache();
93
+ clearStatusStore();
94
+ fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
95
+ originalHome = process.env.HOME;
96
+ process.env.HOME = HOME_OVERRIDE;
97
+ });
98
+ afterEach(async () => {
99
+ await app?.close();
100
+ if (originalHome !== undefined) process.env.HOME = originalHome;
101
+ else delete process.env.HOME;
102
+ fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
103
+ });
104
+
105
+ it("returns every discovered plugin with manifest summary + status", async () => {
106
+ const repoRoot = makeRepoRootWithPlugin("act-a", "Act A");
107
+ // Seed status so the row carries the runtime view too.
108
+ getPluginStatusStore().setStatus({
109
+ id: "act-a",
110
+ displayName: "Act A",
111
+ enabled: true,
112
+ loaded: true,
113
+ claims: 0,
114
+ });
115
+ app = await makeApp(repoRoot);
116
+
117
+ const res = await app.inject({ method: "GET", url: "/api/plugins" });
118
+ expect(res.statusCode).toBe(200);
119
+ const body = res.json() as { success: boolean; plugins: any[] };
120
+ expect(body.success).toBe(true);
121
+ expect(body.plugins).toHaveLength(1);
122
+ expect(body.plugins[0].id).toBe("act-a");
123
+ expect(body.plugins[0].displayName).toBe("Act A");
124
+ expect(body.plugins[0].status.enabled).toBe(true);
125
+ });
126
+ });
127
+
128
+ describe("POST /api/plugins/:id/toggle", () => {
129
+ let app: FastifyInstance;
130
+ let broadcasts: unknown[];
131
+ beforeEach(() => {
132
+ clearDiscoveryCache();
133
+ clearStatusStore();
134
+ fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
135
+ originalHome = process.env.HOME;
136
+ process.env.HOME = HOME_OVERRIDE;
137
+ broadcasts = [];
138
+ });
139
+ afterEach(async () => {
140
+ await app?.close();
141
+ if (originalHome !== undefined) process.env.HOME = originalHome;
142
+ else delete process.env.HOME;
143
+ fs.rmSync(HOME_OVERRIDE, { recursive: true, force: true });
144
+ });
145
+
146
+ it("persists enabled=false to config.json and broadcasts plugin_config_update", async () => {
147
+ const repoRoot = makeRepoRootWithPlugin("act-b", "Act B");
148
+ app = await makeApp(repoRoot, broadcasts);
149
+
150
+ const res = await app.inject({
151
+ method: "POST",
152
+ url: "/api/plugins/act-b/toggle",
153
+ payload: { enabled: false },
154
+ });
155
+ expect(res.statusCode).toBe(200);
156
+ const body = res.json() as { success: boolean; restartRequired: boolean };
157
+ expect(body.restartRequired).toBe(true);
158
+
159
+ const cfgPath = path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json");
160
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
161
+ expect(cfg.plugins["act-b"].enabled).toBe(false);
162
+
163
+ expect(broadcasts).toHaveLength(1);
164
+ expect((broadcasts[0] as any).type).toBe("plugin_config_update");
165
+ expect((broadcasts[0] as any).id).toBe("act-b");
166
+ });
167
+
168
+ it("returns 404 for unknown plugin id", async () => {
169
+ const repoRoot = makeRepoRootWithPlugin("act-c", "Act C");
170
+ app = await makeApp(repoRoot, broadcasts);
171
+
172
+ const res = await app.inject({
173
+ method: "POST",
174
+ url: "/api/plugins/no-such/toggle",
175
+ payload: { enabled: false },
176
+ });
177
+ expect(res.statusCode).toBe(404);
178
+ // config.json should not have been created
179
+ const cfgPath = path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json");
180
+ expect(fs.existsSync(cfgPath)).toBe(false);
181
+ });
182
+
183
+ it("returns 400 when body.enabled is not a boolean", async () => {
184
+ const repoRoot = makeRepoRootWithPlugin("act-d", "Act D");
185
+ app = await makeApp(repoRoot, broadcasts);
186
+
187
+ const res = await app.inject({
188
+ method: "POST",
189
+ url: "/api/plugins/act-d/toggle",
190
+ payload: { enabled: "yes" },
191
+ });
192
+ expect(res.statusCode).toBe(400);
193
+ });
194
+
195
+ // ---- Dependency-graph cascade tests (Robert's Layer 2 spec) ------------
196
+
197
+ it("enabling a plugin with a missing dep returns 409 + blockers", async () => {
198
+ const repoRoot = makeRepoRootWithPluginPair(
199
+ "cascade-blocker",
200
+ { id: "orphan-a", displayName: "Orphan A" },
201
+ { id: "orphan-b", displayName: "Orphan B", dependsOn: ["missing-x"], enabledInConfig: false },
202
+ );
203
+ app = await makeApp(repoRoot, broadcasts);
204
+
205
+ const res = await app.inject({
206
+ method: "POST",
207
+ url: "/api/plugins/orphan-b/toggle",
208
+ payload: { enabled: true },
209
+ });
210
+ expect(res.statusCode).toBe(409);
211
+ const body = res.json() as { success: boolean; reason: string; blockers: string[] };
212
+ expect(body.reason).toBe("blockers");
213
+ expect(body.blockers).toEqual(["missing-x"]);
214
+ });
215
+
216
+ it("enabling cascades disabled deps and writes atomically", async () => {
217
+ const repoRoot = makeRepoRootWithPluginPair(
218
+ "cascade-enable",
219
+ { id: "cas-a", displayName: "Cas A", enabledInConfig: false },
220
+ { id: "cas-b", displayName: "Cas B", dependsOn: ["cas-a"], enabledInConfig: false },
221
+ );
222
+ app = await makeApp(repoRoot, broadcasts);
223
+
224
+ const res = await app.inject({
225
+ method: "POST",
226
+ url: "/api/plugins/cas-b/toggle",
227
+ payload: { enabled: true },
228
+ });
229
+ expect(res.statusCode).toBe(200);
230
+ const body = res.json() as { success: boolean; cascade: { enable: string[] } };
231
+ expect(body.cascade.enable).toEqual(["cas-a"]);
232
+
233
+ const cfg = JSON.parse(
234
+ fs.readFileSync(path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"), "utf-8"),
235
+ );
236
+ expect(cfg.plugins["cas-a"].enabled).toBe(true);
237
+ expect(cfg.plugins["cas-b"].enabled).toBe(true);
238
+
239
+ // Two broadcasts, one per affected id.
240
+ const ids = broadcasts.map((m) => (m as { id: string }).id).sort();
241
+ expect(ids).toEqual(["cas-a", "cas-b"]);
242
+ });
243
+
244
+ it("disabling cascades enabled dependents and writes atomically", async () => {
245
+ const repoRoot = makeRepoRootWithPluginPair(
246
+ "cascade-disable",
247
+ { id: "dis-a", displayName: "Dis A", enabledInConfig: true },
248
+ { id: "dis-b", displayName: "Dis B", dependsOn: ["dis-a"], enabledInConfig: true },
249
+ );
250
+ app = await makeApp(repoRoot, broadcasts);
251
+
252
+ const res = await app.inject({
253
+ method: "POST",
254
+ url: "/api/plugins/dis-a/toggle",
255
+ payload: { enabled: false },
256
+ });
257
+ expect(res.statusCode).toBe(200);
258
+ const body = res.json() as { success: boolean; cascade: { disable: string[] } };
259
+ expect(body.cascade.disable).toEqual(["dis-b"]);
260
+
261
+ const cfg = JSON.parse(
262
+ fs.readFileSync(path.join(HOME_OVERRIDE, ".pi", "dashboard", "config.json"), "utf-8"),
263
+ );
264
+ expect(cfg.plugins["dis-a"].enabled).toBe(false);
265
+ expect(cfg.plugins["dis-b"].enabled).toBe(false);
266
+ });
267
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tests for PluginIntentCache — server-side replay store for plugin intents.
3
+ *
4
+ * See change: adopt-server-driven-intent-rendering.
5
+ */
6
+ import { describe, it, expect, beforeEach } from "vitest";
7
+ import { PluginIntentCache } from "../plugin-intent-cache.js";
8
+ import type { IntentNode } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/intent-types.js";
9
+
10
+ const sampleIntent: IntentNode = {
11
+ primitive: "ui:action-list",
12
+ props: { actions: [{ label: "Run X" }] },
13
+ };
14
+
15
+ describe("PluginIntentCache", () => {
16
+ let cache: PluginIntentCache;
17
+
18
+ beforeEach(() => {
19
+ cache = new PluginIntentCache();
20
+ });
21
+
22
+ it("set + getForSession returns the cached intent", () => {
23
+ cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
24
+ const entries = cache.getForSession("abc");
25
+ expect(entries).toHaveLength(1);
26
+ expect(entries[0].pluginId).toBe("flows");
27
+ expect(entries[0].intent).toEqual(sampleIntent);
28
+ });
29
+
30
+ it("setting null intent clears the slot", () => {
31
+ cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
32
+ cache.set("flows", "abc", "session-card-action-bar", null);
33
+ const entries = cache.getForSession("abc");
34
+ expect(entries).toHaveLength(0);
35
+ });
36
+
37
+ it("clearForSession removes only entries for that session", () => {
38
+ cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
39
+ cache.set("flows", "xyz", "session-card-action-bar", sampleIntent);
40
+ cache.clearForSession("abc");
41
+ expect(cache.getForSession("abc")).toHaveLength(0);
42
+ expect(cache.getForSession("xyz")).toHaveLength(1);
43
+ });
44
+
45
+ it("supports global slots with sessionId=null", () => {
46
+ cache.set("honcho", null, "settings-section", sampleIntent);
47
+ const entries = cache.getForSession(null);
48
+ expect(entries).toHaveLength(1);
49
+ expect(entries[0].sessionId).toBeNull();
50
+ });
51
+
52
+ it("two plugins can occupy the same slot for the same session", () => {
53
+ cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
54
+ cache.set("jj", "abc", "session-card-action-bar", sampleIntent);
55
+ const entries = cache.getForSession("abc");
56
+ expect(entries).toHaveLength(2);
57
+ expect(new Set(entries.map((e) => e.pluginId))).toEqual(new Set(["flows", "jj"]));
58
+ });
59
+
60
+ it("same (pluginId, sessionId, slot) overwrites", () => {
61
+ const newer: IntentNode = { primitive: "ui:status-pill", props: { text: "updated" } };
62
+ cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
63
+ cache.set("flows", "abc", "session-card-action-bar", newer);
64
+ const entries = cache.getForSession("abc");
65
+ expect(entries).toHaveLength(1);
66
+ expect(entries[0].intent).toEqual(newer);
67
+ });
68
+
69
+ it("reset clears everything", () => {
70
+ cache.set("flows", "abc", "session-card-action-bar", sampleIntent);
71
+ cache.set("honcho", null, "settings-section", sampleIntent);
72
+ cache.reset();
73
+ expect(cache.getAll()).toHaveLength(0);
74
+ });
75
+ });