@effect-agent/testing 0.0.1-beta.5 → 0.1.0-beta.10

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/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "@effect-agent/testing",
3
- "version": "0.0.1-beta.5",
3
+ "version": "0.1.0-beta.10",
4
+ "exports": {
5
+ ".": {
6
+ "types": "./dist/index.d.mts",
7
+ "default": "./dist/index.mjs"
8
+ }
9
+ },
10
+ "dependencies": {
11
+ "@effect-agent/capabilities": "0.1.0-beta.10",
12
+ "@effect-agent/core": "0.1.0-beta.10",
13
+ "@effect-agent/engine": "0.1.0-beta.10",
14
+ "@effect-agent/platform-node": "0.1.0-beta.10",
15
+ "@effect-agent/sandbox": "0.1.0-beta.10",
16
+ "@effect-agent/session": "0.1.0-beta.10",
17
+ "@effect-agent/storage-memory": "0.1.0-beta.10",
18
+ "@effect-agent/storage-sqlite": "0.1.0-beta.10",
19
+ "effect": "4.0.0-beta.107"
20
+ },
4
21
  "description": "Scripted models, deterministic fixtures, and adapter conformance kits for testing Effect Agent applications.",
5
22
  "license": "MIT",
6
23
  "repository": {
@@ -13,12 +30,6 @@
13
30
  "src"
14
31
  ],
15
32
  "type": "module",
16
- "exports": {
17
- ".": {
18
- "types": "./dist/index.d.mts",
19
- "default": "./dist/index.mjs"
20
- }
21
- },
22
33
  "publishConfig": {
23
34
  "access": "public"
24
35
  },
@@ -27,16 +38,6 @@
27
38
  "check": "tsc --noEmit -p tsconfig.json",
28
39
  "test": "vp test --passWithNoTests"
29
40
  },
