@crouton-kit/crouter 0.3.61 → 0.3.63
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/builtin-views/canvas/core.mjs +2 -1
- package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +46 -0
- package/dist/clients/attach/__tests__/transport-relay.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/transport-relay.test.js +176 -0
- package/dist/clients/attach/attach-cmd.js +747 -743
- package/dist/clients/attach/canvas-panels.d.ts +4 -4
- package/dist/clients/attach/canvas-panels.js +108 -37
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/context-message.d.ts +3 -1
- package/dist/clients/attach/context-message.js +30 -21
- package/dist/clients/attach/graph-overlay.d.ts +8 -1
- package/dist/clients/attach/graph-overlay.js +201 -22
- package/dist/clients/attach/transport-relay.d.ts +51 -0
- package/dist/clients/attach/transport-relay.js +196 -0
- package/dist/clients/attach/transport-socket.d.ts +29 -0
- package/dist/clients/attach/transport-socket.js +164 -0
- package/dist/clients/attach/transport.d.ts +32 -0
- package/dist/clients/attach/transport.js +26 -0
- package/dist/clients/attach/view-socket.d.ts +40 -44
- package/dist/clients/attach/view-socket.js +94 -197
- package/dist/commands/memory/delete.js +3 -3
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +8 -6
- package/dist/commands/memory/shared.js +23 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/node-worktree.js +9 -9
- package/dist/commands/node.js +71 -23
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +17 -2
- package/dist/core/__tests__/broker-snapshot-history.test.js +12 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +79 -0
- package/dist/core/__tests__/context-intro.test.js +11 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +9 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +12 -4
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +17 -3
- package/dist/core/__tests__/kickoff.test.js +21 -0
- package/dist/core/__tests__/spawn-root.test.js +60 -0
- package/dist/core/__tests__/worktree.test.js +24 -1
- package/dist/core/canvas/browse/app.d.ts +2 -0
- package/dist/core/canvas/browse/app.js +83 -48
- package/dist/core/canvas/db.js +3 -2
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/nav-model.d.ts +5 -3
- package/dist/core/canvas/nav-model.js +7 -6
- package/dist/core/canvas/paths.d.ts +8 -0
- package/dist/core/canvas/paths.js +10 -0
- package/dist/core/canvas/render.d.ts +6 -0
- package/dist/core/canvas/render.js +117 -20
- package/dist/core/canvas/source.d.ts +26 -0
- package/dist/core/canvas/source.js +29 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +4 -3
- package/dist/core/config.d.ts +18 -2
- package/dist/core/config.js +88 -10
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/memory-resolver.d.ts +9 -6
- package/dist/core/memory-resolver.js +28 -4
- package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
- package/dist/core/profiles/manifest.d.ts +2 -0
- package/dist/core/profiles/manifest.js +40 -7
- package/dist/core/profiles/select.d.ts +4 -4
- package/dist/core/profiles/select.js +13 -13
- package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
- package/dist/core/runtime/bearings.d.ts +5 -4
- package/dist/core/runtime/bearings.js +12 -6
- package/dist/core/runtime/broker.js +59 -7
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/host.js +43 -3
- package/dist/core/runtime/kickoff.js +6 -0
- package/dist/core/runtime/launch.d.ts +6 -1
- package/dist/core/runtime/launch.js +7 -1
- package/dist/core/runtime/nodes.d.ts +7 -6
- package/dist/core/runtime/nodes.js +3 -3
- package/dist/core/runtime/situational-context.d.ts +12 -0
- package/dist/core/runtime/situational-context.js +48 -0
- package/dist/core/runtime/spawn-env.d.ts +27 -0
- package/dist/core/runtime/spawn-env.js +169 -0
- package/dist/core/runtime/spawn.d.ts +16 -7
- package/dist/core/runtime/spawn.js +8 -2
- package/dist/core/substrate/session-cache.js +7 -1
- package/dist/core/substrate/subject.d.ts +1 -1
- package/dist/core/view/remote-canvas-target.d.ts +3 -0
- package/dist/core/view/remote-canvas-target.js +25 -0
- package/dist/core/worktree.d.ts +4 -4
- package/dist/core/worktree.js +41 -19
- package/dist/daemon/crtrd.js +32 -0
- package/dist/daemon/predicate-eval.js +12 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +238 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +12 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +41 -5
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/web-client/assets/index-BnmSLNLa.css +2 -0
- package/dist/web-client/assets/{index-BQufdMhB.js → index-BvzxXXGU.js} +18 -18
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +5 -3
- package/package.json +2 -2
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-FjfN-jvN.css +0 -2
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
- package/dist/web-client/workbox-98f7a950.js +0 -1
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
* @typedef {Object} Fault
|
|
63
63
|
* @property {'pi→provider'|'viewer↔broker'|'relay↔broker'|'browser↔relay'|'cli→exa'|'daemon→node'} link
|
|
64
64
|
* @property {string} op
|
|
65
|
-
* @property {'rate-limit'|'overloaded'|'connection'|'auth'|'protocol'|'other'|'wedged'} kind
|
|
65
|
+
* @property {'rate-limit'|'overloaded'|'connection'|'auth'|'protocol'|'other'|'wedged'|'model-not-found'} kind
|
|
66
66
|
* @property {FaultRetry} retry
|
|
67
67
|
* @property {string} message The raw engine error (capped).
|
|
68
68
|
* @property {string} since ISO 8601 — when the fault was first observed.
|
|
@@ -189,6 +189,7 @@ export function hangingLabel(kind) {
|
|
|
189
189
|
if (kind === 'auth') return 'auth';
|
|
190
190
|
if (kind === 'protocol') return 'protocol';
|
|
191
191
|
if (kind === 'wedged') return 'wedged';
|
|
192
|
+
if (kind === 'model-not-found') return 'model-not-found';
|
|
192
193
|
return 'other';
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Regression coverage for the attach viewer's collapsed hidden-context
|
|
2
|
+
// rendering (context-message.ts + its dispatch in chat-view.ts).
|
|
3
|
+
//
|
|
4
|
+
// Both hidden-context custom types — `crtr-context` (fresh-birth bearings) and
|
|
5
|
+
// `crtr-situational-context` (an existing node's ambient update, sent by
|
|
6
|
+
// canvas-inbox-watcher.ts) — must render through the same collapsed
|
|
7
|
+
// ContextMessageComponent, never the generic always-expanded
|
|
8
|
+
// CustomMessageComponent: body text stays hidden until the user expands it
|
|
9
|
+
// (Ctrl+O). These tests pin (a) the collapsed component itself never leaks
|
|
10
|
+
// body text before expansion, for both labels it serves, and (b)
|
|
11
|
+
// chat-view.ts's source wires the situational customType to that same
|
|
12
|
+
// collapsed component (a full ChatView needs a live TUI/Container from
|
|
13
|
+
// pi-tui, so this source-contract check is the smallest meaningful assertion
|
|
14
|
+
// of the dispatch).
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import test from 'node:test';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { ContextMessageComponent } from '../context-message.js';
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const CHAT_VIEW = readFileSync(join(HERE, '..', 'chat-view.ts'), 'utf8');
|
|
23
|
+
const identity = (s) => s;
|
|
24
|
+
test('collapsed by default: body text is not rendered until expanded', () => {
|
|
25
|
+
const body = '<situational-context>\nthe user is mid-checkout\n</situational-context>';
|
|
26
|
+
const collapsed = new ContextMessageComponent(body, false, identity, identity, 'situational context', ' hint');
|
|
27
|
+
const collapsedLines = collapsed.render(80).join('\n');
|
|
28
|
+
assert.match(collapsedLines, /\[situational context\]/);
|
|
29
|
+
assert.doesNotMatch(collapsedLines, /mid-checkout/, 'collapsed state must never leak the body');
|
|
30
|
+
collapsed.setExpanded(true);
|
|
31
|
+
const expandedLines = collapsed.render(80).join('\n');
|
|
32
|
+
assert.match(expandedLines, /mid-checkout/, 'expanded state (Ctrl+O) still shows the full body — the LLM-visible content is never lost, only the default view folds it');
|
|
33
|
+
});
|
|
34
|
+
test('the crtr-context label/hint keep their defaults when callers omit the new params', () => {
|
|
35
|
+
const bearings = new ContextMessageComponent('some bearings body', false, identity, identity);
|
|
36
|
+
const line = bearings.render(80).join('\n');
|
|
37
|
+
assert.match(line, /\[crtr context\]/);
|
|
38
|
+
assert.match(line, /orienting bearings/);
|
|
39
|
+
});
|
|
40
|
+
test('chat-view.ts dispatches the situational customType to the collapsed component, not the generic fallback', () => {
|
|
41
|
+
assert.match(CHAT_VIEW, /SITUATIONAL_CONTEXT_CUSTOM_TYPE/, 'must reference the constant, not a duplicated literal');
|
|
42
|
+
const situationalBranch = CHAT_VIEW.slice(CHAT_VIEW.indexOf('customType === SITUATIONAL_CONTEXT_CUSTOM_TYPE'));
|
|
43
|
+
const nextBraceClose = situationalBranch.indexOf('\n }');
|
|
44
|
+
const branchBody = situationalBranch.slice(0, nextBraceClose === -1 ? undefined : nextBraceClose);
|
|
45
|
+
assert.match(branchBody, /new ContextMessageComponent\(/, 'must render collapsed via the same component crtr-context uses, not CustomMessageComponent');
|
|
46
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// transport-relay.test.ts — mock-relay coverage for RelayTransport
|
|
2
|
+
// (`../transport-relay.ts`), the REMOTE half of the attach Transport seam.
|
|
3
|
+
// Fast tier: a plain `ws` WebSocketServer standing in for model-auth-front's
|
|
4
|
+
// relay, no pi boot, no broker. Exercises exactly what the handoff doc
|
|
5
|
+
// flagged as unverified: the Authorization header, the one-JSON-frame-per-
|
|
6
|
+
// WS-message wire, the 1011/1008 "gone" close-code classification (and its
|
|
7
|
+
// NOT-gone counterparts), and a non-101 handshake response (bad token) not
|
|
8
|
+
// hanging connect().
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import { WebSocketServer } from 'ws';
|
|
12
|
+
import { RelayTransport } from '../transport-relay.js';
|
|
13
|
+
import { BrokerUnavailableError } from '../transport.js';
|
|
14
|
+
/** Stand up a bare `ws` server on an ephemeral loopback port — the "relay" a
|
|
15
|
+
* RelayTransport dials. Records the Authorization header + accepted sockets
|
|
16
|
+
* of every connection so a test can assert on the wire, not just behavior. */
|
|
17
|
+
function startMockRelay() {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const wss = new WebSocketServer({ port: 0 });
|
|
20
|
+
const authHeaders = [];
|
|
21
|
+
const sockets = [];
|
|
22
|
+
wss.on('connection', (ws, req) => {
|
|
23
|
+
authHeaders.push(req.headers.authorization ?? '');
|
|
24
|
+
sockets.push(ws);
|
|
25
|
+
});
|
|
26
|
+
wss.once('listening', () => {
|
|
27
|
+
const addr = wss.address();
|
|
28
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
29
|
+
resolve({
|
|
30
|
+
target: { previewEndpoint: `http://127.0.0.1:${port}`, relayToken: 'test-token' },
|
|
31
|
+
authHeaders,
|
|
32
|
+
sockets,
|
|
33
|
+
close: () => new Promise((res) => wss.close(() => res())),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Race connect/error the SAME way `attach-cmd.ts`'s runAttach does, cleaning
|
|
39
|
+
* up whichever listener didn't fire so later `once('error', ...)` assertions
|
|
40
|
+
* in the same test see only the events they're meant to. */
|
|
41
|
+
function connectTransport(transport) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const onConnect = () => {
|
|
44
|
+
transport.off('error', onError);
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
const onError = (err) => {
|
|
48
|
+
transport.off('connect', onConnect);
|
|
49
|
+
reject(err);
|
|
50
|
+
};
|
|
51
|
+
transport.once('connect', onConnect);
|
|
52
|
+
transport.once('error', onError);
|
|
53
|
+
transport.connect();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
test('RelayTransport sends Authorization: Bearer <token> on connect', async () => {
|
|
57
|
+
const relay = await startMockRelay();
|
|
58
|
+
const transport = new RelayTransport('node-auth', relay.target);
|
|
59
|
+
await connectTransport(transport);
|
|
60
|
+
assert.equal(relay.authHeaders[0], 'Bearer test-token');
|
|
61
|
+
transport.destroy();
|
|
62
|
+
await relay.close();
|
|
63
|
+
});
|
|
64
|
+
test('RelayTransport round-trips one JSON frame per WS message, both directions', async () => {
|
|
65
|
+
const relay = await startMockRelay();
|
|
66
|
+
const transport = new RelayTransport('node-echo', relay.target);
|
|
67
|
+
await connectTransport(transport);
|
|
68
|
+
// server → client: one broker-protocol frame, one WS message.
|
|
69
|
+
const ack = { type: 'ack', for: 'hello', ok: true, detail: 'welcome' };
|
|
70
|
+
const framePromise = new Promise((resolve) => transport.once('frame', resolve));
|
|
71
|
+
relay.sockets[0].send(JSON.stringify(ack));
|
|
72
|
+
assert.deepEqual(await framePromise, ack);
|
|
73
|
+
// client → server: send() produces exactly one WS text message per frame.
|
|
74
|
+
const hello = { type: 'hello', role: 'controller', client_id: 'c1' };
|
|
75
|
+
const serverMessages = [];
|
|
76
|
+
relay.sockets[0].on('message', (data) => serverMessages.push(JSON.parse(data.toString())));
|
|
77
|
+
transport.send(hello);
|
|
78
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
79
|
+
assert.deepEqual(serverMessages, [hello]);
|
|
80
|
+
transport.destroy();
|
|
81
|
+
await relay.close();
|
|
82
|
+
});
|
|
83
|
+
test('a 1011 close with "no running broker for <id>" surfaces BrokerUnavailableError', async () => {
|
|
84
|
+
const relay = await startMockRelay();
|
|
85
|
+
const transport = new RelayTransport('node-gone', relay.target);
|
|
86
|
+
await connectTransport(transport);
|
|
87
|
+
const errorPromise = new Promise((resolve) => transport.once('error', resolve));
|
|
88
|
+
relay.sockets[0].close(1011, 'no running broker for node-gone');
|
|
89
|
+
const err = await errorPromise;
|
|
90
|
+
assert.ok(err instanceof BrokerUnavailableError, `expected BrokerUnavailableError, got ${err}`);
|
|
91
|
+
await relay.close();
|
|
92
|
+
});
|
|
93
|
+
test('a 1000 normal close does NOT surface BrokerUnavailableError', async () => {
|
|
94
|
+
const relay = await startMockRelay();
|
|
95
|
+
const transport = new RelayTransport('node-normal', relay.target);
|
|
96
|
+
await connectTransport(transport);
|
|
97
|
+
let sawError;
|
|
98
|
+
transport.on('error', (err) => {
|
|
99
|
+
sawError = err;
|
|
100
|
+
});
|
|
101
|
+
const closePromise = new Promise((resolve) => transport.once('close', resolve));
|
|
102
|
+
relay.sockets[0].close(1000, 'normal');
|
|
103
|
+
await closePromise;
|
|
104
|
+
assert.equal(sawError, undefined);
|
|
105
|
+
await relay.close();
|
|
106
|
+
});
|
|
107
|
+
test('an immediate post-upgrade 1011 "no node <id>" close is still caught by a listener installed BEFORE connect() is dialed', async () => {
|
|
108
|
+
// Mirrors server.ts's bridgeConnection exactly: the relay ACCEPTS the WS
|
|
109
|
+
// upgrade unconditionally, then discovers synchronously (no data exchange,
|
|
110
|
+
// no delay from app logic) that the node doesn't exist and closes 1011 —
|
|
111
|
+
// in the SAME tick the 'connection' handler runs. This is the ordering that
|
|
112
|
+
// broke `attach-cmd.ts`'s original startup race: the transport's 'connect'
|
|
113
|
+
// (WS 'open') is structurally guaranteed to fire before any close frame can
|
|
114
|
+
// be parsed (a close can't precede the handshake that establishes the
|
|
115
|
+
// connection), so a TEMPORARY connect-race listener torn down as soon as
|
|
116
|
+
// 'connect' resolves always misses this close. The fix installs a listener
|
|
117
|
+
// that is never removed instead — reproduced here.
|
|
118
|
+
const wss = new WebSocketServer({ port: 0 });
|
|
119
|
+
wss.on('connection', (ws) => {
|
|
120
|
+
ws.close(1011, 'no node node-gone-fast');
|
|
121
|
+
});
|
|
122
|
+
await new Promise((resolve) => wss.once('listening', resolve));
|
|
123
|
+
const addr = wss.address();
|
|
124
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
125
|
+
const transport = new RelayTransport('node-gone-fast', {
|
|
126
|
+
previewEndpoint: `http://127.0.0.1:${port}`,
|
|
127
|
+
relayToken: 'test-token',
|
|
128
|
+
});
|
|
129
|
+
// Reproduce attach-cmd.ts's durable-listener fix: installed BEFORE
|
|
130
|
+
// connect() is dialed, and never removed — exactly the guard that survives
|
|
131
|
+
// the gap a temporary connect-race listener does not.
|
|
132
|
+
let terminalError;
|
|
133
|
+
transport.on('error', (err) => {
|
|
134
|
+
if (terminalError === undefined)
|
|
135
|
+
terminalError = err;
|
|
136
|
+
});
|
|
137
|
+
await new Promise((resolve) => {
|
|
138
|
+
transport.once('connect', resolve);
|
|
139
|
+
transport.connect();
|
|
140
|
+
});
|
|
141
|
+
// Proves the structural ordering the fix accounts for: 'connect' has ALREADY
|
|
142
|
+
// resolved here, before the close was ever observed — a naive connect-race
|
|
143
|
+
// would have already declared success and torn down its error listener.
|
|
144
|
+
// Compare a boolean rather than `terminalError` itself — `assert.equal`'s
|
|
145
|
+
// TS signature asserts the narrowed type on its first argument, which would
|
|
146
|
+
// otherwise wrongly pin `terminalError` to `undefined` for the rest of this
|
|
147
|
+
// test even though the listener above can still reassign it.
|
|
148
|
+
assert.equal(terminalError === undefined, true, "connect resolves before the close is observed — that's the bug");
|
|
149
|
+
// The close lands shortly after — caught by the durable listener, not a
|
|
150
|
+
// fresh temporary one (there is none left by now).
|
|
151
|
+
await new Promise((resolve) => transport.once('close', resolve));
|
|
152
|
+
assert.ok(terminalError !== undefined && terminalError instanceof BrokerUnavailableError, `expected the durable listener to have recorded a BrokerUnavailableError, got ${terminalError}`);
|
|
153
|
+
transport.destroy();
|
|
154
|
+
await new Promise((res) => wss.close(() => res()));
|
|
155
|
+
});
|
|
156
|
+
test('a non-101 handshake response (401, bad token) rejects connect() with a clear error, not a hang', async () => {
|
|
157
|
+
const wss = new WebSocketServer({
|
|
158
|
+
port: 0,
|
|
159
|
+
verifyClient: (_info, cb) => cb(false, 401, 'unauthorized'),
|
|
160
|
+
});
|
|
161
|
+
await new Promise((resolve) => wss.once('listening', resolve));
|
|
162
|
+
const addr = wss.address();
|
|
163
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
164
|
+
const transport = new RelayTransport('node-401', {
|
|
165
|
+
previewEndpoint: `http://127.0.0.1:${port}`,
|
|
166
|
+
relayToken: 'bad-token',
|
|
167
|
+
});
|
|
168
|
+
const outcome = await Promise.race([
|
|
169
|
+
connectTransport(transport).then(() => 'connected', (err) => err),
|
|
170
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timed out — connect() hung on a non-101 handshake response')), 3000)),
|
|
171
|
+
]);
|
|
172
|
+
assert.notEqual(outcome, 'connected');
|
|
173
|
+
assert.ok(outcome instanceof Error);
|
|
174
|
+
assert.match(outcome.message, /relay transport error/);
|
|
175
|
+
await new Promise((res) => wss.close(() => res()));
|
|
176
|
+
});
|