@moltzap/client 2026.503.3 → 2026.504.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 (227) hide show
  1. package/dist/auth.d.ts +10 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +8 -5
  4. package/dist/auth.js.map +1 -1
  5. package/dist/channel-core.d.ts +1 -4
  6. package/dist/channel-core.d.ts.map +1 -1
  7. package/dist/channel-core.js +0 -9
  8. package/dist/channel-core.js.map +1 -1
  9. package/dist/channel-core.test.js +90 -71
  10. package/dist/channel-core.test.js.map +1 -1
  11. package/dist/cli/commands/agents.d.ts.map +1 -1
  12. package/dist/cli/commands/agents.js +5 -3
  13. package/dist/cli/commands/agents.js.map +1 -1
  14. package/dist/cli/commands/apps.d.ts +9 -27
  15. package/dist/cli/commands/apps.d.ts.map +1 -1
  16. package/dist/cli/commands/apps.js +36 -57
  17. package/dist/cli/commands/apps.js.map +1 -1
  18. package/dist/cli/commands/apps.test.js +63 -75
  19. package/dist/cli/commands/apps.test.js.map +1 -1
  20. package/dist/cli/commands/contacts.d.ts +1 -5
  21. package/dist/cli/commands/contacts.d.ts.map +1 -1
  22. package/dist/cli/commands/contacts.js +14 -27
  23. package/dist/cli/commands/contacts.js.map +1 -1
  24. package/dist/cli/commands/conversations-v2.test.js +34 -29
  25. package/dist/cli/commands/conversations-v2.test.js.map +1 -1
  26. package/dist/cli/commands/conversations.d.ts +7 -3
  27. package/dist/cli/commands/conversations.d.ts.map +1 -1
  28. package/dist/cli/commands/conversations.js +63 -48
  29. package/dist/cli/commands/conversations.js.map +1 -1
  30. package/dist/cli/commands/messages.d.ts +7 -3
  31. package/dist/cli/commands/messages.d.ts.map +1 -1
  32. package/dist/cli/commands/messages.js +13 -15
  33. package/dist/cli/commands/messages.js.map +1 -1
  34. package/dist/cli/commands/messages.test.js +30 -31
  35. package/dist/cli/commands/messages.test.js.map +1 -1
  36. package/dist/cli/commands/ping.d.ts +9 -0
  37. package/dist/cli/commands/ping.d.ts.map +1 -1
  38. package/dist/cli/commands/ping.js +17 -4
  39. package/dist/cli/commands/ping.js.map +1 -1
  40. package/dist/cli/commands/presence.d.ts.map +1 -1
  41. package/dist/cli/commands/presence.js +6 -4
  42. package/dist/cli/commands/presence.js.map +1 -1
  43. package/dist/cli/commands/register.d.ts.map +1 -1
  44. package/dist/cli/commands/register.js +2 -1
  45. package/dist/cli/commands/register.js.map +1 -1
  46. package/dist/cli/commands/send.d.ts.map +1 -1
  47. package/dist/cli/commands/send.js +22 -11
  48. package/dist/cli/commands/send.js.map +1 -1
  49. package/dist/cli/commands/send.test.js +12 -9
  50. package/dist/cli/commands/send.test.js.map +1 -1
  51. package/dist/cli/commands/status.js +2 -2
  52. package/dist/cli/commands/status.js.map +1 -1
  53. package/dist/cli/commands/test-transport.d.ts +10 -0
  54. package/dist/cli/commands/test-transport.d.ts.map +1 -0
  55. package/dist/cli/commands/test-transport.js +29 -0
  56. package/dist/cli/commands/test-transport.js.map +1 -0
  57. package/dist/cli/commands/whoami.d.ts.map +1 -1
  58. package/dist/cli/commands/whoami.js +5 -1
  59. package/dist/cli/commands/whoami.js.map +1 -1
  60. package/dist/cli/config.d.ts +42 -6
  61. package/dist/cli/config.d.ts.map +1 -1
  62. package/dist/cli/config.js +91 -27
  63. package/dist/cli/config.js.map +1 -1
  64. package/dist/cli/http-client.d.ts +11 -1
  65. package/dist/cli/http-client.d.ts.map +1 -1
  66. package/dist/cli/http-client.js +13 -4
  67. package/dist/cli/http-client.js.map +1 -1
  68. package/dist/cli/index.d.ts.map +1 -1
  69. package/dist/cli/index.js +2 -3
  70. package/dist/cli/index.js.map +1 -1
  71. package/dist/cli/profile.d.ts +3 -4
  72. package/dist/cli/profile.d.ts.map +1 -1
  73. package/dist/cli/profile.js +42 -29
  74. package/dist/cli/profile.js.map +1 -1
  75. package/dist/cli/runtime.d.ts.map +1 -1
  76. package/dist/cli/runtime.js +9 -4
  77. package/dist/cli/runtime.js.map +1 -1
  78. package/dist/cli/socket-client.d.ts +29 -4
  79. package/dist/cli/socket-client.d.ts.map +1 -1
  80. package/dist/cli/socket-client.js +52 -21
  81. package/dist/cli/socket-client.js.map +1 -1
  82. package/dist/cli/transport.d.ts +6 -9
  83. package/dist/cli/transport.d.ts.map +1 -1
  84. package/dist/cli/transport.js +47 -37
  85. package/dist/cli/transport.js.map +1 -1
  86. package/dist/cli/transport.test.js +19 -16
  87. package/dist/cli/transport.test.js.map +1 -1
  88. package/dist/index.d.ts +2 -3
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +2 -0
  93. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +1 -0
  94. package/dist/internal/__tests__/app-callback-partition-key.test.js +33 -0
  95. package/dist/internal/__tests__/app-callback-partition-key.test.js.map +1 -0
  96. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +2 -0
  97. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +1 -0
  98. package/dist/internal/__tests__/{s2c-partition-worker.test.js → app-callback-partition-worker.test.js} +6 -8
  99. package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +1 -0
  100. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts +2 -0
  101. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
  102. package/dist/internal/__tests__/{s2c-partitioned-dispatcher-real-ws.test.js → app-callback-partitioned-dispatcher-real-ws.test.js} +69 -93
  103. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.js.map +1 -0
  104. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +2 -0
  105. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +1 -0
  106. package/dist/internal/__tests__/{s2c-partitioned-dispatcher.test.js → app-callback-partitioned-dispatcher.test.js} +36 -69
  107. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +1 -0
  108. package/dist/internal/__tests__/app-callback-test-requests.d.ts +58 -0
  109. package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +1 -0
  110. package/dist/internal/__tests__/app-callback-test-requests.js +124 -0
  111. package/dist/internal/__tests__/app-callback-test-requests.js.map +1 -0
  112. package/dist/internal/{s2c-dispatcher-errors.d.ts → app-callback-dispatcher-errors.d.ts} +5 -21
  113. package/dist/internal/app-callback-dispatcher-errors.d.ts.map +1 -0
  114. package/dist/internal/{s2c-dispatcher-errors.js → app-callback-dispatcher-errors.js} +3 -13
  115. package/dist/internal/app-callback-dispatcher-errors.js.map +1 -0
  116. package/dist/internal/app-callback-partition-key.d.ts +68 -0
  117. package/dist/internal/app-callback-partition-key.d.ts.map +1 -0
  118. package/dist/internal/app-callback-partition-key.js +90 -0
  119. package/dist/internal/app-callback-partition-key.js.map +1 -0
  120. package/dist/internal/{s2c-partition-worker.d.ts → app-callback-partition-worker.d.ts} +5 -5
  121. package/dist/internal/app-callback-partition-worker.d.ts.map +1 -0
  122. package/dist/internal/{s2c-partition-worker.js → app-callback-partition-worker.js} +5 -5
  123. package/dist/internal/app-callback-partition-worker.js.map +1 -0
  124. package/dist/internal/{s2c-partitioned-dispatcher.d.ts → app-callback-partitioned-dispatcher.d.ts} +9 -10
  125. package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +1 -0
  126. package/dist/internal/{s2c-partitioned-dispatcher.js → app-callback-partitioned-dispatcher.js} +13 -21
  127. package/dist/internal/app-callback-partitioned-dispatcher.js.map +1 -0
  128. package/dist/runtime/close-info.d.ts +15 -16
  129. package/dist/runtime/close-info.d.ts.map +1 -1
  130. package/dist/runtime/close-info.js +12 -8
  131. package/dist/runtime/close-info.js.map +1 -1
  132. package/dist/runtime/errors.d.ts +2 -28
  133. package/dist/runtime/errors.d.ts.map +1 -1
  134. package/dist/runtime/errors.js +0 -9
  135. package/dist/runtime/errors.js.map +1 -1
  136. package/dist/runtime/errors.test.js +4 -3
  137. package/dist/runtime/errors.test.js.map +1 -1
  138. package/dist/runtime/frame.d.ts +17 -18
  139. package/dist/runtime/frame.d.ts.map +1 -1
  140. package/dist/runtime/frame.js +86 -31
  141. package/dist/runtime/frame.js.map +1 -1
  142. package/dist/runtime/frame.test.js +15 -11
  143. package/dist/runtime/frame.test.js.map +1 -1
  144. package/dist/runtime/index.d.ts +1 -1
  145. package/dist/runtime/index.d.ts.map +1 -1
  146. package/dist/runtime/index.js +1 -1
  147. package/dist/runtime/index.js.map +1 -1
  148. package/dist/runtime/local-service-commands.d.ts +7 -0
  149. package/dist/runtime/local-service-commands.d.ts.map +1 -0
  150. package/dist/runtime/local-service-commands.js +6 -0
  151. package/dist/runtime/local-service-commands.js.map +1 -0
  152. package/dist/runtime/runtime.test.js +3 -3
  153. package/dist/runtime/runtime.test.js.map +1 -1
  154. package/dist/runtime/subscribers.d.ts +37 -33
  155. package/dist/runtime/subscribers.d.ts.map +1 -1
  156. package/dist/runtime/subscribers.js +22 -21
  157. package/dist/runtime/subscribers.js.map +1 -1
  158. package/dist/runtime/subscribers.test.js +50 -31
  159. package/dist/runtime/subscribers.test.js.map +1 -1
  160. package/dist/service.d.ts +24 -39
  161. package/dist/service.d.ts.map +1 -1
  162. package/dist/service.js +190 -171
  163. package/dist/service.js.map +1 -1
  164. package/dist/service.test.js +117 -175
  165. package/dist/service.test.js.map +1 -1
  166. package/dist/test/index.d.ts +9 -1
  167. package/dist/test/index.d.ts.map +1 -1
  168. package/dist/test/index.js +14 -3
  169. package/dist/test/index.js.map +1 -1
  170. package/dist/test-utils/channel-service-fixture.d.ts +1 -2
  171. package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
  172. package/dist/test-utils/channel-service-fixture.js +28 -20
  173. package/dist/test-utils/channel-service-fixture.js.map +1 -1
  174. package/dist/test-utils/conformance-adapter.d.ts +2 -2
  175. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  176. package/dist/test-utils/conformance-adapter.js +78 -54
  177. package/dist/test-utils/conformance-adapter.js.map +1 -1
  178. package/dist/test-utils/fake-service.d.ts +14 -39
  179. package/dist/test-utils/fake-service.d.ts.map +1 -1
  180. package/dist/test-utils/fake-service.js +33 -39
  181. package/dist/test-utils/fake-service.js.map +1 -1
  182. package/dist/test-utils/ids.d.ts +9 -0
  183. package/dist/test-utils/ids.d.ts.map +1 -0
  184. package/dist/test-utils/ids.js +52 -0
  185. package/dist/test-utils/ids.js.map +1 -0
  186. package/dist/test-utils/index.d.ts +10 -2
  187. package/dist/test-utils/index.d.ts.map +1 -1
  188. package/dist/test-utils/index.js +25 -9
  189. package/dist/test-utils/index.js.map +1 -1
  190. package/dist/ws-client.d.ts +59 -64
  191. package/dist/ws-client.d.ts.map +1 -1
  192. package/dist/ws-client.js +228 -175
  193. package/dist/ws-client.js.map +1 -1
  194. package/dist/ws-client.test.js +190 -176
  195. package/dist/ws-client.test.js.map +1 -1
  196. package/package.json +3 -3
  197. package/dist/cli/commands/permissions.d.ts +0 -66
  198. package/dist/cli/commands/permissions.d.ts.map +0 -1
  199. package/dist/cli/commands/permissions.js +0 -101
  200. package/dist/cli/commands/permissions.js.map +0 -1
  201. package/dist/cli/commands/permissions.test.d.ts +0 -2
  202. package/dist/cli/commands/permissions.test.d.ts.map +0 -1
  203. package/dist/cli/commands/permissions.test.js +0 -126
  204. package/dist/cli/commands/permissions.test.js.map +0 -1
  205. package/dist/internal/__tests__/s2c-partition-key.test.d.ts +0 -2
  206. package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +0 -1
  207. package/dist/internal/__tests__/s2c-partition-key.test.js +0 -230
  208. package/dist/internal/__tests__/s2c-partition-key.test.js.map +0 -1
  209. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +0 -2
  210. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +0 -1
  211. package/dist/internal/__tests__/s2c-partition-worker.test.js.map +0 -1
  212. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +0 -2
  213. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +0 -1
  214. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +0 -1
  215. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +0 -2
  216. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +0 -1
  217. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +0 -1
  218. package/dist/internal/s2c-dispatcher-errors.d.ts.map +0 -1
  219. package/dist/internal/s2c-dispatcher-errors.js.map +0 -1
  220. package/dist/internal/s2c-partition-key.d.ts +0 -71
  221. package/dist/internal/s2c-partition-key.d.ts.map +0 -1
  222. package/dist/internal/s2c-partition-key.js +0 -140
  223. package/dist/internal/s2c-partition-key.js.map +0 -1
  224. package/dist/internal/s2c-partition-worker.d.ts.map +0 -1
  225. package/dist/internal/s2c-partition-worker.js.map +0 -1
  226. package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +0 -1
  227. package/dist/internal/s2c-partitioned-dispatcher.js.map +0 -1
