@blackbelt-technology/pi-agent-dashboard 0.5.3 → 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 +10 -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
@@ -1,55 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { extractSessionUpdates } from "../event-status-extraction.js";
3
- import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
4
-
5
- function makeEvent(eventType: string, data: Record<string, unknown> = {}): DashboardEvent {
6
- return { eventType, timestamp: Date.now(), data };
7
- }
8
-
9
- describe("extractSessionUpdates flow events", () => {
10
- it("flow_started extracts flow metadata", () => {
11
- const updates = extractSessionUpdates(makeEvent("flow_started", {
12
- flowName: "research-and-build",
13
- steps: [
14
- { id: "r", stepType: "agent", agent: "researcher" },
15
- { id: "d", stepType: "agent", agent: "developer" },
16
- { id: "f1", stepType: "fork", question: "which?" },
17
- ],
18
- }));
19
- expect(updates).toEqual({
20
- activeFlowName: "research-and-build",
21
- flowAgentsTotal: 2,
22
- flowAgentsDone: 0,
23
- flowStatus: "running",
24
- });
25
- });
26
-
27
- it("flow_agent_complete returns sentinel for increment", () => {
28
- const updates = extractSessionUpdates(makeEvent("flow_agent_complete", {
29
- agentName: "researcher",
30
- result: { success: true },
31
- }));
32
- expect(updates).toEqual({ flowAgentsDone: -1 });
33
- });
34
-
35
- it("flow_complete extracts status", () => {
36
- const updates = extractSessionUpdates(makeEvent("flow_complete", {
37
- status: "error",
38
- flowName: "test",
39
- }));
40
- expect(updates).toEqual({ flowStatus: "error" });
41
- });
42
-
43
- it("flow_complete defaults to success", () => {
44
- const updates = extractSessionUpdates(makeEvent("flow_complete", {
45
- flowName: "test",
46
- }));
47
- expect(updates).toEqual({ flowStatus: "success" });
48
- });
49
-
50
- it("other flow events return null", () => {
51
- expect(extractSessionUpdates(makeEvent("flow_tool_call"))).toBeNull();
52
- expect(extractSessionUpdates(makeEvent("flow_assistant_text"))).toBeNull();
53
- expect(extractSessionUpdates(makeEvent("flow_loop_iteration"))).toBeNull();
54
- });
55
- });
@@ -1,149 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import {
6
- parseVersion,
7
- legacyPathUnder,
8
- detectLegacyPiInstalls,
9
- uninstallLegacyPi,
10
- LEGACY_PI_PACKAGE,
11
- type LegacyPiInstall,
12
- } from "../legacy-pi-cleanup.js";
13
-
14
- describe("parseVersion", () => {
15
- it("returns version from valid json", () => {
16
- expect(parseVersion('{"name":"x","version":"1.2.3"}')).toBe("1.2.3");
17
- });
18
- it("returns null on parse error", () => {
19
- expect(parseVersion("not json")).toBeNull();
20
- });
21
- it("returns null when version missing", () => {
22
- expect(parseVersion('{"name":"x"}')).toBeNull();
23
- });
24
- });
25
-
26
- describe("legacyPathUnder", () => {
27
- it("joins node_modules with legacy package", () => {
28
- const p = legacyPathUnder("/tmp/nm");
29
- expect(p.endsWith(path.join("@mariozechner", "pi-coding-agent"))).toBe(true);
30
- });
31
- });
32
-
33
- describe("detectLegacyPiInstalls (filesystem)", () => {
34
- let tmpHome: string;
35
- let origHome: string | undefined;
36
-
37
- beforeEach(() => {
38
- tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "legacy-pi-test-"));
39
- origHome = process.env.HOME;
40
- process.env.HOME = tmpHome;
41
- });
42
-
43
- afterEach(() => {
44
- if (origHome !== undefined) process.env.HOME = origHome;
45
- fs.rmSync(tmpHome, { recursive: true, force: true });
46
- });
47
-
48
- function plantLegacy(scopeDir: string, version: string): string {
49
- const pkgDir = path.join(scopeDir, ...LEGACY_PI_PACKAGE.split("/"));
50
- fs.mkdirSync(pkgDir, { recursive: true });
51
- fs.writeFileSync(
52
- path.join(pkgDir, "package.json"),
53
- JSON.stringify({ name: LEGACY_PI_PACKAGE, version }),
54
- );
55
- return pkgDir;
56
- }
57
-
58
- it("returns empty when nothing planted", () => {
59
- // Note: this still consults `npm root -g` from the real system; if that
60
- // happens to contain @mariozechner/pi-coding-agent the test would
61
- // see it. We accept that and only assert npx-cache + managed are empty.
62
- const found = detectLegacyPiInstalls();
63
- expect(found.filter((f) => f.scope !== "npm-global")).toEqual([]);
64
- });
65
-
66
- it("detects npx-cache install", () => {
67
- plantLegacy(path.join(tmpHome, ".npm", "_npx", "abc123", "node_modules"), "0.73.1");
68
- const found = detectLegacyPiInstalls().filter((f) => f.scope === "npx-cache");
69
- expect(found).toHaveLength(1);
70
- expect(found[0].version).toBe("0.73.1");
71
- });
72
-
73
- it("detects managed install", () => {
74
- plantLegacy(path.join(tmpHome, ".pi-dashboard", "node_modules"), "0.70.0");
75
- const found = detectLegacyPiInstalls().filter((f) => f.scope === "managed");
76
- expect(found).toHaveLength(1);
77
- expect(found[0].version).toBe("0.70.0");
78
- });
79
-
80
- it("detects multiple npx-cache installs", () => {
81
- plantLegacy(path.join(tmpHome, ".npm", "_npx", "h1", "node_modules"), "0.72.0");
82
- plantLegacy(path.join(tmpHome, ".npm", "_npx", "h2", "node_modules"), "0.73.0");
83
- const found = detectLegacyPiInstalls().filter((f) => f.scope === "npx-cache");
84
- expect(found).toHaveLength(2);
85
- expect(found.map((f) => f.version).sort()).toEqual(["0.72.0", "0.73.0"]);
86
- });
87
- });
88
-
89
- describe("uninstallLegacyPi (filesystem subset)", () => {
90
- let tmpHome: string;
91
- let origHome: string | undefined;
92
-
93
- beforeEach(() => {
94
- tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "legacy-pi-rm-test-"));
95
- origHome = process.env.HOME;
96
- process.env.HOME = tmpHome;
97
- });
98
-
99
- afterEach(() => {
100
- if (origHome !== undefined) process.env.HOME = origHome;
101
- fs.rmSync(tmpHome, { recursive: true, force: true });
102
- });
103
-
104
- function plant(scope: "managed" | "npx-cache", version: string): LegacyPiInstall {
105
- const base =
106
- scope === "managed"
107
- ? path.join(tmpHome, ".pi-dashboard", "node_modules")
108
- : path.join(tmpHome, ".npm", "_npx", "x1", "node_modules");
109
- const pkgDir = path.join(base, ...LEGACY_PI_PACKAGE.split("/"));
110
- fs.mkdirSync(pkgDir, { recursive: true });
111
- fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify({ version }));
112
- return { scope, path: pkgDir, version };
113
- }
114
-
115
- it("removes managed install via rm -rf", () => {
116
- const install = plant("managed", "0.70.0");
117
- expect(fs.existsSync(install.path)).toBe(true);
118
- const results = uninstallLegacyPi([install]);
119
- expect(results[0]).toEqual({ scope: "managed", path: install.path, removed: true });
120
- expect(fs.existsSync(install.path)).toBe(false);
121
- });
122
-
123
- it("removes npx-cache install via rm -rf", () => {
124
- const install = plant("npx-cache", "0.73.1");
125
- const results = uninstallLegacyPi([install]);
126
- expect(results[0].removed).toBe(true);
127
- expect(fs.existsSync(install.path)).toBe(false);
128
- });
129
-
130
- it("returns error result when path does not exist (rm force suppresses, returns removed=true)", () => {
131
- // fs.rmSync with force:true treats missing paths as success.
132
- const install: LegacyPiInstall = {
133
- scope: "managed",
134
- path: path.join(tmpHome, "does-not-exist"),
135
- version: null,
136
- };
137
- const results = uninstallLegacyPi([install]);
138
- expect(results[0].removed).toBe(true);
139
- });
140
-
141
- it("processes multiple installs independently", () => {
142
- const a = plant("managed", "0.70.0");
143
- const b = plant("npx-cache", "0.73.0");
144
- const results = uninstallLegacyPi([a, b]);
145
- expect(results.every((r) => r.removed)).toBe(true);
146
- expect(fs.existsSync(a.path)).toBe(false);
147
- expect(fs.existsSync(b.path)).toBe(false);
148
- });
149
- });
@@ -1,180 +0,0 @@
1
- /**
2
- * Unit tests for the post-install OpenSpec + pi-resources force-refresh
3
- * portion of `runPostInstallRepair`.
4
- *
5
- * See change: fix-openspec-buttons-after-bootstrap-install.
6
- */
7
- import { describe, it, expect, vi, beforeEach } from "vitest";
8
- import { runPostInstallRepair } from "../server.js";
9
-
10
- interface SpyRegistry { rescan: ReturnType<typeof vi.fn>; }
11
- interface SpyDirSvc {
12
- knownDirectories: ReturnType<typeof vi.fn>;
13
- getOpenSpecData: ReturnType<typeof vi.fn>;
14
- refreshOpenSpec: ReturnType<typeof vi.fn>;
15
- refreshPiResources: ReturnType<typeof vi.fn>;
16
- }
17
- interface SpyGateway { broadcastToAll: ReturnType<typeof vi.fn>; }
18
-
19
- function makeRegistry(): SpyRegistry { return { rescan: vi.fn() }; }
20
- function makeGateway(): SpyGateway { return { broadcastToAll: vi.fn() }; }
21
-
22
- describe("runPostInstallRepair: openspec + pi-resources refresh", () => {
23
- let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
24
-
25
- beforeEach(() => {
26
- consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
27
- });
28
-
29
- it("calls refreshOpenSpec(cwd) once for every known directory", async () => {
30
- const registry = makeRegistry();
31
- const cwds = ["/p1", "/p2", "/p3"];
32
- const directoryService: SpyDirSvc = {
33
- knownDirectories: vi.fn(() => cwds),
34
- getOpenSpecData: vi.fn(() => ({ initialized: false, changes: [] })),
35
- refreshOpenSpec: vi.fn(async () => ({ initialized: true, changes: [{ name: "x" } as never] })),
36
- refreshPiResources: vi.fn(async () => ({})),
37
- };
38
- const browserGateway = makeGateway();
39
-
40
- await runPostInstallRepair({
41
- registry: registry as never,
42
- directoryService: directoryService as never,
43
- browserGateway: browserGateway as never,
44
- });
45
-
46
- expect(directoryService.refreshOpenSpec).toHaveBeenCalledTimes(3);
47
- expect(directoryService.refreshOpenSpec).toHaveBeenNthCalledWith(1, "/p1");
48
- expect(directoryService.refreshOpenSpec).toHaveBeenNthCalledWith(2, "/p2");
49
- expect(directoryService.refreshOpenSpec).toHaveBeenNthCalledWith(3, "/p3");
50
- });
51
-
52
- it("broadcasts openspec_update for each cwd whose prior cache was empty/undefined", async () => {
53
- const cwds = ["/a", "/b"];
54
- const fresh = { initialized: true, changes: [{ name: "c1" } as never] };
55
- const directoryService: SpyDirSvc = {
56
- knownDirectories: vi.fn(() => cwds),
57
- // Prior was undefined for /a, empty for /b — both should broadcast.
58
- getOpenSpecData: vi.fn((cwd: string) =>
59
- cwd === "/a" ? undefined : { initialized: false, changes: [] }
60
- ),
61
- refreshOpenSpec: vi.fn(async () => fresh),
62
- refreshPiResources: vi.fn(async () => ({})),
63
- };
64
- const browserGateway = makeGateway();
65
-
66
- await runPostInstallRepair({
67
- registry: makeRegistry() as never,
68
- directoryService: directoryService as never,
69
- browserGateway: browserGateway as never,
70
- });
71
-
72
- const broadcasts = browserGateway.broadcastToAll.mock.calls
73
- .map((c: unknown[]) => c[0])
74
- .filter((m: any) => m?.type === "openspec_update");
75
- expect(broadcasts).toHaveLength(2);
76
- expect(broadcasts).toContainEqual({ type: "openspec_update", cwd: "/a", data: fresh });
77
- expect(broadcasts).toContainEqual({ type: "openspec_update", cwd: "/b", data: fresh });
78
- });
79
-
80
- it("does not broadcast openspec_update when refreshed data equals prior data", async () => {
81
- const same = { initialized: true, changes: [{ name: "stable" } as never] };
82
- const directoryService: SpyDirSvc = {
83
- knownDirectories: vi.fn(() => ["/p"]),
84
- getOpenSpecData: vi.fn(() => same),
85
- refreshOpenSpec: vi.fn(async () => same),
86
- refreshPiResources: vi.fn(async () => ({})),
87
- };
88
- const browserGateway = makeGateway();
89
-
90
- await runPostInstallRepair({
91
- registry: makeRegistry() as never,
92
- directoryService: directoryService as never,
93
- browserGateway: browserGateway as never,
94
- });
95
-
96
- const broadcasts = browserGateway.broadcastToAll.mock.calls
97
- .map((c: unknown[]) => c[0])
98
- .filter((m: any) => m?.type === "openspec_update");
99
- expect(broadcasts).toHaveLength(0);
100
- });
101
-
102
- it("isolates a per-cwd refresh failure — the other cwd still refreshes and broadcasts", async () => {
103
- const cwds = ["/good", "/bad"];
104
- const fresh = { initialized: true, changes: [{ name: "ok" } as never] };
105
- const directoryService: SpyDirSvc = {
106
- knownDirectories: vi.fn(() => cwds),
107
- getOpenSpecData: vi.fn(() => undefined),
108
- refreshOpenSpec: vi.fn(async (cwd: string) => {
109
- if (cwd === "/bad") throw new Error("boom");
110
- return fresh;
111
- }),
112
- refreshPiResources: vi.fn(async () => ({})),
113
- };
114
- const browserGateway = makeGateway();
115
-
116
- await runPostInstallRepair({
117
- registry: makeRegistry() as never,
118
- directoryService: directoryService as never,
119
- browserGateway: browserGateway as never,
120
- });
121
-
122
- // Both cwds attempted.
123
- expect(directoryService.refreshOpenSpec).toHaveBeenCalledTimes(2);
124
- // Only the good one broadcasts.
125
- const broadcasts = browserGateway.broadcastToAll.mock.calls
126
- .map((c: unknown[]) => c[0])
127
- .filter((m: any) => m?.type === "openspec_update" && m.cwd === "/good");
128
- expect(broadcasts).toHaveLength(1);
129
- // Failure was logged, not propagated.
130
- expect(consoleErrorSpy).toHaveBeenCalled();
131
- });
132
-
133
- it("calls refreshPiResources(cwd) once for every known directory", async () => {
134
- const cwds = ["/a", "/b", "/c"];
135
- const directoryService: SpyDirSvc = {
136
- knownDirectories: vi.fn(() => cwds),
137
- getOpenSpecData: vi.fn(() => undefined),
138
- refreshOpenSpec: vi.fn(async () => ({ initialized: false, changes: [] })),
139
- refreshPiResources: vi.fn(async () => ({})),
140
- };
141
-
142
- await runPostInstallRepair({
143
- registry: makeRegistry() as never,
144
- directoryService: directoryService as never,
145
- browserGateway: makeGateway() as never,
146
- });
147
-
148
- expect(directoryService.refreshPiResources).toHaveBeenCalledTimes(3);
149
- expect(directoryService.refreshPiResources).toHaveBeenCalledWith("/a");
150
- expect(directoryService.refreshPiResources).toHaveBeenCalledWith("/b");
151
- expect(directoryService.refreshPiResources).toHaveBeenCalledWith("/c");
152
- });
153
-
154
- it("a refreshPiResources failure does not block other cwds or openspec broadcasts", async () => {
155
- const cwds = ["/good", "/bad"];
156
- const fresh = { initialized: true, changes: [{ name: "ok" } as never] };
157
- const directoryService: SpyDirSvc = {
158
- knownDirectories: vi.fn(() => cwds),
159
- getOpenSpecData: vi.fn(() => undefined),
160
- refreshOpenSpec: vi.fn(async () => fresh),
161
- refreshPiResources: vi.fn(async (cwd: string) => {
162
- if (cwd === "/bad") throw new Error("pi-resources blew up");
163
- return {};
164
- }),
165
- };
166
- const browserGateway = makeGateway();
167
-
168
- await runPostInstallRepair({
169
- registry: makeRegistry() as never,
170
- directoryService: directoryService as never,
171
- browserGateway: browserGateway as never,
172
- });
173
-
174
- expect(directoryService.refreshPiResources).toHaveBeenCalledTimes(2);
175
- const openSpecBroadcasts = browserGateway.broadcastToAll.mock.calls
176
- .map((c: unknown[]) => c[0])
177
- .filter((m: any) => m?.type === "openspec_update");
178
- expect(openSpecBroadcasts).toHaveLength(2);
179
- });
180
- });
@@ -1,134 +0,0 @@
1
- /**
2
- * Unit tests for the centralized post-install rescan hook.
3
- *
4
- * Asserts that the helper invoked from the bootstrap-state subscribe
5
- * callback calls `registry.rescan()` (no arg → full registry invalidate)
6
- * exactly once on `installing → ready` and never on other transitions.
7
- *
8
- * See change: fix-openspec-buttons-after-bootstrap-install.
9
- */
10
- import { describe, it, expect, vi } from "vitest";
11
- import { runPostInstallRepair, makeBootstrapTransitionHandler } from "../server.js";
12
-
13
- interface SpyRegistry {
14
- rescan: ReturnType<typeof vi.fn>;
15
- }
16
-
17
- interface SpyDirSvc {
18
- knownDirectories: ReturnType<typeof vi.fn>;
19
- getOpenSpecData: ReturnType<typeof vi.fn>;
20
- refreshOpenSpec: ReturnType<typeof vi.fn>;
21
- refreshPiResources: ReturnType<typeof vi.fn>;
22
- }
23
-
24
- interface SpyGateway {
25
- broadcastToAll: ReturnType<typeof vi.fn>;
26
- }
27
-
28
- function makeRegistry(): SpyRegistry {
29
- return { rescan: vi.fn() };
30
- }
31
-
32
- function makeDirSvc(): SpyDirSvc {
33
- return {
34
- knownDirectories: vi.fn(() => []),
35
- getOpenSpecData: vi.fn(() => undefined),
36
- refreshOpenSpec: vi.fn(async () => ({ initialized: false, changes: [] })),
37
- refreshPiResources: vi.fn(async () => ({ initialized: false, packages: [] })),
38
- };
39
- }
40
-
41
- function makeGateway(): SpyGateway {
42
- return { broadcastToAll: vi.fn() };
43
- }
44
-
45
- describe("runPostInstallRepair", () => {
46
- it("calls registry.rescan() with no argument (full registry)", async () => {
47
- const registry = makeRegistry();
48
- const directoryService = makeDirSvc();
49
- const browserGateway = makeGateway();
50
- await runPostInstallRepair({
51
- registry: registry as never,
52
- directoryService: directoryService as never,
53
- browserGateway: browserGateway as never,
54
- });
55
- expect(registry.rescan).toHaveBeenCalledTimes(1);
56
- expect(registry.rescan).toHaveBeenCalledWith();
57
- });
58
- });
59
-
60
- describe("makeBootstrapTransitionHandler", () => {
61
- it("invokes the post-install repair exactly once on installing → ready", async () => {
62
- const repair = vi.fn(async () => undefined);
63
- const flushAll = vi.fn(async () => undefined);
64
- const handler = makeBootstrapTransitionHandler({
65
- onTransitionToReady: repair,
66
- flushQueue: flushAll,
67
- });
68
- handler({ status: "installing" } as never);
69
- handler({ status: "ready" } as never);
70
- // Wait one microtask tick for the fire-and-forget to fire.
71
- await Promise.resolve();
72
- expect(repair).toHaveBeenCalledTimes(1);
73
- expect(flushAll).toHaveBeenCalledTimes(1);
74
- });
75
-
76
- it("does not call repair on ready → ready", async () => {
77
- const repair = vi.fn(async () => undefined);
78
- const flushAll = vi.fn(async () => undefined);
79
- const handler = makeBootstrapTransitionHandler({
80
- onTransitionToReady: repair,
81
- flushQueue: flushAll,
82
- });
83
- // Initial state defaults to ready, so first ready-snapshot is a no-op.
84
- handler({ status: "ready" } as never);
85
- handler({ status: "ready" } as never);
86
- await Promise.resolve();
87
- expect(repair).not.toHaveBeenCalled();
88
- expect(flushAll).not.toHaveBeenCalled();
89
- });
90
-
91
- it("does not call repair on installing → failed", async () => {
92
- const repair = vi.fn(async () => undefined);
93
- const flushAll = vi.fn(async () => undefined);
94
- const handler = makeBootstrapTransitionHandler({
95
- onTransitionToReady: repair,
96
- flushQueue: flushAll,
97
- });
98
- handler({ status: "installing" } as never);
99
- handler({ status: "failed" } as never);
100
- await Promise.resolve();
101
- expect(repair).not.toHaveBeenCalled();
102
- expect(flushAll).not.toHaveBeenCalled();
103
- });
104
-
105
- it("does not call repair on the very first subscribe snapshot", async () => {
106
- // Bootstrap state defaults to "ready"; the first emitted snapshot
107
- // (e.g. from an immediate broadcast) should NOT trigger the hook.
108
- const repair = vi.fn(async () => undefined);
109
- const flushAll = vi.fn(async () => undefined);
110
- const handler = makeBootstrapTransitionHandler({
111
- onTransitionToReady: repair,
112
- flushQueue: flushAll,
113
- });
114
- handler({ status: "ready" } as never);
115
- await Promise.resolve();
116
- expect(repair).not.toHaveBeenCalled();
117
- });
118
-
119
- it("calls repair on a second installing → ready cycle (e.g. user retry)", async () => {
120
- const repair = vi.fn(async () => undefined);
121
- const flushAll = vi.fn(async () => undefined);
122
- const handler = makeBootstrapTransitionHandler({
123
- onTransitionToReady: repair,
124
- flushQueue: flushAll,
125
- });
126
- handler({ status: "installing" } as never);
127
- handler({ status: "ready" } as never);
128
- handler({ status: "installing" } as never);
129
- handler({ status: "ready" } as never);
130
- await Promise.resolve();
131
- expect(repair).toHaveBeenCalledTimes(2);
132
- expect(flushAll).toHaveBeenCalledTimes(2);
133
- });
134
- });
@@ -1,91 +0,0 @@
1
- /**
2
- * Tests for POST /api/electron/reextract
3
- * See change: simplify-electron-bootstrap-derived-state (task 6.4 / 6.9).
4
- */
5
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
6
- import Fastify, { type FastifyInstance } from "fastify";
7
- import { registerSystemRoutes } from "../routes/system-routes.js";
8
- import type { BootstrapStateStore, BootstrapState } from "../bootstrap-state.js";
9
-
10
- function noGuard() {
11
- return async () => { /* allow all */ };
12
- }
13
-
14
- function makeBootstrapState(starter: string): BootstrapStateStore {
15
- return {
16
- get: () => ({
17
- status: "ready",
18
- starter: starter as any,
19
- installable: { total: 0, installed: 0, failed: [] },
20
- } as BootstrapState),
21
- set: () => {},
22
- subscribe: () => () => {},
23
- } as unknown as BootstrapStateStore;
24
- }
25
-
26
- function makeNoopDeps(bootstrapState?: BootstrapStateStore) {
27
- return {
28
- sessionManager: { listActive: () => [], listAll: () => [] } as never,
29
- preferencesStore: { flush: () => {} } as never,
30
- metaPersistence: { flushAll: () => {} } as never,
31
- config: { port: 8000, piPort: 9999, dev: false } as never,
32
- networkGuard: noGuard(),
33
- bootstrapState,
34
- };
35
- }
36
-
37
- describe("POST /api/electron/reextract", () => {
38
- let fastify: FastifyInstance;
39
-
40
- beforeEach(async () => {
41
- fastify = Fastify();
42
- });
43
-
44
- afterEach(async () => {
45
- await fastify.close();
46
- });
47
-
48
- it("returns 403 when starter is Bridge", async () => {
49
- const deps = makeNoopDeps(makeBootstrapState("Bridge"));
50
- registerSystemRoutes(fastify, deps);
51
- await fastify.ready();
52
-
53
- const res = await fastify.inject({ method: "POST", url: "/api/electron/reextract" });
54
- expect(res.statusCode).toBe(403);
55
- const body = res.json() as Record<string, unknown>;
56
- expect(body.error).toBe("reextract_not_allowed");
57
- expect(body.starter).toBe("Bridge");
58
- });
59
-
60
- it("returns 403 when starter is Standalone", async () => {
61
- const deps = makeNoopDeps(makeBootstrapState("Standalone"));
62
- registerSystemRoutes(fastify, deps);
63
- await fastify.ready();
64
-
65
- const res = await fastify.inject({ method: "POST", url: "/api/electron/reextract" });
66
- expect(res.statusCode).toBe(403);
67
- const body = res.json() as Record<string, unknown>;
68
- expect(body.error).toBe("reextract_not_allowed");
69
- expect(body.starter).toBe("Standalone");
70
- });
71
-
72
- it("returns 202 when starter is Electron", async () => {
73
- const deps = makeNoopDeps(makeBootstrapState("Electron"));
74
- registerSystemRoutes(fastify, deps);
75
- await fastify.ready();
76
-
77
- const res = await fastify.inject({ method: "POST", url: "/api/electron/reextract" });
78
- expect(res.statusCode).toBe(202);
79
- const body = res.json() as Record<string, unknown>;
80
- expect(body.ok).toBe(true);
81
- });
82
-
83
- it("returns 403 when no bootstrapState (defaults to Standalone)", async () => {
84
- const deps = makeNoopDeps(undefined);
85
- registerSystemRoutes(fastify, deps);
86
- await fastify.ready();
87
-
88
- const res = await fastify.inject({ method: "POST", url: "/api/electron/reextract" });
89
- expect(res.statusCode).toBe(403);
90
- });
91
- });