@blackbelt-technology/pi-agent-dashboard 0.2.0

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 +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. package/packages/shared/tsconfig.json +8 -0
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const { mockedExecSync } = vi.hoisted(() => ({
4
+ mockedExecSync: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("node:child_process", () => ({
8
+ default: { execSync: mockedExecSync },
9
+ execSync: mockedExecSync,
10
+ }));
11
+
12
+ import { detectEditors, isProcessRunning, isProcessRunningWin32, EDITORS, type DetectedEditor } from "../editor-registry.js";
13
+
14
+ describe("editor-registry", () => {
15
+ beforeEach(() => {
16
+ vi.resetAllMocks();
17
+ });
18
+
19
+ describe("EDITORS", () => {
20
+ it("should have entries for zed, vscode, and idea", () => {
21
+ expect(EDITORS).toHaveLength(3);
22
+ expect(EDITORS.map((e) => e.id)).toEqual(["zed", "vscode", "idea"]);
23
+ });
24
+
25
+ it("should have win32 patterns for vscode and idea", () => {
26
+ const vscode = EDITORS.find(e => e.id === "vscode")!;
27
+ const idea = EDITORS.find(e => e.id === "idea")!;
28
+ expect(vscode.processPattern.win32).toBe("Code.exe");
29
+ expect(vscode.winCli).toBe("code.cmd");
30
+ expect(idea.processPattern.win32).toBe("idea64.exe");
31
+ expect(idea.winCli).toBe("idea64.exe");
32
+ });
33
+
34
+ it("should not have win32 pattern for zed", () => {
35
+ const zed = EDITORS.find(e => e.id === "zed")!;
36
+ expect(zed.processPattern.win32).toBeUndefined();
37
+ });
38
+ });
39
+
40
+ describe("isProcessRunningWin32", () => {
41
+ it("returns true when tasklist finds process", () => {
42
+ mockedExecSync.mockReturnValue("Code.exe 12345 Console 1 150,000 K");
43
+ expect(isProcessRunningWin32("Code.exe")).toBe(true);
44
+ });
45
+
46
+ it("returns false when tasklist shows no matching process", () => {
47
+ mockedExecSync.mockReturnValue("INFO: No tasks are running which match the specified criteria.");
48
+ expect(isProcessRunningWin32("Code.exe")).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe("isProcessRunning", () => {
53
+ it("should return true when pgrep finds matching process", () => {
54
+ mockedExecSync.mockReturnValue(Buffer.from("12345\n"));
55
+ expect(isProcessRunning("/Applications/Zed.app")).toBe(true);
56
+ });
57
+
58
+ it("should return false when pgrep finds no match", () => {
59
+ mockedExecSync.mockImplementation(() => {
60
+ throw new Error("exit code 1");
61
+ });
62
+ expect(isProcessRunning("/Applications/Zed.app")).toBe(false);
63
+ });
64
+
65
+ it("should return false when pgrep is not available", () => {
66
+ mockedExecSync.mockImplementation(() => {
67
+ const err = new Error("command not found") as any;
68
+ err.status = 127;
69
+ throw err;
70
+ });
71
+ expect(isProcessRunning("/Applications/Zed.app")).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe("detectEditors", () => {
76
+ it("should return editor when process is running AND CLI is available", () => {
77
+ mockedExecSync.mockImplementation((cmd) => {
78
+ const s = String(cmd);
79
+ if (s.includes("pgrep")) {
80
+ // Only Zed is running
81
+ if (s.includes("Zed")) return Buffer.from("12345\n");
82
+ throw new Error("not found");
83
+ }
84
+ if (s.includes("which")) {
85
+ if (s.includes("zed")) return Buffer.from("/usr/local/bin/zed\n");
86
+ throw new Error("not found");
87
+ }
88
+ throw new Error("unexpected command");
89
+ });
90
+
91
+ const result = detectEditors("/some/project");
92
+ expect(result).toEqual([{ id: "zed", name: "Zed" }]);
93
+ });
94
+
95
+ it("should return empty when process not running even if CLI available", () => {
96
+ mockedExecSync.mockImplementation((cmd) => {
97
+ const s = String(cmd);
98
+ if (s.includes("pgrep")) throw new Error("not found");
99
+ if (s.includes("which")) return Buffer.from("/usr/local/bin/zed\n");
100
+ throw new Error("unexpected command");
101
+ });
102
+
103
+ const result = detectEditors("/some/project");
104
+ expect(result).toEqual([]);
105
+ });
106
+
107
+ it("should return empty when process running but CLI not available", () => {
108
+ mockedExecSync.mockImplementation((cmd) => {
109
+ const s = String(cmd);
110
+ if (s.includes("pgrep")) return Buffer.from("12345\n");
111
+ if (s.includes("which")) throw new Error("not found");
112
+ throw new Error("unexpected command");
113
+ });
114
+
115
+ const result = detectEditors("/some/project");
116
+ expect(result).toEqual([]);
117
+ });
118
+
119
+ it("should return multiple editors when multiple are running", () => {
120
+ mockedExecSync.mockImplementation((cmd) => {
121
+ const s = String(cmd);
122
+ if (s.includes("pgrep")) {
123
+ if (s.includes("Zed") || s.includes("zed")) return Buffer.from("12345\n");
124
+ if (s.includes("Visual Studio Code") || s.includes("code")) return Buffer.from("67890\n");
125
+ throw new Error("not found");
126
+ }
127
+ if (s.includes("which")) {
128
+ if (s.includes("zed")) return Buffer.from("/usr/local/bin/zed\n");
129
+ if (s.includes("code")) return Buffer.from("/usr/local/bin/code\n");
130
+ throw new Error("not found");
131
+ }
132
+ throw new Error("unexpected command");
133
+ });
134
+
135
+ const result = detectEditors("/some/project");
136
+ expect(result).toEqual([
137
+ { id: "zed", name: "Zed" },
138
+ { id: "vscode", name: "VS Code" },
139
+ ]);
140
+ });
141
+
142
+ it("should return empty when no editors are running", () => {
143
+ mockedExecSync.mockImplementation(() => {
144
+ throw new Error("not found");
145
+ });
146
+
147
+ const result = detectEditors("/some/project");
148
+ expect(result).toEqual([]);
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,55 @@
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
+ });
@@ -0,0 +1,58 @@
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: { type: eventType, ...data } };
7
+ }
8
+
9
+ describe("extractSessionUpdates", () => {
10
+ it("should return streaming status on agent_start", () => {
11
+ const updates = extractSessionUpdates(makeEvent("agent_start"));
12
+ expect(updates).toEqual({ status: "streaming", currentTool: null });
13
+ });
14
+
15
+ it("should return idle status on agent_end", () => {
16
+ const updates = extractSessionUpdates(makeEvent("agent_end"));
17
+ expect(updates).toEqual({ status: "idle", currentTool: null });
18
+ });
19
+
20
+ it("should return currentTool on tool_execution_start", () => {
21
+ const updates = extractSessionUpdates(makeEvent("tool_execution_start", { toolName: "Read" }));
22
+ expect(updates).toEqual({ currentTool: "Read" });
23
+ });
24
+
25
+ it("should clear currentTool on tool_execution_end", () => {
26
+ const updates = extractSessionUpdates(makeEvent("tool_execution_end", { toolName: "Read" }));
27
+ expect(updates).toEqual({ currentTool: null });
28
+ });
29
+
30
+ it("should extract model from model_select event", () => {
31
+ const updates = extractSessionUpdates(
32
+ makeEvent("model_select", {
33
+ model: { provider: "anthropic", id: "claude-opus-4-6" },
34
+ })
35
+ );
36
+ expect(updates).toEqual({ model: "anthropic/claude-opus-4-6" });
37
+ });
38
+
39
+ it("should extract model and thinkingLevel from model_select event", () => {
40
+ const updates = extractSessionUpdates(
41
+ makeEvent("model_select", {
42
+ model: { provider: "anthropic", id: "claude-opus-4-6" },
43
+ thinkingLevel: "high",
44
+ })
45
+ );
46
+ expect(updates).toEqual({ model: "anthropic/claude-opus-4-6", thinkingLevel: "high" });
47
+ });
48
+
49
+ it("should return null for model_select without model data", () => {
50
+ expect(extractSessionUpdates(makeEvent("model_select"))).toBeNull();
51
+ });
52
+
53
+ it("should return null for unrelated events", () => {
54
+ expect(extractSessionUpdates(makeEvent("message_update"))).toBeNull();
55
+ expect(extractSessionUpdates(makeEvent("session_compact"))).toBeNull();
56
+ expect(extractSessionUpdates(makeEvent("turn_start"))).toBeNull();
57
+ });
58
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+
6
+ // We test the module's exported function
7
+ import { ensureBridgeExtensionRegistered } from "../extension-register.js";
8
+
9
+ describe("ensureBridgeExtensionRegistered", () => {
10
+ let tmpDir: string;
11
+ let settingsPath: string;
12
+ let origHome: string | undefined;
13
+
14
+ beforeEach(() => {
15
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ext-reg-test-"));
16
+ settingsPath = path.join(tmpDir, ".pi", "agent", "settings.json");
17
+ origHome = process.env.HOME;
18
+ process.env.HOME = tmpDir;
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.env.HOME = origHome;
23
+ fs.rmSync(tmpDir, { recursive: true, force: true });
24
+ });
25
+
26
+ it("should be a no-op when no bundled extension exists", () => {
27
+ // In dev mode, the extension directory relative to server/src is the real
28
+ // packages/extension, but in test context __dirname doesn't point to a bundle.
29
+ // The function should not crash and should not create settings.json
30
+ ensureBridgeExtensionRegistered();
31
+ // No assertion needed — just verify no crash
32
+ });
33
+
34
+ it("should add extension path to empty settings file", () => {
35
+ // Create a fake bundled extension at the expected relative path
36
+ // extension-register.ts resolves __dirname/../../../extension relative to server/src
37
+ // We can't easily test the real path detection, so we test the settings write logic
38
+ // by directly calling with a mocked path
39
+
40
+ // Create settings dir
41
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
42
+ fs.writeFileSync(settingsPath, "{}");
43
+
44
+ // Since we can't mock __dirname easily, we test the settings logic directly
45
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
46
+ expect(settings.packages).toBeUndefined();
47
+ });
48
+
49
+ it("should not crash on malformed settings.json", () => {
50
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
51
+ fs.writeFileSync(settingsPath, "not valid json{{{");
52
+ ensureBridgeExtensionRegistered();
53
+ // Should not throw
54
+ });
55
+
56
+ it("should not crash when settings directory does not exist", () => {
57
+ // HOME points to tmpDir but .pi/agent/ doesn't exist
58
+ ensureBridgeExtensionRegistered();
59
+ // Should not throw
60
+ });
61
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tests for the file read endpoint logic.
3
+ * Uses lightweight path validation tests since integration tests
4
+ * require a full server (covered by manual testing).
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import path from "node:path";
8
+
9
+ /**
10
+ * Extracted path containment check — same logic used in the endpoint.
11
+ */
12
+ function isPathContained(cwd: string, relPath: string): boolean {
13
+ const resolved = path.resolve(cwd, relPath);
14
+ return resolved.startsWith(cwd + path.sep) || resolved === cwd;
15
+ }
16
+
17
+ describe("file endpoint path validation", () => {
18
+ it("should allow a simple relative path", () => {
19
+ expect(isPathContained("/project", "readme.md")).toBe(true);
20
+ });
21
+
22
+ it("should allow a nested relative path", () => {
23
+ expect(isPathContained("/project", "openspec/changes/foo/proposal.md")).toBe(true);
24
+ });
25
+
26
+ it("should allow a subdirectory path", () => {
27
+ expect(isPathContained("/project", "openspec/changes/foo/specs")).toBe(true);
28
+ });
29
+
30
+ it("should reject path traversal with ../", () => {
31
+ expect(isPathContained("/project", "../../etc/passwd")).toBe(false);
32
+ });
33
+
34
+ it("should reject path traversal that resolves outside cwd", () => {
35
+ expect(isPathContained("/project/sub", "../other/file.md")).toBe(false);
36
+ });
37
+
38
+ it("should allow path with ../ that stays inside cwd", () => {
39
+ expect(isPathContained("/project", "a/../b/file.md")).toBe(true);
40
+ });
41
+
42
+ it("should reject absolute path outside cwd", () => {
43
+ expect(isPathContained("/project", "/etc/passwd")).toBe(false);
44
+ });
45
+
46
+ it("should allow cwd itself", () => {
47
+ expect(isPathContained("/project", ".")).toBe(true);
48
+ });
49
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Tests for handleForceKill in session-action-handler.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { handleForceKill } from "../browser-handlers/session-action-handler.js";
6
+ import type { BrowserHandlerContext } from "../browser-handlers/handler-context.js";
7
+
8
+ function createMockContext(sessionOverrides?: Record<string, any>): BrowserHandlerContext & { sent: any[]; broadcasts: any[] } {
9
+ const sent: any[] = [];
10
+ const broadcasts: any[] = [];
11
+ return {
12
+ ws: {} as any,
13
+ sessionManager: {
14
+ get: vi.fn().mockReturnValue({
15
+ id: "sess-1",
16
+ cwd: "/test",
17
+ status: "streaming",
18
+ pid: 99999,
19
+ ...sessionOverrides,
20
+ }),
21
+ update: vi.fn(),
22
+ } as any,
23
+ eventStore: {} as any,
24
+ piGateway: {
25
+ closeSession: vi.fn().mockReturnValue(true),
26
+ sendToSession: vi.fn().mockReturnValue(true),
27
+ } as any,
28
+ pendingForkRegistry: undefined,
29
+ headlessPidRegistry: {
30
+ killBySessionId: vi.fn().mockReturnValue(false),
31
+ } as any,
32
+ pendingResumeRegistry: {} as any,
33
+ sendTo: vi.fn((_ws, msg) => sent.push(msg)),
34
+ broadcast: vi.fn((msg) => broadcasts.push(msg)),
35
+ getSubscribers: vi.fn().mockReturnValue([]),
36
+ trackUiRequest: vi.fn(),
37
+ replayPendingUiRequests: vi.fn(),
38
+ markReplaying: vi.fn(),
39
+ clearReplaying: vi.fn(),
40
+ sent,
41
+ broadcasts,
42
+ } as any;
43
+ }
44
+
45
+ describe("handleForceKill", () => {
46
+ beforeEach(() => {
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ it("should close bridge WebSocket and mark session ended when no PID", async () => {
51
+ const ctx = createMockContext({ pid: undefined });
52
+
53
+ await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
54
+
55
+ expect(ctx.piGateway.closeSession).toHaveBeenCalledWith("sess-1");
56
+ expect(ctx.sessionManager.update).toHaveBeenCalledWith("sess-1", expect.objectContaining({ status: "ended" }));
57
+
58
+ const result = ctx.sent.find((m: any) => m.type === "force_kill_result");
59
+ expect(result).toBeDefined();
60
+ expect(result.success).toBe(true);
61
+ expect(result.message).toContain("no PID");
62
+ });
63
+
64
+ it("should send SIGTERM and mark session ended for valid PID", async () => {
65
+ // Use a PID that doesn't exist so SIGTERM throws
66
+ const ctx = createMockContext({ pid: 2147483647 });
67
+
68
+ await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
69
+
70
+ expect(ctx.piGateway.closeSession).toHaveBeenCalledWith("sess-1");
71
+ expect(ctx.sessionManager.update).toHaveBeenCalledWith("sess-1", expect.objectContaining({ status: "ended" }));
72
+
73
+ const result = ctx.sent.find((m: any) => m.type === "force_kill_result");
74
+ expect(result).toBeDefined();
75
+ expect(result.success).toBe(true);
76
+ expect(result.message).toContain("already exited");
77
+ });
78
+
79
+ it("should broadcast session_updated with ended status", async () => {
80
+ const ctx = createMockContext({ pid: undefined });
81
+
82
+ await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
83
+
84
+ const update = ctx.broadcasts.find((m: any) => m.type === "session_updated");
85
+ expect(update).toBeDefined();
86
+ expect(update.updates.status).toBe("ended");
87
+ });
88
+
89
+ it("should always close the bridge WebSocket", async () => {
90
+ const ctx = createMockContext({ pid: 12345 });
91
+
92
+ await handleForceKill({ type: "force_kill", sessionId: "sess-1" }, ctx);
93
+
94
+ expect(ctx.piGateway.closeSession).toHaveBeenCalledWith("sess-1");
95
+ });
96
+
97
+ it("should return success: false when session not found", async () => {
98
+ const ctx = createMockContext();
99
+ (ctx.sessionManager.get as any).mockReturnValue(undefined);
100
+
101
+ await handleForceKill({ type: "force_kill", sessionId: "unknown" }, ctx);
102
+
103
+ const result = ctx.sent.find((m: any) => m.type === "force_kill_result");
104
+ expect(result).toBeDefined();
105
+ expect(result.success).toBe(false);
106
+ expect(result.message).toContain("not found");
107
+ expect(ctx.piGateway.closeSession).not.toHaveBeenCalled();
108
+ });
109
+ });