@@ -14,14 +14,50 @@
14
14
  * `TestClock` (see the `describe("reconnect backoff")` block for details).
15
15
  */
16
16
  import { execSync } from "node:child_process";
17
+ import { createServer } from "node:net";
17
18
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
19
  import { it as itEffect } from "@effect/vitest";
19
20
  import { Cause, Duration, Effect, Exit, Fiber, Option, Scope, TestClock, } from "effect";
20
21
  import * as NodeSocketServer from "@effect/platform-node/NodeSocketServer";
21
22
  import * as Socket from "@effect/platform/Socket";
22
- import { PROTOCOL_VERSION } from "@moltzap/protocol";
23
23
  import { MoltZapWsClient, RPC_TIMEOUT_MS, } from "./ws-client.js";
24
- import { RpcServerError, RpcTimeoutError } from "./runtime/errors.js";
24
+ import { RpcServerError, RpcTimeoutError } from "@moltzap/protocol";
25
+ import { AppsOnClose, AppsOnJoin, Connect, ConversationsList, JSON_RPC_VERSION, MessagesSend, MessageReceivedNotificationDefinition, PROTOCOL_VERSION, agentId, conversationId, jsonRpcStringId, messageId, notificationFrame, requestFrame, responseFrame, validators, } from "@moltzap/protocol";
26
+ import { onCloseParams, onJoinParams, SESSION_A, SESSION_B, } from "./internal/__tests__/app-callback-test-requests.js";
27
+ // ── Test server helpers ────────────────────────────────────────────────
28
+ const LOCALHOST_HOST = "127.0.0.1";
29
+ const TEST_AGENT_ID = agentId("11111111-1111-4111-8111-111111111111");
30
+ const RECONNECT_AGENT_ID = agentId("22222222-2222-4222-8222-222222222222");
31
+ const TEST_MESSAGE_ID = messageId("44444444-4444-4444-8444-444444444444");
32
+ const TEST_CONVERSATION_ID = conversationId("33333333-3333-4333-8333-333333333333");
33
+ const TEST_POLICY = {
34
+ maxMessageBytes: 1_000_000,
35
+ maxPartsPerMessage: 10,
36
+ maxTextLength: 32_768,
37
+ maxGroupParticipants: 100,
38
+ heartbeatIntervalMs: 30_000,
39
+ rateLimits: {
40
+ messagesPerMinute: 60,
41
+ requestsPerMinute: 120,
42
+ },
43
+ };
44
+ const helloOk = (agentId = TEST_AGENT_ID) => ({
45
+ protocolVersion: PROTOCOL_VERSION,
46
+ agentId,
47
+ conversations: [],
48
+ unreadCounts: {},
49
+ policy: TEST_POLICY,
50
+ });
51
+ const TEST_MESSAGE = {
52
+ id: TEST_MESSAGE_ID,
53
+ conversationId: TEST_CONVERSATION_ID,
54
+ senderId: TEST_AGENT_ID,
55
+ parts: [{ type: "text", text: "hello" }],
56
+ createdAt: "2026-05-03T00:00:00.000Z",
57
+ };
58
+ const messageReceivedFrame = () => notificationFrame(MessageReceivedNotificationDefinition, {
59
+ message: TEST_MESSAGE,
60
+ });
25
61
  /**
26
62
  * Spin up an in-process `@effect/platform` WS server on `127.0.0.1:0`.
27
63
  * Caller owns the provided scope; when it closes, the server shuts down.
@@ -29,7 +65,7 @@ import { RpcServerError, RpcTimeoutError } from "./runtime/errors.js";
29
65
  const startTestServer = (handler) => Effect.gen(function* () {
30
66
  const server = yield* NodeSocketServer.makeWebSocket({
31
67
  port: 0,
32
- host: "127.0.0.1",
68
+ host: LOCALHOST_HOST,
33
69
  });
34
70
  const addr = server.address;
35
71
  if (addr._tag !== "TcpAddress") {
@@ -58,12 +94,31 @@ const startTestServer = (handler) => Effect.gen(function* () {
58
94
  }))
59
95
  .pipe(Effect.ignore));
60
96
  return {
61
- url: `http://${addr.hostname}:${addr.port}`,
97
+ url: `http://${LOCALHOST_HOST}:${addr.port}`,
62
98
  get connections() {
63
99
  return connections;
64
100
  },
65
101
  };
66
102
  });
103
+ const findClosedLocalPort = () => new Promise((resolve, reject) => {
104
+ const server = createServer();
105
+ server.once("error", reject);
106
+ server.listen(0, LOCALHOST_HOST, () => {
107
+ const address = server.address();
108
+ if (address === null || typeof address === "string") {
109
+ server.close();
110
+ reject(new Error("expected TCP server address"));
111
+ return;
112
+ }
113
+ server.close((err) => {
114
+ if (err) {
115
+ reject(err);
116
+ return;
117
+ }
118
+ resolve(address.port);
119
+ });
120
+ });
121
+ });
67
122
  // ── Logger helper ──────────────────────────────────────────────────────
68
123
  function makeLogger() {
69
124
  return {
@@ -73,7 +128,7 @@ function makeLogger() {
73
128
  };
74
129
  }
75
130
  const connectP = (client) => Effect.runPromise(client.connect().pipe(Effect.catchTag("RpcTimeoutError", (err) => Effect.fail(new Error(`RPC timeout: ${err.method}`))), Effect.catchAll((err) => Effect.fail(new Error(err.message)))));
76
- const sendRpcP = (client, method, params) => Effect.runPromise(client.sendRpc(method, params).pipe(Effect.catchTag("RpcTimeoutError", (err) => Effect.fail(new Error(`RPC timeout: ${err.method}`))), Effect.catchAll((err) => Effect.fail(new Error(err.message)))));
131
+ const sendRpcP = (client, definition, params) => Effect.runPromise(client.sendRpc(definition, params).pipe(Effect.catchTag("RpcTimeoutError", (err) => Effect.fail(new Error(`RPC timeout: ${err.method}`))), Effect.catchAll((err) => Effect.fail(new Error(err.message)))));
77
132
  const closeClient = (client) => client.close();
78
133
  /**
79
134
  * Start a server whose handler auto-responds to `auth/connect` and forwards
@@ -81,15 +136,15 @@ const closeClient = (client) => client.close();
81
136
  * about post-handshake behaviour.
82
137
  */
