@teneo-protocol/sdk 1.0.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 (281) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.test.example +14 -0
  3. package/.eslintrc.json +26 -0
  4. package/.github/workflows/claude-code-review.yml +78 -0
  5. package/.github/workflows/claude-reviewer.yml +64 -0
  6. package/.github/workflows/publish-npm.yml +38 -0
  7. package/.github/workflows/push-to-main.yml +23 -0
  8. package/.node-version +1 -0
  9. package/.prettierrc +11 -0
  10. package/Dockerfile +25 -0
  11. package/LICENCE +661 -0
  12. package/README.md +709 -0
  13. package/dist/constants.d.ts +42 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +45 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/core/websocket-client.d.ts +261 -0
  18. package/dist/core/websocket-client.d.ts.map +1 -0
  19. package/dist/core/websocket-client.js +875 -0
  20. package/dist/core/websocket-client.js.map +1 -0
  21. package/dist/formatters/response-formatter.d.ts +354 -0
  22. package/dist/formatters/response-formatter.d.ts.map +1 -0
  23. package/dist/formatters/response-formatter.js +575 -0
  24. package/dist/formatters/response-formatter.js.map +1 -0
  25. package/dist/handlers/message-handler-registry.d.ts +155 -0
  26. package/dist/handlers/message-handler-registry.d.ts.map +1 -0
  27. package/dist/handlers/message-handler-registry.js +216 -0
  28. package/dist/handlers/message-handler-registry.js.map +1 -0
  29. package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
  30. package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
  31. package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
  32. package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
  33. package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
  34. package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
  35. package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
  36. package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
  37. package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
  38. package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
  39. package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
  40. package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
  41. package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
  42. package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
  43. package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
  44. package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
  45. package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
  46. package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
  47. package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
  48. package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
  49. package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
  50. package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
  51. package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
  52. package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
  53. package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
  54. package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
  55. package/dist/handlers/message-handlers/base-handler.js +83 -0
  56. package/dist/handlers/message-handlers/base-handler.js.map +1 -0
  57. package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
  58. package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
  59. package/dist/handlers/message-handlers/challenge-handler.js +47 -0
  60. package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
  61. package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
  62. package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
  63. package/dist/handlers/message-handlers/error-message-handler.js +29 -0
  64. package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
  65. package/dist/handlers/message-handlers/index.d.ts +28 -0
  66. package/dist/handlers/message-handlers/index.d.ts.map +1 -0
  67. package/dist/handlers/message-handlers/index.js +100 -0
  68. package/dist/handlers/message-handlers/index.js.map +1 -0
  69. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
  70. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
  71. package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
  72. package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
  73. package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
  74. package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
  75. package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
  76. package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
  77. package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
  78. package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
  79. package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
  80. package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
  81. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
  82. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
  83. package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
  84. package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
  85. package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
  86. package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
  87. package/dist/handlers/message-handlers/task-response-handler.js +44 -0
  88. package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
  89. package/dist/handlers/message-handlers/types.d.ts +51 -0
  90. package/dist/handlers/message-handlers/types.d.ts.map +1 -0
  91. package/dist/handlers/message-handlers/types.js +7 -0
  92. package/dist/handlers/message-handlers/types.js.map +1 -0
  93. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
  94. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
  95. package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
  96. package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
  97. package/dist/handlers/webhook-handler.d.ts +202 -0
  98. package/dist/handlers/webhook-handler.d.ts.map +1 -0
  99. package/dist/handlers/webhook-handler.js +511 -0
  100. package/dist/handlers/webhook-handler.js.map +1 -0
  101. package/dist/index.d.ts +71 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +217 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/managers/agent-registry.d.ts +173 -0
  106. package/dist/managers/agent-registry.d.ts.map +1 -0
  107. package/dist/managers/agent-registry.js +310 -0
  108. package/dist/managers/agent-registry.js.map +1 -0
  109. package/dist/managers/connection-manager.d.ts +134 -0
  110. package/dist/managers/connection-manager.d.ts.map +1 -0
  111. package/dist/managers/connection-manager.js +176 -0
  112. package/dist/managers/connection-manager.js.map +1 -0
  113. package/dist/managers/index.d.ts +9 -0
  114. package/dist/managers/index.d.ts.map +1 -0
  115. package/dist/managers/index.js +16 -0
  116. package/dist/managers/index.js.map +1 -0
  117. package/dist/managers/message-router.d.ts +112 -0
  118. package/dist/managers/message-router.d.ts.map +1 -0
  119. package/dist/managers/message-router.js +260 -0
  120. package/dist/managers/message-router.js.map +1 -0
  121. package/dist/managers/room-manager.d.ts +165 -0
  122. package/dist/managers/room-manager.d.ts.map +1 -0
  123. package/dist/managers/room-manager.js +227 -0
  124. package/dist/managers/room-manager.js.map +1 -0
  125. package/dist/teneo-sdk.d.ts +703 -0
  126. package/dist/teneo-sdk.d.ts.map +1 -0
  127. package/dist/teneo-sdk.js +907 -0
  128. package/dist/teneo-sdk.js.map +1 -0
  129. package/dist/types/config.d.ts +1047 -0
  130. package/dist/types/config.d.ts.map +1 -0
  131. package/dist/types/config.js +720 -0
  132. package/dist/types/config.js.map +1 -0
  133. package/dist/types/error-codes.d.ts +29 -0
  134. package/dist/types/error-codes.d.ts.map +1 -0
  135. package/dist/types/error-codes.js +41 -0
  136. package/dist/types/error-codes.js.map +1 -0
  137. package/dist/types/events.d.ts +616 -0
  138. package/dist/types/events.d.ts.map +1 -0
  139. package/dist/types/events.js +261 -0
  140. package/dist/types/events.js.map +1 -0
  141. package/dist/types/health.d.ts +40 -0
  142. package/dist/types/health.d.ts.map +1 -0
  143. package/dist/types/health.js +6 -0
  144. package/dist/types/health.js.map +1 -0
  145. package/dist/types/index.d.ts +10 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +123 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/messages.d.ts +3734 -0
  150. package/dist/types/messages.d.ts.map +1 -0
  151. package/dist/types/messages.js +482 -0
  152. package/dist/types/messages.js.map +1 -0
  153. package/dist/types/validation.d.ts +81 -0
  154. package/dist/types/validation.d.ts.map +1 -0
  155. package/dist/types/validation.js +115 -0
  156. package/dist/types/validation.js.map +1 -0
  157. package/dist/utils/bounded-queue.d.ts +127 -0
  158. package/dist/utils/bounded-queue.d.ts.map +1 -0
  159. package/dist/utils/bounded-queue.js +181 -0
  160. package/dist/utils/bounded-queue.js.map +1 -0
  161. package/dist/utils/circuit-breaker.d.ts +141 -0
  162. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  163. package/dist/utils/circuit-breaker.js +215 -0
  164. package/dist/utils/circuit-breaker.js.map +1 -0
  165. package/dist/utils/deduplication-cache.d.ts +110 -0
  166. package/dist/utils/deduplication-cache.d.ts.map +1 -0
  167. package/dist/utils/deduplication-cache.js +177 -0
  168. package/dist/utils/deduplication-cache.js.map +1 -0
  169. package/dist/utils/event-waiter.d.ts +101 -0
  170. package/dist/utils/event-waiter.d.ts.map +1 -0
  171. package/dist/utils/event-waiter.js +118 -0
  172. package/dist/utils/event-waiter.js.map +1 -0
  173. package/dist/utils/index.d.ts +51 -0
  174. package/dist/utils/index.d.ts.map +1 -0
  175. package/dist/utils/index.js +72 -0
  176. package/dist/utils/index.js.map +1 -0
  177. package/dist/utils/logger.d.ts +22 -0
  178. package/dist/utils/logger.d.ts.map +1 -0
  179. package/dist/utils/logger.js +91 -0
  180. package/dist/utils/logger.js.map +1 -0
  181. package/dist/utils/rate-limiter.d.ts +122 -0
  182. package/dist/utils/rate-limiter.d.ts.map +1 -0
  183. package/dist/utils/rate-limiter.js +190 -0
  184. package/dist/utils/rate-limiter.js.map +1 -0
  185. package/dist/utils/retry-policy.d.ts +191 -0
  186. package/dist/utils/retry-policy.d.ts.map +1 -0
  187. package/dist/utils/retry-policy.js +225 -0
  188. package/dist/utils/retry-policy.js.map +1 -0
  189. package/dist/utils/secure-private-key.d.ts +113 -0
  190. package/dist/utils/secure-private-key.d.ts.map +1 -0
  191. package/dist/utils/secure-private-key.js +188 -0
  192. package/dist/utils/secure-private-key.js.map +1 -0
  193. package/dist/utils/signature-verifier.d.ts +143 -0
  194. package/dist/utils/signature-verifier.d.ts.map +1 -0
  195. package/dist/utils/signature-verifier.js +238 -0
  196. package/dist/utils/signature-verifier.js.map +1 -0
  197. package/dist/utils/ssrf-validator.d.ts +36 -0
  198. package/dist/utils/ssrf-validator.d.ts.map +1 -0
  199. package/dist/utils/ssrf-validator.js +195 -0
  200. package/dist/utils/ssrf-validator.js.map +1 -0
  201. package/examples/.env.example +17 -0
  202. package/examples/basic-usage.ts +211 -0
  203. package/examples/production-dashboard/.env.example +153 -0
  204. package/examples/production-dashboard/package.json +39 -0
  205. package/examples/production-dashboard/public/dashboard.html +642 -0
  206. package/examples/production-dashboard/server.ts +753 -0
  207. package/examples/webhook-integration.ts +239 -0
  208. package/examples/x-influencer-battle-redesign.html +1065 -0
  209. package/examples/x-influencer-battle-server.ts +217 -0
  210. package/examples/x-influencer-battle.html +787 -0
  211. package/package.json +65 -0
  212. package/src/constants.ts +43 -0
  213. package/src/core/websocket-client.test.ts +512 -0
  214. package/src/core/websocket-client.ts +1056 -0
  215. package/src/formatters/response-formatter.test.ts +571 -0
  216. package/src/formatters/response-formatter.ts +677 -0
  217. package/src/handlers/message-handler-registry.ts +239 -0
  218. package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
  219. package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
  220. package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
  221. package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
  222. package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
  223. package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
  224. package/src/handlers/message-handlers/base-handler.ts +101 -0
  225. package/src/handlers/message-handlers/challenge-handler.ts +57 -0
  226. package/src/handlers/message-handlers/error-message-handler.ts +27 -0
  227. package/src/handlers/message-handlers/index.ts +77 -0
  228. package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
  229. package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
  230. package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
  231. package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
  232. package/src/handlers/message-handlers/task-response-handler.ts +45 -0
  233. package/src/handlers/message-handlers/types.ts +77 -0
  234. package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
  235. package/src/handlers/webhook-handler.test.ts +789 -0
  236. package/src/handlers/webhook-handler.ts +576 -0
  237. package/src/index.ts +269 -0
  238. package/src/managers/agent-registry.test.ts +466 -0
  239. package/src/managers/agent-registry.ts +347 -0
  240. package/src/managers/connection-manager.ts +195 -0
  241. package/src/managers/index.ts +9 -0
  242. package/src/managers/message-router.ts +349 -0
  243. package/src/managers/room-manager.ts +248 -0
  244. package/src/teneo-sdk.ts +1022 -0
  245. package/src/types/config.test.ts +325 -0
  246. package/src/types/config.ts +799 -0
  247. package/src/types/error-codes.ts +44 -0
  248. package/src/types/events.test.ts +302 -0
  249. package/src/types/events.ts +382 -0
  250. package/src/types/health.ts +46 -0
  251. package/src/types/index.ts +199 -0
  252. package/src/types/messages.test.ts +660 -0
  253. package/src/types/messages.ts +570 -0
  254. package/src/types/validation.ts +123 -0
  255. package/src/utils/bounded-queue.test.ts +356 -0
  256. package/src/utils/bounded-queue.ts +205 -0
  257. package/src/utils/circuit-breaker.test.ts +394 -0
  258. package/src/utils/circuit-breaker.ts +262 -0
  259. package/src/utils/deduplication-cache.test.ts +380 -0
  260. package/src/utils/deduplication-cache.ts +198 -0
  261. package/src/utils/event-waiter.test.ts +381 -0
  262. package/src/utils/event-waiter.ts +172 -0
  263. package/src/utils/index.ts +74 -0
  264. package/src/utils/logger.ts +87 -0
  265. package/src/utils/rate-limiter.test.ts +341 -0
  266. package/src/utils/rate-limiter.ts +211 -0
  267. package/src/utils/retry-policy.test.ts +558 -0
  268. package/src/utils/retry-policy.ts +272 -0
  269. package/src/utils/secure-private-key.test.ts +356 -0
  270. package/src/utils/secure-private-key.ts +205 -0
  271. package/src/utils/signature-verifier.test.ts +464 -0
  272. package/src/utils/signature-verifier.ts +298 -0
  273. package/src/utils/ssrf-validator.test.ts +372 -0
  274. package/src/utils/ssrf-validator.ts +224 -0
  275. package/tests/integration/real-server.test.ts +740 -0
  276. package/tests/integration/websocket.test.ts +381 -0
  277. package/tests/integration-setup.ts +16 -0
  278. package/tests/setup.ts +34 -0
  279. package/tsconfig.json +32 -0
  280. package/vitest.config.ts +42 -0
  281. package/vitest.integration.config.ts +23 -0
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@teneo-protocol/sdk",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript SDK for external platforms to interact with Teneo agents",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [
8
+ "teneo",
9
+ "protocol",
10
+ "sdk",
11
+ "websocket",
12
+ "ai",
13
+ "agents",
14
+ "coordinator"
15
+ ],
16
+ "author": "Teneo Protocol",
17
+ "license": "AGPL-3.0",
18
+ "dependencies": {
19
+ "eventemitter3": "^5.0.1",
20
+ "hono": "^4.9.12",
21
+ "node-fetch": "^3.3.2",
22
+ "pino": "^8.17.2",
23
+ "uuid": "^9.0.1",
24
+ "viem": "^2.37.11",
25
+ "ws": "^8.16.0",
26
+ "zod": "^3.22.4"
27
+ },
28
+ "devDependencies": {
29
+ "@faker-js/faker": "^10.0.0",
30
+ "@testing-library/jest-dom": "^6.9.1",
31
+ "@types/node": "^20.10.5",
32
+ "@types/uuid": "^9.0.7",
33
+ "@types/ws": "^8.5.10",
34
+ "@typescript-eslint/eslint-plugin": "^6.15.0",
35
+ "@typescript-eslint/parser": "^6.15.0",
36
+ "@vitest/coverage-v8": "^3.2.4",
37
+ "@vitest/ui": "^3.2.4",
38
+ "dotenv": "^17.2.3",
39
+ "eslint": "^8.56.0",
40
+ "eslint-config-prettier": "^9.1.0",
41
+ "eslint-plugin-prettier": "^5.0.1",
42
+ "happy-dom": "^19.0.2",
43
+ "msw": "^2.11.3",
44
+ "pino-pretty": "^13.1.2",
45
+ "prettier": "^3.1.1",
46
+ "typescript": "^5.3.3",
47
+ "vitest": "^3.2.4",
48
+ "ws-mock": "^0.1.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "clean": "rm -rf dist",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:ui": "vitest --ui",
57
+ "test:unit": "vitest run src/**/*.test.ts",
58
+ "test:integration": "vitest run --config vitest.integration.config.ts",
59
+ "lint": "eslint . --ext .ts",
60
+ "lint:fix": "eslint . --ext .ts --fix",
61
+ "format": "prettier --write \"src/**/*.ts\"",
62
+ "example:battle": "ts-node examples/x-influencer-battle-server.ts",
63
+ "example:dashboard": "cd examples/production-dashboard && bun run server.ts"
64
+ }
65
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Timeout constants for various SDK operations
3
+ */
4
+ export const TIMEOUTS = {
5
+ /** Wait time for cached auth response */
6
+ CACHED_AUTH_WAIT: 1000,
7
+ /** WebSocket ping interval */
8
+ PING_INTERVAL: 30_000,
9
+ /** Default message response timeout */
10
+ DEFAULT_MESSAGE_TIMEOUT: 30_000,
11
+ /** Authentication timeout */
12
+ AUTH_TIMEOUT: 30_000,
13
+ /** Initial connection timeout */
14
+ CONNECTION_TIMEOUT: 30_000,
15
+ /** Webhook delivery timeout */
16
+ WEBHOOK_TIMEOUT: 10_000,
17
+ /** Auth state polling interval */
18
+ AUTH_POLL_INTERVAL: 100
19
+ } as const;
20
+
21
+ /**
22
+ * Retry and backoff constants
23
+ */
24
+ export const RETRY = {
25
+ /** Base delay for exponential backoff */
26
+ BASE_DELAY: 1000,
27
+ /** Maximum retry delay */
28
+ MAX_DELAY: 30_000,
29
+ /** Maximum webhook retry delay */
30
+ MAX_WEBHOOK_DELAY: 30_000,
31
+ /** Maximum reconnection delay */
32
+ MAX_RECONNECT_DELAY: 60_000
33
+ } as const;
34
+
35
+ /**
36
+ * Limits and constraints
37
+ */
38
+ export const LIMITS = {
39
+ /** Maximum message size (2MB) */
40
+ MAX_MESSAGE_SIZE: 2 * 1024 * 1024,
41
+ /** Random jitter for reconnection */
42
+ MAX_JITTER: 1000
43
+ } as const;
@@ -0,0 +1,512 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { WebSocketClient } from "./websocket-client";
3
+ import { ConnectionError, AuthenticationError, TimeoutError } from "../types/events";
4
+ import type { SDKConfig } from "../types/config";
5
+ import WebSocket from "ws";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+
8
+ // Mock WebSocket
9
+ vi.mock("ws", () => {
10
+ return {
11
+ default: vi.fn(),
12
+ WebSocket: vi.fn()
13
+ };
14
+ });
15
+
16
+ // Mock viem
17
+ vi.mock("viem/accounts", () => ({
18
+ privateKeyToAccount: vi.fn()
19
+ }));
20
+
21
+ describe("WebSocketClient", () => {
22
+ let client: WebSocketClient;
23
+ let mockConfig: SDKConfig;
24
+ let mockWs: any;
25
+ let mockAccount: any;
26
+
27
+ // Helper to simulate successful authentication
28
+ const simulateAuth = (client: any) => {
29
+ // Update auth state to bypass authentication wait
30
+ client.updateAuthState({ authenticated: true });
31
+ client.emit("ready");
32
+ };
33
+
34
+ beforeEach(() => {
35
+ vi.useFakeTimers();
36
+
37
+ mockConfig = {
38
+ wsUrl: "wss://example.com/ws",
39
+ privateKey: "0x1234567890123456789012345678901234567890123456789012345678901234",
40
+ reconnect: true,
41
+ reconnectDelay: 1000,
42
+ maxReconnectAttempts: 3,
43
+ connectionTimeout: 10000,
44
+ messageTimeout: 10000,
45
+ logLevel: "info"
46
+ } as SDKConfig;
47
+
48
+ // Create mock WebSocket instance
49
+ mockWs = {
50
+ on: vi.fn(),
51
+ once: vi.fn(),
52
+ send: vi.fn((data: any, callback?: any) => callback && callback()),
53
+ close: vi.fn(),
54
+ ping: vi.fn((callback?: any) => callback && callback()),
55
+ terminate: vi.fn(),
56
+ readyState: WebSocket.OPEN,
57
+ CONNECTING: 0,
58
+ OPEN: 1,
59
+ CLOSING: 2,
60
+ CLOSED: 3,
61
+ removeAllListeners: vi.fn(),
62
+ removeListener: vi.fn()
63
+ };
64
+
65
+ // Mock WebSocket constructor
66
+ (WebSocket as any).mockImplementation(() => mockWs);
67
+
68
+ // Mock viem account
69
+ mockAccount = {
70
+ address: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb6",
71
+ signMessage: vi.fn().mockResolvedValue("0xmocked_signature")
72
+ };
73
+ (privateKeyToAccount as any).mockReturnValue(mockAccount);
74
+
75
+ client = new WebSocketClient(mockConfig);
76
+ });
77
+
78
+ afterEach(() => {
79
+ client.disconnect();
80
+ vi.clearAllMocks();
81
+ vi.clearAllTimers();
82
+ vi.useRealTimers();
83
+ });
84
+
85
+ describe("constructor", () => {
86
+ it("should initialize with config", () => {
87
+ expect(client).toBeDefined();
88
+ // SEC-3: Private key should be removed from config after initialization
89
+ const expectedConfig = { ...mockConfig, privateKey: undefined };
90
+ expect((client as any).config).toStrictEqual(expectedConfig);
91
+ expect(client.isConnected).toBe(false);
92
+ expect(client.isAuthenticated).toBe(false);
93
+ });
94
+
95
+ it("should initialize wallet account", () => {
96
+ expect(privateKeyToAccount).toHaveBeenCalledWith(mockConfig.privateKey);
97
+ expect((client as any).account).toBe(mockAccount);
98
+ });
99
+ });
100
+
101
+ describe("connect", () => {
102
+ it("should establish WebSocket connection", async () => {
103
+ const connectPromise = client.connect();
104
+
105
+ // Simulate connection open
106
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
107
+ if (openHandler) {
108
+ // Simulate successful auth immediately
109
+ simulateAuth(client);
110
+ await openHandler();
111
+ }
112
+
113
+ await connectPromise;
114
+
115
+ expect(WebSocket).toHaveBeenCalled();
116
+ expect(client.isConnected).toBe(true);
117
+ });
118
+
119
+ it("should emit connection events", async () => {
120
+ const openHandler = vi.fn();
121
+
122
+ client.on("connection:open", openHandler);
123
+
124
+ const connectPromise = client.connect();
125
+
126
+ // Simulate connection open
127
+ const wsOpenHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
128
+ if (wsOpenHandler) {
129
+ simulateAuth(client);
130
+ await wsOpenHandler();
131
+ }
132
+
133
+ await connectPromise;
134
+
135
+ expect(openHandler).toHaveBeenCalled();
136
+ });
137
+
138
+ it("should handle connection error", async () => {
139
+ const errorHandler = vi.fn();
140
+ client.on("connection:error", errorHandler);
141
+
142
+ const connectPromise = client.connect();
143
+
144
+ // Simulate connection error
145
+ const wsErrorHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "error")?.[1];
146
+ const error = new Error("Connection failed");
147
+ wsErrorHandler?.(error);
148
+
149
+ try {
150
+ await connectPromise;
151
+ } catch (e) {
152
+ expect(e).toBeInstanceOf(ConnectionError);
153
+ }
154
+
155
+ expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
156
+ });
157
+
158
+ it.skip("should not connect if already connected", async () => {
159
+ // First connection
160
+ const connectPromise1 = client.connect();
161
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
162
+ if (openHandler) {
163
+ simulateAuth(client);
164
+ await openHandler();
165
+ }
166
+ await connectPromise1;
167
+
168
+ // Clear mock calls
169
+ (WebSocket as any).mockClear();
170
+
171
+ // Second connection attempt - should disconnect first
172
+ const connectPromise2 = client.connect();
173
+ const openHandler2 = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
174
+ if (openHandler2) {
175
+ simulateAuth(client);
176
+ await openHandler2();
177
+ }
178
+ await connectPromise2;
179
+
180
+ // WebSocket should be created again (due to disconnect in connect())
181
+ expect(WebSocket).toHaveBeenCalled();
182
+ }, 15000);
183
+
184
+ it("should setup message handler", async () => {
185
+ const connectPromise = client.connect();
186
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
187
+ if (openHandler) {
188
+ simulateAuth(client);
189
+ await openHandler();
190
+ }
191
+ await connectPromise;
192
+
193
+ const messageHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "message")?.[1];
194
+ expect(messageHandler).toBeDefined();
195
+ });
196
+
197
+ it("should setup close handler", async () => {
198
+ const connectPromise = client.connect();
199
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
200
+ if (openHandler) {
201
+ simulateAuth(client);
202
+ await openHandler();
203
+ }
204
+ await connectPromise;
205
+
206
+ const closeHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "close")?.[1];
207
+ expect(closeHandler).toBeDefined();
208
+ });
209
+ });
210
+
211
+ describe("sendMessage", () => {
212
+ beforeEach(async () => {
213
+ // Connect first
214
+ const connectPromise = client.connect();
215
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
216
+ if (openHandler) {
217
+ simulateAuth(client);
218
+ await openHandler();
219
+ }
220
+ await connectPromise;
221
+ });
222
+
223
+ it("should send message when connected", async () => {
224
+ const message = { type: "message" as const, content: "test" };
225
+ await client.sendMessage(message);
226
+
227
+ expect(mockWs.send).toHaveBeenCalledWith(
228
+ expect.stringContaining('"content":"test"'),
229
+ expect.any(Function)
230
+ );
231
+ });
232
+
233
+ it("should throw error when not connected", async () => {
234
+ // Create a new client that's never connected
235
+ const disconnectedClient = new WebSocketClient(mockConfig);
236
+
237
+ const message = { type: "message" as const, content: "test" };
238
+ await expect(disconnectedClient.sendMessage(message)).rejects.toThrow();
239
+ });
240
+
241
+ it("should handle send errors", async () => {
242
+ mockWs.send.mockImplementationOnce((data: any, callback: any) => {
243
+ callback(new Error("Send failed"));
244
+ });
245
+
246
+ const message = { type: "message" as const, content: "test" };
247
+ await expect(client.sendMessage(message)).rejects.toThrow("Send failed");
248
+ });
249
+ });
250
+
251
+ describe("reconnection logic", () => {
252
+ beforeEach(async () => {
253
+ // Connect first
254
+ const connectPromise = client.connect();
255
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
256
+ if (openHandler) {
257
+ simulateAuth(client);
258
+ await openHandler();
259
+ }
260
+ await connectPromise;
261
+ });
262
+
263
+ it.skip("should attempt reconnection on disconnect", async () => {
264
+ const reconnectingHandler = vi.fn();
265
+ client.on("connection:reconnecting", reconnectingHandler);
266
+
267
+ // Simulate disconnect
268
+ const closeHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "close")?.[1];
269
+ closeHandler?.(1006, "Connection lost");
270
+
271
+ // Should emit reconnecting event
272
+ expect(reconnectingHandler).toHaveBeenCalledWith(1);
273
+
274
+ // Clear previous mock
275
+ (WebSocket as any).mockClear();
276
+
277
+ // Advance timer to trigger reconnection
278
+ vi.advanceTimersByTime(mockConfig.reconnectDelay!);
279
+
280
+ // Should create new WebSocket
281
+ expect(WebSocket).toHaveBeenCalledWith(mockConfig.wsUrl);
282
+ });
283
+
284
+ it.skip("should use exponential backoff for reconnection", async () => {
285
+ // Simulate multiple failed reconnection attempts
286
+ for (let i = 1; i <= 3; i++) {
287
+ const closeHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "close")?.[1];
288
+ closeHandler?.(1006, "Connection lost");
289
+
290
+ (WebSocket as any).mockClear();
291
+
292
+ // Calculate expected delay with exponential backoff
293
+ const expectedDelay = mockConfig.reconnectDelay! * Math.pow(2, i - 1);
294
+ vi.advanceTimersByTime(expectedDelay);
295
+
296
+ if (i < 3) {
297
+ expect(WebSocket).toHaveBeenCalled();
298
+ }
299
+ }
300
+ });
301
+
302
+ it.skip("should stop reconnecting after max attempts", async () => {
303
+ const errorHandler = vi.fn();
304
+ client.on("error", errorHandler);
305
+
306
+ // Simulate max failed attempts
307
+ for (let i = 1; i <= mockConfig.maxReconnectAttempts! + 1; i++) {
308
+ const closeHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "close")?.[1];
309
+ closeHandler?.(1006, "Connection lost");
310
+
311
+ if (i <= mockConfig.maxReconnectAttempts!) {
312
+ vi.advanceTimersByTime(mockConfig.reconnectDelay! * Math.pow(2, i - 1));
313
+ }
314
+ }
315
+
316
+ // Should emit error after max attempts
317
+ expect(errorHandler).toHaveBeenCalledWith(
318
+ expect.objectContaining({
319
+ message: expect.stringContaining("Max reconnection attempts")
320
+ })
321
+ );
322
+ });
323
+
324
+ it.skip("should not reconnect if reconnect is false", () => {
325
+ const noReconnectConfig = { ...mockConfig, reconnect: false };
326
+ const noReconnectClient = new WebSocketClient(noReconnectConfig);
327
+
328
+ // Connect
329
+ noReconnectClient.connect();
330
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
331
+ openHandler?.();
332
+
333
+ (WebSocket as any).mockClear();
334
+
335
+ // Simulate disconnect
336
+ const closeHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "close")?.[1];
337
+ closeHandler?.(1000, "Normal closure");
338
+
339
+ // Advance timers
340
+ vi.advanceTimersByTime(10000);
341
+
342
+ // Should not attempt reconnection
343
+ expect(WebSocket).not.toHaveBeenCalled();
344
+
345
+ noReconnectClient.disconnect();
346
+ });
347
+ });
348
+
349
+ describe("message handling", () => {
350
+ beforeEach(async () => {
351
+ const connectPromise = client.connect();
352
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
353
+ if (openHandler) {
354
+ simulateAuth(client);
355
+ await openHandler();
356
+ }
357
+ await connectPromise;
358
+ });
359
+
360
+ it("should handle and validate incoming messages", () => {
361
+ const messageHandler = vi.fn();
362
+ client.on("message:received", messageHandler);
363
+
364
+ const wsMessageHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "message")?.[1];
365
+
366
+ const validMessage = JSON.stringify({
367
+ type: "task_response",
368
+ content: "test response",
369
+ from: "agent-1",
370
+ content_type: "text/plain",
371
+ data: {
372
+ task_id: "task-123",
373
+ success: true
374
+ }
375
+ });
376
+
377
+ wsMessageHandler?.(Buffer.from(validMessage));
378
+
379
+ expect(messageHandler).toHaveBeenCalledWith(
380
+ expect.objectContaining({
381
+ type: "task_response",
382
+ content: "test response"
383
+ })
384
+ );
385
+ });
386
+
387
+ it("should handle invalid messages", () => {
388
+ const errorHandler = vi.fn();
389
+ client.on("message:error", errorHandler);
390
+
391
+ const wsMessageHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "message")?.[1];
392
+
393
+ // Invalid JSON
394
+ wsMessageHandler?.(Buffer.from("invalid json"));
395
+
396
+ expect(errorHandler).toHaveBeenCalled();
397
+ });
398
+
399
+ it.skip("should emit typed events for specific messages", () => {
400
+ const agentSelectedHandler = vi.fn();
401
+ const agentResponseHandler = vi.fn();
402
+
403
+ client.on("agent:selected", agentSelectedHandler);
404
+ client.on("agent:response", agentResponseHandler);
405
+
406
+ const wsMessageHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "message")?.[1];
407
+
408
+ // Agent selected message
409
+ wsMessageHandler?.(
410
+ Buffer.from(
411
+ JSON.stringify({
412
+ type: "agent_selected",
413
+ content: "selected",
414
+ from: "coordinator",
415
+ reasoning: "Best match",
416
+ data: {
417
+ agent_id: "agent-1",
418
+ agent_name: "Test Agent",
419
+ user_request: "help"
420
+ }
421
+ })
422
+ )
423
+ );
424
+
425
+ expect(agentSelectedHandler).toHaveBeenCalled();
426
+ });
427
+ });
428
+
429
+ describe("disconnect", () => {
430
+ beforeEach(async () => {
431
+ const connectPromise = client.connect();
432
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
433
+ if (openHandler) {
434
+ simulateAuth(client);
435
+ await openHandler();
436
+ }
437
+ await connectPromise;
438
+ });
439
+
440
+ it.skip("should close WebSocket connection", () => {
441
+ client.disconnect();
442
+
443
+ expect(mockWs.close).toHaveBeenCalledWith(1000, "Client disconnect");
444
+ expect(client.isConnected).toBe(false);
445
+ });
446
+
447
+ it("should clear intervals", () => {
448
+ const clearIntervalSpy = vi.spyOn(global, "clearInterval");
449
+
450
+ client.disconnect();
451
+
452
+ expect(clearIntervalSpy).toHaveBeenCalled();
453
+ });
454
+
455
+ it("should emit disconnect event", () => {
456
+ const disconnectHandler = vi.fn();
457
+ client.on("disconnect", disconnectHandler);
458
+
459
+ client.disconnect();
460
+
461
+ expect(disconnectHandler).toHaveBeenCalled();
462
+ });
463
+
464
+ it.skip("should handle multiple disconnect calls", () => {
465
+ client.disconnect();
466
+ client.disconnect();
467
+ client.disconnect();
468
+
469
+ expect(mockWs.close).toHaveBeenCalledTimes(1);
470
+ });
471
+ });
472
+
473
+ describe("getConnectionState", () => {
474
+ it("should return current connection state", () => {
475
+ const state = client.getConnectionState();
476
+
477
+ expect(state).toEqual(
478
+ expect.objectContaining({
479
+ connected: false,
480
+ authenticated: false,
481
+ reconnecting: false,
482
+ reconnectAttempts: 0
483
+ })
484
+ );
485
+ });
486
+
487
+ it("should update state after connection", async () => {
488
+ const connectPromise = client.connect();
489
+ const openHandler = mockWs.on.mock.calls.find((call: any) => call[0] === "open")?.[1];
490
+ if (openHandler) {
491
+ simulateAuth(client);
492
+ await openHandler();
493
+ }
494
+ await connectPromise;
495
+
496
+ const state = client.getConnectionState();
497
+ expect(state.connected).toBe(true);
498
+ });
499
+ });
500
+
501
+ describe("getAuthState", () => {
502
+ it("should return current auth state", () => {
503
+ const state = client.getAuthState();
504
+
505
+ expect(state).toEqual(
506
+ expect.objectContaining({
507
+ authenticated: false
508
+ })
509
+ );
510
+ });
511
+ });
512
+ });