@linzumi/cli 1.0.118 → 1.0.119
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/README.md +1 -1
- package/dist/index.js +336 -336
- package/package.json +1 -1
- package/scripts/build-editor-parity-oracle.mjs +1523 -0
- package/scripts/build-forwarding-parity-oracle.mjs +1390 -0
- package/scripts/build-mcp-parity-oracle.mjs +357 -0
- package/scripts/build-mcp-selector-parity-oracle.mjs +65 -0
- package/scripts/build-vm-sandbox-parity-oracle.mjs +1266 -0
- package/scripts/build.mjs +41 -0
|
@@ -0,0 +1,1390 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 14, forwarding).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the forwarding area of
|
|
4
|
+
// the ReScript commander port. The oracle bundles the REAL TS forwarding
|
|
5
|
+
// modules - forwardTunnelProtocol.ts (the binary frame codec),
|
|
6
|
+
// portForwardApproval.ts (the pure approval policy), portForwardWatcher.ts
|
|
7
|
+
// (the diff core + the real watcher), portScanScheduler.ts (parsers + the
|
|
8
|
+
// scheduling/backoff decision core), localForwarding.ts (the legacy channel
|
|
9
|
+
// plane + the raw TCP bridge), and forwardTunnel.ts (the streaming tunnel
|
|
10
|
+
// state machine) - behind the standard one-shot BATCH driver (a JSON array
|
|
11
|
+
// of cases on stdin, a JSON array of results on stdout).
|
|
12
|
+
//
|
|
13
|
+
// PROCESS MODE `tunnel-serve`: the oracle becomes the scripted server-side
|
|
14
|
+
// tunnel endpoint (a real `ws` WebSocketServer) plus real loopback target
|
|
15
|
+
// servers (node http + a ws echo). Each incoming tunnel connection replays
|
|
16
|
+
// the same scripted frame choreography and prints ONE normalized transcript
|
|
17
|
+
// line - so the ReScript connectForwardTunnel (dialing from the conformance
|
|
18
|
+
// process) and the REAL TS connectForwardTunnel (dialed in-process on the
|
|
19
|
+
// "run-ts" stdin command) are recorded by the SAME scripted server and must
|
|
20
|
+
// produce byte-equal transcripts. Volatile material (Date headers, the
|
|
21
|
+
// generation uuid, chunk boundaries) normalizes; body BYTES stay exact
|
|
22
|
+
// (coalesced per stream).
|
|
23
|
+
//
|
|
24
|
+
// SECRETS: synthetic case payloads only; .differential/ is gitignored.
|
|
25
|
+
import { mkdir } from 'node:fs/promises';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { build } from 'esbuild';
|
|
29
|
+
|
|
30
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
31
|
+
|
|
32
|
+
const driverSource = `
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import { createServer } from 'node:http';
|
|
35
|
+
import { gzipSync } from 'node:zlib';
|
|
36
|
+
import { WebSocketServer, WebSocket as WsWebSocket } from 'ws';
|
|
37
|
+
import {
|
|
38
|
+
decodeForwardTunnelBytes,
|
|
39
|
+
encodeForwardTunnelFrame,
|
|
40
|
+
forwardTunnelChunkBytes,
|
|
41
|
+
} from './src/forwardTunnelProtocol';
|
|
42
|
+
import {
|
|
43
|
+
approvedTargetFromRequest,
|
|
44
|
+
forwardPreviewPath,
|
|
45
|
+
pendingRequestFromCandidate,
|
|
46
|
+
portForwardPromptLabel,
|
|
47
|
+
portForwardPromptReason,
|
|
48
|
+
reviewPortForwardCandidate,
|
|
49
|
+
revocationCapabilities,
|
|
50
|
+
} from './src/portForwardApproval';
|
|
51
|
+
import {
|
|
52
|
+
changedForwardCandidates,
|
|
53
|
+
commandLabel,
|
|
54
|
+
debouncedForwardCandidateChanges,
|
|
55
|
+
descendantPidSet,
|
|
56
|
+
detectedForwardCandidates,
|
|
57
|
+
sameForwardCandidate,
|
|
58
|
+
stableForwardCandidates,
|
|
59
|
+
startPortForwardWatcher,
|
|
60
|
+
} from './src/portForwardWatcher';
|
|
61
|
+
import {
|
|
62
|
+
createPortScanScheduler,
|
|
63
|
+
parseLsofCwdRows,
|
|
64
|
+
parseLsofListenRows,
|
|
65
|
+
parseProcessRows,
|
|
66
|
+
} from './src/portScanScheduler';
|
|
67
|
+
import {
|
|
68
|
+
clientAcceptsGzipForTest,
|
|
69
|
+
compressibleResponseForTest,
|
|
70
|
+
createForwardTcpManager,
|
|
71
|
+
decodeBase64BodyForTest,
|
|
72
|
+
forwardRequestHeadersForTest,
|
|
73
|
+
forwardResponseHeadersForTest,
|
|
74
|
+
handleForwardHttpRequest,
|
|
75
|
+
localForwardUrlForTest,
|
|
76
|
+
patchForwardBodyForTest,
|
|
77
|
+
shouldGzipResponseForTest,
|
|
78
|
+
} from './src/localForwarding';
|
|
79
|
+
import {
|
|
80
|
+
connectForwardTunnel,
|
|
81
|
+
forwardTunnelUrl,
|
|
82
|
+
localForwardWebSocketUrl,
|
|
83
|
+
openForwardRequestForTest,
|
|
84
|
+
rawResponseHeadersForTest,
|
|
85
|
+
tunnelFetchResponseHeadersForTest,
|
|
86
|
+
tunnelRequestHeadersForTest,
|
|
87
|
+
webSocketProtocolsForTest,
|
|
88
|
+
} from './src/forwardTunnel';
|
|
89
|
+
|
|
90
|
+
function outcome(run) {
|
|
91
|
+
try {
|
|
92
|
+
const value = run();
|
|
93
|
+
return value === undefined ? { defined: false } : { defined: true, value };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
threw: true,
|
|
97
|
+
message: error instanceof Error ? error.message : String(error),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function drainMicrotasks() {
|
|
103
|
+
for (let turn = 0; turn < 12; turn += 1) {
|
|
104
|
+
await Promise.resolve();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sha256Hex(bytes) {
|
|
109
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- byte/frame specs (shared vocabulary with the ReScript conformance) --------------
|
|
113
|
+
|
|
114
|
+
function bytesOfSpec(spec) {
|
|
115
|
+
if (spec.kind === 'utf8') {
|
|
116
|
+
return new TextEncoder().encode(spec.text);
|
|
117
|
+
}
|
|
118
|
+
if (spec.kind === 'hex') {
|
|
119
|
+
return Uint8Array.from(Buffer.from(spec.hex, 'hex'));
|
|
120
|
+
}
|
|
121
|
+
if (spec.kind === 'fill') {
|
|
122
|
+
const bytes = new Uint8Array(spec.length);
|
|
123
|
+
bytes.fill(spec.byte ?? 0);
|
|
124
|
+
return bytes;
|
|
125
|
+
}
|
|
126
|
+
if (spec.kind === 'pattern') {
|
|
127
|
+
// Deterministic non-constant payload: byte i = (seed + i * 31) % 256.
|
|
128
|
+
const bytes = new Uint8Array(spec.length);
|
|
129
|
+
for (let i = 0; i < spec.length; i += 1) {
|
|
130
|
+
bytes[i] = (spec.seed + i * 31) % 256;
|
|
131
|
+
}
|
|
132
|
+
return bytes;
|
|
133
|
+
}
|
|
134
|
+
if (spec.kind === 'headerBody') {
|
|
135
|
+
// Hand-built frame: raw header hex + a generated body (for decode-side
|
|
136
|
+
// refusals the encoder itself refuses to produce).
|
|
137
|
+
const header = Uint8Array.from(Buffer.from(spec.headerHex, 'hex'));
|
|
138
|
+
const body = bytesOfSpec(spec.body);
|
|
139
|
+
const bytes = new Uint8Array(header.byteLength + body.byteLength);
|
|
140
|
+
bytes.set(header, 0);
|
|
141
|
+
bytes.set(body, header.byteLength);
|
|
142
|
+
return bytes;
|
|
143
|
+
}
|
|
144
|
+
throw new Error('unknown bytes spec kind ' + String(spec.kind));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function frameOfSpec(spec) {
|
|
148
|
+
const base = { type: spec.type, streamId: spec.streamId };
|
|
149
|
+
if (spec.type === 'request_body_end' || spec.type === 'response_end') {
|
|
150
|
+
return base;
|
|
151
|
+
}
|
|
152
|
+
if (spec.type === 'request_body_chunk' || spec.type === 'response_body_chunk') {
|
|
153
|
+
return { ...base, payload: bytesOfSpec(spec.payload) };
|
|
154
|
+
}
|
|
155
|
+
if (spec.type === 'websocket_message') {
|
|
156
|
+
return { ...base, opcode: spec.opcode, payload: bytesOfSpec(spec.payload) };
|
|
157
|
+
}
|
|
158
|
+
return { ...base, payload: spec.payload };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function frameVerdict(frame) {
|
|
162
|
+
const base = { type: frame.type, streamId: frame.streamId };
|
|
163
|
+
if (frame.type === 'request_body_end' || frame.type === 'response_end') {
|
|
164
|
+
return base;
|
|
165
|
+
}
|
|
166
|
+
if (frame.type === 'request_body_chunk' || frame.type === 'response_body_chunk') {
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
payloadByteLength: frame.payload.byteLength,
|
|
170
|
+
payloadSha256: sha256Hex(frame.payload),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (frame.type === 'websocket_message') {
|
|
174
|
+
return {
|
|
175
|
+
...base,
|
|
176
|
+
opcode: frame.opcode,
|
|
177
|
+
payloadByteLength: frame.payload.byteLength,
|
|
178
|
+
payloadSha256: sha256Hex(frame.payload),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { ...base, payload: frame.payload };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleEncodeFrame(testCase) {
|
|
185
|
+
try {
|
|
186
|
+
const encoded = encodeForwardTunnelFrame(frameOfSpec(testCase.frame));
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
byteLength: encoded.byteLength,
|
|
190
|
+
sha256: sha256Hex(encoded),
|
|
191
|
+
headHex: Buffer.from(encoded.subarray(0, 64)).toString('hex'),
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function decodeInputOfSpec(spec) {
|
|
199
|
+
if (spec.kind === 'frame') {
|
|
200
|
+
return encodeForwardTunnelFrame(frameOfSpec(spec.frame));
|
|
201
|
+
}
|
|
202
|
+
if (spec.kind === 'mutate') {
|
|
203
|
+
const bytes = Uint8Array.from(encodeForwardTunnelFrame(frameOfSpec(spec.frame)));
|
|
204
|
+
bytes[spec.index] = bytes[spec.index] ^ spec.xor;
|
|
205
|
+
return bytes;
|
|
206
|
+
}
|
|
207
|
+
return bytesOfSpec(spec);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function handleDecodeFrame(testCase) {
|
|
211
|
+
const decoded = decodeForwardTunnelBytes(decodeInputOfSpec(testCase.bytes));
|
|
212
|
+
return decoded.ok
|
|
213
|
+
? { ok: true, frame: frameVerdict(decoded.frame) }
|
|
214
|
+
: { ok: false, error: decoded.error };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- approval -------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
function candidateOfJson(value) {
|
|
220
|
+
return {
|
|
221
|
+
port: value.port,
|
|
222
|
+
pid: value.pid,
|
|
223
|
+
command: value.command,
|
|
224
|
+
...(value.portKind === undefined ? {} : { portKind: value.portKind }),
|
|
225
|
+
...(value.cwd === undefined ? {} : { cwd: value.cwd }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleApprovalReview(testCase) {
|
|
230
|
+
return outcome(() =>
|
|
231
|
+
reviewPortForwardCandidate({
|
|
232
|
+
candidate: candidateOfJson(testCase.candidate),
|
|
233
|
+
threadBound: testCase.threadBound,
|
|
234
|
+
...(testCase.suppressedPorts === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: { suppressedPorts: new Set(testCase.suppressedPorts) }),
|
|
237
|
+
approvedPorts: new Set(testCase.approvedPorts),
|
|
238
|
+
approvedTargets: new Map(
|
|
239
|
+
(testCase.approvedTargets ?? []).map(([port, target]) => [
|
|
240
|
+
port,
|
|
241
|
+
candidateOfJson(target),
|
|
242
|
+
])
|
|
243
|
+
),
|
|
244
|
+
...(testCase.dismissedTargets === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: {
|
|
247
|
+
dismissedTargets: new Map(
|
|
248
|
+
testCase.dismissedTargets.map(([port, target]) => [
|
|
249
|
+
port,
|
|
250
|
+
candidateOfJson(target),
|
|
251
|
+
])
|
|
252
|
+
),
|
|
253
|
+
}),
|
|
254
|
+
pendingRequests: testCase.pendingRequests ?? [],
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function handleApprovalHelpers(testCase) {
|
|
260
|
+
if (testCase.helper === 'pendingFromCandidate') {
|
|
261
|
+
return outcome(() =>
|
|
262
|
+
pendingRequestFromCandidate({
|
|
263
|
+
requestId: testCase.requestId,
|
|
264
|
+
sourceSeq: testCase.sourceSeq,
|
|
265
|
+
candidate: candidateOfJson(testCase.candidate),
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (testCase.helper === 'targetFromRequest') {
|
|
270
|
+
return outcome(() => approvedTargetFromRequest(testCase.request));
|
|
271
|
+
}
|
|
272
|
+
if (testCase.helper === 'promptLabel') {
|
|
273
|
+
return outcome(() => portForwardPromptLabel(candidateOfJson(testCase.candidate)));
|
|
274
|
+
}
|
|
275
|
+
if (testCase.helper === 'promptReason') {
|
|
276
|
+
return outcome(() => portForwardPromptReason(candidateOfJson(testCase.candidate)));
|
|
277
|
+
}
|
|
278
|
+
if (testCase.helper === 'previewPath') {
|
|
279
|
+
return outcome(() => forwardPreviewPath(testCase.runnerId, testCase.port));
|
|
280
|
+
}
|
|
281
|
+
if (testCase.helper === 'revocationCapabilities') {
|
|
282
|
+
return outcome(() => revocationCapabilities(testCase.capabilities, testCase.port));
|
|
283
|
+
}
|
|
284
|
+
return { threw: true, message: 'unknown approval helper' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- watcher pure core ------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
function portMapOfEntries(entries) {
|
|
290
|
+
return new Map(entries.map(([port, value]) => [port, value]));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function portMapJson(map) {
|
|
294
|
+
return Array.from(map.entries());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function handleWatchPure(testCase) {
|
|
298
|
+
if (testCase.fn === 'parseProcessRows') {
|
|
299
|
+
return outcome(() => parseProcessRows(testCase.text));
|
|
300
|
+
}
|
|
301
|
+
if (testCase.fn === 'parseLsofListenRows') {
|
|
302
|
+
return outcome(() => parseLsofListenRows(testCase.text));
|
|
303
|
+
}
|
|
304
|
+
if (testCase.fn === 'parseLsofCwdRows') {
|
|
305
|
+
return outcome(() => Array.from(parseLsofCwdRows(testCase.text).entries()));
|
|
306
|
+
}
|
|
307
|
+
if (testCase.fn === 'descendantPidSet') {
|
|
308
|
+
return outcome(() => Array.from(descendantPidSet(testCase.rows, testCase.rootPid)));
|
|
309
|
+
}
|
|
310
|
+
if (testCase.fn === 'commandLabel') {
|
|
311
|
+
return outcome(() => commandLabel(testCase.command));
|
|
312
|
+
}
|
|
313
|
+
if (testCase.fn === 'sameForwardCandidate') {
|
|
314
|
+
return outcome(() =>
|
|
315
|
+
sameForwardCandidate(candidateOfJson(testCase.left), candidateOfJson(testCase.right))
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (testCase.fn === 'detectedForwardCandidates') {
|
|
319
|
+
return outcome(() =>
|
|
320
|
+
detectedForwardCandidates(
|
|
321
|
+
testCase.sockets,
|
|
322
|
+
new Set(testCase.descendantPids),
|
|
323
|
+
new Map(testCase.processCwds ?? []),
|
|
324
|
+
new Set(testCase.commanderBoundPids ?? []),
|
|
325
|
+
new Set(testCase.commanderBoundPorts ?? []),
|
|
326
|
+
new Set(testCase.watchPorts ?? [])
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (testCase.fn === 'stableSchedule') {
|
|
331
|
+
// Chained scans through stableForwardCandidates.
|
|
332
|
+
let observed = new Map();
|
|
333
|
+
const trace = [];
|
|
334
|
+
for (const scan of testCase.scans) {
|
|
335
|
+
const result = stableForwardCandidates(
|
|
336
|
+
observed,
|
|
337
|
+
scan.candidates.map(candidateOfJson),
|
|
338
|
+
scan.nowMs,
|
|
339
|
+
testCase.debounceMs
|
|
340
|
+
);
|
|
341
|
+
observed = result.nextObservedByPort;
|
|
342
|
+
trace.push({
|
|
343
|
+
stable: result.stableCandidates,
|
|
344
|
+
observed: portMapJson(result.nextObservedByPort),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return { trace };
|
|
348
|
+
}
|
|
349
|
+
if (testCase.fn === 'debouncedSchedule') {
|
|
350
|
+
let observed = portMapOfEntries(testCase.initialObserved ?? []);
|
|
351
|
+
let missing = new Map(
|
|
352
|
+
(testCase.initialMissing ?? []).map(([port, entry]) => [port, entry])
|
|
353
|
+
);
|
|
354
|
+
const trace = [];
|
|
355
|
+
for (const scan of testCase.scans) {
|
|
356
|
+
const result = debouncedForwardCandidateChanges(
|
|
357
|
+
observed,
|
|
358
|
+
missing,
|
|
359
|
+
scan.candidates.map(candidateOfJson),
|
|
360
|
+
scan.nowMs,
|
|
361
|
+
testCase.lostDebounceMs
|
|
362
|
+
);
|
|
363
|
+
observed = result.nextObservedByPort;
|
|
364
|
+
missing = result.nextMissingByPort;
|
|
365
|
+
trace.push({
|
|
366
|
+
changed: result.changedCandidates,
|
|
367
|
+
lost: result.lostCandidates,
|
|
368
|
+
observed: portMapJson(result.nextObservedByPort),
|
|
369
|
+
missing: portMapJson(result.nextMissingByPort),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return { trace };
|
|
373
|
+
}
|
|
374
|
+
if (testCase.fn === 'changedSchedule') {
|
|
375
|
+
let observed = new Map();
|
|
376
|
+
const trace = [];
|
|
377
|
+
for (const scan of testCase.scans) {
|
|
378
|
+
const result = changedForwardCandidates(observed, scan.candidates.map(candidateOfJson));
|
|
379
|
+
observed = result.nextObservedByPort;
|
|
380
|
+
trace.push({
|
|
381
|
+
changed: result.changedCandidates,
|
|
382
|
+
lost: result.lostCandidates,
|
|
383
|
+
observed: portMapJson(result.nextObservedByPort),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return { trace };
|
|
387
|
+
}
|
|
388
|
+
return { threw: true, message: 'unknown watch fn' };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- http/tunnel pure seams -------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
function headersEntries(headers) {
|
|
394
|
+
return Array.from(headers.entries());
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function handleHttpPure(testCase) {
|
|
398
|
+
if (testCase.fn === 'decodeBase64Body') {
|
|
399
|
+
return outcome(() => {
|
|
400
|
+
const decoded = decodeBase64BodyForTest(testCase.value);
|
|
401
|
+
return decoded === undefined
|
|
402
|
+
? { decoded: false }
|
|
403
|
+
: { decoded: true, byteLength: decoded.byteLength, sha256: sha256Hex(decoded) };
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (testCase.fn === 'forwardRequestHeaders') {
|
|
407
|
+
return outcome(() => headersEntries(forwardRequestHeadersForTest(testCase.headers)));
|
|
408
|
+
}
|
|
409
|
+
if (testCase.fn === 'forwardResponseHeaders') {
|
|
410
|
+
return outcome(() => {
|
|
411
|
+
const headers = new Headers();
|
|
412
|
+
for (const [name, value] of testCase.entries) {
|
|
413
|
+
headers.set(name, value);
|
|
414
|
+
}
|
|
415
|
+
return forwardResponseHeadersForTest(headers);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (testCase.fn === 'patchForwardBody') {
|
|
419
|
+
return outcome(() => {
|
|
420
|
+
const headers = new Headers();
|
|
421
|
+
for (const [name, value] of testCase.entries) {
|
|
422
|
+
headers.set(name, value);
|
|
423
|
+
}
|
|
424
|
+
return patchForwardBodyForTest(
|
|
425
|
+
testCase.path,
|
|
426
|
+
headers,
|
|
427
|
+
Buffer.from(testCase.bodyUtf8, 'utf8')
|
|
428
|
+
).toString('utf8');
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (testCase.fn === 'shouldGzipResponse') {
|
|
432
|
+
return outcome(() => {
|
|
433
|
+
const headers = new Headers();
|
|
434
|
+
for (const [name, value] of testCase.entries) {
|
|
435
|
+
headers.set(name, value);
|
|
436
|
+
}
|
|
437
|
+
return shouldGzipResponseForTest(
|
|
438
|
+
testCase.control,
|
|
439
|
+
headers,
|
|
440
|
+
Buffer.from(bytesOfSpec(testCase.body))
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (testCase.fn === 'clientAcceptsGzip') {
|
|
445
|
+
return outcome(() => clientAcceptsGzipForTest(testCase.headers));
|
|
446
|
+
}
|
|
447
|
+
if (testCase.fn === 'compressibleResponse') {
|
|
448
|
+
return outcome(() => {
|
|
449
|
+
const headers = new Headers();
|
|
450
|
+
for (const [name, value] of testCase.entries) {
|
|
451
|
+
headers.set(name, value);
|
|
452
|
+
}
|
|
453
|
+
return compressibleResponseForTest(testCase.path, headers);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (testCase.fn === 'localForwardUrl') {
|
|
457
|
+
return outcome(() =>
|
|
458
|
+
localForwardUrlForTest(
|
|
459
|
+
testCase.scheme,
|
|
460
|
+
testCase.port,
|
|
461
|
+
testCase.path,
|
|
462
|
+
testCase.queryString ?? undefined
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (testCase.fn === 'forwardTunnelUrl') {
|
|
467
|
+
return outcome(() =>
|
|
468
|
+
forwardTunnelUrl(testCase.baseUrl, testCase.runnerId, testCase.token, testCase.generation)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (testCase.fn === 'localForwardWebSocketUrl') {
|
|
472
|
+
return outcome(() =>
|
|
473
|
+
localForwardWebSocketUrl(
|
|
474
|
+
testCase.scheme,
|
|
475
|
+
testCase.port,
|
|
476
|
+
testCase.path,
|
|
477
|
+
testCase.queryString ?? undefined
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (testCase.fn === 'openForwardRequest') {
|
|
482
|
+
return outcome(() => {
|
|
483
|
+
const request = openForwardRequestForTest(testCase.payload);
|
|
484
|
+
return request === undefined
|
|
485
|
+
? { parsed: false }
|
|
486
|
+
: {
|
|
487
|
+
parsed: true,
|
|
488
|
+
port: request.port,
|
|
489
|
+
method: request.method,
|
|
490
|
+
path: request.path,
|
|
491
|
+
queryString: request.queryString ?? null,
|
|
492
|
+
headerEntries: headersEntries(request.headers),
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (testCase.fn === 'tunnelRequestHeaders') {
|
|
497
|
+
return outcome(() => headersEntries(tunnelRequestHeadersForTest(testCase.headers)));
|
|
498
|
+
}
|
|
499
|
+
if (testCase.fn === 'tunnelFetchResponseHeaders') {
|
|
500
|
+
return outcome(() => {
|
|
501
|
+
const headers = new Headers();
|
|
502
|
+
for (const [name, value] of testCase.entries) {
|
|
503
|
+
headers.set(name, value);
|
|
504
|
+
}
|
|
505
|
+
return tunnelFetchResponseHeadersForTest(headers);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (testCase.fn === 'rawResponseHeaders') {
|
|
509
|
+
return outcome(() => rawResponseHeadersForTest(testCase.headers));
|
|
510
|
+
}
|
|
511
|
+
if (testCase.fn === 'webSocketProtocols') {
|
|
512
|
+
return outcome(() => {
|
|
513
|
+
const headers = new Headers();
|
|
514
|
+
for (const [name, value] of testCase.entries) {
|
|
515
|
+
headers.set(name, value);
|
|
516
|
+
}
|
|
517
|
+
return webSocketProtocolsForTest(headers) ?? null;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return { threw: true, message: 'unknown http fn' };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// --- TCP manager flow ---------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
async function handleTcpFlow(testCase) {
|
|
526
|
+
const pushed = [];
|
|
527
|
+
const sockets = [];
|
|
528
|
+
const manager = createForwardTcpManager(
|
|
529
|
+
{
|
|
530
|
+
push: (topic, event, payload) => {
|
|
531
|
+
pushed.push({ topic, event, payload });
|
|
532
|
+
if (testCase.pushRejects === true) {
|
|
533
|
+
return Promise.reject(new Error('scripted push failure'));
|
|
534
|
+
}
|
|
535
|
+
return Promise.resolve({ ok: true });
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
testCase.topic,
|
|
539
|
+
() => testCase.allowedPorts,
|
|
540
|
+
(port) => {
|
|
541
|
+
const listeners = new Map();
|
|
542
|
+
const record = {
|
|
543
|
+
port,
|
|
544
|
+
writes: [],
|
|
545
|
+
ended: false,
|
|
546
|
+
destroyed: false,
|
|
547
|
+
writable: true,
|
|
548
|
+
emit: (event, ...args) => {
|
|
549
|
+
for (const listener of listeners.get(event) ?? []) {
|
|
550
|
+
listener(...args);
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
const socket = {
|
|
555
|
+
get destroyed() {
|
|
556
|
+
return record.destroyed;
|
|
557
|
+
},
|
|
558
|
+
get writable() {
|
|
559
|
+
return record.writable;
|
|
560
|
+
},
|
|
561
|
+
on: (event, listener) => {
|
|
562
|
+
listeners.set(event, [...(listeners.get(event) ?? []), listener]);
|
|
563
|
+
return socket;
|
|
564
|
+
},
|
|
565
|
+
write: (data) => {
|
|
566
|
+
record.writes.push(Buffer.from(data).toString('base64'));
|
|
567
|
+
return true;
|
|
568
|
+
},
|
|
569
|
+
end: () => {
|
|
570
|
+
record.ended = true;
|
|
571
|
+
record.writable = false;
|
|
572
|
+
},
|
|
573
|
+
destroy: () => {
|
|
574
|
+
record.destroyed = true;
|
|
575
|
+
record.writable = false;
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
record.socket = socket;
|
|
579
|
+
sockets.push(record);
|
|
580
|
+
return socket;
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
for (const step of testCase.script ?? []) {
|
|
584
|
+
if (step.step === 'open') {
|
|
585
|
+
manager.handle({ type: 'forward_tcp_open', socketId: step.socketId, port: step.port });
|
|
586
|
+
} else if (step.step === 'send') {
|
|
587
|
+
manager.handle({
|
|
588
|
+
type: 'forward_tcp_send',
|
|
589
|
+
socketId: step.socketId,
|
|
590
|
+
bodyBase64: step.bodyBase64,
|
|
591
|
+
});
|
|
592
|
+
} else if (step.step === 'close') {
|
|
593
|
+
manager.handle({ type: 'forward_tcp_close', socketId: step.socketId });
|
|
594
|
+
} else if (step.step === 'closeAll') {
|
|
595
|
+
manager.close();
|
|
596
|
+
} else if (step.step === 'socketEvent') {
|
|
597
|
+
const record = sockets[step.index];
|
|
598
|
+
if (step.event === 'data') {
|
|
599
|
+
record?.emit('data', Buffer.from(step.bodyBase64, 'base64'));
|
|
600
|
+
} else {
|
|
601
|
+
record?.emit(step.event);
|
|
602
|
+
}
|
|
603
|
+
} else if (step.step === 'setWritable') {
|
|
604
|
+
const record = sockets[step.index];
|
|
605
|
+
if (record) {
|
|
606
|
+
record.writable = step.value;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
await drainMicrotasks();
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
pushed,
|
|
613
|
+
sockets: sockets.map((record) => ({
|
|
614
|
+
port: record.port,
|
|
615
|
+
writes: record.writes,
|
|
616
|
+
ended: record.ended,
|
|
617
|
+
destroyed: record.destroyed,
|
|
618
|
+
})),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// --- legacy HTTP forward (real handler, real loopback server) --------------------------------
|
|
623
|
+
|
|
624
|
+
function bodyOfRouteSpec(spec) {
|
|
625
|
+
if (spec.kind === 'gzip-utf8') {
|
|
626
|
+
return gzipSync(Buffer.from(spec.text, 'utf8'));
|
|
627
|
+
}
|
|
628
|
+
if (spec.kind === 'utf8') {
|
|
629
|
+
return Buffer.from(spec.text, 'utf8');
|
|
630
|
+
}
|
|
631
|
+
if (spec.kind === 'repeat') {
|
|
632
|
+
return Buffer.from(spec.text.repeat(spec.count), 'utf8');
|
|
633
|
+
}
|
|
634
|
+
return Buffer.from(bytesOfSpec(spec));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function startScriptedHttpServer(spec) {
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
const observed = [];
|
|
640
|
+
const server = createServer((request, response) => {
|
|
641
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
642
|
+
observed.push({
|
|
643
|
+
method: request.method,
|
|
644
|
+
path: url.pathname,
|
|
645
|
+
search: url.search,
|
|
646
|
+
host: request.headers.host ?? null,
|
|
647
|
+
});
|
|
648
|
+
const route = (spec.routes ?? []).find((entry) => entry.path === url.pathname);
|
|
649
|
+
if (route === undefined) {
|
|
650
|
+
response.writeHead(404, { 'content-type': 'text/plain' });
|
|
651
|
+
response.end('unscripted route');
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
response.writeHead(route.status ?? 200, Object.fromEntries(route.headers ?? []));
|
|
655
|
+
response.end(bodyOfRouteSpec(route.body ?? { kind: 'utf8', text: '' }));
|
|
656
|
+
});
|
|
657
|
+
server.listen(0, '127.0.0.1', () => {
|
|
658
|
+
resolve({
|
|
659
|
+
port: server.address().port,
|
|
660
|
+
observed,
|
|
661
|
+
stop: () => new Promise((resolveStop) => server.close(() => resolveStop())),
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function substitutePort(value, port) {
|
|
668
|
+
if (value === '$PORT') {
|
|
669
|
+
return port;
|
|
670
|
+
}
|
|
671
|
+
if (Array.isArray(value)) {
|
|
672
|
+
return value.map((entry) => substitutePort(entry, port));
|
|
673
|
+
}
|
|
674
|
+
if (typeof value === 'object' && value !== null) {
|
|
675
|
+
return Object.fromEntries(
|
|
676
|
+
Object.entries(value).map(([key, entry]) => [key, substitutePort(entry, port)])
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return value;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function handleHttpForward(testCase) {
|
|
683
|
+
let server;
|
|
684
|
+
let port = testCase.targetPort;
|
|
685
|
+
if (testCase.server !== undefined) {
|
|
686
|
+
server = await startScriptedHttpServer(testCase.server);
|
|
687
|
+
port = server.port;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const control = substitutePort(testCase.control, port);
|
|
691
|
+
const allowedPorts = substitutePort(testCase.allowedPorts, port);
|
|
692
|
+
const reply = await handleForwardHttpRequest(control, allowedPorts);
|
|
693
|
+
return { reply, observed: server?.observed ?? [] };
|
|
694
|
+
} finally {
|
|
695
|
+
await server?.stop();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// --- watcher flow over a scripted scheduler ------------------------------------------------------
|
|
700
|
+
|
|
701
|
+
async function handleWatcherFlow(testCase) {
|
|
702
|
+
const events = [];
|
|
703
|
+
let currentNow = 0;
|
|
704
|
+
let subscriberRef;
|
|
705
|
+
const scheduler = {
|
|
706
|
+
subscribe: (subscriber) => {
|
|
707
|
+
subscriberRef = subscriber;
|
|
708
|
+
return { unsubscribe: () => events.push({ t: 'unsubscribed' }) };
|
|
709
|
+
},
|
|
710
|
+
subscriberCount: () => (subscriberRef === undefined ? 0 : 1),
|
|
711
|
+
isRunning: () => subscriberRef !== undefined,
|
|
712
|
+
};
|
|
713
|
+
const extraScript = [...(testCase.extraWatchPortsScript ?? [])];
|
|
714
|
+
let watcher;
|
|
715
|
+
try {
|
|
716
|
+
watcher = startPortForwardWatcher({
|
|
717
|
+
rootPid: testCase.rootPid ?? undefined,
|
|
718
|
+
...(testCase.debounceMs === undefined ? {} : { debounceMs: testCase.debounceMs }),
|
|
719
|
+
...(testCase.lostDebounceMs === undefined
|
|
720
|
+
? {}
|
|
721
|
+
: { lostDebounceMs: testCase.lostDebounceMs }),
|
|
722
|
+
...(testCase.intervalMs === undefined ? {} : { intervalMs: testCase.intervalMs }),
|
|
723
|
+
...(testCase.commanderBoundPids === undefined
|
|
724
|
+
? {}
|
|
725
|
+
: { commanderBoundPids: testCase.commanderBoundPids }),
|
|
726
|
+
...(testCase.commanderBoundPorts === undefined
|
|
727
|
+
? {}
|
|
728
|
+
: { commanderBoundPorts: testCase.commanderBoundPorts }),
|
|
729
|
+
...(testCase.watchPorts === undefined ? {} : { watchPorts: testCase.watchPorts }),
|
|
730
|
+
...(testCase.extraWatchPortsScript === undefined
|
|
731
|
+
? {}
|
|
732
|
+
: {
|
|
733
|
+
extraWatchPorts: () => {
|
|
734
|
+
const entry = extraScript.length > 1 ? extraScript.shift() : extraScript[0];
|
|
735
|
+
events.push({ t: 'extraWatchPortsRead' });
|
|
736
|
+
if (entry === 'throw') {
|
|
737
|
+
throw new Error('scripted registry failure');
|
|
738
|
+
}
|
|
739
|
+
return entry ?? [];
|
|
740
|
+
},
|
|
741
|
+
}),
|
|
742
|
+
scheduler,
|
|
743
|
+
nowMs: () => currentNow,
|
|
744
|
+
onCandidate: (candidate) => {
|
|
745
|
+
events.push({ t: 'candidate', candidate });
|
|
746
|
+
},
|
|
747
|
+
onCandidateLost: (candidate) => {
|
|
748
|
+
events.push({ t: 'lost', candidate });
|
|
749
|
+
},
|
|
750
|
+
onListenersObserved: (listeners) => {
|
|
751
|
+
events.push({ t: 'listeners', listeners });
|
|
752
|
+
},
|
|
753
|
+
onError: (error) => {
|
|
754
|
+
events.push({ t: 'error', message: error.message });
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
} catch (error) {
|
|
758
|
+
return { threw: error instanceof Error ? error.message : String(error), events };
|
|
759
|
+
}
|
|
760
|
+
for (const scan of testCase.scans ?? []) {
|
|
761
|
+
currentNow = scan.nowMs;
|
|
762
|
+
if (scan.backoff !== undefined) {
|
|
763
|
+
subscriberRef.onBackoff?.({
|
|
764
|
+
consecutiveFailures: scan.backoff.consecutiveFailures,
|
|
765
|
+
intervalMs: scan.backoff.intervalMs,
|
|
766
|
+
error: new Error(scan.backoff.message),
|
|
767
|
+
});
|
|
768
|
+
await drainMicrotasks();
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (scan.recovered !== undefined) {
|
|
772
|
+
subscriberRef.onRecovered?.({ failedScans: scan.recovered.failedScans });
|
|
773
|
+
await drainMicrotasks();
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// Fresh array identities per scan, exactly like the real scheduler.
|
|
777
|
+
const processRows = (scan.processRows ?? []).map((row) => ({ ...row }));
|
|
778
|
+
const sockets = (scan.sockets ?? []).map((row) => ({ ...row }));
|
|
779
|
+
const pids = subscriberRef.candidatePids(processRows, sockets);
|
|
780
|
+
events.push({ t: 'candidatePids', pids });
|
|
781
|
+
await subscriberRef.deliver({
|
|
782
|
+
processRows,
|
|
783
|
+
listenSockets: sockets,
|
|
784
|
+
processCwds: new Map(scan.cwds ?? []),
|
|
785
|
+
scannedAtMs: scan.nowMs,
|
|
786
|
+
});
|
|
787
|
+
await drainMicrotasks();
|
|
788
|
+
}
|
|
789
|
+
watcher.close();
|
|
790
|
+
return { events };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// --- scheduler flow with gated probes ---------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
async function handleSchedulerFlow(testCase) {
|
|
796
|
+
const events = [];
|
|
797
|
+
const ticks = [...testCase.ticks];
|
|
798
|
+
let nowIndex = 0;
|
|
799
|
+
const nows = testCase.nows ?? [];
|
|
800
|
+
let cwdProbeCalls = 0;
|
|
801
|
+
let scanCalls = 0;
|
|
802
|
+
let currentTick;
|
|
803
|
+
let resolveDone;
|
|
804
|
+
const done = new Promise((resolve) => {
|
|
805
|
+
resolveDone = resolve;
|
|
806
|
+
});
|
|
807
|
+
const scheduler = createPortScanScheduler({
|
|
808
|
+
intervalMs: testCase.intervalMs,
|
|
809
|
+
backoffIntervalsMs: testCase.backoffIntervalsMs,
|
|
810
|
+
...(testCase.scanBudgetMs === undefined ? {} : { scanBudgetMs: testCase.scanBudgetMs }),
|
|
811
|
+
nowMs: () => {
|
|
812
|
+
const value = nows.length === 0 ? 0 : nows[Math.min(nowIndex, nows.length - 1)];
|
|
813
|
+
nowIndex += 1;
|
|
814
|
+
return value;
|
|
815
|
+
},
|
|
816
|
+
probes: {
|
|
817
|
+
scanProcesses: () => {
|
|
818
|
+
const tick = ticks.shift();
|
|
819
|
+
if (tick === undefined) {
|
|
820
|
+
// Script exhausted: the (N+1)th scan is the completion signal;
|
|
821
|
+
// park forever, the driver unsubscribes.
|
|
822
|
+
resolveDone();
|
|
823
|
+
return new Promise(() => undefined);
|
|
824
|
+
}
|
|
825
|
+
events.push({ t: 'scan', index: scanCalls });
|
|
826
|
+
scanCalls += 1;
|
|
827
|
+
if (tick.processes === 'throw') {
|
|
828
|
+
currentTick = undefined;
|
|
829
|
+
return Promise.reject(new Error(tick.errorMessage ?? 'scripted ps failure'));
|
|
830
|
+
}
|
|
831
|
+
if (tick.processes === 'never') {
|
|
832
|
+
currentTick = undefined;
|
|
833
|
+
return new Promise(() => undefined);
|
|
834
|
+
}
|
|
835
|
+
currentTick = tick;
|
|
836
|
+
return Promise.resolve(tick.processes ?? []);
|
|
837
|
+
},
|
|
838
|
+
scanListenSockets: () => Promise.resolve(currentTick?.sockets ?? []),
|
|
839
|
+
scanProcessCwds: (_pids) => {
|
|
840
|
+
cwdProbeCalls += 1;
|
|
841
|
+
return Promise.resolve(new Map(currentTick?.cwds ?? []));
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
const subscription = scheduler.subscribe({
|
|
846
|
+
candidatePids: (_rows, sockets) => sockets.map((socket) => socket.pid),
|
|
847
|
+
deliver: (snapshot) => {
|
|
848
|
+
events.push({
|
|
849
|
+
t: 'deliver',
|
|
850
|
+
pids: snapshot.processRows.map((row) => row.pid),
|
|
851
|
+
sockets: snapshot.listenSockets.map((socket) => socket.port),
|
|
852
|
+
cwds: Array.from(snapshot.processCwds.entries()),
|
|
853
|
+
});
|
|
854
|
+
},
|
|
855
|
+
onError: (error) => {
|
|
856
|
+
events.push({ t: 'error', message: error.message });
|
|
857
|
+
},
|
|
858
|
+
onBackoff: (info) => {
|
|
859
|
+
events.push({
|
|
860
|
+
t: 'backoff',
|
|
861
|
+
consecutiveFailures: info.consecutiveFailures,
|
|
862
|
+
intervalMs: info.intervalMs,
|
|
863
|
+
message: info.error.message,
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
onRecovered: (info) => {
|
|
867
|
+
events.push({ t: 'recovered', failedScans: info.failedScans });
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
const guard = setTimeout(() => resolveDone(), 10_000);
|
|
871
|
+
await done;
|
|
872
|
+
clearTimeout(guard);
|
|
873
|
+
await drainMicrotasks();
|
|
874
|
+
subscription.unsubscribe();
|
|
875
|
+
events.push({
|
|
876
|
+
t: 'final',
|
|
877
|
+
cwdProbeCalls,
|
|
878
|
+
subscriberCount: scheduler.subscriberCount(),
|
|
879
|
+
isRunning: scheduler.isRunning(),
|
|
880
|
+
});
|
|
881
|
+
return { events };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// --- streaming tunnel: scripted-socket flow ---------------------------------------------------------
|
|
885
|
+
|
|
886
|
+
class ScriptedSocket extends EventTarget {
|
|
887
|
+
static CONNECTING = 0;
|
|
888
|
+
static OPEN = 1;
|
|
889
|
+
static CLOSING = 2;
|
|
890
|
+
static CLOSED = 3;
|
|
891
|
+
|
|
892
|
+
constructor(url, protocols) {
|
|
893
|
+
super();
|
|
894
|
+
this.url = String(url);
|
|
895
|
+
this.protocols =
|
|
896
|
+
protocols === undefined ? null : Array.isArray(protocols) ? protocols : [protocols];
|
|
897
|
+
this.readyState = ScriptedSocket.CONNECTING;
|
|
898
|
+
this.sent = [];
|
|
899
|
+
this.binaryType = 'blob';
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
send(message) {
|
|
903
|
+
if (typeof message === 'string') {
|
|
904
|
+
this.sent.push({ kind: 'text', text: message });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const bytes =
|
|
908
|
+
message instanceof ArrayBuffer ? new Uint8Array(message) : Uint8Array.from(message);
|
|
909
|
+
this.sent.push({ kind: 'binary', hex: Buffer.from(bytes).toString('hex') });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
close() {
|
|
913
|
+
if (this.readyState === ScriptedSocket.CLOSED) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
this.readyState = ScriptedSocket.CLOSED;
|
|
917
|
+
this.sent.push({ kind: 'closed_by_client' });
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
scriptOpen() {
|
|
921
|
+
if (this.readyState !== ScriptedSocket.CONNECTING) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
this.readyState = ScriptedSocket.OPEN;
|
|
925
|
+
this.dispatchEvent(new Event('open'));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
scriptFail() {
|
|
929
|
+
this.dispatchEvent(new Event('error'));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
scriptClose(code, reason) {
|
|
933
|
+
this.readyState = ScriptedSocket.CLOSED;
|
|
934
|
+
this.dispatchEvent(Object.assign(new Event('close'), { code, reason }));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
scriptMessage(data) {
|
|
938
|
+
this.dispatchEvent(Object.assign(new Event('message'), { data }));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function handleTunnelFlow(testCase) {
|
|
943
|
+
const originalWebSocket = globalThis.WebSocket;
|
|
944
|
+
const localSockets = [];
|
|
945
|
+
const tunnelSockets = [];
|
|
946
|
+
|
|
947
|
+
class ScriptedLocalWebSocket extends ScriptedSocket {
|
|
948
|
+
constructor(url, protocols) {
|
|
949
|
+
super(url, protocols);
|
|
950
|
+
localSockets.push(this);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
globalThis.WebSocket = ScriptedLocalWebSocket;
|
|
955
|
+
try {
|
|
956
|
+
const tunnel = await (async () => {
|
|
957
|
+
const connectPromise = connectForwardTunnel({
|
|
958
|
+
kandanUrl: testCase.serviceUrl ?? 'ws://scripted.test',
|
|
959
|
+
token: 'synthetic-token',
|
|
960
|
+
runnerId: 'commander-1',
|
|
961
|
+
allowedPorts: () => testCase.allowedPorts ?? [],
|
|
962
|
+
socketFactory: (url) => {
|
|
963
|
+
const socket = new ScriptedSocket(url);
|
|
964
|
+
tunnelSockets.push(socket);
|
|
965
|
+
queueMicrotask(() => socket.scriptOpen());
|
|
966
|
+
return socket;
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
return connectPromise;
|
|
970
|
+
})();
|
|
971
|
+
const trace = [];
|
|
972
|
+
const tunnelSocket = tunnelSockets[0];
|
|
973
|
+
for (const step of testCase.script ?? []) {
|
|
974
|
+
if (step.step === 'deliver') {
|
|
975
|
+
const bytes = encodeForwardTunnelFrame(frameOfSpec(step.frame));
|
|
976
|
+
tunnelSocket.scriptMessage(
|
|
977
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
978
|
+
);
|
|
979
|
+
} else if (step.step === 'deliverRepeat') {
|
|
980
|
+
for (let i = 0; i < step.count; i += 1) {
|
|
981
|
+
const bytes = encodeForwardTunnelFrame(frameOfSpec(step.frame));
|
|
982
|
+
tunnelSocket.scriptMessage(
|
|
983
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
} else if (step.step === 'localOpen') {
|
|
987
|
+
localSockets[step.index]?.scriptOpen();
|
|
988
|
+
} else if (step.step === 'localFail') {
|
|
989
|
+
localSockets[step.index]?.scriptFail();
|
|
990
|
+
} else if (step.step === 'localClose') {
|
|
991
|
+
localSockets[step.index]?.scriptClose(step.code, step.reason);
|
|
992
|
+
} else if (step.step === 'localMessage') {
|
|
993
|
+
const socket = localSockets[step.index];
|
|
994
|
+
if (step.text !== undefined) {
|
|
995
|
+
socket?.scriptMessage(step.text);
|
|
996
|
+
} else {
|
|
997
|
+
const bytes = Buffer.from(step.base64, 'base64');
|
|
998
|
+
socket?.scriptMessage(
|
|
999
|
+
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
} else if (step.step === 'closeTunnel') {
|
|
1003
|
+
tunnel.close();
|
|
1004
|
+
} else if (step.step === 'note') {
|
|
1005
|
+
trace.push({ t: 'note', note: step.note });
|
|
1006
|
+
}
|
|
1007
|
+
await drainMicrotasks();
|
|
1008
|
+
}
|
|
1009
|
+
tunnel.close();
|
|
1010
|
+
await drainMicrotasks();
|
|
1011
|
+
return {
|
|
1012
|
+
trace,
|
|
1013
|
+
tunnelFrames: tunnelSocket.sent.map((entry) =>
|
|
1014
|
+
entry.kind === 'binary'
|
|
1015
|
+
? (() => {
|
|
1016
|
+
const decoded = decodeForwardTunnelBytes(
|
|
1017
|
+
Uint8Array.from(Buffer.from(entry.hex, 'hex'))
|
|
1018
|
+
);
|
|
1019
|
+
return decoded.ok
|
|
1020
|
+
? { frame: frameVerdict(decoded.frame), hexSha256: sha256Hex(entry.hex) }
|
|
1021
|
+
: { undecodable: true, hex: entry.hex };
|
|
1022
|
+
})()
|
|
1023
|
+
: entry
|
|
1024
|
+
),
|
|
1025
|
+
localSockets: localSockets.map((socket) => ({
|
|
1026
|
+
url: socket.url,
|
|
1027
|
+
protocols: socket.protocols,
|
|
1028
|
+
sent: socket.sent,
|
|
1029
|
+
})),
|
|
1030
|
+
};
|
|
1031
|
+
} finally {
|
|
1032
|
+
globalThis.WebSocket = originalWebSocket;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// --- tunnel-serve process mode ------------------------------------------------------------------------
|
|
1037
|
+
|
|
1038
|
+
function normalizeHeaderList(headers) {
|
|
1039
|
+
return headers.map((entry) =>
|
|
1040
|
+
entry.name.toLowerCase() === 'date' ? { name: entry.name, value: '<date>' } : entry
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function normalizeTranscript(frames) {
|
|
1045
|
+
const normalized = [];
|
|
1046
|
+
for (const frame of frames) {
|
|
1047
|
+
if (frame.type === 'response_body_chunk') {
|
|
1048
|
+
const last = normalized[normalized.length - 1];
|
|
1049
|
+
if (last !== undefined && last.type === 'response_body' && last.streamId === frame.streamId) {
|
|
1050
|
+
last.chunksBase64.push(Buffer.from(frame.payload).toString('base64'));
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
normalized.push({
|
|
1054
|
+
type: 'response_body',
|
|
1055
|
+
streamId: frame.streamId,
|
|
1056
|
+
chunksBase64: [Buffer.from(frame.payload).toString('base64')],
|
|
1057
|
+
});
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (frame.type === 'response_headers') {
|
|
1061
|
+
normalized.push({
|
|
1062
|
+
type: frame.type,
|
|
1063
|
+
streamId: frame.streamId,
|
|
1064
|
+
status: frame.payload.status,
|
|
1065
|
+
headers: normalizeHeaderList(frame.payload.headers ?? []),
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (frame.type === 'websocket_message') {
|
|
1070
|
+
normalized.push({
|
|
1071
|
+
type: frame.type,
|
|
1072
|
+
streamId: frame.streamId,
|
|
1073
|
+
opcode: frame.opcode,
|
|
1074
|
+
base64: Buffer.from(frame.payload).toString('base64'),
|
|
1075
|
+
});
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
normalized.push({
|
|
1079
|
+
type: frame.type,
|
|
1080
|
+
streamId: frame.streamId,
|
|
1081
|
+
...(frame.payload === undefined ? {} : { payload: frame.payload }),
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
// Coalesce body chunk lists into one byte-exact base64 (chunk boundaries
|
|
1085
|
+
// are OS timing, not protocol).
|
|
1086
|
+
return normalized.map((entry) =>
|
|
1087
|
+
entry.type === 'response_body'
|
|
1088
|
+
? {
|
|
1089
|
+
type: entry.type,
|
|
1090
|
+
streamId: entry.streamId,
|
|
1091
|
+
bodyBase64: Buffer.concat(
|
|
1092
|
+
entry.chunksBase64.map((chunk) => Buffer.from(chunk, 'base64'))
|
|
1093
|
+
).toString('base64'),
|
|
1094
|
+
}
|
|
1095
|
+
: entry
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function runTunnelServe() {
|
|
1100
|
+
let stdinBuffer = '';
|
|
1101
|
+
const lineWaiters = [];
|
|
1102
|
+
const pendingLines = [];
|
|
1103
|
+
process.stdin.setEncoding('utf8');
|
|
1104
|
+
process.stdin.on('data', (chunk) => {
|
|
1105
|
+
stdinBuffer += chunk;
|
|
1106
|
+
let index = stdinBuffer.indexOf('\\n');
|
|
1107
|
+
while (index >= 0) {
|
|
1108
|
+
const line = stdinBuffer.slice(0, index);
|
|
1109
|
+
stdinBuffer = stdinBuffer.slice(index + 1);
|
|
1110
|
+
const waiter = lineWaiters.shift();
|
|
1111
|
+
if (waiter === undefined) {
|
|
1112
|
+
pendingLines.push(line);
|
|
1113
|
+
} else {
|
|
1114
|
+
waiter(line);
|
|
1115
|
+
}
|
|
1116
|
+
index = stdinBuffer.indexOf('\\n');
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
const nextLine = () =>
|
|
1120
|
+
pendingLines.length > 0
|
|
1121
|
+
? Promise.resolve(pendingLines.shift())
|
|
1122
|
+
: new Promise((resolve) => lineWaiters.push(resolve));
|
|
1123
|
+
|
|
1124
|
+
const spec = JSON.parse(await nextLine());
|
|
1125
|
+
|
|
1126
|
+
// Local scripted HTTP target.
|
|
1127
|
+
const localEventsPerConnection = [];
|
|
1128
|
+
const httpServer = createServer((request, response) => {
|
|
1129
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
1130
|
+
const active = localEventsPerConnection[localEventsPerConnection.length - 1];
|
|
1131
|
+
active?.push({ t: 'http_request', method: request.method, path: url.pathname, search: url.search });
|
|
1132
|
+
const route = (spec.localHttp?.routes ?? []).find((entry) => entry.path === url.pathname);
|
|
1133
|
+
if (route === undefined) {
|
|
1134
|
+
response.writeHead(404, { 'content-type': 'text/plain' });
|
|
1135
|
+
response.end('unscripted route');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (route.mode === 'hold-after-first-chunk') {
|
|
1139
|
+
response.writeHead(route.status ?? 200, Object.fromEntries(route.headers ?? []));
|
|
1140
|
+
response.write(bodyOfRouteSpec(route.body ?? { kind: 'utf8', text: 'open' }));
|
|
1141
|
+
request.on('close', () => {
|
|
1142
|
+
active?.push({ t: 'local_abort' });
|
|
1143
|
+
});
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (route.method !== undefined && route.method !== request.method) {
|
|
1147
|
+
response.writeHead(405, { 'content-type': 'text/plain' });
|
|
1148
|
+
response.end('wrong method');
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (route.echoBody === true) {
|
|
1152
|
+
const chunks = [];
|
|
1153
|
+
request.on('data', (chunk) => chunks.push(chunk));
|
|
1154
|
+
request.on('end', () => {
|
|
1155
|
+
const body = Buffer.concat(chunks);
|
|
1156
|
+
active?.push({ t: 'http_body', base64: body.toString('base64') });
|
|
1157
|
+
response.writeHead(route.status ?? 200, Object.fromEntries(route.headers ?? []));
|
|
1158
|
+
response.end(Buffer.concat([Buffer.from('echo:'), body]));
|
|
1159
|
+
});
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
response.writeHead(route.status ?? 200, Object.fromEntries(route.headers ?? []));
|
|
1163
|
+
response.end(bodyOfRouteSpec(route.body ?? { kind: 'utf8', text: '' }));
|
|
1164
|
+
});
|
|
1165
|
+
await new Promise((resolve) => httpServer.listen(0, '127.0.0.1', resolve));
|
|
1166
|
+
const localHttpPort = httpServer.address().port;
|
|
1167
|
+
|
|
1168
|
+
// Local ws echo target.
|
|
1169
|
+
let wsEchoPort = 0;
|
|
1170
|
+
let wsEchoServer;
|
|
1171
|
+
if (spec.wsEcho === true) {
|
|
1172
|
+
wsEchoServer = new WebSocketServer({
|
|
1173
|
+
host: '127.0.0.1',
|
|
1174
|
+
port: 0,
|
|
1175
|
+
// Accept the first requested subprotocol so subprotocol-carrying
|
|
1176
|
+
// clients (both impls dial with the forwarded header) can open.
|
|
1177
|
+
handleProtocols: (protocols) => protocols.values().next().value ?? false,
|
|
1178
|
+
});
|
|
1179
|
+
wsEchoServer.on('connection', (socket, request) => {
|
|
1180
|
+
const active = localEventsPerConnection[localEventsPerConnection.length - 1];
|
|
1181
|
+
active?.push({
|
|
1182
|
+
t: 'ws_connect',
|
|
1183
|
+
path: request.url,
|
|
1184
|
+
protocol: request.headers['sec-websocket-protocol'] ?? null,
|
|
1185
|
+
});
|
|
1186
|
+
socket.on('message', (data, isBinary) => {
|
|
1187
|
+
if (isBinary) {
|
|
1188
|
+
socket.send(data);
|
|
1189
|
+
} else {
|
|
1190
|
+
socket.send('echo:' + data.toString());
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
await new Promise((resolve) => wsEchoServer.on('listening', resolve));
|
|
1195
|
+
wsEchoPort = wsEchoServer.address().port;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const substitute = (value) => {
|
|
1199
|
+
if (value === '$HTTP_PORT') {
|
|
1200
|
+
return localHttpPort;
|
|
1201
|
+
}
|
|
1202
|
+
if (value === '$WS_PORT') {
|
|
1203
|
+
return wsEchoPort;
|
|
1204
|
+
}
|
|
1205
|
+
if (Array.isArray(value)) {
|
|
1206
|
+
return value.map(substitute);
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof value === 'object' && value !== null) {
|
|
1209
|
+
return Object.fromEntries(
|
|
1210
|
+
Object.entries(value).map(([key, entry]) => [key, substitute(entry)])
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
return value;
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// The scripted server-side tunnel endpoint.
|
|
1217
|
+
const tunnelServer = new WebSocketServer({ host: '127.0.0.1', port: 0 });
|
|
1218
|
+
tunnelServer.on('connection', (socket, request) => {
|
|
1219
|
+
const localEvents = [];
|
|
1220
|
+
localEventsPerConnection.push(localEvents);
|
|
1221
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
1222
|
+
const frames = [];
|
|
1223
|
+
let finished = false;
|
|
1224
|
+
const doneOn = spec.doneOn ?? { frameType: 'response_end', count: 1 };
|
|
1225
|
+
const reactions = (spec.reactions ?? []).map((reaction) => ({ ...reaction, fired: false }));
|
|
1226
|
+
let doneSeen = 0;
|
|
1227
|
+
let doneTimer;
|
|
1228
|
+
const finish = () => {
|
|
1229
|
+
if (finished) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
finished = true;
|
|
1233
|
+
process.stdout.write(
|
|
1234
|
+
JSON.stringify({
|
|
1235
|
+
transcript: {
|
|
1236
|
+
connectPath: url.pathname,
|
|
1237
|
+
token: url.searchParams.get('token'),
|
|
1238
|
+
generation: url.searchParams.get('generation') === null ? null : '<generation>',
|
|
1239
|
+
frames: normalizeTranscript(frames),
|
|
1240
|
+
localEvents,
|
|
1241
|
+
},
|
|
1242
|
+
}) + '\\n'
|
|
1243
|
+
);
|
|
1244
|
+
// The socket deliberately stays OPEN: closing it would trip the
|
|
1245
|
+
// client's 250ms reconnect, replaying the script and interleaving
|
|
1246
|
+
// extra transcript lines. The driver closes its client handle after
|
|
1247
|
+
// reading the transcript.
|
|
1248
|
+
};
|
|
1249
|
+
// A per-connection safety net so a diverging impl still yields a
|
|
1250
|
+
// transcript to compare (with a timeout marker) instead of hanging CI.
|
|
1251
|
+
const safety = setTimeout(() => {
|
|
1252
|
+
frames.push({ type: 'transcript_timeout', streamId: 0 });
|
|
1253
|
+
finish();
|
|
1254
|
+
}, 15_000);
|
|
1255
|
+
safety.unref?.();
|
|
1256
|
+
socket.on('message', (data) => {
|
|
1257
|
+
const bytes = Buffer.isBuffer(data) ? Uint8Array.from(data) : new Uint8Array(data);
|
|
1258
|
+
const decoded = decodeForwardTunnelBytes(bytes);
|
|
1259
|
+
if (!decoded.ok) {
|
|
1260
|
+
frames.push({ type: 'undecodable', streamId: 0 });
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
frames.push(decoded.frame);
|
|
1264
|
+
for (const reaction of reactions) {
|
|
1265
|
+
if (!reaction.fired && decoded.frame.type === reaction.on) {
|
|
1266
|
+
reaction.fired = true;
|
|
1267
|
+
socket.send(encodeForwardTunnelFrame(frameOfSpec(substitute(reaction.frame))));
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (decoded.frame.type === (doneOn.frameType ?? 'response_end')) {
|
|
1271
|
+
doneSeen += 1;
|
|
1272
|
+
if (doneSeen >= (doneOn.count ?? 1)) {
|
|
1273
|
+
// Small settle window: trailing frames on the wire (e.g. the
|
|
1274
|
+
// response_end after a body) land before the transcript prints.
|
|
1275
|
+
clearTimeout(doneTimer);
|
|
1276
|
+
doneTimer = setTimeout(() => {
|
|
1277
|
+
clearTimeout(safety);
|
|
1278
|
+
finish();
|
|
1279
|
+
}, doneOn.settleMs ?? 150);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
for (const step of spec.script ?? []) {
|
|
1284
|
+
socket.send(encodeForwardTunnelFrame(frameOfSpec(substitute(step.frame))));
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
await new Promise((resolve) => tunnelServer.on('listening', resolve));
|
|
1288
|
+
const tunnelPort = tunnelServer.address().port;
|
|
1289
|
+
|
|
1290
|
+
process.stdout.write(
|
|
1291
|
+
JSON.stringify({ tunnelPort, localHttpPort, wsEchoPort }) + '\\n'
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
const openHandles = [];
|
|
1295
|
+
for (;;) {
|
|
1296
|
+
const command = await nextLine();
|
|
1297
|
+
if (command === 'run-ts') {
|
|
1298
|
+
const allowed = [localHttpPort, wsEchoPort]
|
|
1299
|
+
.filter((port) => port > 0)
|
|
1300
|
+
.concat(spec.extraAllowedPorts ?? []);
|
|
1301
|
+
const handle = await connectForwardTunnel({
|
|
1302
|
+
kandanUrl: 'ws://127.0.0.1:' + tunnelPort,
|
|
1303
|
+
token: 'synthetic-token',
|
|
1304
|
+
runnerId: 'commander-1',
|
|
1305
|
+
allowedPorts: () => allowed,
|
|
1306
|
+
});
|
|
1307
|
+
openHandles.push(handle);
|
|
1308
|
+
} else if (command === 'exit') {
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
for (const handle of openHandles) {
|
|
1313
|
+
handle.close();
|
|
1314
|
+
}
|
|
1315
|
+
await new Promise((resolve) => tunnelServer.close(resolve));
|
|
1316
|
+
wsEchoServer?.close();
|
|
1317
|
+
await new Promise((resolve) => httpServer.close(resolve));
|
|
1318
|
+
process.exit(0);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// --- batch dispatch --------------------------------------------------------------------------------------
|
|
1322
|
+
|
|
1323
|
+
async function handle(testCase) {
|
|
1324
|
+
switch (testCase.op) {
|
|
1325
|
+
case 'encodeFrame':
|
|
1326
|
+
return handleEncodeFrame(testCase);
|
|
1327
|
+
case 'decodeFrame':
|
|
1328
|
+
return handleDecodeFrame(testCase);
|
|
1329
|
+
case 'chunkBytesConstant':
|
|
1330
|
+
return { value: forwardTunnelChunkBytes };
|
|
1331
|
+
case 'approvalReview':
|
|
1332
|
+
return handleApprovalReview(testCase);
|
|
1333
|
+
case 'approvalHelpers':
|
|
1334
|
+
return handleApprovalHelpers(testCase);
|
|
1335
|
+
case 'watchPure':
|
|
1336
|
+
return handleWatchPure(testCase);
|
|
1337
|
+
case 'httpPure':
|
|
1338
|
+
return handleHttpPure(testCase);
|
|
1339
|
+
case 'tcpFlow':
|
|
1340
|
+
return handleTcpFlow(testCase);
|
|
1341
|
+
case 'httpForward':
|
|
1342
|
+
return handleHttpForward(testCase);
|
|
1343
|
+
case 'watcherFlow':
|
|
1344
|
+
return handleWatcherFlow(testCase);
|
|
1345
|
+
case 'schedulerFlow':
|
|
1346
|
+
return handleSchedulerFlow(testCase);
|
|
1347
|
+
case 'tunnelFlow':
|
|
1348
|
+
return handleTunnelFlow(testCase);
|
|
1349
|
+
default:
|
|
1350
|
+
return { threw: true, message: 'unknown op ' + String(testCase.op) };
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (process.argv[2] === 'tunnel-serve') {
|
|
1355
|
+
await runTunnelServe();
|
|
1356
|
+
} else {
|
|
1357
|
+
let stdin = '';
|
|
1358
|
+
process.stdin.setEncoding('utf8');
|
|
1359
|
+
for await (const chunk of process.stdin) {
|
|
1360
|
+
stdin += chunk;
|
|
1361
|
+
}
|
|
1362
|
+
const cases = JSON.parse(stdin);
|
|
1363
|
+
const results = [];
|
|
1364
|
+
for (const testCase of cases) {
|
|
1365
|
+
results.push(await handle(testCase));
|
|
1366
|
+
}
|
|
1367
|
+
// No process.exit here: a large result array must fully flush stdout.
|
|
1368
|
+
process.stdout.write(JSON.stringify(results));
|
|
1369
|
+
}
|
|
1370
|
+
`;
|
|
1371
|
+
|
|
1372
|
+
await mkdir(join(packageRoot, '.differential'), { recursive: true });
|
|
1373
|
+
|
|
1374
|
+
await build({
|
|
1375
|
+
stdin: {
|
|
1376
|
+
contents: driverSource,
|
|
1377
|
+
resolveDir: packageRoot,
|
|
1378
|
+
sourcefile: 'forwarding-parity-driver.ts',
|
|
1379
|
+
loader: 'ts',
|
|
1380
|
+
},
|
|
1381
|
+
bundle: true,
|
|
1382
|
+
platform: 'node',
|
|
1383
|
+
target: 'node20',
|
|
1384
|
+
format: 'esm',
|
|
1385
|
+
outfile: join(packageRoot, '.differential/forwarding-parity-oracle.mjs'),
|
|
1386
|
+
minify: false,
|
|
1387
|
+
sourcemap: false,
|
|
1388
|
+
legalComments: 'none',
|
|
1389
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
1390
|
+
});
|