83
138
  const startHandshakingServer = (handler) => startTestServer((conn, raw) => Effect.gen(function* () {
84
- const frame = JSON.parse(raw);
85
- if (frame.method === "auth/connect") {
86
- yield* conn.send(JSON.stringify({
87
- jsonrpc: "2.0",
88
- type: "response",
89
- direction: "c2s",
90
- id: frame.id,
91
- result: { agentId: "agent-xyz", protocol: PROTOCOL_VERSION },
92
- }));
139
+ const parsed = JSON.parse(raw);
140
+ if (!validators.requestFrame(parsed)) {
141
+ return yield* Effect.die("expected JSON-RPC request frame");
142
+ }
143
+ const frame = parsed;
144
+ if (frame.method === Connect.name) {
145
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
146
+ result: helloOk(),
147
+ })));
93
148
  return;
94
149
  }
95
150
  yield* handler(conn, raw, frame);
@@ -124,10 +179,10 @@ async function waitFor(pred, { maxMs = 2000 } = {}) {
124
179
  }
125
180
  }
126
181
  /**
127
- * Build a `MoltZapWsClient`. Spec #222 OQ-4 deletion: `onEvent` is no
182
+ * Build a `MoltZapWsClient`. Spec #222 OQ-4 deletion: `onNotification` is no
128
183
  * longer a constructor option; tests that previously stashed an
129
- * `onEvent` callback now register a `subscribe({}, …)` subscription
130
- * post-construction. The helper accepts the same `onEvent` callback as
184
+ * `onNotification` callback now register a `subscribe({}, …)` subscription
185
+ * post-construction. The helper accepts the same `onNotification` callback as
131
186
  * a convenience and wires it through the new subscription registry,
132
187
  * keeping migration noise local to the helper.
133
188
  */
