@shykaruu/jarvis-brain 0.4.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 (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,409 @@
1
+ import { test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { WebSocketServer, type WSMessage } from './websocket.ts';
3
+
4
+ let server: WebSocketServer;
5
+
6
+ beforeEach(() => {
7
+ server = new WebSocketServer(3143); // Use different port for tests
8
+ });
9
+
10
+ afterEach(() => {
11
+ if (server.isRunning()) {
12
+ server.stop();
13
+ }
14
+ });
15
+
16
+ test('WebSocketServer - initialization', () => {
17
+ expect(server.isRunning()).toBe(false);
18
+ expect(server.getPort()).toBe(3143);
19
+ expect(server.getClientCount()).toBe(0);
20
+ });
21
+
22
+ test('WebSocketServer - start and stop', () => {
23
+ server.start();
24
+ expect(server.isRunning()).toBe(true);
25
+
26
+ server.stop();
27
+ expect(server.isRunning()).toBe(false);
28
+ expect(server.getClientCount()).toBe(0);
29
+ });
30
+
31
+ test('WebSocketServer - health endpoint', async () => {
32
+ server.start();
33
+
34
+ const response = await fetch('http://localhost:3143/health');
35
+ expect(response.ok).toBe(true);
36
+
37
+ const data = await response.json() as any;
38
+ expect(data.status).toBe('ok');
39
+ expect(data.clients).toBe(0);
40
+ expect(typeof data.uptime).toBe('number');
41
+
42
+ server.stop();
43
+ });
44
+
45
+ test('WebSocketServer - root endpoint returns 404 without static dir', async () => {
46
+ server.start();
47
+
48
+ const response = await fetch('http://localhost:3143/');
49
+ expect(response.status).toBe(404);
50
+
51
+ server.stop();
52
+ });
53
+
54
+ test('WebSocketServer - WebSocket connection', async () => {
55
+ let connectCalled = false;
56
+ let disconnectCalled = false;
57
+
58
+ server.setHandler({
59
+ async onMessage(msg, _ws) {
60
+ return {
61
+ type: 'status',
62
+ payload: { echo: msg.payload },
63
+ timestamp: Date.now(),
64
+ };
65
+ },
66
+ onConnect(_ws) {
67
+ connectCalled = true;
68
+ },
69
+ onDisconnect(_ws) {
70
+ disconnectCalled = true;
71
+ },
72
+ });
73
+
74
+ server.start();
75
+
76
+ // Connect WebSocket client
77
+ const ws = new WebSocket('ws://localhost:3143/ws');
78
+
79
+ await new Promise<void>((resolve) => {
80
+ ws.onopen = () => {
81
+ expect(server.getClientCount()).toBe(1);
82
+ expect(connectCalled).toBe(true);
83
+ resolve();
84
+ };
85
+ });
86
+
87
+ // Send message and receive response
88
+ const received = await new Promise<WSMessage>((resolve) => {
89
+ ws.onmessage = (event) => {
90
+ resolve(JSON.parse(event.data));
91
+ };
92
+
93
+ const testMessage: WSMessage = {
94
+ type: 'chat',
95
+ payload: 'Hello J.A.R.V.I.S.',
96
+ timestamp: Date.now(),
97
+ };
98
+
99
+ ws.send(JSON.stringify(testMessage));
100
+ });
101
+
102
+ expect(received.type).toBe('status');
103
+ expect(received.payload).toEqual({ echo: 'Hello J.A.R.V.I.S.' });
104
+
105
+ // Close connection
106
+ ws.close();
107
+
108
+ await new Promise((resolve) => setTimeout(resolve, 100));
109
+ expect(disconnectCalled).toBe(true);
110
+ expect(server.getClientCount()).toBe(0);
111
+
112
+ server.stop();
113
+ });
114
+
115
+ test('WebSocketServer - broadcast', async () => {
116
+ server.start();
117
+
118
+ const clients: WebSocket[] = [];
119
+ const messages: WSMessage[][] = [[], []];
120
+
121
+ // Connect two clients
122
+ for (let i = 0; i < 2; i++) {
123
+ const ws = new WebSocket('ws://localhost:3143/ws');
124
+ clients.push(ws);
125
+
126
+ await new Promise<void>((resolve) => {
127
+ ws.onopen = () => resolve();
128
+ });
129
+
130
+ ws.onmessage = (event) => {
131
+ messages[i]!.push(JSON.parse(event.data));
132
+ };
133
+ }
134
+
135
+ expect(server.getClientCount()).toBe(2);
136
+
137
+ // Broadcast message
138
+ const broadcastMsg: WSMessage = {
139
+ type: 'status',
140
+ payload: { text: 'Broadcast to all' },
141
+ timestamp: Date.now(),
142
+ };
143
+
144
+ server.broadcast(broadcastMsg);
145
+
146
+ // Wait for messages to arrive
147
+ await new Promise((resolve) => setTimeout(resolve, 100));
148
+
149
+ expect(messages[0]!.length).toBe(1);
150
+ expect(messages[1]!.length).toBe(1);
151
+ expect(messages[0]![0]!.payload).toEqual({ text: 'Broadcast to all' });
152
+ expect(messages[1]![0]!.payload).toEqual({ text: 'Broadcast to all' });
153
+
154
+ clients.forEach((ws) => ws.close());
155
+ server.stop();
156
+ });
157
+
158
+ test('WebSocketServer - binary message routing', async () => {
159
+ let receivedBinary: Buffer | null = null;
160
+ let receivedFromWs: any = null;
161
+
162
+ server.setHandler({
163
+ async onMessage(msg, _ws) { return undefined; },
164
+ async onBinaryMessage(data, ws) {
165
+ receivedBinary = data;
166
+ receivedFromWs = ws;
167
+ },
168
+ onConnect(_ws) {},
169
+ onDisconnect(_ws) {},
170
+ });
171
+
172
+ server.start();
173
+
174
+ const ws = new WebSocket('ws://localhost:3143/ws');
175
+ await new Promise<void>((resolve) => { ws.onopen = () => resolve(); });
176
+
177
+ // Send binary data
178
+ const testData = new Uint8Array([1, 2, 3, 4, 5]);
179
+ ws.send(testData.buffer);
180
+
181
+ await new Promise((resolve) => setTimeout(resolve, 200));
182
+ expect(receivedBinary).not.toBeNull();
183
+ expect(receivedBinary!.length).toBe(5);
184
+ expect(receivedFromWs).not.toBeNull();
185
+
186
+ ws.close();
187
+ server.stop();
188
+ });
189
+
190
+ test('WebSocketServer - sendBinary reaches client', async () => {
191
+ let serverWsRef: any = null;
192
+
193
+ server.setHandler({
194
+ async onMessage(msg, ws) {
195
+ serverWsRef = ws;
196
+ return { type: 'status', payload: { ok: true }, timestamp: Date.now() };
197
+ },
198
+ onConnect(_ws) {},
199
+ onDisconnect(_ws) {},
200
+ });
201
+
202
+ server.start();
203
+
204
+ const ws = new WebSocket('ws://localhost:3143/ws');
205
+ ws.binaryType = 'arraybuffer';
206
+
207
+ let receivedBinary: ArrayBuffer | null = null;
208
+
209
+ await new Promise<void>((resolve) => { ws.onopen = () => resolve(); });
210
+
211
+ ws.onmessage = (e) => {
212
+ if (e.data instanceof ArrayBuffer) {
213
+ receivedBinary = e.data;
214
+ }
215
+ };
216
+
217
+ // Send a JSON message first to capture the server ws ref
218
+ ws.send(JSON.stringify({ type: 'status', payload: {}, timestamp: Date.now() }));
219
+ await new Promise((resolve) => setTimeout(resolve, 200));
220
+
221
+ expect(serverWsRef).not.toBeNull();
222
+
223
+ // Send binary from server to client
224
+ server.sendBinary(serverWsRef, Buffer.from([10, 20, 30]));
225
+ await new Promise((resolve) => setTimeout(resolve, 200));
226
+
227
+ expect(receivedBinary).not.toBeNull();
228
+ expect(new Uint8Array(receivedBinary!)).toEqual(new Uint8Array([10, 20, 30]));
229
+
230
+ ws.close();
231
+ server.stop();
232
+ });
233
+
234
+ // --- Auth tests (use dedicated ports to avoid port reuse timing issues) ---
235
+
236
+ test('WebSocketServer - auth token blocks unauthenticated requests', async () => {
237
+ const authServer = new WebSocketServer(3150);
238
+ authServer.setAuthToken('test-secret-123');
239
+ authServer.start();
240
+
241
+ try {
242
+ // Health is public — still accessible
243
+ const health = await fetch('http://localhost:3150/health');
244
+ expect(health.ok).toBe(true);
245
+
246
+ // API without cookie → 401 JSON
247
+ const api = await fetch('http://localhost:3150/api/health');
248
+ expect(api.status).toBe(401);
249
+ const body = await api.json() as any;
250
+ expect(body.error).toBe('Unauthorized');
251
+
252
+ // Dashboard without cookie → 401 HTML with hash-to-query bootstrap script
253
+ const dash = await fetch('http://localhost:3150/');
254
+ expect(dash.status).toBe(401);
255
+ const html = await dash.text();
256
+ expect(html).toContain("location.replace");
257
+ expect(html).toContain(".get('token')");
258
+
259
+ // Query param with valid token → 302 + Set-Cookie
260
+ const withToken = await fetch('http://localhost:3150/?token=test-secret-123', { redirect: 'manual' });
261
+ expect(withToken.status).toBe(302);
262
+ expect(withToken.headers.get('Set-Cookie')).toContain('token=test-secret-123');
263
+ expect(withToken.headers.get('Location')).toBe('/');
264
+
265
+ // Query param with wrong token → 401
266
+ const wrongToken = await fetch('http://localhost:3150/?token=wrong', { redirect: 'manual' });
267
+ expect(wrongToken.status).toBe(401);
268
+ } finally {
269
+ authServer.stop();
270
+ }
271
+ });
272
+
273
+ test('WebSocketServer - auth token allows requests with valid cookie', async () => {
274
+ const authServer = new WebSocketServer(3151);
275
+ authServer.setAuthToken('test-secret-123');
276
+ authServer.setApiRoutes({
277
+ '/api/health': {
278
+ GET: () => Response.json({ status: 'ok' }),
279
+ },
280
+ });
281
+ authServer.start();
282
+
283
+ try {
284
+ const res = await fetch('http://localhost:3151/api/health', {
285
+ headers: { Cookie: 'token=test-secret-123' },
286
+ });
287
+ expect(res.ok).toBe(true);
288
+ const data = await res.json() as any;
289
+ expect(data.status).toBe('ok');
290
+ } finally {
291
+ authServer.stop();
292
+ }
293
+ });
294
+
295
+ test('WebSocketServer - auth token rejects wrong cookie', async () => {
296
+ const authServer = new WebSocketServer(3152);
297
+ authServer.setAuthToken('test-secret-123');
298
+ authServer.start();
299
+
300
+ try {
301
+ const res = await fetch('http://localhost:3152/api/health', {
302
+ headers: { Cookie: 'token=wrong-token' },
303
+ });
304
+ expect(res.status).toBe(401);
305
+ } finally {
306
+ authServer.stop();
307
+ }
308
+ });
309
+
310
+ test('WebSocketServer - public routes bypass auth', async () => {
311
+ const authServer = new WebSocketServer(3153);
312
+ authServer.setAuthToken('test-secret-123');
313
+ authServer.start();
314
+
315
+ try {
316
+ // /health is public
317
+ const health = await fetch('http://localhost:3153/health');
318
+ expect(health.ok).toBe(true);
319
+
320
+ // OPTIONS (CORS preflight) is public
321
+ const options = await fetch('http://localhost:3153/api/anything', { method: 'OPTIONS' });
322
+ expect(options.status).not.toBe(401);
323
+ } finally {
324
+ authServer.stop();
325
+ }
326
+ });
327
+
328
+ test('WebSocketServer - WebSocket blocked without auth cookie', async () => {
329
+ const authServer = new WebSocketServer(3154);
330
+ authServer.setAuthToken('test-secret-123');
331
+ authServer.start();
332
+
333
+ try {
334
+ // Regular HTTP fetch to /ws without cookie → 401 JSON
335
+ const res = await fetch('http://localhost:3154/ws');
336
+ expect(res.status).toBe(401);
337
+ } finally {
338
+ authServer.stop();
339
+ }
340
+ });
341
+
342
+ test('WebSocketServer - WebSocket allowed with auth cookie', async () => {
343
+ const authServer = new WebSocketServer(3155);
344
+ authServer.setAuthToken('test-secret-123');
345
+ authServer.start();
346
+
347
+ try {
348
+ const ws = new WebSocket('ws://localhost:3155/ws', {
349
+ headers: { Cookie: 'token=test-secret-123' },
350
+ } as any);
351
+
352
+ const connected = await new Promise<boolean>((resolve) => {
353
+ ws.onopen = () => resolve(true);
354
+ ws.onerror = () => resolve(false);
355
+ setTimeout(() => resolve(false), 2000);
356
+ });
357
+
358
+ expect(connected).toBe(true);
359
+ ws.close();
360
+ await new Promise((resolve) => setTimeout(resolve, 100));
361
+ } finally {
362
+ authServer.stop();
363
+ }
364
+ });
365
+
366
+ test('WebSocketServer - sendToClient unicasts JSON', async () => {
367
+ let serverWsRef: any = null;
368
+
369
+ server.setHandler({
370
+ async onMessage(msg, ws) {
371
+ serverWsRef = ws;
372
+ return undefined; // No auto-response
373
+ },
374
+ onConnect(_ws) {},
375
+ onDisconnect(_ws) {},
376
+ });
377
+
378
+ server.start();
379
+
380
+ const ws = new WebSocket('ws://localhost:3143/ws');
381
+ const received: WSMessage[] = [];
382
+
383
+ await new Promise<void>((resolve) => { ws.onopen = () => resolve(); });
384
+
385
+ ws.onmessage = (e) => {
386
+ if (typeof e.data === 'string') {
387
+ received.push(JSON.parse(e.data));
388
+ }
389
+ };
390
+
391
+ // Trigger to get ws ref
392
+ ws.send(JSON.stringify({ type: 'command', payload: {}, timestamp: Date.now() }));
393
+ await new Promise((resolve) => setTimeout(resolve, 200));
394
+
395
+ // Unicast a tts_start message
396
+ server.sendToClient(serverWsRef, {
397
+ type: 'tts_start',
398
+ payload: { requestId: 'test-123' },
399
+ timestamp: Date.now(),
400
+ });
401
+ await new Promise((resolve) => setTimeout(resolve, 200));
402
+
403
+ expect(received.length).toBe(1);
404
+ expect(received[0]!.type).toBe('tts_start');
405
+ expect((received[0]!.payload as any).requestId).toBe('test-123');
406
+
407
+ ws.close();
408
+ server.stop();
409
+ });