@hraness/direct 0.7.5
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/LICENSE +21 -0
- package/README.md +436 -0
- package/dist/core/index.js +162 -0
- package/dist/index-1csg00w4.js +1167 -0
- package/dist/index-6mdfd2ey.js +464 -0
- package/dist/index-7n1h75n6.js +616 -0
- package/dist/index.js +232 -0
- package/dist/react.js +32 -0
- package/dist/testing/index.js +1069 -0
- package/dist/tooling/bombadil.js +2117 -0
- package/dist/tooling/browser-verification-entry.js +1499 -0
- package/dist/tooling/bundle-boundary.js +119 -0
- package/dist/web.js +605 -0
- package/package.json +179 -0
- package/skills/direct/AGENTS.md +13 -0
- package/skills/direct/SKILL.md +49 -0
- package/skills/direct/agents/openai.yaml +4 -0
- package/skills/direct/references/adoption.md +131 -0
- package/skills/direct/references/install.md +91 -0
- package/skills/direct/references/verification.md +247 -0
- package/src/core/coverage.ts +336 -0
- package/src/core/definition.ts +378 -0
- package/src/core/effects.ts +88 -0
- package/src/core/fixture.ts +185 -0
- package/src/core/ids.ts +77 -0
- package/src/core/index.ts +13 -0
- package/src/core/json-value.ts +7 -0
- package/src/core/json.ts +593 -0
- package/src/core/query.ts +230 -0
- package/src/core/reason.ts +16 -0
- package/src/core/resource.ts +10 -0
- package/src/core/result.ts +19 -0
- package/src/core/runtime.ts +229 -0
- package/src/core/scenario.ts +149 -0
- package/src/core/store.ts +784 -0
- package/src/index.ts +51 -0
- package/src/react.ts +54 -0
- package/src/testing/activity.ts +228 -0
- package/src/testing/coverage-binding.ts +99 -0
- package/src/testing/evidence.ts +59 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/manifest.ts +559 -0
- package/src/testing/probe.ts +446 -0
- package/src/testing/scripted-transport.ts +775 -0
- package/src/testing/session.ts +525 -0
- package/src/tooling/bombadil-campaign.ts +288 -0
- package/src/tooling/bombadil-internal.d.ts +46 -0
- package/src/tooling/bombadil-runner.ts +1424 -0
- package/src/tooling/bombadil.ts +27 -0
- package/src/tooling/browser-verification-entry.ts +32 -0
- package/src/tooling/browser-verification.ts +916 -0
- package/src/tooling/bundle-boundary.ts +159 -0
- package/src/web/browser-bridge.ts +296 -0
- package/src/web/browser.ts +277 -0
- package/src/web/fetch-firewall.ts +251 -0
- package/src/web.ts +27 -0
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDirectStore
|
|
3
|
+
} from "../index-6mdfd2ey.js";
|
|
4
|
+
import {
|
|
5
|
+
DIRECT_CATALOG_HASH_ALGORITHM,
|
|
6
|
+
DIRECT_PROBE_SCHEMA,
|
|
7
|
+
DIRECT_SESSION_MANIFEST_SCHEMA,
|
|
8
|
+
MAX_DIRECT_PROBE_COUNTERS,
|
|
9
|
+
createDirectProbe,
|
|
10
|
+
createDirectSessionManifest,
|
|
11
|
+
parseDirectProbeSnapshot,
|
|
12
|
+
parseDirectSessionManifest
|
|
13
|
+
} from "../index-7n1h75n6.js";
|
|
14
|
+
import {
|
|
15
|
+
canonicalJson,
|
|
16
|
+
createCoverageCatalogSnapshot,
|
|
17
|
+
createLogicalRuntime,
|
|
18
|
+
err,
|
|
19
|
+
isRecord,
|
|
20
|
+
ok,
|
|
21
|
+
parseAndCloneWorld,
|
|
22
|
+
parseCoverageCatalogSnapshot,
|
|
23
|
+
parseJsonValue,
|
|
24
|
+
parseLogicalRuntimeSnapshot,
|
|
25
|
+
parseTaggedStableHash,
|
|
26
|
+
renderUnknownReason
|
|
27
|
+
} from "../index-1csg00w4.js";
|
|
28
|
+
|
|
29
|
+
// src/testing/activity.ts
|
|
30
|
+
function storeErrorMessage(cause) {
|
|
31
|
+
return renderUnknownReason(cause, "Direct store operation failed");
|
|
32
|
+
}
|
|
33
|
+
function operationError(reason) {
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
code: "operation-id-failed",
|
|
36
|
+
message: renderUnknownReason(reason),
|
|
37
|
+
operation: null,
|
|
38
|
+
storeError: null,
|
|
39
|
+
reason
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function closedScopeError() {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
code: "scope-closed",
|
|
45
|
+
message: "The Direct activity scope is closed",
|
|
46
|
+
operation: null,
|
|
47
|
+
storeError: null,
|
|
48
|
+
reason: null
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function storeError(code, operation, cause) {
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
code,
|
|
54
|
+
message: storeErrorMessage(cause),
|
|
55
|
+
operation,
|
|
56
|
+
storeError: cause,
|
|
57
|
+
reason: null
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function createDirectActivityScope(store, runtime, options = {}) {
|
|
61
|
+
const signal = options.signal;
|
|
62
|
+
const isClosed = () => signal?.aborted === true;
|
|
63
|
+
const begin = (namespace = "activity") => {
|
|
64
|
+
if (isClosed())
|
|
65
|
+
return err(closedScopeError());
|
|
66
|
+
let operation;
|
|
67
|
+
try {
|
|
68
|
+
operation = runtime.nextOperationId(namespace);
|
|
69
|
+
} catch (reason) {
|
|
70
|
+
return err(operationError(reason));
|
|
71
|
+
}
|
|
72
|
+
if (isClosed())
|
|
73
|
+
return err(closedScopeError());
|
|
74
|
+
const currentGeneration = store.getSnapshot().generation;
|
|
75
|
+
const started = store.beginActivity(currentGeneration, operation);
|
|
76
|
+
if (!started.ok) {
|
|
77
|
+
return err(storeError("store-begin-failed", operation, started.error));
|
|
78
|
+
}
|
|
79
|
+
if (isClosed()) {
|
|
80
|
+
const settled = started.value.settle();
|
|
81
|
+
return settled.ok ? err(closedScopeError()) : err(storeError("store-settle-failed", operation, settled.error));
|
|
82
|
+
}
|
|
83
|
+
let released = false;
|
|
84
|
+
let releaseResult = null;
|
|
85
|
+
const lease = Object.freeze({
|
|
86
|
+
generation: currentGeneration,
|
|
87
|
+
operation,
|
|
88
|
+
isReleased: () => released,
|
|
89
|
+
release: () => {
|
|
90
|
+
if (releaseResult !== null)
|
|
91
|
+
return releaseResult;
|
|
92
|
+
released = true;
|
|
93
|
+
const settled = started.value.settle();
|
|
94
|
+
releaseResult = settled.ok ? ok(true) : err(storeError("store-settle-failed", operation, settled.error));
|
|
95
|
+
return releaseResult;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return ok(lease);
|
|
99
|
+
};
|
|
100
|
+
const run = async (namespace, work) => {
|
|
101
|
+
const started = begin(namespace);
|
|
102
|
+
if (!started.ok) {
|
|
103
|
+
return err(Object.freeze({
|
|
104
|
+
code: "begin-failed",
|
|
105
|
+
message: started.error.message,
|
|
106
|
+
operation: null,
|
|
107
|
+
workError: null,
|
|
108
|
+
activityError: started.error
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
let workResult;
|
|
112
|
+
try {
|
|
113
|
+
workResult = ok(await work());
|
|
114
|
+
} catch (reason) {
|
|
115
|
+
workResult = err(reason);
|
|
116
|
+
}
|
|
117
|
+
const released = started.value.release();
|
|
118
|
+
if (workResult.ok && released.ok)
|
|
119
|
+
return ok(workResult.value);
|
|
120
|
+
if (!workResult.ok && released.ok) {
|
|
121
|
+
return err(Object.freeze({
|
|
122
|
+
code: "work-failed",
|
|
123
|
+
message: renderUnknownReason(workResult.error),
|
|
124
|
+
operation: started.value.operation,
|
|
125
|
+
workError: workResult.error,
|
|
126
|
+
activityError: null
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
if (workResult.ok && !released.ok) {
|
|
130
|
+
return err(Object.freeze({
|
|
131
|
+
code: "settlement-failed",
|
|
132
|
+
message: released.error.message,
|
|
133
|
+
operation: started.value.operation,
|
|
134
|
+
workError: null,
|
|
135
|
+
activityError: released.error
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
if (!workResult.ok && !released.ok) {
|
|
139
|
+
return err(Object.freeze({
|
|
140
|
+
code: "work-and-settlement-failed",
|
|
141
|
+
message: `${renderUnknownReason(workResult.error)}; settlement failed: ${released.error.message}`,
|
|
142
|
+
operation: started.value.operation,
|
|
143
|
+
workError: workResult.error,
|
|
144
|
+
activityError: released.error
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
throw new Error("Unreachable activity result");
|
|
148
|
+
};
|
|
149
|
+
return Object.freeze({ begin, run });
|
|
150
|
+
}
|
|
151
|
+
// src/testing/coverage-binding.ts
|
|
152
|
+
function sameEntry(actual, expected) {
|
|
153
|
+
if (actual.key !== expected.key || actual.mode !== expected.mode || actual.claim !== expected.claim || actual.scenarios.length !== expected.scenarios.length)
|
|
154
|
+
return false;
|
|
155
|
+
return actual.scenarios.every((scenario, index) => scenario === expected.scenarios[index]);
|
|
156
|
+
}
|
|
157
|
+
function parseExpectedCoverageCatalogSnapshot(input, expected) {
|
|
158
|
+
const parsed = parseCoverageCatalogSnapshot(input);
|
|
159
|
+
if (!parsed.ok) {
|
|
160
|
+
return err(Object.freeze({
|
|
161
|
+
code: "invalid-coverage",
|
|
162
|
+
message: parsed.error.message,
|
|
163
|
+
coverageError: parsed.error
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (parsed.value.schema !== expected.schema || parsed.value.entries.length !== expected.entries.length || parsed.value.entries.some((entry, index) => {
|
|
168
|
+
const expectedEntry = expected.entries[index];
|
|
169
|
+
return expectedEntry === undefined || !sameEntry(entry, expectedEntry);
|
|
170
|
+
})) {
|
|
171
|
+
return err(Object.freeze({
|
|
172
|
+
code: "coverage-mismatch",
|
|
173
|
+
message: "Published Direct coverage does not exactly match the authored definition",
|
|
174
|
+
coverageError: null
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
} catch (reason) {
|
|
178
|
+
return err(Object.freeze({
|
|
179
|
+
code: "invalid-definition",
|
|
180
|
+
message: renderUnknownReason(reason, "Expected Direct coverage could not be inspected"),
|
|
181
|
+
coverageError: null
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
return ok(expected);
|
|
185
|
+
}
|
|
186
|
+
function parseDefinitionCoverageSnapshot(input, definition) {
|
|
187
|
+
let expected;
|
|
188
|
+
try {
|
|
189
|
+
expected = createCoverageCatalogSnapshot(definition.coverage);
|
|
190
|
+
} catch (reason) {
|
|
191
|
+
return err(Object.freeze({
|
|
192
|
+
code: "invalid-definition",
|
|
193
|
+
message: renderUnknownReason(reason, "Direct definition coverage could not be inspected"),
|
|
194
|
+
coverageError: null
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
return parseExpectedCoverageCatalogSnapshot(input, expected);
|
|
198
|
+
}
|
|
199
|
+
// src/testing/evidence.ts
|
|
200
|
+
function classifyCoverageEvidence(entry, facts) {
|
|
201
|
+
const directVerified = facts.directEvidence === "verified";
|
|
202
|
+
if (entry.mode === "direct") {
|
|
203
|
+
return directVerified ? "verified" : "direct-required";
|
|
204
|
+
}
|
|
205
|
+
let exercised = 0;
|
|
206
|
+
for (const scenario of entry.scenarios) {
|
|
207
|
+
if (facts.exercisedScenarios.has(scenario))
|
|
208
|
+
exercised += 1;
|
|
209
|
+
}
|
|
210
|
+
if (exercised === 0) {
|
|
211
|
+
return directVerified && entry.mode === "mixed" ? "partial" : "not-exercised";
|
|
212
|
+
}
|
|
213
|
+
if (exercised < entry.scenarios.length) {
|
|
214
|
+
return "partial";
|
|
215
|
+
}
|
|
216
|
+
if (entry.mode === "fixture") {
|
|
217
|
+
return "verified";
|
|
218
|
+
}
|
|
219
|
+
return directVerified ? "verified" : "fixture-verified";
|
|
220
|
+
}
|
|
221
|
+
// src/testing/session.ts
|
|
222
|
+
function frozenMessages(messages) {
|
|
223
|
+
return Object.freeze([...messages]);
|
|
224
|
+
}
|
|
225
|
+
function isPromiseLike(value) {
|
|
226
|
+
return (typeof value === "object" && value !== null || typeof value === "function") && typeof Reflect.get(value, "then") === "function";
|
|
227
|
+
}
|
|
228
|
+
function freezeCounterSources(sources) {
|
|
229
|
+
return Object.freeze([...sources]);
|
|
230
|
+
}
|
|
231
|
+
function prepareSessionObservation(input) {
|
|
232
|
+
const candidate = input;
|
|
233
|
+
if (!isRecord(candidate)) {
|
|
234
|
+
throw new Error("Direct observation must be an object");
|
|
235
|
+
}
|
|
236
|
+
const pending = input.pending;
|
|
237
|
+
const violations = input.violations;
|
|
238
|
+
const readRemainingWork = input.readRemainingWork;
|
|
239
|
+
if (pending !== undefined && !Array.isArray(pending)) {
|
|
240
|
+
throw new Error("Direct observation pending counters must be an array");
|
|
241
|
+
}
|
|
242
|
+
if (violations !== undefined && !Array.isArray(violations)) {
|
|
243
|
+
throw new Error("Direct observation violation counters must be an array");
|
|
244
|
+
}
|
|
245
|
+
if (readRemainingWork !== undefined && typeof readRemainingWork !== "function") {
|
|
246
|
+
throw new Error("Direct observation remaining-work reader must be a function");
|
|
247
|
+
}
|
|
248
|
+
return Object.freeze({
|
|
249
|
+
...pending === undefined ? {} : { pending: freezeCounterSources(pending) },
|
|
250
|
+
...violations === undefined ? {} : { violations: freezeCounterSources(violations) },
|
|
251
|
+
...readRemainingWork === undefined ? {} : { readRemainingWork }
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function sessionError(error, cleanupErrors = []) {
|
|
255
|
+
const failures = frozenMessages(cleanupErrors);
|
|
256
|
+
switch (error.code) {
|
|
257
|
+
case "invalid-options":
|
|
258
|
+
case "activation-failed":
|
|
259
|
+
case "store-failed":
|
|
260
|
+
case "harness-failed":
|
|
261
|
+
case "observation-failed":
|
|
262
|
+
case "probe-failed":
|
|
263
|
+
return Object.freeze({ ...error, cleanupErrors: failures });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function runCleanup(controller, cleanups) {
|
|
267
|
+
const failures = [];
|
|
268
|
+
try {
|
|
269
|
+
controller.abort();
|
|
270
|
+
} catch (reason) {
|
|
271
|
+
failures.push(`Abort failed: ${renderUnknownReason(reason)}`);
|
|
272
|
+
}
|
|
273
|
+
for (const cleanup of [...cleanups].reverse()) {
|
|
274
|
+
try {
|
|
275
|
+
const returned = cleanup();
|
|
276
|
+
if (returned !== undefined) {
|
|
277
|
+
if (isPromiseLike(returned)) {
|
|
278
|
+
Promise.resolve(returned).catch(() => {
|
|
279
|
+
return;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
failures.push("Direct cleanup must complete synchronously and return undefined");
|
|
283
|
+
}
|
|
284
|
+
} catch (reason) {
|
|
285
|
+
failures.push(renderUnknownReason(reason, "Direct cleanup failed"));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return frozenMessages(failures);
|
|
289
|
+
}
|
|
290
|
+
function activateSession(definition, activation) {
|
|
291
|
+
switch (activation.kind) {
|
|
292
|
+
case "query":
|
|
293
|
+
return definition.activate(activation.source);
|
|
294
|
+
case "scenario":
|
|
295
|
+
return definition.activateScenario(activation.scenario);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function createDirectSession(options) {
|
|
299
|
+
let definition;
|
|
300
|
+
let requestedActivation;
|
|
301
|
+
let createHarness;
|
|
302
|
+
let observeHarness;
|
|
303
|
+
let parseWorld;
|
|
304
|
+
let sleep;
|
|
305
|
+
let storeOptions;
|
|
306
|
+
try {
|
|
307
|
+
definition = options.definition;
|
|
308
|
+
parseWorld = definition.parseWorld;
|
|
309
|
+
const activationInput = options.activation;
|
|
310
|
+
if (activationInput.kind === "query") {
|
|
311
|
+
requestedActivation = Object.freeze({ kind: "query", source: activationInput.source });
|
|
312
|
+
} else if (activationInput.kind === "scenario") {
|
|
313
|
+
requestedActivation = Object.freeze({ kind: "scenario", scenario: activationInput.scenario });
|
|
314
|
+
} else {
|
|
315
|
+
throw new Error("Direct session activation kind must be query or scenario");
|
|
316
|
+
}
|
|
317
|
+
createHarness = options.create;
|
|
318
|
+
observeHarness = options.observe;
|
|
319
|
+
sleep = options.sleep;
|
|
320
|
+
const storeOptionsInput = options.storeOptions;
|
|
321
|
+
if (storeOptionsInput === undefined) {
|
|
322
|
+
storeOptions = undefined;
|
|
323
|
+
} else {
|
|
324
|
+
const onListenerError = storeOptionsInput.onListenerError;
|
|
325
|
+
const validateReplacements = storeOptionsInput.validateReplacements;
|
|
326
|
+
storeOptions = Object.freeze({
|
|
327
|
+
...onListenerError === undefined ? {} : { onListenerError },
|
|
328
|
+
...validateReplacements === undefined ? {} : { validateReplacements }
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
} catch (reason) {
|
|
332
|
+
return err(sessionError({
|
|
333
|
+
code: "invalid-options",
|
|
334
|
+
message: renderUnknownReason(reason, "Direct session options could not be inspected"),
|
|
335
|
+
queryError: null,
|
|
336
|
+
storeError: null,
|
|
337
|
+
probeError: null
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
let activationSource;
|
|
341
|
+
let activationScenario;
|
|
342
|
+
let activationRoute;
|
|
343
|
+
let activationWorld;
|
|
344
|
+
let activationRuntime;
|
|
345
|
+
let activationHash;
|
|
346
|
+
try {
|
|
347
|
+
const activated = activateSession(definition, requestedActivation);
|
|
348
|
+
if (!activated.ok) {
|
|
349
|
+
const queryError = Object.freeze({
|
|
350
|
+
code: activated.error.code,
|
|
351
|
+
message: activated.error.message
|
|
352
|
+
});
|
|
353
|
+
return err(sessionError({
|
|
354
|
+
code: "activation-failed",
|
|
355
|
+
message: queryError.message,
|
|
356
|
+
queryError,
|
|
357
|
+
storeError: null,
|
|
358
|
+
probeError: null
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
const candidate = activated.value;
|
|
362
|
+
if (candidate.kind !== "active")
|
|
363
|
+
throw new Error("Direct activation kind must be active");
|
|
364
|
+
if (candidate.source !== "scenario" && candidate.source !== "fixture") {
|
|
365
|
+
throw new Error("Direct activation source must be scenario or fixture");
|
|
366
|
+
}
|
|
367
|
+
if (typeof candidate.scenario !== "string") {
|
|
368
|
+
throw new Error("Direct activation scenario must be a string");
|
|
369
|
+
}
|
|
370
|
+
if (typeof candidate.route !== "string") {
|
|
371
|
+
throw new Error("Direct activation route must be a string");
|
|
372
|
+
}
|
|
373
|
+
const parsedActivationHash = parseTaggedStableHash(candidate.activationHash);
|
|
374
|
+
if (!parsedActivationHash.ok)
|
|
375
|
+
throw new Error(parsedActivationHash.error.message);
|
|
376
|
+
const parsedRuntime = parseLogicalRuntimeSnapshot(candidate.runtime);
|
|
377
|
+
if (!parsedRuntime.ok)
|
|
378
|
+
throw new Error(parsedRuntime.error.message);
|
|
379
|
+
activationSource = candidate.source;
|
|
380
|
+
activationScenario = candidate.scenario;
|
|
381
|
+
activationRoute = candidate.route;
|
|
382
|
+
activationWorld = candidate.world;
|
|
383
|
+
activationRuntime = parsedRuntime.value;
|
|
384
|
+
activationHash = parsedActivationHash.value;
|
|
385
|
+
} catch (reason) {
|
|
386
|
+
return err(sessionError({
|
|
387
|
+
code: "invalid-options",
|
|
388
|
+
message: renderUnknownReason(reason, "Direct session activation failed unexpectedly"),
|
|
389
|
+
queryError: null,
|
|
390
|
+
storeError: null,
|
|
391
|
+
probeError: null
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
const store = storeOptions === undefined ? createDirectStore(activationWorld, parseWorld) : createDirectStore(activationWorld, parseWorld, storeOptions);
|
|
395
|
+
if (!store.ok) {
|
|
396
|
+
return err(sessionError({
|
|
397
|
+
code: "store-failed",
|
|
398
|
+
message: store.error.message,
|
|
399
|
+
queryError: null,
|
|
400
|
+
storeError: store.error,
|
|
401
|
+
probeError: null
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
const clock = sleep === undefined ? createLogicalRuntime(activationRuntime) : createLogicalRuntime(activationRuntime, sleep);
|
|
405
|
+
const activation = Object.freeze({
|
|
406
|
+
kind: "active",
|
|
407
|
+
source: activationSource,
|
|
408
|
+
scenario: activationScenario,
|
|
409
|
+
route: activationRoute,
|
|
410
|
+
world: store.value.getSnapshot().world,
|
|
411
|
+
runtime: clock.snapshot(),
|
|
412
|
+
activationHash
|
|
413
|
+
});
|
|
414
|
+
const createdManifest = createDirectSessionManifest(definition, activation);
|
|
415
|
+
if (!createdManifest.ok) {
|
|
416
|
+
return err(sessionError({
|
|
417
|
+
code: "invalid-options",
|
|
418
|
+
message: createdManifest.error.message,
|
|
419
|
+
queryError: null,
|
|
420
|
+
storeError: null,
|
|
421
|
+
probeError: null
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
const manifest = createdManifest.value;
|
|
425
|
+
const controller = new AbortController;
|
|
426
|
+
const activity = createDirectActivityScope(store.value, clock, { signal: controller.signal });
|
|
427
|
+
const cleanups = [];
|
|
428
|
+
let registrationOpen = true;
|
|
429
|
+
const context = Object.freeze({
|
|
430
|
+
activation,
|
|
431
|
+
world: activation.world,
|
|
432
|
+
store: store.value,
|
|
433
|
+
clock,
|
|
434
|
+
activity,
|
|
435
|
+
signal: controller.signal,
|
|
436
|
+
onDispose: (cleanup) => {
|
|
437
|
+
if (!registrationOpen) {
|
|
438
|
+
throw new Error("Direct cleanup must be registered during synchronous session construction");
|
|
439
|
+
}
|
|
440
|
+
cleanups.push(cleanup);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
let harness;
|
|
445
|
+
try {
|
|
446
|
+
harness = createHarness(context);
|
|
447
|
+
if (isPromiseLike(harness)) {
|
|
448
|
+
Promise.resolve(harness).catch(() => {
|
|
449
|
+
return;
|
|
450
|
+
});
|
|
451
|
+
throw new Error("Direct harness construction must complete synchronously");
|
|
452
|
+
}
|
|
453
|
+
} catch (reason) {
|
|
454
|
+
registrationOpen = false;
|
|
455
|
+
const cleanupErrors = runCleanup(controller, cleanups);
|
|
456
|
+
return err(sessionError({
|
|
457
|
+
code: "harness-failed",
|
|
458
|
+
message: renderUnknownReason(reason, "Direct harness construction failed"),
|
|
459
|
+
queryError: null,
|
|
460
|
+
storeError: null,
|
|
461
|
+
probeError: null
|
|
462
|
+
}, cleanupErrors));
|
|
463
|
+
}
|
|
464
|
+
let observation;
|
|
465
|
+
try {
|
|
466
|
+
const observed = observeHarness === undefined ? Object.freeze({}) : observeHarness(harness, context);
|
|
467
|
+
if (isPromiseLike(observed)) {
|
|
468
|
+
Promise.resolve(observed).catch(() => {
|
|
469
|
+
return;
|
|
470
|
+
});
|
|
471
|
+
throw new Error("Direct observation construction must complete synchronously");
|
|
472
|
+
}
|
|
473
|
+
observation = prepareSessionObservation(observed);
|
|
474
|
+
} catch (reason) {
|
|
475
|
+
registrationOpen = false;
|
|
476
|
+
const cleanupErrors = runCleanup(controller, cleanups);
|
|
477
|
+
return err(sessionError({
|
|
478
|
+
code: "observation-failed",
|
|
479
|
+
message: renderUnknownReason(reason, "Direct observation construction failed"),
|
|
480
|
+
queryError: null,
|
|
481
|
+
storeError: null,
|
|
482
|
+
probeError: null
|
|
483
|
+
}, cleanupErrors));
|
|
484
|
+
}
|
|
485
|
+
registrationOpen = false;
|
|
486
|
+
const probe = createDirectProbe({
|
|
487
|
+
store: store.value,
|
|
488
|
+
activationHash: activation.activationHash,
|
|
489
|
+
...observation.pending === undefined ? {} : { pending: observation.pending },
|
|
490
|
+
...observation.violations === undefined ? {} : { violations: observation.violations },
|
|
491
|
+
...observation.readRemainingWork === undefined ? {} : { readRemainingWork: observation.readRemainingWork }
|
|
492
|
+
});
|
|
493
|
+
if (!probe.ok) {
|
|
494
|
+
const cleanupErrors = runCleanup(controller, cleanups);
|
|
495
|
+
return err(sessionError({
|
|
496
|
+
code: "probe-failed",
|
|
497
|
+
message: probe.error.message,
|
|
498
|
+
queryError: null,
|
|
499
|
+
storeError: null,
|
|
500
|
+
probeError: probe.error
|
|
501
|
+
}, cleanupErrors));
|
|
502
|
+
}
|
|
503
|
+
let disposed = false;
|
|
504
|
+
let disposalErrors = Object.freeze([]);
|
|
505
|
+
const onDispose = (cleanup) => {
|
|
506
|
+
if (typeof cleanup !== "function") {
|
|
507
|
+
return err(Object.freeze({
|
|
508
|
+
code: "invalid-cleanup",
|
|
509
|
+
message: "Direct cleanup must be a function"
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
if (disposed) {
|
|
513
|
+
return err(Object.freeze({
|
|
514
|
+
code: "session-disposed",
|
|
515
|
+
message: "Cannot register cleanup on a disposed Direct session"
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
cleanups.push(cleanup);
|
|
519
|
+
return ok(true);
|
|
520
|
+
};
|
|
521
|
+
const dispose = () => {
|
|
522
|
+
if (disposed)
|
|
523
|
+
return;
|
|
524
|
+
disposed = true;
|
|
525
|
+
disposalErrors = runCleanup(controller, cleanups);
|
|
526
|
+
return;
|
|
527
|
+
};
|
|
528
|
+
return ok(Object.freeze({
|
|
529
|
+
activation,
|
|
530
|
+
world: activation.world,
|
|
531
|
+
store: store.value,
|
|
532
|
+
clock,
|
|
533
|
+
activity,
|
|
534
|
+
harness,
|
|
535
|
+
probe: probe.value,
|
|
536
|
+
manifest,
|
|
537
|
+
coverage: manifest.coverage,
|
|
538
|
+
signal: controller.signal,
|
|
539
|
+
onDispose,
|
|
540
|
+
dispose,
|
|
541
|
+
isDisposed: () => disposed,
|
|
542
|
+
disposalErrors: () => disposalErrors
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
// src/testing/scripted-transport.ts
|
|
546
|
+
var DEFAULT_SCRIPTED_TRANSPORT_LIMITS = Object.freeze({
|
|
547
|
+
maxSteps: 1e4,
|
|
548
|
+
maxEventsPerStep: 1000,
|
|
549
|
+
maxRecordedInternalErrors: 32,
|
|
550
|
+
maxLogicalDelayPerStepMs: 60000,
|
|
551
|
+
maxTotalLogicalDelayMs: 3600000
|
|
552
|
+
});
|
|
553
|
+
var STEP_KEYS = new Set(["request", "outcome", "delayMs", "eventsBefore", "eventsAfter"]);
|
|
554
|
+
var OUTCOME_KEYS = new Set(["kind", "value", "error"]);
|
|
555
|
+
var LIMIT_KEYS = new Set([
|
|
556
|
+
"maxSteps",
|
|
557
|
+
"maxEventsPerStep",
|
|
558
|
+
"maxRecordedInternalErrors",
|
|
559
|
+
"maxLogicalDelayPerStepMs",
|
|
560
|
+
"maxTotalLogicalDelayMs"
|
|
561
|
+
]);
|
|
562
|
+
function definitionError(code, message, step = null) {
|
|
563
|
+
return Object.freeze({ code, message, step });
|
|
564
|
+
}
|
|
565
|
+
function validateLimits(limits) {
|
|
566
|
+
for (const key of Object.keys(limits)) {
|
|
567
|
+
if (!LIMIT_KEYS.has(key)) {
|
|
568
|
+
return err(definitionError("invalid-limits", `Unknown scripted transport limit: ${key}`));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const [name, value] of [
|
|
572
|
+
["maxSteps", limits.maxSteps],
|
|
573
|
+
["maxEventsPerStep", limits.maxEventsPerStep],
|
|
574
|
+
["maxRecordedInternalErrors", limits.maxRecordedInternalErrors]
|
|
575
|
+
]) {
|
|
576
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1e5) {
|
|
577
|
+
return err(definitionError("invalid-limits", `${name} must be a positive safe integer no greater than 100000`));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const [name, value] of [
|
|
581
|
+
["maxLogicalDelayPerStepMs", limits.maxLogicalDelayPerStepMs],
|
|
582
|
+
["maxTotalLogicalDelayMs", limits.maxTotalLogicalDelayMs]
|
|
583
|
+
]) {
|
|
584
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
585
|
+
return err(definitionError("invalid-limits", `${name} must be a non-negative safe integer`));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return ok(Object.freeze({ ...limits }));
|
|
589
|
+
}
|
|
590
|
+
function parseOwned(input, parser, label, step) {
|
|
591
|
+
const parsed = parseAndCloneWorld(input, parser);
|
|
592
|
+
return parsed.ok ? parsed : err(definitionError("invalid-step", `${label}: ${parsed.error.message}`, step));
|
|
593
|
+
}
|
|
594
|
+
function parseEvents(input, parser, label, step) {
|
|
595
|
+
if (!Array.isArray(input)) {
|
|
596
|
+
return err(definitionError("invalid-step", `${label} must be an array`, step));
|
|
597
|
+
}
|
|
598
|
+
const events = [];
|
|
599
|
+
for (const candidate of input) {
|
|
600
|
+
const parsed = parseOwned(candidate, parser, label, step);
|
|
601
|
+
if (!parsed.ok)
|
|
602
|
+
return parsed;
|
|
603
|
+
events.push(parsed.value);
|
|
604
|
+
}
|
|
605
|
+
return ok(Object.freeze(events));
|
|
606
|
+
}
|
|
607
|
+
function parseSteps(options, limits) {
|
|
608
|
+
const json = parseJsonValue(options.steps);
|
|
609
|
+
if (!json.ok || !Array.isArray(json.value)) {
|
|
610
|
+
return err(definitionError("invalid-script", json.ok ? "Transport steps must be an array" : json.error.message));
|
|
611
|
+
}
|
|
612
|
+
if (json.value.length > limits.maxSteps) {
|
|
613
|
+
return err(definitionError("script-too-large", `Transport script exceeds ${String(limits.maxSteps)} steps`));
|
|
614
|
+
}
|
|
615
|
+
const steps = [];
|
|
616
|
+
let totalLogicalDelayMs = 0;
|
|
617
|
+
for (const [index, candidate] of json.value.entries()) {
|
|
618
|
+
if (!isRecord(candidate)) {
|
|
619
|
+
return err(definitionError("invalid-step", "Transport steps must be objects", index));
|
|
620
|
+
}
|
|
621
|
+
for (const key of Object.keys(candidate)) {
|
|
622
|
+
if (!STEP_KEYS.has(key)) {
|
|
623
|
+
return err(definitionError("invalid-step", `Unknown transport step key: ${key}`, index));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!("request" in candidate) || !("outcome" in candidate)) {
|
|
627
|
+
return err(definitionError("invalid-step", "Transport steps require request and outcome", index));
|
|
628
|
+
}
|
|
629
|
+
const request = parseOwned(candidate.request, options.parseRequest, "Invalid scripted request", index);
|
|
630
|
+
if (!request.ok)
|
|
631
|
+
return request;
|
|
632
|
+
const requestCanonical = canonicalJson(request.value);
|
|
633
|
+
if (!requestCanonical.ok) {
|
|
634
|
+
return err(definitionError("invalid-step", requestCanonical.error.message, index));
|
|
635
|
+
}
|
|
636
|
+
if (!isRecord(candidate.outcome)) {
|
|
637
|
+
return err(definitionError("invalid-step", "Transport outcome must be an object", index));
|
|
638
|
+
}
|
|
639
|
+
for (const key of Object.keys(candidate.outcome)) {
|
|
640
|
+
if (!OUTCOME_KEYS.has(key)) {
|
|
641
|
+
return err(definitionError("invalid-step", `Unknown transport outcome key: ${key}`, index));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
let outcome;
|
|
645
|
+
if (candidate.outcome.kind === "response") {
|
|
646
|
+
if (!("value" in candidate.outcome) || "error" in candidate.outcome) {
|
|
647
|
+
return err(definitionError("invalid-step", "A response outcome requires only value", index));
|
|
648
|
+
}
|
|
649
|
+
const response = parseOwned(candidate.outcome.value, options.parseResponse, "Invalid scripted response", index);
|
|
650
|
+
if (!response.ok)
|
|
651
|
+
return response;
|
|
652
|
+
outcome = Object.freeze({ kind: "response", value: response.value });
|
|
653
|
+
} else if (candidate.outcome.kind === "failure") {
|
|
654
|
+
if (!("error" in candidate.outcome) || "value" in candidate.outcome) {
|
|
655
|
+
return err(definitionError("invalid-step", "A failure outcome requires only error", index));
|
|
656
|
+
}
|
|
657
|
+
const failure = parseOwned(candidate.outcome.error, options.parseFailure, "Invalid scripted failure", index);
|
|
658
|
+
if (!failure.ok)
|
|
659
|
+
return failure;
|
|
660
|
+
outcome = Object.freeze({ kind: "failure", error: failure.value });
|
|
661
|
+
} else {
|
|
662
|
+
return err(definitionError("invalid-step", "Transport outcome kind must be response or failure", index));
|
|
663
|
+
}
|
|
664
|
+
const delayMs = "delayMs" in candidate ? candidate.delayMs : 0;
|
|
665
|
+
if (typeof delayMs !== "number" || !Number.isSafeInteger(delayMs) || delayMs < 0) {
|
|
666
|
+
return err(definitionError("invalid-step", "Transport delayMs must be a non-negative safe integer", index));
|
|
667
|
+
}
|
|
668
|
+
if (delayMs > limits.maxLogicalDelayPerStepMs) {
|
|
669
|
+
return err(definitionError("script-too-large", `Transport delayMs exceeds the per-step limit of ${String(limits.maxLogicalDelayPerStepMs)}`, index));
|
|
670
|
+
}
|
|
671
|
+
if (totalLogicalDelayMs > limits.maxTotalLogicalDelayMs - delayMs) {
|
|
672
|
+
return err(definitionError("script-too-large", `Transport script exceeds the total logical delay limit of ${String(limits.maxTotalLogicalDelayMs)}`, index));
|
|
673
|
+
}
|
|
674
|
+
totalLogicalDelayMs += delayMs;
|
|
675
|
+
const eventsBeforeInput = "eventsBefore" in candidate ? candidate.eventsBefore : [];
|
|
676
|
+
const eventsAfterInput = "eventsAfter" in candidate ? candidate.eventsAfter : [];
|
|
677
|
+
if (!Array.isArray(eventsBeforeInput) || !Array.isArray(eventsAfterInput)) {
|
|
678
|
+
return err(definitionError("invalid-step", "eventsBefore and eventsAfter must be arrays", index));
|
|
679
|
+
}
|
|
680
|
+
if (eventsBeforeInput.length > limits.maxEventsPerStep || eventsAfterInput.length > limits.maxEventsPerStep - eventsBeforeInput.length) {
|
|
681
|
+
return err(definitionError("script-too-large", `Transport step exceeds the combined event limit of ${String(limits.maxEventsPerStep)}`, index));
|
|
682
|
+
}
|
|
683
|
+
const eventsBefore = parseEvents(eventsBeforeInput, options.parseEvent, "eventsBefore", index);
|
|
684
|
+
if (!eventsBefore.ok)
|
|
685
|
+
return eventsBefore;
|
|
686
|
+
const eventsAfter = parseEvents(eventsAfterInput, options.parseEvent, "eventsAfter", index);
|
|
687
|
+
if (!eventsAfter.ok)
|
|
688
|
+
return eventsAfter;
|
|
689
|
+
steps.push(Object.freeze({
|
|
690
|
+
request: request.value,
|
|
691
|
+
requestCanonical: requestCanonical.value,
|
|
692
|
+
outcome,
|
|
693
|
+
delayMs,
|
|
694
|
+
eventsBefore: eventsBefore.value,
|
|
695
|
+
eventsAfter: eventsAfter.value
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
return ok(Object.freeze(steps));
|
|
699
|
+
}
|
|
700
|
+
function transportError(code, message, step, expectedRequest, actualRequest, remainingSteps, pendingDeliveries) {
|
|
701
|
+
return Object.freeze({
|
|
702
|
+
code,
|
|
703
|
+
message,
|
|
704
|
+
step,
|
|
705
|
+
expectedRequest,
|
|
706
|
+
actualRequest,
|
|
707
|
+
remainingSteps,
|
|
708
|
+
pendingDeliveries
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function runtimeFailureMessage(error) {
|
|
712
|
+
return `Logical wait failed: ${renderUnknownReason(error, "Unknown logical runtime failure")}`;
|
|
713
|
+
}
|
|
714
|
+
function isPromiseLike2(value) {
|
|
715
|
+
return (typeof value === "object" && value !== null || typeof value === "function") && typeof Reflect.get(value, "then") === "function";
|
|
716
|
+
}
|
|
717
|
+
function synchronousReturnViolation(value, label) {
|
|
718
|
+
if (value === undefined)
|
|
719
|
+
return null;
|
|
720
|
+
if (isPromiseLike2(value)) {
|
|
721
|
+
Promise.resolve(value).catch(() => {
|
|
722
|
+
return;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return new Error(`${label} must complete synchronously and return undefined`);
|
|
726
|
+
}
|
|
727
|
+
function createExactScriptedTransport(options) {
|
|
728
|
+
let captured;
|
|
729
|
+
try {
|
|
730
|
+
captured = Object.freeze({
|
|
731
|
+
wait: options.runtime.wait,
|
|
732
|
+
parseRequest: options.parseRequest,
|
|
733
|
+
parseResponse: options.parseResponse,
|
|
734
|
+
parseEvent: options.parseEvent,
|
|
735
|
+
parseFailure: options.parseFailure,
|
|
736
|
+
steps: options.steps,
|
|
737
|
+
beginActivity: options.activity?.begin,
|
|
738
|
+
activityNamespace: options.activityNamespace ?? "transport",
|
|
739
|
+
limits: options.limits ?? DEFAULT_SCRIPTED_TRANSPORT_LIMITS,
|
|
740
|
+
onListenerError: options.onListenerError
|
|
741
|
+
});
|
|
742
|
+
} catch (reason) {
|
|
743
|
+
return err(definitionError("invalid-options", renderUnknownReason(reason, "Scripted transport options could not be inspected")));
|
|
744
|
+
}
|
|
745
|
+
let limits;
|
|
746
|
+
try {
|
|
747
|
+
limits = validateLimits(captured.limits);
|
|
748
|
+
} catch (reason) {
|
|
749
|
+
return err(definitionError("invalid-options", renderUnknownReason(reason, "Scripted transport limits could not be inspected")));
|
|
750
|
+
}
|
|
751
|
+
if (!limits.ok)
|
|
752
|
+
return limits;
|
|
753
|
+
const parsedSteps = parseSteps(captured, limits.value);
|
|
754
|
+
if (!parsedSteps.ok)
|
|
755
|
+
return parsedSteps;
|
|
756
|
+
const listeners = new Set;
|
|
757
|
+
const activeCancellations = new Set;
|
|
758
|
+
const disposalController = new AbortController;
|
|
759
|
+
const internalErrors = [];
|
|
760
|
+
let droppedInternalErrors = 0;
|
|
761
|
+
let nextStep = 0;
|
|
762
|
+
let pending = 0;
|
|
763
|
+
let disposed = false;
|
|
764
|
+
let idleWaiters = [];
|
|
765
|
+
const remainingSteps = () => parsedSteps.value.length - nextStep;
|
|
766
|
+
const recordInternalError = (reason) => {
|
|
767
|
+
const message = renderUnknownReason(reason, "Uninspectable internal transport failure").slice(0, 500);
|
|
768
|
+
if (internalErrors.length < limits.value.maxRecordedInternalErrors)
|
|
769
|
+
internalErrors.push(message);
|
|
770
|
+
else
|
|
771
|
+
droppedInternalErrors += 1;
|
|
772
|
+
};
|
|
773
|
+
const notifyIdle = () => {
|
|
774
|
+
if (pending !== 0)
|
|
775
|
+
return;
|
|
776
|
+
const waiting = idleWaiters;
|
|
777
|
+
idleWaiters = [];
|
|
778
|
+
for (const resolve of waiting)
|
|
779
|
+
resolve();
|
|
780
|
+
};
|
|
781
|
+
const reportListenerError = (reason) => {
|
|
782
|
+
if (captured.onListenerError === undefined)
|
|
783
|
+
return;
|
|
784
|
+
try {
|
|
785
|
+
const returned = captured.onListenerError(reason);
|
|
786
|
+
const violation = synchronousReturnViolation(returned, "Scripted transport listener-error reporters");
|
|
787
|
+
if (violation !== null)
|
|
788
|
+
recordInternalError(violation);
|
|
789
|
+
} catch (reporterReason) {
|
|
790
|
+
recordInternalError(reporterReason);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const emit = (events) => {
|
|
794
|
+
if (disposed)
|
|
795
|
+
return false;
|
|
796
|
+
for (const event of events) {
|
|
797
|
+
if (disposed)
|
|
798
|
+
return false;
|
|
799
|
+
for (const listener of [...listeners]) {
|
|
800
|
+
if (disposed)
|
|
801
|
+
return false;
|
|
802
|
+
try {
|
|
803
|
+
const returned = listener(event);
|
|
804
|
+
const violation = synchronousReturnViolation(returned, "Scripted transport listeners");
|
|
805
|
+
if (violation !== null) {
|
|
806
|
+
recordInternalError(violation);
|
|
807
|
+
reportListenerError(violation);
|
|
808
|
+
}
|
|
809
|
+
} catch (reason) {
|
|
810
|
+
recordInternalError(reason);
|
|
811
|
+
reportListenerError(reason);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return !disposed;
|
|
816
|
+
};
|
|
817
|
+
const currentError = (code, message, step = null, expected = null, actual = null) => transportError(code, message, step, expected, actual, remainingSteps(), pending);
|
|
818
|
+
const internalError = () => {
|
|
819
|
+
if (internalErrors.length === 0 && droppedInternalErrors === 0)
|
|
820
|
+
return null;
|
|
821
|
+
const suffix = droppedInternalErrors === 0 ? "" : `; ${String(droppedInternalErrors)} more omitted`;
|
|
822
|
+
return currentError("internal-failure", `${internalErrors.join("; ")}${suffix}`);
|
|
823
|
+
};
|
|
824
|
+
const settleLease = (lease) => {
|
|
825
|
+
if (lease === null)
|
|
826
|
+
return;
|
|
827
|
+
try {
|
|
828
|
+
const released = lease.release();
|
|
829
|
+
if (!released.ok)
|
|
830
|
+
recordInternalError(released.error);
|
|
831
|
+
} catch (reason) {
|
|
832
|
+
recordInternalError(reason);
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
const beginLease = () => {
|
|
836
|
+
if (captured.beginActivity === undefined)
|
|
837
|
+
return ok(null);
|
|
838
|
+
try {
|
|
839
|
+
const started = captured.beginActivity(captured.activityNamespace);
|
|
840
|
+
return started.ok ? ok(started.value) : err(renderUnknownReason(started.error, "Activity scope failed"));
|
|
841
|
+
} catch (reason) {
|
|
842
|
+
return err(renderUnknownReason(reason, "Activity scope threw"));
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
const fail = (error) => {
|
|
846
|
+
recordInternalError(`${error.code}: ${error.message}`);
|
|
847
|
+
return err(Object.freeze({ kind: "transport", error }));
|
|
848
|
+
};
|
|
849
|
+
const disposedFailure = (step = null, expected = null, actual = null) => err(Object.freeze({
|
|
850
|
+
kind: "transport",
|
|
851
|
+
error: currentError("transport-disposed", "The scripted transport has been disposed", step, expected, actual)
|
|
852
|
+
}));
|
|
853
|
+
const dispose = () => {
|
|
854
|
+
if (disposed)
|
|
855
|
+
return;
|
|
856
|
+
disposed = true;
|
|
857
|
+
listeners.clear();
|
|
858
|
+
for (const cancel of [...activeCancellations]) {
|
|
859
|
+
try {
|
|
860
|
+
cancel();
|
|
861
|
+
} catch (reason) {
|
|
862
|
+
recordInternalError(reason);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
activeCancellations.clear();
|
|
866
|
+
try {
|
|
867
|
+
disposalController.abort();
|
|
868
|
+
} catch (reason) {
|
|
869
|
+
recordInternalError(reason);
|
|
870
|
+
}
|
|
871
|
+
notifyIdle();
|
|
872
|
+
return;
|
|
873
|
+
};
|
|
874
|
+
const request = (input) => {
|
|
875
|
+
if (disposed)
|
|
876
|
+
return Promise.resolve(disposedFailure());
|
|
877
|
+
const parsedRequest = parseAndCloneWorld(input, captured.parseRequest);
|
|
878
|
+
if (!parsedRequest.ok) {
|
|
879
|
+
if (disposed)
|
|
880
|
+
return Promise.resolve(disposedFailure());
|
|
881
|
+
return Promise.resolve(fail(currentError("invalid-request", parsedRequest.error.message)));
|
|
882
|
+
}
|
|
883
|
+
if (disposed)
|
|
884
|
+
return Promise.resolve(disposedFailure(null, null, parsedRequest.value));
|
|
885
|
+
const requestCanonical = canonicalJson(parsedRequest.value);
|
|
886
|
+
if (!requestCanonical.ok) {
|
|
887
|
+
return Promise.resolve(fail(currentError("invalid-request", requestCanonical.error.message)));
|
|
888
|
+
}
|
|
889
|
+
if (disposed)
|
|
890
|
+
return Promise.resolve(disposedFailure(null, null, parsedRequest.value));
|
|
891
|
+
const step = parsedSteps.value[nextStep];
|
|
892
|
+
if (step === undefined) {
|
|
893
|
+
return Promise.resolve(fail(currentError("unexpected-request", "Transport received a request after its script was exhausted", nextStep, null, parsedRequest.value)));
|
|
894
|
+
}
|
|
895
|
+
if (requestCanonical.value !== step.requestCanonical) {
|
|
896
|
+
return Promise.resolve(fail(currentError("request-mismatch", `Request does not match scripted step ${String(nextStep)}`, nextStep, step.request, parsedRequest.value)));
|
|
897
|
+
}
|
|
898
|
+
const stepIndex = nextStep;
|
|
899
|
+
nextStep += 1;
|
|
900
|
+
pending += 1;
|
|
901
|
+
const lease = beginLease();
|
|
902
|
+
if (!lease.ok) {
|
|
903
|
+
pending -= 1;
|
|
904
|
+
notifyIdle();
|
|
905
|
+
if (disposed) {
|
|
906
|
+
return Promise.resolve(disposedFailure(stepIndex, step.request, parsedRequest.value));
|
|
907
|
+
}
|
|
908
|
+
return Promise.resolve(fail(currentError("activity-failed", lease.error, stepIndex, step.request, parsedRequest.value)));
|
|
909
|
+
}
|
|
910
|
+
if (disposed) {
|
|
911
|
+
settleLease(lease.value);
|
|
912
|
+
pending -= 1;
|
|
913
|
+
notifyIdle();
|
|
914
|
+
return Promise.resolve(disposedFailure(stepIndex, step.request, parsedRequest.value));
|
|
915
|
+
}
|
|
916
|
+
return new Promise((resolve) => {
|
|
917
|
+
let completed = false;
|
|
918
|
+
let finalized = false;
|
|
919
|
+
const finalize = (deliverAfterEvents) => {
|
|
920
|
+
if (finalized)
|
|
921
|
+
return;
|
|
922
|
+
finalized = true;
|
|
923
|
+
activeCancellations.delete(cancel);
|
|
924
|
+
if (deliverAfterEvents && !disposed)
|
|
925
|
+
emit(step.eventsAfter);
|
|
926
|
+
settleLease(lease.value);
|
|
927
|
+
if (pending > 0)
|
|
928
|
+
pending -= 1;
|
|
929
|
+
else
|
|
930
|
+
recordInternalError("Transport pending-delivery accounting underflow");
|
|
931
|
+
notifyIdle();
|
|
932
|
+
};
|
|
933
|
+
const scheduleFinalize = (deliverAfterEvents) => {
|
|
934
|
+
try {
|
|
935
|
+
queueMicrotask(() => finalize(deliverAfterEvents));
|
|
936
|
+
} catch (reason) {
|
|
937
|
+
recordInternalError(reason);
|
|
938
|
+
finalize(deliverAfterEvents);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const complete = (result, deliverAfterEvents) => {
|
|
942
|
+
if (completed)
|
|
943
|
+
return;
|
|
944
|
+
completed = true;
|
|
945
|
+
resolve(result);
|
|
946
|
+
scheduleFinalize(deliverAfterEvents);
|
|
947
|
+
};
|
|
948
|
+
const completeTransportFailure = (code, message) => {
|
|
949
|
+
complete(fail(currentError(code, message, stepIndex, step.request, parsedRequest.value)), false);
|
|
950
|
+
};
|
|
951
|
+
const cancel = () => {
|
|
952
|
+
if (!completed) {
|
|
953
|
+
completed = true;
|
|
954
|
+
resolve(disposedFailure(stepIndex, step.request, parsedRequest.value));
|
|
955
|
+
}
|
|
956
|
+
finalize(false);
|
|
957
|
+
};
|
|
958
|
+
activeCancellations.add(cancel);
|
|
959
|
+
if (disposed) {
|
|
960
|
+
cancel();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const execute = async () => {
|
|
964
|
+
if (!emit(step.eventsBefore)) {
|
|
965
|
+
cancel();
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
let waited;
|
|
969
|
+
try {
|
|
970
|
+
waited = await captured.wait(step.delayMs, disposalController.signal);
|
|
971
|
+
} catch (reason) {
|
|
972
|
+
if (disposed) {
|
|
973
|
+
cancel();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
completeTransportFailure("logical-wait-failed", `Logical wait failed: ${renderUnknownReason(reason, "Logical wait threw")}`);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (disposed) {
|
|
980
|
+
cancel();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (!waited.ok) {
|
|
984
|
+
completeTransportFailure("logical-wait-failed", runtimeFailureMessage(waited.error));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const outcome = step.outcome.kind === "response" ? ok(step.outcome.value) : err(Object.freeze({
|
|
988
|
+
kind: "scripted",
|
|
989
|
+
failure: step.outcome.error
|
|
990
|
+
}));
|
|
991
|
+
complete(outcome, true);
|
|
992
|
+
};
|
|
993
|
+
execute().catch((reason) => {
|
|
994
|
+
completeTransportFailure("internal-failure", `Transport delivery failed: ${renderUnknownReason(reason)}`);
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
const assertNoInternalError = () => {
|
|
999
|
+
const failure = internalError();
|
|
1000
|
+
return failure === null ? ok(true) : err(failure);
|
|
1001
|
+
};
|
|
1002
|
+
const transport = {
|
|
1003
|
+
request,
|
|
1004
|
+
subscribe: (listener) => {
|
|
1005
|
+
if (disposed)
|
|
1006
|
+
return () => {
|
|
1007
|
+
return;
|
|
1008
|
+
};
|
|
1009
|
+
listeners.add(listener);
|
|
1010
|
+
let active = true;
|
|
1011
|
+
return () => {
|
|
1012
|
+
if (!active)
|
|
1013
|
+
return;
|
|
1014
|
+
active = false;
|
|
1015
|
+
listeners.delete(listener);
|
|
1016
|
+
return;
|
|
1017
|
+
};
|
|
1018
|
+
},
|
|
1019
|
+
dispose,
|
|
1020
|
+
isDisposed: () => disposed,
|
|
1021
|
+
remainingSteps,
|
|
1022
|
+
pendingDeliveries: () => pending,
|
|
1023
|
+
violationCount: () => internalErrors.length + droppedInternalErrors,
|
|
1024
|
+
whenIdle: async () => {
|
|
1025
|
+
while (pending !== 0) {
|
|
1026
|
+
await new Promise((resolve) => {
|
|
1027
|
+
idleWaiters.push(resolve);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return assertNoInternalError();
|
|
1031
|
+
},
|
|
1032
|
+
assertDrained: () => {
|
|
1033
|
+
const failure = internalError();
|
|
1034
|
+
if (failure !== null)
|
|
1035
|
+
return err(failure);
|
|
1036
|
+
if (pending !== 0) {
|
|
1037
|
+
return err(currentError("pending-deliveries", `${String(pending)} transport deliveries are still pending`));
|
|
1038
|
+
}
|
|
1039
|
+
if (remainingSteps() !== 0) {
|
|
1040
|
+
return err(currentError("remaining-steps", `${String(remainingSteps())} scripted transport steps remain`));
|
|
1041
|
+
}
|
|
1042
|
+
return ok(true);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
return ok(Object.freeze(transport));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/testing/index.ts
|
|
1049
|
+
function parseCoverageCatalogSnapshot2(input) {
|
|
1050
|
+
return parseCoverageCatalogSnapshot(input);
|
|
1051
|
+
}
|
|
1052
|
+
export {
|
|
1053
|
+
parseExpectedCoverageCatalogSnapshot,
|
|
1054
|
+
parseDirectSessionManifest,
|
|
1055
|
+
parseDirectProbeSnapshot,
|
|
1056
|
+
parseDefinitionCoverageSnapshot,
|
|
1057
|
+
parseCoverageCatalogSnapshot2 as parseCoverageCatalogSnapshot,
|
|
1058
|
+
createExactScriptedTransport,
|
|
1059
|
+
createDirectSessionManifest,
|
|
1060
|
+
createDirectSession,
|
|
1061
|
+
createDirectProbe,
|
|
1062
|
+
createDirectActivityScope,
|
|
1063
|
+
classifyCoverageEvidence,
|
|
1064
|
+
MAX_DIRECT_PROBE_COUNTERS,
|
|
1065
|
+
DIRECT_SESSION_MANIFEST_SCHEMA,
|
|
1066
|
+
DIRECT_PROBE_SCHEMA,
|
|
1067
|
+
DIRECT_CATALOG_HASH_ALGORITHM,
|
|
1068
|
+
DEFAULT_SCRIPTED_TRANSPORT_LIMITS
|
|
1069
|
+
};
|