@@ -139,8 +194,8 @@ function makeClient(url, overrides) {
139
194
  onReconnect: overrides?.onReconnect ?? (() => { }),
140
195
  logger: overrides?.logger ?? makeLogger(),
141
196
  });
142
- if (overrides?.onEvent !== undefined) {
143
- const cb = overrides.onEvent;
197
+ if (overrides?.onNotification !== undefined) {
198
+ const cb = overrides.onNotification;
144
199
  Effect.runSync(client.subscribe({}, (frame) => Effect.sync(() => cb(frame))));
145
200
  }
146
201
  return client;
@@ -178,7 +233,8 @@ describe("§5.1 connect() does not hang on pre-open failure", () => {
178
233
  // Observed: ECONNREFUSED fires in single-digit ms locally; give
179
234
  // generous CI headroom via the 15s assertion but keep test timeout at
180
235
  // 20s to avoid flakes on slow runners.
181
- const client = makeClient("http://127.0.0.1:1");
236
+ const refusedPort = await findClosedLocalPort();
237
+ const client = makeClient(`http://${LOCALHOST_HOST}:${refusedPort}`);
182
238
  const t0 = Date.now();
183
239
  try {
184
240
  await connectP(client);
@@ -196,7 +252,7 @@ describe("§5.1 connect() does not hang on pre-open failure", () => {
196
252
  const server = yield* startHandshakingServer(() => Effect.void);
197
253
  const client = makeClient(server.url);
198
254
  const hello = (yield* Effect.promise(() => connectP(client)));
199
- expect(hello.agentId).toBe("agent-xyz");
255
+ expect(hello.agentId).toBe(TEST_AGENT_ID);
200
256
  expect(client.helloOk).toEqual(hello);
201
257
  yield* closeClient(client);
202
258
  }));
@@ -213,8 +269,8 @@ describe("§5.2 pending RPCs fail on disconnect", () => {
213
269
  const server = yield* startHandshakingServer(() => Effect.void);
214
270
  const client = makeClient(server.url);
215
271
  yield* Effect.promise(() => connectP(client));
216
- const rpcP = sendRpcP(client, "messages/send", {
217
- conversationId: "c1",
272
+ const rpcP = sendRpcP(client, MessagesSend, {
273
+ conversationId: TEST_CONVERSATION_ID,
218
274
  parts: [{ type: "text", text: "hi" }],
219
275
  });
220
276
  // Wait for the RPC frame to land on the server.
@@ -251,8 +307,8 @@ describe("§5.3 sendRpc does NOT retry on timeout (TestClock)", () => {
251
307
  yield* Effect.promise(() => connectP(client));
252
308
  const serverConn = server.connections[0];
253
309
  const beforeCount = serverConn.received.length;
254
- const rpcFiber = yield* Effect.fork(client.sendRpc("messages/send", {
255
- conversationId: "c1",
310
+ const rpcFiber = yield* Effect.fork(client.sendRpc(MessagesSend, {
311
+ conversationId: TEST_CONVERSATION_ID,
256
312
  parts: [{ type: "text", text: "payload" }],
257
313
  }));
258
314
  // Wait for the frame to land on the server using real-time polling.
@@ -288,7 +344,7 @@ describe("§5.3 sendRpc does NOT retry on timeout (TestClock)", () => {
288
344
  const err = failed.value;
289
345
  expect(err).toBeInstanceOf(RpcTimeoutError);
290
346
  if (err instanceof RpcTimeoutError) {
291
- expect(err.method).toBe("messages/send");
347
+ expect(err.method).toBe(MessagesSend.name);
292
348
  expect(err.timeoutMs).toBe(RPC_TIMEOUT_MS);
293
349
  }
294
350
  }
@@ -318,16 +374,16 @@ describe("reconnect backoff", () => {
318
374
  let authResponsesSent = 0;
319
375
  let reconnectHello = null;
320
376
  const server = yield* startTestServer((conn, raw) => Effect.gen(function* () {
321
- const frame = JSON.parse(raw);
322
- if (frame.method === "auth/connect") {
377
+ const parsed = JSON.parse(raw);
378
+ if (!validators.requestFrame(parsed)) {
379
+ return yield* Effect.die("expected JSON-RPC request frame");
380
+ }
381
+ const frame = parsed;
382
+ if (frame.method === Connect.name) {
323
383
  authResponsesSent++;
324
- yield* conn.send(JSON.stringify({
325
- jsonrpc: "2.0",
326
- type: "response",
327
- direction: "c2s",
328
- id: frame.id,
329
- result: { agentId: "agent-1" },
330
- }));
384
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
385
+ result: helloOk(RECONNECT_AGENT_ID),
386
+ })));
331
387
  }
332
388
  }));
333
389
  const client = makeClient(server.url, {
@@ -346,7 +402,7 @@ describe("reconnect backoff", () => {
346
402
  expect(server.connections.length).toBeGreaterThanOrEqual(2);
347
403
  expect(authResponsesSent).toBeGreaterThanOrEqual(2);
348
404
  yield* Effect.promise(() => waitFor(() => reconnectHello !== null, { maxMs: 500 }));
349
- expect(reconnectHello.agentId).toBe("agent-1");
405
+ expect(reconnectHello.agentId).toBe(RECONNECT_AGENT_ID);
350
406
  yield* closeClient(client);
351
407
  }));
352
408
  });
@@ -361,103 +417,87 @@ describe("§5.4 malformed frames are logged but do not affect pending RPCs", ()
361
417
  // Server auto-handshakes. On `conversations/list`, injects a few
362
418
  // malformed inbound frames, then the real response.
363
419
  const server = yield* startHandshakingServer((conn, _raw, frame) => Effect.gen(function* () {
364
- if (frame.method !== "conversations/list")
420
+ if (frame.method !== ConversationsList.name)
365
421
  return;
366
- // Inject: non-JSON, then missing-id response, then unknown type.
422
+ // Inject: non-JSON, then a missing-id response-like frame, then
423
+ // an unknown object shape.
367
424
  yield* conn.send("not json at all");
368
- yield* conn.send(JSON.stringify({ type: "response" }));
369
- yield* conn.send(JSON.stringify({ type: "unknown", id: frame.id }));
425
+ yield* conn.send(JSON.stringify({ jsonrpc: JSON_RPC_VERSION, result: {} }));
426
+ yield* conn.send(JSON.stringify({ jsonrpc: JSON_RPC_VERSION, id: frame.id }));
370
427
  // Real well-formed response.
371
- yield* conn.send(JSON.stringify({
372
- jsonrpc: "2.0",
373
- type: "response",
374
- direction: "c2s",
375
- id: frame.id,
428
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
376
429
  result: { conversations: [] },
377
- }));
430
+ })));
378
431
  }));
379
432
  const client = makeClient(server.url, { logger });
380
433
  yield* Effect.promise(() => connectP(client));
381
- const result = (yield* Effect.promise(() => sendRpcP(client, "conversations/list", {})));
434
+ const result = (yield* Effect.promise(() => sendRpcP(client, ConversationsList, {})));
382
435
  expect(result.conversations).toEqual([]);
383
436
  // Logger saw at least one malformed-frame warning.
384
437
  expect(logger.warn).toHaveBeenCalled();
385
438
  yield* closeClient(client);
386
439
  }));
387
440
  });
388
- it("accepts a padded chunk that contains both an event and the response", async () => {
441
+ it("accepts a padded chunk that contains both a notification and the response", async () => {
389
442
  await withTestServer(Effect.gen(function* () {
390
443
  const logger = makeLogger();
391
444
  const events = [];
392
445
  const server = yield* startHandshakingServer((conn, _raw, frame) => Effect.gen(function* () {
393
- if (frame.method !== "conversations/list")
446
+ if (frame.method !== ConversationsList.name)
394
447
  return;
395
- yield* conn.send(JSON.stringify({
396
- jsonrpc: "2.0",
397
- type: "event",
398
- event: "messages/received",
399
- data: { message: { id: "m-1", conversationId: "c-1" } },
400
- }) +
448
+ yield* conn.send(JSON.stringify(messageReceivedFrame()) +
401
449
  "\u0000" +
402
- JSON.stringify({
403
- jsonrpc: "2.0",
404
- type: "response",
405
- direction: "c2s",
406
- id: frame.id,
450
+ JSON.stringify(responseFrame(frame.id, {
407
451
  result: { conversations: [] },
408
- }));
452
+ })));
409
453
  }));
410
454
  const client = makeClient(server.url, {
411
455
  logger,
412
- onEvent: (event) => events.push(event),
456
+ onNotification: (event) => events.push(event),
413
457
  });
414
458
  yield* Effect.promise(() => connectP(client));
415
- const result = (yield* Effect.promise(() => sendRpcP(client, "conversations/list", {})));
459
+ const result = (yield* Effect.promise(() => sendRpcP(client, ConversationsList, {})));
416
460
  expect(result.conversations).toEqual([]);
417
461
  expect(events).toHaveLength(1);
418
- expect(events[0]).toMatchObject({ event: "messages/received" });
462
+ expect(events[0]).toMatchObject({
463
+ method: MessageReceivedNotificationDefinition.name,
464
+ });
419
465
  expect(logger.warn).not.toHaveBeenCalled();
420
466
  yield* closeClient(client);
421
467
  }));
422
468
  });
423
- it("routes a well-formed event frame to onEvent", async () => {
469
+ it("routes a well-formed notification frame to onNotification", async () => {
424
470
  await withTestServer(Effect.gen(function* () {
425
471
  const events = [];
426
- const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({
427
- jsonrpc: "2.0",
428
- type: "event",
429
- event: "message.received",
430
- data: { message: { id: "m-1", conversationId: "c-1" } },
431
- })));
472
+ const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify(messageReceivedFrame())));
432
473
  const client = makeClient(server.url, {
433
- onEvent: (e) => events.push(e),
474
+ onNotification: (e) => events.push(e),
434
475
  });
435
476
  yield* Effect.promise(() => connectP(client));
436
- // Fire-and-forget: the server responds with an out-of-band event
477
+ // Fire-and-forget: the server responds with an out-of-band notification
437
478
  // rather than an RPC response, so the noop Deferred never resolves.
438
479
  // Awaiting it would wedge the test for the full RPC_TIMEOUT_MS.
439
- void sendRpcP(client, "noop", {}).catch(() => { });
480
+ void sendRpcP(client, ConversationsList, {}).catch(() => { });
440
481
  yield* Effect.promise(() => waitFor(() => events.length > 0, { maxMs: 2000 }));
441
482
  expect(events[0]).toMatchObject({
442
- type: "event",
443
- event: "message.received",
483
+ method: MessageReceivedNotificationDefinition.name,
444
484
  });
445
485
  yield* closeClient(client);
446
486
  }));
447
487
  });
448
- it("does NOT route an event frame missing the event name field", async () => {
488
+ it("does NOT route a notification frame missing the method field", async () => {
449
489
  await withTestServer(Effect.gen(function* () {
450
490
  const events = [];
451
491
  const logger = makeLogger();
452
- // Send a malformed event on the first post-handshake frame.
453
- const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({ type: "event", data: {} })));
492
+ // Send a malformed notification on the first post-handshake frame.
493
+ const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({ jsonrpc: JSON_RPC_VERSION, params: {} })));
454
494
  const client = makeClient(server.url, {
455
- onEvent: (e) => events.push(e),
495
+ onNotification: (e) => events.push(e),
456
496
  logger,
457
497
  });
458
498
  yield* Effect.promise(() => connectP(client));
459
499
  // Fire-and-forget: see the well-formed-event test above for rationale.
460
- void sendRpcP(client, "noop", {}).catch(() => { });
500
+ void sendRpcP(client, ConversationsList, {}).catch(() => { });
461
501
  yield* Effect.promise(() => new Promise((r) => setTimeout(r, 50)));
462
502
  expect(events).toHaveLength(0);
463
503
  expect(logger.warn).toHaveBeenCalled();
@@ -484,7 +524,7 @@ describe("malformed-frame log cadence (MALFORMED_LOG_EVERY)", () => {
484
524
  logger.warn.mockClear();
485
525
  // Fire-and-forget: server responds with 101 malformed frames, no
486
526
  // actual RPC response, so awaiting the noop would wedge the test.
487
- void sendRpcP(client, "noop", {}).catch(() => { });
527
+ void sendRpcP(client, ConversationsList, {}).catch(() => { });
488
528
  // Wait for the malformed frames to flush through the reader fiber.
489
529
  yield* Effect.promise(() => new Promise((r) => setTimeout(r, 300)));
490
530
  expect(logger.warn).toHaveBeenCalledTimes(3);
@@ -506,7 +546,7 @@ describe("close() interleaved with a pending RPC", () => {
506
546
  const server = yield* startHandshakingServer(() => Effect.void);
507
547
  const client = makeClient(server.url);
508
548
  yield* Effect.promise(() => connectP(client));
509
- const rpcP = sendRpcP(client, "conversations/list", {});
549
+ const rpcP = sendRpcP(client, ConversationsList, {});
510
550
  yield* Effect.promise(() => waitFor(() => server.connections[0].received.length >= 2));
511
551
  const beforeMs = Date.now();
512
552
  yield* closeClient(client);
@@ -526,7 +566,7 @@ describe("socket error after connect", () => {
526
566
  const server = yield* startHandshakingServer((conn) => conn.close(1011));
527
567
  const client = makeClient(server.url, { logger });
528
568
  yield* Effect.promise(() => connectP(client));
529
- const rpcP = sendRpcP(client, "conversations/list", {});
569
+ const rpcP = sendRpcP(client, ConversationsList, {});
530
570
  yield* Effect.promise(() => expect(rpcP).rejects.toThrow(/WebSocket not connected/));
531
571
  // Logger captured the WebSocket error (warn level).
532
572
  expect(logger.warn).toHaveBeenCalled();
@@ -541,40 +581,19 @@ describe("sendRpc(RpcDefinition, params) — typed manifest overload", () => {
541
581
  it("uses definition.name as the wire-level method string", async () => {
542
582
  await withTestServer(Effect.gen(function* () {
543
583
  const { AgentsLookupByName } = yield* Effect.promise(() => import("@moltzap/protocol"));
544
- // Echo the method name + params back as the result so the test can
545
- // verify both the wire-level method and the forwarded params.
546
- const server = yield* startHandshakingServer((conn, _raw, frame) => conn.send(JSON.stringify({
547
- jsonrpc: "2.0",
548
- type: "response",
549
- direction: "c2s",
550
- id: frame.id,
551
- result: {
552
- echoedMethod: frame.method,
553
- echoedParams: frame.params,
554
- agents: [],
555
- },
556
- })));
557
- const client = makeClient(server.url);
558
- yield* Effect.promise(() => connectP(client));
559
- const result = (yield* Effect.promise(() => Effect.runPromise(client.sendRpc(AgentsLookupByName, { names: ["alice"] }))));
560
- expect(result.echoedMethod).toBe("agents/lookupByName");
561
- expect(result.echoedParams).toEqual({ names: ["alice"] });
562
- yield* closeClient(client);
563
- }));
564
- });
565
- it("preserves the legacy string overload for back-compat", async () => {
566
- await withTestServer(Effect.gen(function* () {
567
- const server = yield* startHandshakingServer((conn, _raw, frame) => conn.send(JSON.stringify({
568
- jsonrpc: "2.0",
569
- type: "response",
570
- direction: "c2s",
571
- id: frame.id,
572
- result: { echoedMethod: frame.method, agents: [] },
573
- })));
584
+ const captured = { current: null };
585
+ const server = yield* startHandshakingServer((conn, _raw, frame) => Effect.gen(function* () {
586
+ captured.current = frame;
587
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
588
+ result: { agents: [] },
589
+ })));
590
+ }));
574
591
  const client = makeClient(server.url);
575
592
  yield* Effect.promise(() => connectP(client));
576
- const result = (yield* Effect.promise(() => sendRpcP(client, "agents/lookupByName", { names: ["bob"] })));
577
- expect(result.echoedMethod).toBe("agents/lookupByName");
593
+ const result = yield* Effect.promise(() => Effect.runPromise(client.sendRpc(AgentsLookupByName, { names: ["alice"] })));
594
+ expect(result.agents).toEqual([]);
595
+ expect(captured.current?.method).toBe(AgentsLookupByName.name);
596
+ expect(captured.current?.params).toEqual({ names: ["alice"] });
578
597
  yield* closeClient(client);
579
598
  }));
580
599
  });
@@ -673,55 +692,49 @@ describe("spec #222 §5.4 — onDisconnect close metadata (V7)", () => {
673
692
  });
674
693
  // ─────────────────────────────────────────────────────────────────────
675
694
  // Phase 1.0 (B.1) gating tests — client-side server-initiated RPC
676
- // (handleServerRpc + dispatcher fiber + s2c response write-back)
695
+ // (handleServerRpc + dispatcher fiber + appCallback response write-back)
677
696
  // ─────────────────────────────────────────────────────────────────────
678
697
  describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
679
- it("dispatches an inbound s2c request to the registered handler and writes the response back", async () => {
698
+ it("dispatches an inbound appCallback request to the registered handler and writes the response back", async () => {
680
699
  await withTestServer(Effect.gen(function* () {
681
700
  // Server: auto-handshake; immediately AFTER replying to
682
- // auth/connect, send an s2c request to the client; capture every
701
+ // auth/connect, send an appCallback request to the client; capture every
683
702
  // subsequent inbound frame the client writes back. We're testing
684
703
  // the client's dispatcher fiber + handler registry + response
685
704
  // encoding.
686
705
  const server = yield* startTestServer((conn, raw) => Effect.gen(function* () {
687
- const frame = JSON.parse(raw);
688
- if (frame.method === "auth/connect") {
689
- yield* conn.send(JSON.stringify({
690
- jsonrpc: "2.0",
691
- type: "response",
692
- direction: "c2s",
693
- id: frame.id,
694
- result: { agentId: "agent-xyz", protocol: PROTOCOL_VERSION },
695
- }));
696
- // Fire the s2c request straight after auth response. The
706
+ const parsed = JSON.parse(raw);
707
+ if (!validators.requestFrame(parsed)) {
708
+ return yield* Effect.die("expected JSON-RPC request frame");
709
+ }
710
+ const frame = parsed;
711
+ if (frame.method === Connect.name) {
712
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
713
+ result: helloOk(),
714
+ })));
715
+ // Fire the appCallback request straight after auth response. The
697
716
  // client's dispatcher fiber was forked into the connect
698
717
  // scope BEFORE auth/connect was sent, so the inbound queue
699
718
  // is live the moment we land here.
700
- yield* conn.send(JSON.stringify({
701
- jsonrpc: "2.0",
702
- type: "request",
703
- direction: "s2c",
704
- id: "srv-test-1",
705
- method: "apps/onJoin",
706
- params: { sessionId: "sess-A" },
707
- }));
719
+ yield* conn.send(JSON.stringify(requestFrame(jsonRpcStringId("srv-test-1"), AppsOnJoin, onJoinParams(SESSION_A))));
708
720
  }
709
721
  }));
710
722
  const client = makeClient(server.url);
711
723
  // Register a handler BEFORE connect so the dispatcher fiber sees
712
- // it on the very first inbound s2c request.
713
- yield* client.handleServerRpc("apps/onJoin", (params) => Effect.succeed({
724
+ // it on the very first inbound appCallback request.
725
+ yield* client.handleServerRpc(AppsOnJoin, (params) => Effect.succeed({
714
726
  ack: true,
715
727
  saw: params.sessionId,
716
728
  }));
717
729
  yield* Effect.promise(() => connectP(client));
718
730
  // Wait for the response frame the dispatcher writes back. The
719
- // server records every inbound frame in `received` — the s2c
731
+ // server records every inbound frame in `received` — the appCallback
720
732
  // response should appear after the client's auth/connect.
721
733
  const responseRaw = yield* Effect.promise(() => waitFor(() => server.connections[0].received.some((r) => {
722
734
  try {
723
735
  const parsed = JSON.parse(r);
724
- return (parsed.type === "response" && parsed.id === "srv-test-1");
736
+ return (validators.responseFrame(parsed) &&
737
+ parsed.id === "srv-test-1");
725
738
  }
726
739
  catch {
727
740
  return false;
@@ -729,51 +742,44 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
729
742
  }), { maxMs: 2000 })).pipe(Effect.flatMap(() => Effect.sync(() => server.connections[0].received.find((r) => {
730
743
  try {
731
744
  const parsed = JSON.parse(r);
732
- return parsed.id === "srv-test-1";
745
+ return (validators.responseFrame(parsed) &&
746
+ parsed.id === "srv-test-1");
733
747
  }
734
748
  catch {
735
749
  return false;
736
750
  }
737
751
  }))));
738
752
  const parsedResponse = JSON.parse(responseRaw);
739
- expect(parsedResponse.type).toBe("response");
740
- expect(parsedResponse.direction).toBe("s2c");
753
+ expect(validators.responseFrame(parsedResponse)).toBe(true);
754
+ if (!validators.responseFrame(parsedResponse))
755
+ return;
741
756
  expect(parsedResponse.id).toBe("srv-test-1");
742
- expect(parsedResponse.result.ack).toBe(true);
743
- expect(parsedResponse.result.saw).toBe("sess-A");
757
+ expect("result" in parsedResponse).toBe(true);
758
+ if (!("result" in parsedResponse))
759
+ return;
760
+ const result = parsedResponse.result;
761
+ expect(result.ack).toBe(true);
762
+ expect(result.saw).toBe(SESSION_A);
744
763
  yield* closeClient(client);
745
764
  }));
746
765
  });
747
766
  it("encodes a typed RpcServerError from the handler as a `response` frame with `error`", async () => {
748
767
  await withTestServer(Effect.gen(function* () {
749
768
  const server = yield* startTestServer((conn, raw) => Effect.gen(function* () {
750
- const frame = JSON.parse(raw);
751
- if (frame.method === "auth/connect") {
752
- yield* conn.send(JSON.stringify({
753
- jsonrpc: "2.0",
754
- type: "response",
755
- direction: "c2s",
756
- id: frame.id,
757
- result: { agentId: "agent-xyz", protocol: PROTOCOL_VERSION },
758
- }));
759
- yield* conn.send(JSON.stringify({
760
- jsonrpc: "2.0",
761
- type: "request",
762
- direction: "s2c",
763
- id: "srv-err-1",
764
- method: "apps/onClose",
765
- // Spec #356: the partitioned dispatcher
766
- // narrow-validates routing fields; sessionId is
767
- // required for every s2c hook. Synthetic test
768
- // payloads must include it or the dispatcher
769
- // rejects the offer with a `-32602` wire error
770
- // before reaching the handler.
771
- params: { sessionId: "sess-err-1" },
772
- }));
769
+ const parsed = JSON.parse(raw);
770
+ if (!validators.requestFrame(parsed)) {
771
+ return yield* Effect.die("expected JSON-RPC request frame");
772
+ }
773
+ const frame = parsed;
774
+ if (frame.method === Connect.name) {
775
+ yield* conn.send(JSON.stringify(responseFrame(frame.id, {
776
+ result: helloOk(),
777
+ })));
778
+ yield* conn.send(JSON.stringify(requestFrame(jsonRpcStringId("srv-err-1"), AppsOnClose, onCloseParams(SESSION_B))));
773
779
  }
774
780
  }));
775
781
  const client = makeClient(server.url);
776
- yield* client.handleServerRpc("apps/onClose", () => Effect.fail(new RpcServerError({
782
+ yield* client.handleServerRpc(AppsOnClose, () => Effect.fail(new RpcServerError({
777
783
  code: -32099,
778
784
  message: "domain-rejected",
779
785
  data: { reason: "test" },
@@ -782,7 +788,9 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
782
788
  yield* Effect.promise(() => waitFor(() => server.connections[0].received.some((r) => {
783
789
  try {
784
790
  const parsed = JSON.parse(r);
785
- return (parsed.id === "srv-err-1" && parsed.error !== undefined);
791
+ return (validators.responseFrame(parsed) &&
792
+ parsed.id === "srv-err-1" &&
793
+ "error" in parsed);
786
794
  }
787
795
  catch {
788
796
  return false;
@@ -790,14 +798,20 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
790
798
  }), { maxMs: 2000 }));
791
799
  const found = server.connections[0].received.find((r) => {
792
800
  try {
793
- return JSON.parse(r).id === "srv-err-1";
801
+ const parsed = JSON.parse(r);
802
+ return (validators.responseFrame(parsed) && parsed.id === "srv-err-1");
794
803
  }
795
804
  catch {
796
805
  return false;
797
806
  }
798
807
  });
799
808
  const parsed = JSON.parse(found);
800
- expect(parsed.direction).toBe("s2c");
809
+ expect(validators.responseFrame(parsed)).toBe(true);
810
+ if (!validators.responseFrame(parsed))
811
+ return;
812
+ expect("error" in parsed).toBe(true);
813
+ if (!("error" in parsed))
814
+ return;
801
815
  expect(parsed.error.code).toBe(-32099);
802
816
  expect(parsed.error.message).toBe("domain-rejected");
803
817
  expect(parsed.error.data.reason).toBe("test");
@@ -809,9 +823,9 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
809
823
  serverUrl: "http://127.0.0.1:1",
810
824
  agentKey: "test",
811
825
  });
812
- const first = await Effect.runPromiseExit(client.handleServerRpc("apps/onJoin", () => Effect.succeed({})));
826
+ const first = await Effect.runPromiseExit(client.handleServerRpc(AppsOnJoin, () => Effect.succeed({})));
813
827
  expect(Exit.isSuccess(first)).toBe(true);
814
- const second = await Effect.runPromiseExit(client.handleServerRpc("apps/onJoin", () => Effect.succeed({})));
828
+ const second = await Effect.runPromiseExit(client.handleServerRpc(AppsOnJoin, () => Effect.succeed({})));
815
829
  expect(Exit.isFailure(second)).toBe(true);
816
830
  if (Exit.isFailure(second)) {
817
831
  const err = Cause.failureOption(second.cause);
@@ -826,7 +840,7 @@ describe("Phase 1.0 (B.1) — handleServerRpc round-trip", () => {
826
840
  // ─────────────────────────────────────────────────────────────────────
827
841
  // Regression gate (review-295): runSync(client.close()) and
828
842
  // runSync(client.disconnect()) must not throw AsyncFiberException after
829
- // the s2c queue + dispatcher were added. The s2c machinery lives off the
843
+ // the appCallback queue + dispatcher were added. The appCallback machinery lives off the
830
844
  // per-connect Scope (allocated inline + forked via runtime.runFork) so
831
845
  // `Scope.close` stays sync; teardown of the queue and dispatcher fiber
832
846
  // is dispatched via `runFork` from close()/disconnectSync().