@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.
- package/dist/auth.d.ts +10 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +8 -5
- package/dist/auth.js.map +1 -1
- package/dist/channel-core.d.ts +1 -4
- package/dist/channel-core.d.ts.map +1 -1
- package/dist/channel-core.js +0 -9
- package/dist/channel-core.js.map +1 -1
- package/dist/channel-core.test.js +90 -71
- package/dist/channel-core.test.js.map +1 -1
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +5 -3
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/apps.d.ts +9 -27
- package/dist/cli/commands/apps.d.ts.map +1 -1
- package/dist/cli/commands/apps.js +36 -57
- package/dist/cli/commands/apps.js.map +1 -1
- package/dist/cli/commands/apps.test.js +63 -75
- package/dist/cli/commands/apps.test.js.map +1 -1
- package/dist/cli/commands/contacts.d.ts +1 -5
- package/dist/cli/commands/contacts.d.ts.map +1 -1
- package/dist/cli/commands/contacts.js +14 -27
- package/dist/cli/commands/contacts.js.map +1 -1
- package/dist/cli/commands/conversations-v2.test.js +34 -29
- package/dist/cli/commands/conversations-v2.test.js.map +1 -1
- package/dist/cli/commands/conversations.d.ts +7 -3
- package/dist/cli/commands/conversations.d.ts.map +1 -1
- package/dist/cli/commands/conversations.js +63 -48
- package/dist/cli/commands/conversations.js.map +1 -1
- package/dist/cli/commands/messages.d.ts +7 -3
- package/dist/cli/commands/messages.d.ts.map +1 -1
- package/dist/cli/commands/messages.js +13 -15
- package/dist/cli/commands/messages.js.map +1 -1
- package/dist/cli/commands/messages.test.js +30 -31
- package/dist/cli/commands/messages.test.js.map +1 -1
- package/dist/cli/commands/ping.d.ts +9 -0
- package/dist/cli/commands/ping.d.ts.map +1 -1
- package/dist/cli/commands/ping.js +17 -4
- package/dist/cli/commands/ping.js.map +1 -1
- package/dist/cli/commands/presence.d.ts.map +1 -1
- package/dist/cli/commands/presence.js +6 -4
- package/dist/cli/commands/presence.js.map +1 -1
- package/dist/cli/commands/register.d.ts.map +1 -1
- package/dist/cli/commands/register.js +2 -1
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/send.d.ts.map +1 -1
- package/dist/cli/commands/send.js +22 -11
- package/dist/cli/commands/send.js.map +1 -1
- package/dist/cli/commands/send.test.js +12 -9
- package/dist/cli/commands/send.test.js.map +1 -1
- package/dist/cli/commands/status.js +2 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/test-transport.d.ts +10 -0
- package/dist/cli/commands/test-transport.d.ts.map +1 -0
- package/dist/cli/commands/test-transport.js +29 -0
- package/dist/cli/commands/test-transport.js.map +1 -0
- package/dist/cli/commands/whoami.d.ts.map +1 -1
- package/dist/cli/commands/whoami.js +5 -1
- package/dist/cli/commands/whoami.js.map +1 -1
- package/dist/cli/config.d.ts +42 -6
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +91 -27
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/http-client.d.ts +11 -1
- package/dist/cli/http-client.d.ts.map +1 -1
- package/dist/cli/http-client.js +13 -4
- package/dist/cli/http-client.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/profile.d.ts +3 -4
- package/dist/cli/profile.d.ts.map +1 -1
- package/dist/cli/profile.js +42 -29
- package/dist/cli/profile.js.map +1 -1
- package/dist/cli/runtime.d.ts.map +1 -1
- package/dist/cli/runtime.js +9 -4
- package/dist/cli/runtime.js.map +1 -1
- package/dist/cli/socket-client.d.ts +29 -4
- package/dist/cli/socket-client.d.ts.map +1 -1
- package/dist/cli/socket-client.js +52 -21
- package/dist/cli/socket-client.js.map +1 -1
- package/dist/cli/transport.d.ts +6 -9
- package/dist/cli/transport.d.ts.map +1 -1
- package/dist/cli/transport.js +47 -37
- package/dist/cli/transport.js.map +1 -1
- package/dist/cli/transport.test.js +19 -16
- package/dist/cli/transport.test.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +1 -0
- package/dist/internal/__tests__/app-callback-partition-key.test.js +33 -0
- package/dist/internal/__tests__/app-callback-partition-key.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +1 -0
- package/dist/internal/__tests__/{s2c-partition-worker.test.js → app-callback-partition-worker.test.js} +6 -8
- package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
- package/dist/internal/__tests__/{s2c-partitioned-dispatcher-real-ws.test.js → app-callback-partitioned-dispatcher-real-ws.test.js} +69 -93
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +1 -0
- package/dist/internal/__tests__/{s2c-partitioned-dispatcher.test.js → app-callback-partitioned-dispatcher.test.js} +36 -69
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-test-requests.d.ts +58 -0
- package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +1 -0
- package/dist/internal/__tests__/app-callback-test-requests.js +124 -0
- package/dist/internal/__tests__/app-callback-test-requests.js.map +1 -0
- package/dist/internal/{s2c-dispatcher-errors.d.ts → app-callback-dispatcher-errors.d.ts} +5 -21
- package/dist/internal/app-callback-dispatcher-errors.d.ts.map +1 -0
- package/dist/internal/{s2c-dispatcher-errors.js → app-callback-dispatcher-errors.js} +3 -13
- package/dist/internal/app-callback-dispatcher-errors.js.map +1 -0
- package/dist/internal/app-callback-partition-key.d.ts +68 -0
- package/dist/internal/app-callback-partition-key.d.ts.map +1 -0
- package/dist/internal/app-callback-partition-key.js +90 -0
- package/dist/internal/app-callback-partition-key.js.map +1 -0
- package/dist/internal/{s2c-partition-worker.d.ts → app-callback-partition-worker.d.ts} +5 -5
- package/dist/internal/app-callback-partition-worker.d.ts.map +1 -0
- package/dist/internal/{s2c-partition-worker.js → app-callback-partition-worker.js} +5 -5
- package/dist/internal/app-callback-partition-worker.js.map +1 -0
- package/dist/internal/{s2c-partitioned-dispatcher.d.ts → app-callback-partitioned-dispatcher.d.ts} +9 -10
- package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +1 -0
- package/dist/internal/{s2c-partitioned-dispatcher.js → app-callback-partitioned-dispatcher.js} +13 -21
- package/dist/internal/app-callback-partitioned-dispatcher.js.map +1 -0
- package/dist/runtime/close-info.d.ts +15 -16
- package/dist/runtime/close-info.d.ts.map +1 -1
- package/dist/runtime/close-info.js +12 -8
- package/dist/runtime/close-info.js.map +1 -1
- package/dist/runtime/errors.d.ts +2 -28
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +0 -9
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/errors.test.js +4 -3
- package/dist/runtime/errors.test.js.map +1 -1
- package/dist/runtime/frame.d.ts +17 -18
- package/dist/runtime/frame.d.ts.map +1 -1
- package/dist/runtime/frame.js +86 -31
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/frame.test.js +15 -11
- package/dist/runtime/frame.test.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/local-service-commands.d.ts +7 -0
- package/dist/runtime/local-service-commands.d.ts.map +1 -0
- package/dist/runtime/local-service-commands.js +6 -0
- package/dist/runtime/local-service-commands.js.map +1 -0
- package/dist/runtime/runtime.test.js +3 -3
- package/dist/runtime/runtime.test.js.map +1 -1
- package/dist/runtime/subscribers.d.ts +37 -33
- package/dist/runtime/subscribers.d.ts.map +1 -1
- package/dist/runtime/subscribers.js +22 -21
- package/dist/runtime/subscribers.js.map +1 -1
- package/dist/runtime/subscribers.test.js +50 -31
- package/dist/runtime/subscribers.test.js.map +1 -1
- package/dist/service.d.ts +24 -39
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +190 -171
- package/dist/service.js.map +1 -1
- package/dist/service.test.js +117 -175
- package/dist/service.test.js.map +1 -1
- package/dist/test/index.d.ts +9 -1
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +14 -3
- package/dist/test/index.js.map +1 -1
- package/dist/test-utils/channel-service-fixture.d.ts +1 -2
- package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
- package/dist/test-utils/channel-service-fixture.js +28 -20
- package/dist/test-utils/channel-service-fixture.js.map +1 -1
- package/dist/test-utils/conformance-adapter.d.ts +2 -2
- package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
- package/dist/test-utils/conformance-adapter.js +78 -54
- package/dist/test-utils/conformance-adapter.js.map +1 -1
- package/dist/test-utils/fake-service.d.ts +14 -39
- package/dist/test-utils/fake-service.d.ts.map +1 -1
- package/dist/test-utils/fake-service.js +33 -39
- package/dist/test-utils/fake-service.js.map +1 -1
- package/dist/test-utils/ids.d.ts +9 -0
- package/dist/test-utils/ids.d.ts.map +1 -0
- package/dist/test-utils/ids.js +52 -0
- package/dist/test-utils/ids.js.map +1 -0
- package/dist/test-utils/index.d.ts +10 -2
- package/dist/test-utils/index.d.ts.map +1 -1
- package/dist/test-utils/index.js +25 -9
- package/dist/test-utils/index.js.map +1 -1
- package/dist/ws-client.d.ts +59 -64
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +228 -175
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-client.test.js +190 -176
- package/dist/ws-client.test.js.map +1 -1
- package/package.json +3 -3
- package/dist/cli/commands/permissions.d.ts +0 -66
- package/dist/cli/commands/permissions.d.ts.map +0 -1
- package/dist/cli/commands/permissions.js +0 -101
- package/dist/cli/commands/permissions.js.map +0 -1
- package/dist/cli/commands/permissions.test.d.ts +0 -2
- package/dist/cli/commands/permissions.test.d.ts.map +0 -1
- package/dist/cli/commands/permissions.test.js +0 -126
- package/dist/cli/commands/permissions.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partition-key.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partition-key.test.js +0 -230
- package/dist/internal/__tests__/s2c-partition-key.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partition-worker.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +0 -1
- package/dist/internal/s2c-dispatcher-errors.d.ts.map +0 -1
- package/dist/internal/s2c-dispatcher-errors.js.map +0 -1
- package/dist/internal/s2c-partition-key.d.ts +0 -71
- package/dist/internal/s2c-partition-key.d.ts.map +0 -1
- package/dist/internal/s2c-partition-key.js +0 -140
- package/dist/internal/s2c-partition-key.js.map +0 -1
- package/dist/internal/s2c-partition-worker.d.ts.map +0 -1
- package/dist/internal/s2c-partition-worker.js.map +0 -1
- package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +0 -1
- package/dist/internal/s2c-partitioned-dispatcher.js.map +0 -1
package/dist/ws-client.test.js
CHANGED
|
@@ -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 "
|
|
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:
|
|
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://${
|
|
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,
|
|
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
|
|
85
|
-
if (
|
|
86
|
-
yield*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
result:
|
|
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: `
|
|
182
|
+
* Build a `MoltZapWsClient`. Spec #222 OQ-4 deletion: `onNotification` is no
|
|
128
183
|
* longer a constructor option; tests that previously stashed an
|
|
129
|
-
* `
|
|
130
|
-
* post-construction. The helper accepts the same `
|
|
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?.
|
|
143
|
-
const cb = overrides.
|
|
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
|
|
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(
|
|
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,
|
|
217
|
-
conversationId:
|
|
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(
|
|
255
|
-
conversationId:
|
|
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(
|
|
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
|
|
322
|
-
if (
|
|
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
|
-
|
|
326
|
-
|
|
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(
|
|
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 !==
|
|
420
|
+
if (frame.method !== ConversationsList.name)
|
|
365
421
|
return;
|
|
366
|
-
// Inject: non-JSON, then missing-id response, then
|
|
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({
|
|
369
|
-
yield* conn.send(JSON.stringify({
|
|
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,
|
|
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
|
|
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 !==
|
|
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
|
-
|
|
456
|
+
onNotification: (event) => events.push(event),
|
|
413
457
|
});
|
|
414
458
|
yield* Effect.promise(() => connectP(client));
|
|
415
|
-
const result = (yield* Effect.promise(() => sendRpcP(client,
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
480
|
+
void sendRpcP(client, ConversationsList, {}).catch(() => { });
|
|
440
481
|
yield* Effect.promise(() => waitFor(() => events.length > 0, { maxMs: 2000 }));
|
|
441
482
|
expect(events[0]).toMatchObject({
|
|
442
|
-
|
|
443
|
-
event: "message.received",
|
|
483
|
+
method: MessageReceivedNotificationDefinition.name,
|
|
444
484
|
});
|
|
445
485
|
yield* closeClient(client);
|
|
446
486
|
}));
|
|
447
487
|
});
|
|
448
|
-
it("does NOT route
|
|
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
|
|
453
|
-
const server = yield* startHandshakingServer((conn) => conn.send(JSON.stringify({
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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 =
|
|
577
|
-
expect(result.
|
|
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 +
|
|
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
|
|
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
|
|
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
|
|
688
|
-
if (
|
|
689
|
-
yield*
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
result:
|
|
695
|
-
}));
|
|
696
|
-
// Fire 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
|
|
713
|
-
yield* client.handleServerRpc(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
740
|
-
|
|
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
|
|
743
|
-
|
|
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
|
|
751
|
-
if (
|
|
752
|
-
yield*
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
result:
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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().
|