30
- "dependencies": {
31
- "@effect-agent/capabilities": "0.0.1-beta.5",
32
- "@effect-agent/core": "0.0.1-beta.5",
33
- "@effect-agent/engine": "0.0.1-beta.5",
34
- "@effect-agent/platform-node": "0.0.1-beta.5",
35
- "@effect-agent/session": "0.0.1-beta.5",
36
- "@effect-agent/storage-memory": "0.0.1-beta.5",
37
- "@effect-agent/storage-sqlite": "0.0.1-beta.5",
38
- "effect": "4.0.0-beta.107"
39
- },
40
41
  "devDependencies": {
41
42
  "@effect/platform-node": "4.0.0-beta.107",
42
43
  "@effect/sql-sqlite-node": "4.0.0-beta.107",
@@ -166,6 +166,11 @@ export const CERTIFICATION_SCENARIOS: ReadonlyArray<CertificationScenario> = [
166
166
  */
167
167
  export const TIER2_UNREACHED_LOCATIONS: ReadonlyArray<DurableRuntimeFailpointLocation> = [
168
168
  "abort:after-intent",
169
+ // Compaction requires a `contextTokenLimit` policy plus prior-Run history
170
+ // none of the six scenario shapes carries; pinned in-process by the
171
+ // RUN-026 rows in `packages/testing/test/durable-runtime.test.ts`
172
+ // (compaction failpoint idempotence across re-drive).
173
+ "compaction:after-canonical-append",
169
174
  "resolve:after-intent",
170
175
  "subagent:after-child-abort-intent",
171
176
  ];
@@ -0,0 +1,30 @@
1
+ import { CodeExecutor, SandboxImplementation } from "@effect-agent/sandbox";
2
+ import { Effect, Schema } from "effect";
3
+ declare const CodeExecutorConformanceViolation_base: Schema.Class<CodeExecutorConformanceViolation, Schema.TaggedStruct<"CodeExecutorConformanceViolation", {
4
+ readonly caseName: Schema.String;
5
+ readonly message: Schema.String;
6
+ }>, import("effect/Cause").YieldableError>;
7
+ /**
8
+ * Shared `CodeExecutor` conformance (TEST-015). Every adapter — the
9
+ * deterministic `unisolated` substitute and each isolated adapter — runs
10
+ * `codeExecutorConformanceCases` verbatim. Enforcement cases that only genuine
11
+ * isolation can prove (ambient network denial, synchronous CPU runaway
12
+ * termination) are NOT here; they belong to isolated adapters only
13
+ * (testing spec §8.1).
14
+ *
15
+ * Cases assume the live `Clock` (the wall-clock case uses a short real
16
+ * deadline) and take one fresh executor pass per case, so a suite may share
17
+ * one executor Layer across cases.
18
+ */
19
+ export declare class CodeExecutorConformanceViolation extends CodeExecutorConformanceViolation_base {
20
+ }
21
+ export interface CodeExecutorConformanceCase {
22
+ readonly name: string;
23
+ readonly run: Effect.Effect<void, CodeExecutorConformanceViolation, CodeExecutor>;
24
+ }
25
+ export interface CodeExecutorConformanceOptions {
26
+ /** The posture the adapter under test must stamp on results and errors. */
27
+ readonly implementation: SandboxImplementation;
28
+ }
29
+ export declare const codeExecutorConformanceCases: (options: CodeExecutorConformanceOptions) => ReadonlyArray<CodeExecutorConformanceCase>;
30
+ export {};
@@ -0,0 +1,500 @@
1
+ import {
2
+ CodeExecutionError,
3
+ CodeExecutionHost,
4
+ CodeExecutionLimits,
5
+ CodeExecutionNamespace,
6
+ CodeExecutionRequest,
7
+ CodeExecutionResult,
8
+ CodeExecutor,
9
+ CodeHostCall,
10
+ CodeHostCallFailure,
11
+ CodeHostCallResult,
12
+ CodeHostCallSuccess,
13
+ NetworkAllowlist,
14
+ NetworkDisabled,
15
+ SandboxImplementation,
16
+ } from "@effect-agent/sandbox";
17
+ import { Deferred, Duration, Effect, Fiber, Schema } from "effect";
18
+
19
+ /**
20
+ * Shared `CodeExecutor` conformance (TEST-015). Every adapter — the
21
+ * deterministic `unisolated` substitute and each isolated adapter — runs
22
+ * `codeExecutorConformanceCases` verbatim. Enforcement cases that only genuine
23
+ * isolation can prove (ambient network denial, synchronous CPU runaway
24
+ * termination) are NOT here; they belong to isolated adapters only
25
+ * (testing spec §8.1).
26
+ *
27
+ * Cases assume the live `Clock` (the wall-clock case uses a short real
28
+ * deadline) and take one fresh executor pass per case, so a suite may share
29
+ * one executor Layer across cases.
30
+ */
31
+ export class CodeExecutorConformanceViolation extends Schema.TaggedError<CodeExecutorConformanceViolation>()(
32
+ "CodeExecutorConformanceViolation",
33
+ {
34
+ caseName: Schema.String,
35
+ message: Schema.String,
36
+ },
37
+ ) {}
38
+
39
+ export interface CodeExecutorConformanceCase {
40
+ readonly name: string;
41
+ readonly run: Effect.Effect<void, CodeExecutorConformanceViolation, CodeExecutor>;
42
+ }
43
+
44
+ export interface CodeExecutorConformanceOptions {
45
+ /** The posture the adapter under test must stamp on results and errors. */
46
+ readonly implementation: SandboxImplementation;
47
+ }
48
+
49
+ const baseLimits = CodeExecutionLimits.make({
50
+ maxSourceBytes: 64 * 1024,
51
+ maxWallTime: Duration.seconds(10),
52
+ maxLogBytes: 16 * 1024,
53
+ maxResultBytes: 64 * 1024,
54
+ maxHostCalls: 8,
55
+ maxHostCallArgumentBytes: 16 * 1024,
56
+ maxHostCallResultBytes: 32 * 1024,
57
+ });
58
+
59
+ const warehouseNamespace = CodeExecutionNamespace.make({
60
+ name: "warehouse",
61
+ methods: ["query", "count"],
62
+ });
63
+
64
+ const makeRequest = (
65
+ source: string,
66
+ overrides?: {
67
+ readonly limits?: CodeExecutionLimits;
68
+ readonly namespaces?: ReadonlyArray<CodeExecutionNamespace>;
69
+ readonly network?: CodeExecutionRequest["network"];
70
+ },
71
+ ): CodeExecutionRequest =>
72
+ CodeExecutionRequest.make({
73
+ language: "javascript",
74
+ source,
75
+ namespaces: overrides?.namespaces ?? [],
76
+ network: overrides?.network ?? NetworkDisabled.make(),
77
+ limits: overrides?.limits ?? baseLimits,
78
+ });
79
+
80
+ const unusedHost: CodeExecutionHost["Service"] = {
81
+ call: () =>
82
+ Effect.die(
83
+ new Error("this conformance case expected no host call to reach the CodeExecutionHost"),
84
+ ),
85
+ };
86
+
87
+ const respondingHost = (
88
+ respond: (call: CodeHostCall) => CodeHostCallResult,
89
+ ): { readonly host: CodeExecutionHost["Service"]; readonly calls: Array<CodeHostCall> } => {
90
+ const calls: Array<CodeHostCall> = [];
91
+ return {
92
+ calls,
93
+ host: {
94
+ call: (call) =>
95
+ Effect.sync(() => {
96
+ calls.push(call);
97
+ return respond(call);
98
+ }),
99
+ },
100
+ };
101
+ };
102
+
103
+ const runPass = (
104
+ request: CodeExecutionRequest,
105
+ host: CodeExecutionHost["Service"],
106
+ ): Effect.Effect<CodeExecutionResult, CodeExecutionError, CodeExecutor> =>
107
+ Effect.gen(function* () {
108
+ const executor = yield* CodeExecutor;
109
+ return yield* executor
110
+ .execute(request)
111
+ .pipe(Effect.provideService(CodeExecutionHost, CodeExecutionHost.of(host)));
112
+ }).pipe(Effect.scoped);
113
+
114
+ const violation = (caseName: string, message: string) =>
115
+ CodeExecutorConformanceViolation.make({ caseName, message });
116
+
117
+ const preview = (value: unknown): string => {
118
+ try {
119
+ return JSON.stringify(value)?.slice(0, 200) ?? String(value).slice(0, 200);
120
+ } catch {
121
+ return String(value).slice(0, 200);
122
+ }
123
+ };
124
+
125
+ const expectSuccess = (
126
+ caseName: string,
127
+ request: CodeExecutionRequest,
128
+ host: CodeExecutionHost["Service"],
129
+ check: (result: CodeExecutionResult) => string | undefined,
130
+ ): Effect.Effect<void, CodeExecutorConformanceViolation, CodeExecutor> =>
131
+ runPass(request, host).pipe(
132
+ Effect.mapError((error) =>
133
+ violation(caseName, `expected success, got ${error._tag}: ${preview(error)}`),
134
+ ),
135
+ Effect.flatMap((result) => {
136
+ const complaint = check(result);
137
+ return complaint === undefined ? Effect.void : Effect.fail(violation(caseName, complaint));
138
+ }),
139
+ );
140
+
141
+ const expectFailure = (
142
+ caseName: string,
143
+ request: CodeExecutionRequest,
144
+ host: CodeExecutionHost["Service"],
145
+ tag: CodeExecutionError["_tag"],
146
+ check?: (error: CodeExecutionError) => string | undefined,
147
+ ): Effect.Effect<void, CodeExecutorConformanceViolation, CodeExecutor> =>
148
+ runPass(request, host).pipe(
149
+ Effect.flip,
150
+ Effect.mapError((result) =>
151
+ violation(caseName, `expected ${tag}, but the pass succeeded with ${preview(result.value)}`),
152
+ ),
153
+ Effect.flatMap((error) => {
154
+ if (error._tag !== tag) {
155
+ return Effect.fail(
156
+ violation(caseName, `expected ${tag}, got ${error._tag}: ${preview(error)}`),
157
+ );
158
+ }
159
+ const complaint = check?.(error);
160
+ return complaint === undefined ? Effect.void : Effect.fail(violation(caseName, complaint));
161
+ }),
162
+ );
163
+
164
+ export const codeExecutorConformanceCases = (
165
+ options: CodeExecutorConformanceOptions,
166
+ ): ReadonlyArray<CodeExecutorConformanceCase> => {
167
+ const posture = options.implementation;
168
+ return [
169
+ {
170
+ name: "TEST-015 executes bounded JSON computation and returns the program value",
171
+ run: expectSuccess(
172
+ "TEST-015 executes bounded JSON computation and returns the program value",
173
+ makeRequest(
174
+ "async () => { const xs = [1, 2, 3].map((n) => n * 2); return { xs, sum: xs.reduce((a, b) => a + b, 0) }; }",
175
+ ),
176
+ unusedHost,
177
+ (result) =>
178
+ JSON.stringify(result.value) === JSON.stringify({ xs: [2, 4, 6], sum: 12 })
179
+ ? undefined
180
+ : `unexpected program value ${preview(result.value)}`,
181
+ ),
182
+ },
183
+ {
184
+ name: "CAP-015 reports its isolation posture honestly in results and errors",
185
+ run: Effect.gen(function* () {
186
+ const caseName = "CAP-015 reports its isolation posture honestly in results and errors";
187
+ const result = yield* runPass(makeRequest("async () => 1"), unusedHost).pipe(
188
+ Effect.mapError((error) => violation(caseName, `expected success, got ${error._tag}`)),
189
+ );
190
+ if (
191
+ result.implementation.isolation !== posture.isolation ||
192
+ result.implementation.identity !== posture.identity
193
+ ) {
194
+ return yield* violation(
195
+ caseName,
196
+ `result posture ${preview(result.implementation)} does not match the declared ${preview(posture)}`,
197
+ );
198
+ }
199
+ const error = yield* runPass(makeRequest("async () => {"), unusedHost).pipe(
200
+ Effect.flip,
201
+ Effect.mapError(() => violation(caseName, "expected the invalid-source pass to fail")),
202
+ );
203
+ // Every expected execution failure carries the posture; an adapter
204
+ // omitting the field must fail this case, not slip past a probe.
205
+ if (
206
+ error.implementation === undefined ||
207
+ error.implementation.isolation !== posture.isolation ||
208
+ error.implementation.identity !== posture.identity
209
+ ) {
210
+ return yield* violation(
211
+ caseName,
212
+ `error posture ${preview(error.implementation)} does not match the declared ${preview(posture)}`,
213
+ );
214
+ }
215
+ }),
216
+ },
217
+ {
218
+ name: "TEST-015 routes host calls through the CodeExecutionHost in program order",
219
+ run: Effect.gen(function* () {
220
+ const caseName =
221
+ "TEST-015 routes host calls through the CodeExecutionHost in program order";
222
+ const { host, calls } = respondingHost((call) =>
223
+ call.method === "query"
224
+ ? CodeHostCallSuccess.make({ value: { rows: [1, 2, 3] } })
225
+ : CodeHostCallSuccess.make({ value: 3 }),
226
+ );
227
+ const result = yield* runPass(
228
+ makeRequest(
229
+ "async () => { const q = await warehouse.query({ sql: 'select' }); const c = await warehouse.count({ table: 't' }); return { rows: q.rows, count: c }; }",
230
+ { namespaces: [warehouseNamespace] },
231
+ ),
232
+ host,
233
+ ).pipe(
234
+ Effect.mapError((error) =>
235
+ violation(caseName, `expected success, got ${error._tag}: ${preview(error)}`),
236
+ ),
237
+ );
238
+ if (JSON.stringify(result.value) !== JSON.stringify({ rows: [1, 2, 3], count: 3 })) {
239
+ return yield* violation(caseName, `unexpected value ${preview(result.value)}`);
240
+ }
241
+ const observed = calls.map((call) => `${call.namespace}.${call.method}`);
242
+ if (JSON.stringify(observed) !== JSON.stringify(["warehouse.query", "warehouse.count"])) {
243
+ return yield* violation(caseName, `unexpected host call order ${preview(observed)}`);
244
+ }
245
+ if (result.resourceUse.hostCalls !== 2) {
246
+ return yield* violation(
247
+ caseName,
248
+ `expected 2 accounted host calls, got ${result.resourceUse.hostCalls}`,
249
+ );
250
+ }
251
+ }),
252
+ },
253
+ {
254
+ name: "TEST-015 a caught failed host call lets the program branch on the envelope",
255
+ run: expectSuccess(
256
+ "TEST-015 a caught failed host call lets the program branch on the envelope",
257
+ makeRequest(
258
+ "async () => { try { await warehouse.query({ sql: 'x' }); return 'unreachable'; } catch (envelope) { return { caught: envelope }; } }",
259
+ { namespaces: [warehouseNamespace] },
260
+ ),
261
+ respondingHost(() =>
262
+ CodeHostCallFailure.make({ error: { _tag: "ToolInputError", message: "bad input" } }),
263
+ ).host,
264
+ (result) =>
265
+ JSON.stringify(result.value) ===
266
+ JSON.stringify({ caught: { _tag: "ToolInputError", message: "bad input" } })
267
+ ? undefined
268
+ : `the envelope did not round-trip: ${preview(result.value)}`,
269
+ ),
270
+ },
271
+ {
272
+ name: "TEST-015 an uncaught failed host call fails the program with the envelope",
273
+ run: expectFailure(
274
+ "TEST-015 an uncaught failed host call fails the program with the envelope",
275
+ makeRequest("async () => warehouse.query({ sql: 'x' })", {
276
+ namespaces: [warehouseNamespace],
277
+ }),
278
+ respondingHost(() =>
279
+ CodeHostCallFailure.make({ error: { _tag: "PolicyDenied", message: "denied" } }),
280
+ ).host,
281
+ "CodeProgramFailedError",
282
+ (error) =>
283
+ error._tag === "CodeProgramFailedError" &&
284
+ error.reason === "rejected" &&
285
+ JSON.stringify(error.thrown) ===
286
+ JSON.stringify({ _tag: "PolicyDenied", message: "denied" })
287
+ ? undefined
288
+ : `unexpected failure detail ${preview(error)}`,
289
+ ),
290
+ },
291
+ {
292
+ name: "TEST-015 fails typed on syntactically invalid source",
293
+ run: expectFailure(
294
+ "TEST-015 fails typed on syntactically invalid source",
295
+ makeRequest("async () => {"),
296
+ unusedHost,
297
+ "CodeSourceError",
298
+ (error) =>
299
+ error._tag === "CodeSourceError" && error.reason === "invalid"
300
+ ? undefined
301
+ : `expected reason invalid, got ${preview(error)}`,
302
+ ),
303
+ },
304
+ {
305
+ name: "TEST-015 fails typed when the expression is not one async function",
306
+ run: expectFailure(
307
+ "TEST-015 fails typed when the expression is not one async function",
308
+ makeRequest("1 + 1"),
309
+ unusedHost,
310
+ "CodeSourceError",
311
+ (error) =>
312
+ error._tag === "CodeSourceError" && error.reason === "not-a-function"
313
+ ? undefined
314
+ : `expected reason not-a-function, got ${preview(error)}`,
315
+ ),
316
+ },
317
+ {
318
+ name: "TEST-015 fails typed on source larger than the declared byte limit",
319
+ run: expectFailure(
320
+ "TEST-015 fails typed on source larger than the declared byte limit",
321
+ makeRequest(`async () => "${"x".repeat(2_000)}"`, {
322
+ limits: CodeExecutionLimits.make({ ...baseLimits, maxSourceBytes: 256 }),
323
+ }),
324
+ unusedHost,
325
+ "CodeSourceError",
326
+ (error) =>
327
+ error._tag === "CodeSourceError" && error.reason === "oversized"
328
+ ? undefined
329
+ : `expected reason oversized, got ${preview(error)}`,
330
+ ),
331
+ },
332
+ {
333
+ name: "TEST-015 terminates a never-settling program at the wall-clock deadline",
334
+ run: expectFailure(
335
+ "TEST-015 terminates a never-settling program at the wall-clock deadline",
336
+ makeRequest("async () => { await new Promise(() => {}); return 1; }", {
337
+ limits: CodeExecutionLimits.make({ ...baseLimits, maxWallTime: Duration.millis(250) }),
338
+ }),
339
+ unusedHost,
340
+ "CodeExecutionTimeoutError",
341
+ ),
342
+ },
343
+ {
344
+ name: "TEST-015 fails typed when console output exceeds its byte budget",
345
+ run: expectFailure(
346
+ "TEST-015 fails typed when console output exceeds its byte budget",
347
+ makeRequest(
348
+ "async () => { for (let i = 0; i < 64; i += 1) { console.log('x'.repeat(256)); } return 1; }",
349
+ { limits: CodeExecutionLimits.make({ ...baseLimits, maxLogBytes: 2_048 }) },
350
+ ),
351
+ unusedHost,
352
+ "CodeOutputLimitError",
353
+ (error) =>
354
+ error._tag === "CodeOutputLimitError" && error.surface === "logs"
355
+ ? undefined
356
+ : `expected surface logs, got ${preview(error)}`,
357
+ ),
358
+ },
359
+ {
360
+ name: "TEST-015 fails typed when the final result exceeds its byte budget",
361
+ run: expectFailure(
362
+ "TEST-015 fails typed when the final result exceeds its byte budget",
363
+ makeRequest("async () => 'y'.repeat(4096)", {
364
+ limits: CodeExecutionLimits.make({ ...baseLimits, maxResultBytes: 1_024 }),
365
+ }),
366
+ unusedHost,
367
+ "CodeOutputLimitError",
368
+ (error) =>
369
+ error._tag === "CodeOutputLimitError" && error.surface === "result"
370
+ ? undefined
371
+ : `expected surface result, got ${preview(error)}`,
372
+ ),
373
+ },
374
+ {
375
+ name: "TEST-015 fails typed when host calls exceed the executor cap",
376
+ run: Effect.gen(function* () {
377
+ const caseName = "TEST-015 fails typed when host calls exceed the executor cap";
378
+ const { host, calls } = respondingHost(() => CodeHostCallSuccess.make({ value: null }));
379
+ yield* expectFailure(
380
+ caseName,
381
+ makeRequest(
382
+ "async () => { await warehouse.query({}); await warehouse.query({}); await warehouse.query({}); return 1; }",
383
+ {
384
+ namespaces: [warehouseNamespace],
385
+ limits: CodeExecutionLimits.make({ ...baseLimits, maxHostCalls: 2 }),
386
+ },
387
+ ),
388
+ host,
389
+ "CodeHostCallLimitError",
390
+ );
391
+ // The cap is enforced before dispatch: the over-limit call must never
392
+ // have reached the host, or an unauthorized side effect already ran.
393
+ if (calls.length !== 2) {
394
+ return yield* violation(
395
+ caseName,
396
+ `expected exactly 2 dispatched host calls under a cap of 2, observed ${calls.length}`,
397
+ );
398
+ }
399
+ }),
400
+ },
401
+ {
402
+ name: "TEST-015 fails typed on a host outcome outside the protocol schema",
403
+ run: expectFailure(
404
+ "TEST-015 fails typed on a host outcome outside the protocol schema",
405
+ makeRequest("async () => warehouse.query({})", { namespaces: [warehouseNamespace] }),
406
+ {
407
+ call: () => Effect.succeed({ bogus: true } as unknown as CodeHostCallResult),
408
+ },
409
+ "CodeExecutionProtocolError",
410
+ ),
411
+ },
412
+ {
413
+ name: "TEST-015 surfaces an uncaught program throw with its bounded log capture",
414
+ run: expectFailure(
415
+ "TEST-015 surfaces an uncaught program throw with its bounded log capture",
416
+ makeRequest(
417
+ "async () => { console.log('before the failure'); throw new Error('deliberate'); }",
418
+ ),
419
+ unusedHost,
420
+ "CodeProgramFailedError",
421
+ (error) =>
422
+ error._tag === "CodeProgramFailedError" &&
423
+ error.reason === "threw" &&
424
+ error.logs.some((line) => line.includes("before the failure"))
425
+ ? undefined
426
+ : `expected a threw failure carrying the log capture, got ${preview(error)}`,
427
+ ),
428
+ },
429
+ {
430
+ name: "TEST-015 fails typed when the program returns a non-JSON value",
431
+ run: expectFailure(
432
+ "TEST-015 fails typed when the program returns a non-JSON value",
433
+ makeRequest("async () => (() => 1)"),
434
+ unusedHost,
435
+ "CodeProgramFailedError",
436
+ (error) =>
437
+ error._tag === "CodeProgramFailedError" && error.reason === "non-json-result"
438
+ ? undefined
439
+ : `expected reason non-json-result, got ${preview(error)}`,
440
+ ),
441
+ },
442
+ {
443
+ name: "CAP-015 rejects a network allowlist it cannot enforce with a typed unsupported error",
444
+ run: expectFailure(
445
+ "CAP-015 rejects a network allowlist it cannot enforce with a typed unsupported error",
446
+ makeRequest("async () => 1", {
447
+ network: NetworkAllowlist.make({ domains: ["example.com"], ports: [443] }),
448
+ }),
449
+ unusedHost,
450
+ "CodeExecutorUnsupportedError",
451
+ (error) =>
452
+ error._tag === "CodeExecutorUnsupportedError" && error.feature === "network"
453
+ ? undefined
454
+ : `expected feature network, got ${preview(error)}`,
455
+ ),
456
+ },
457
+ {
458
+ name: "TEST-015 interruption reaches in-flight host calls and pass teardown",
459
+ run: Effect.gen(function* () {
460
+ const caseName = "TEST-015 interruption reaches in-flight host calls and pass teardown";
461
+ const started = yield* Deferred.make<void>();
462
+ const witness = { hostCallInterrupted: false };
463
+ const host: CodeExecutionHost["Service"] = {
464
+ call: () =>
465
+ Deferred.succeed(started, undefined).pipe(
466
+ Effect.andThen(Effect.never),
467
+ Effect.ensuring(
468
+ Effect.sync(() => {
469
+ witness.hostCallInterrupted = true;
470
+ }),
471
+ ),
472
+ ),
473
+ };
474
+ const fiber = yield* runPass(
475
+ makeRequest("async () => warehouse.query({})", { namespaces: [warehouseNamespace] }),
476
+ host,
477
+ ).pipe(Effect.forkChild);
478
+ // Guard against a broken adapter that settles the pass without ever
479
+ // reaching the host: the case must report a violation, not hang.
480
+ const winner = yield* Effect.raceFirst(
481
+ Deferred.await(started).pipe(Effect.as("started" as const)),
482
+ Fiber.join(fiber).pipe(Effect.exit, Effect.as("exited" as const)),
483
+ );
484
+ if (winner === "exited") {
485
+ return yield* violation(
486
+ caseName,
487
+ "the pass settled before any host call reached the CodeExecutionHost",
488
+ );
489
+ }
490
+ yield* Fiber.interrupt(fiber);
491
+ if (!witness.hostCallInterrupted) {
492
+ return yield* violation(
493
+ caseName,
494
+ "interrupting the pass did not interrupt the in-flight host call",
495
+ );
496
+ }
497
+ }),
498
+ },
499
+ ];
500
+ };