@forgeax/engine-intelligence 0.0.0-dev.8d955ade1c79

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.
@@ -0,0 +1,767 @@
1
+ import { ok } from '@forgeax/engine-types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createIntelligenceRuntime } from '../runtime';
4
+ import {
5
+ bindIntelligencePort,
6
+ createIntelligencePortClient,
7
+ type IntelligenceMessagePort,
8
+ } from '../transport';
9
+ import { type ActivitySink, activityId, type IntelligenceProvider } from '../types';
10
+
11
+ describe('intelligence MessagePort boundary', () => {
12
+ it('moves only structured-cloneable POD across a Worker-style channel', async () => {
13
+ const sinks: ActivitySink[] = [];
14
+ let providerClosed = false;
15
+ const provider: IntelligenceProvider = {
16
+ id: 'port.provider',
17
+ start(_submission, sink) {
18
+ sinks.push(sink);
19
+ return ok(undefined);
20
+ },
21
+ cancel() {
22
+ sinks[0]?.cancelled();
23
+ return ok(undefined);
24
+ },
25
+ async close() {
26
+ providerClosed = true;
27
+ },
28
+ };
29
+ const runtime = createIntelligenceRuntime(provider);
30
+ const channel = new MessageChannel();
31
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
32
+ const client = createIntelligencePortClient(
33
+ 'port.provider',
34
+ channel.port2 as unknown as IntelligenceMessagePort,
35
+ {
36
+ createActivityId: () => activityId('worker-activity'),
37
+ createSessionId: () => 'worker-session',
38
+ },
39
+ );
40
+
41
+ const started = client.submit({ input: 'hello worker' });
42
+ expect(started.ok).toBe(true);
43
+ for (let attempt = 0; attempt < 10 && sinks.length === 0; attempt += 1) {
44
+ await new Promise((resolve) => setTimeout(resolve, 5));
45
+ }
46
+ expect(sinks).toHaveLength(1);
47
+ sinks[0]?.text('hello ');
48
+ sinks[0]?.complete('hello worker');
49
+ const received = [] as ReturnType<typeof client.poll>[number][];
50
+ for (let attempt = 0; attempt < 10 && received.length === 0; attempt += 1) {
51
+ received.push(...client.poll());
52
+ await new Promise((resolve) => setTimeout(resolve, 5));
53
+ }
54
+ expect(structuredClone(received)).toEqual([
55
+ {
56
+ type: 'text-delta',
57
+ activityId: 'worker-activity',
58
+ sequence: 1,
59
+ text: 'hello ',
60
+ },
61
+ {
62
+ type: 'completed',
63
+ activityId: 'worker-activity',
64
+ sequence: 2,
65
+ session: { providerId: 'port.provider', id: 'worker-session' },
66
+ output: 'hello worker',
67
+ },
68
+ ]);
69
+
70
+ await client.close();
71
+ expect(providerClosed).toBe(true);
72
+ await host.close();
73
+ });
74
+
75
+ it('bounds realm submissions and releases capacity after a terminal event', async () => {
76
+ const sinks = new Map<string, ActivitySink>();
77
+ const provider: IntelligenceProvider = {
78
+ id: 'bounded.provider',
79
+ start(submission, sink) {
80
+ sinks.set(submission.id, sink);
81
+ return ok(undefined);
82
+ },
83
+ cancel(id) {
84
+ sinks.get(id)?.cancelled();
85
+ sinks.delete(id);
86
+ return ok(undefined);
87
+ },
88
+ async close() {},
89
+ };
90
+ const runtime = createIntelligenceRuntime(provider, {
91
+ limits: { maxConcurrentActivities: 1 },
92
+ });
93
+ const channel = new MessageChannel();
94
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
95
+ let identity = 0;
96
+ const client = createIntelligencePortClient(
97
+ 'bounded.provider',
98
+ channel.port2 as unknown as IntelligenceMessagePort,
99
+ {
100
+ limits: { maxConcurrentActivities: 1 },
101
+ createActivityId: () => {
102
+ identity += 1;
103
+ return activityId(`bounded-${identity}`);
104
+ },
105
+ },
106
+ );
107
+
108
+ const first = client.submit({ input: 'first' });
109
+ expect(first.ok).toBe(true);
110
+ const second = client.submit({ input: 'second' });
111
+ expect(second.ok).toBe(false);
112
+ if (!second.ok) expect(second.error.code).toBe('intelligence-capacity-exceeded');
113
+
114
+ client.poll();
115
+ client.poll();
116
+ for (let attempt = 0; attempt < 10 && sinks.size === 0; attempt += 1) {
117
+ await new Promise((resolve) => setTimeout(resolve, 5));
118
+ }
119
+ if (!first.ok) return;
120
+ sinks.get(first.value.id)?.complete('done');
121
+ for (let attempt = 0; attempt < 10; attempt += 1) {
122
+ client.poll();
123
+ await new Promise((resolve) => setTimeout(resolve, 5));
124
+ }
125
+ expect(client.submit({ input: 'after terminal' }).ok).toBe(true);
126
+
127
+ await client.close();
128
+ await host.close();
129
+ });
130
+
131
+ it('recovers optimistic capacity after an asynchronous Host rejection', async () => {
132
+ const sinks = new Map<string, ActivitySink>();
133
+ const starts: string[] = [];
134
+ const provider: IntelligenceProvider = {
135
+ id: 'host.limit.drift.provider',
136
+ start(submission, sink) {
137
+ starts.push(submission.input);
138
+ sinks.set(String(submission.id), sink);
139
+ return ok(undefined);
140
+ },
141
+ cancel(id) {
142
+ sinks.get(String(id))?.cancelled();
143
+ sinks.delete(String(id));
144
+ return ok(undefined);
145
+ },
146
+ async close() {},
147
+ };
148
+ const runtime = createIntelligenceRuntime(provider, {
149
+ limits: { maxConcurrentActivities: 1 },
150
+ });
151
+ const channel = new MessageChannel();
152
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
153
+ let identity = 0;
154
+ const client = createIntelligencePortClient(
155
+ provider.id,
156
+ channel.port2 as unknown as IntelligenceMessagePort,
157
+ {
158
+ limits: { maxConcurrentActivities: 2 },
159
+ createActivityId: () => {
160
+ identity += 1;
161
+ return activityId(`host-limit-drift-${identity}`);
162
+ },
163
+ createSessionId: () => 'host-limit-drift-session',
164
+ },
165
+ );
166
+
167
+ try {
168
+ const first = client.submit({ input: 'keep-live' });
169
+ const rejected = client.submit({ input: 'host-reject' });
170
+ expect(first.ok).toBe(true);
171
+ expect(rejected.ok).toBe(true);
172
+ if (!first.ok || !rejected.ok) return;
173
+
174
+ const rejectionEvents: ReturnType<typeof client.poll>[number][] = [];
175
+ for (let attempt = 0; attempt < 40 && rejectionEvents.length === 0; attempt += 1) {
176
+ rejectionEvents.push(
177
+ ...client.poll().filter((event) => event.activityId === rejected.value.id),
178
+ );
179
+ if (rejectionEvents.length === 0) await new Promise((resolve) => setTimeout(resolve, 5));
180
+ }
181
+ expect(starts).toEqual(['keep-live']);
182
+ expect(rejectionEvents).toHaveLength(1);
183
+ expect(rejectionEvents[0]).toEqual({
184
+ type: 'failed',
185
+ activityId: rejected.value.id,
186
+ sequence: 1,
187
+ error: expect.objectContaining({
188
+ code: 'intelligence-capacity-exceeded',
189
+ detail: { limit: 1 },
190
+ }),
191
+ });
192
+ const cancelRejected = client.cancel(rejected.value.id);
193
+ expect(cancelRejected.ok).toBe(false);
194
+ if (!cancelRejected.ok)
195
+ expect(cancelRejected.error.code).toBe('intelligence-activity-not-found');
196
+
197
+ sinks.get(String(first.value.id))?.complete('keep-live-ok');
198
+ let firstEvent: ReturnType<typeof client.poll>[number] | undefined;
199
+ for (let attempt = 0; attempt < 40 && firstEvent === undefined; attempt += 1) {
200
+ firstEvent = client.poll().find((event) => event.activityId === first.value.id);
201
+ if (firstEvent === undefined) await new Promise((resolve) => setTimeout(resolve, 5));
202
+ }
203
+ expect(firstEvent).toEqual(
204
+ expect.objectContaining({
205
+ type: 'completed',
206
+ activityId: first.value.id,
207
+ output: 'keep-live-ok',
208
+ }),
209
+ );
210
+
211
+ const retry = client.submit({ input: 'retry', session: rejected.value.session });
212
+ expect(retry.ok).toBe(true);
213
+ if (!retry.ok) return;
214
+ expect(retry.value.session).toEqual(rejected.value.session);
215
+
216
+ for (let attempt = 0; attempt < 40 && !starts.includes('retry'); attempt += 1) {
217
+ await new Promise((resolve) => setTimeout(resolve, 5));
218
+ }
219
+ expect(starts).toEqual(['keep-live', 'retry']);
220
+ sinks.get(String(retry.value.id))?.complete('retry-ok');
221
+
222
+ let retryEvent: ReturnType<typeof client.poll>[number] | undefined;
223
+ for (let attempt = 0; attempt < 40 && retryEvent === undefined; attempt += 1) {
224
+ retryEvent = client.poll().find((event) => event.activityId === retry.value.id);
225
+ if (retryEvent === undefined) await new Promise((resolve) => setTimeout(resolve, 5));
226
+ }
227
+ expect(retryEvent).toEqual(
228
+ expect.objectContaining({
229
+ type: 'completed',
230
+ activityId: retry.value.id,
231
+ session: rejected.value.session,
232
+ output: 'retry-ok',
233
+ }),
234
+ );
235
+ } finally {
236
+ await client.close();
237
+ await host.close();
238
+ }
239
+ });
240
+
241
+ it('rejects a cross-provider SessionRef before transport or identity side effects', async () => {
242
+ const createProvider = (id: string) => {
243
+ const sinks = new Map<string, ActivitySink>();
244
+ const starts: string[] = [];
245
+ let closeCalls = 0;
246
+ let closed = false;
247
+ const provider: IntelligenceProvider = {
248
+ id,
249
+ start(submission, sink) {
250
+ starts.push(submission.input);
251
+ sinks.set(String(submission.id), sink);
252
+ return ok(undefined);
253
+ },
254
+ cancel(activity) {
255
+ sinks.get(String(activity))?.cancelled();
256
+ sinks.delete(String(activity));
257
+ return ok(undefined);
258
+ },
259
+ async close() {
260
+ closeCalls += 1;
261
+ closed = true;
262
+ sinks.clear();
263
+ },
264
+ };
265
+ return {
266
+ provider,
267
+ sinks,
268
+ starts,
269
+ get closeCalls() {
270
+ return closeCalls;
271
+ },
272
+ get closed() {
273
+ return closed;
274
+ },
275
+ };
276
+ };
277
+ const providerA = createProvider('transport.provider.a');
278
+ const providerB = createProvider('transport.provider.b');
279
+ const channelA = new MessageChannel();
280
+ const channelB = new MessageChannel();
281
+ const submittedA: string[] = [];
282
+ const submittedB: string[] = [];
283
+ const observeSubmissions = (port: MessagePort, target: string[]): void => {
284
+ port.addEventListener('message', (event: MessageEvent<unknown>) => {
285
+ const message = event.data;
286
+ if (typeof message !== 'object' || message === null) return;
287
+ const value = message as {
288
+ readonly kind?: unknown;
289
+ readonly submission?: { readonly input?: unknown };
290
+ };
291
+ if (value.kind === 'intelligence-submit' && typeof value.submission?.input === 'string') {
292
+ target.push(value.submission.input);
293
+ }
294
+ });
295
+ port.start();
296
+ };
297
+ observeSubmissions(channelA.port1, submittedA);
298
+ observeSubmissions(channelB.port1, submittedB);
299
+ const hostA = bindIntelligencePort(
300
+ channelA.port1 as unknown as IntelligenceMessagePort,
301
+ createIntelligenceRuntime(providerA.provider, { limits: { maxConcurrentActivities: 1 } }),
302
+ );
303
+ const hostB = bindIntelligencePort(
304
+ channelB.port1 as unknown as IntelligenceMessagePort,
305
+ createIntelligenceRuntime(providerB.provider, { limits: { maxConcurrentActivities: 1 } }),
306
+ );
307
+ let activityCallsA = 0;
308
+ let sessionCallsA = 0;
309
+ let activityCallsB = 0;
310
+ let sessionCallsB = 0;
311
+ const clientA = createIntelligencePortClient(
312
+ providerA.provider.id,
313
+ channelA.port2 as unknown as IntelligenceMessagePort,
314
+ {
315
+ createActivityId: () => activityId(`transport-a-${++activityCallsA}`),
316
+ createSessionId: () => `transport-a-session-${++sessionCallsA}`,
317
+ },
318
+ );
319
+ const clientB = createIntelligencePortClient(
320
+ providerB.provider.id,
321
+ channelB.port2 as unknown as IntelligenceMessagePort,
322
+ {
323
+ limits: { maxConcurrentActivities: 1 },
324
+ createActivityId: () => activityId(`transport-b-${++activityCallsB}`),
325
+ createSessionId: () => `transport-b-session-${++sessionCallsB}`,
326
+ },
327
+ );
328
+ const waitForEvent = async (
329
+ client: typeof clientA,
330
+ predicate: (event: ReturnType<typeof clientA.poll>[number]) => boolean,
331
+ ): Promise<ReturnType<typeof clientA.poll>[number] | undefined> => {
332
+ for (let attempt = 0; attempt < 40; attempt += 1) {
333
+ const event = client.poll().find(predicate);
334
+ if (event !== undefined) return event;
335
+ await new Promise((resolve) => setTimeout(resolve, 5));
336
+ }
337
+ return undefined;
338
+ };
339
+
340
+ try {
341
+ const first = clientA.submit({ input: 'a-first' });
342
+ expect(first.ok).toBe(true);
343
+ if (!first.ok) return;
344
+ for (let attempt = 0; attempt < 40 && providerA.starts.length === 0; attempt += 1) {
345
+ await new Promise((resolve) => setTimeout(resolve, 5));
346
+ }
347
+ expect(providerA.starts).toEqual(['a-first']);
348
+ providerA.sinks.get(String(first.value.id))?.complete('a-first-ok');
349
+ expect(
350
+ (await waitForEvent(clientA, (event) => event.activityId === first.value.id))?.type,
351
+ ).toBe('completed');
352
+
353
+ const mismatch = clientB.submit({ input: 'b-mismatch', session: first.value.session });
354
+ expect(mismatch.ok).toBe(false);
355
+ if (!mismatch.ok) {
356
+ expect(mismatch.error.code).toBe('intelligence-session-provider-mismatch');
357
+ expect(mismatch.error.detail).toEqual({
358
+ expectedProviderId: providerB.provider.id,
359
+ receivedProviderId: providerA.provider.id,
360
+ });
361
+ }
362
+ expect(activityCallsB).toBe(0);
363
+ expect(sessionCallsB).toBe(0);
364
+ expect(submittedB).toEqual([]);
365
+ expect(providerB.starts).toEqual([]);
366
+
367
+ const validB = clientB.submit({ input: 'b-valid' });
368
+ expect(validB.ok).toBe(true);
369
+ if (!validB.ok) return;
370
+ for (let attempt = 0; attempt < 40 && providerB.starts.length === 0; attempt += 1) {
371
+ await new Promise((resolve) => setTimeout(resolve, 5));
372
+ }
373
+ expect(providerB.starts).toEqual(['b-valid']);
374
+ providerB.sinks.get(String(validB.value.id))?.complete('b-valid-ok');
375
+ expect(
376
+ (await waitForEvent(clientB, (event) => event.activityId === validB.value.id))?.type,
377
+ ).toBe('completed');
378
+
379
+ const retryA = clientA.submit({ input: 'a-retry', session: first.value.session });
380
+ expect(retryA.ok).toBe(true);
381
+ if (!retryA.ok) return;
382
+ expect(retryA.value.session).toEqual(first.value.session);
383
+ for (let attempt = 0; attempt < 40 && providerA.starts.length < 2; attempt += 1) {
384
+ await new Promise((resolve) => setTimeout(resolve, 5));
385
+ }
386
+ expect(providerA.starts).toEqual(['a-first', 'a-retry']);
387
+ providerA.sinks.get(String(retryA.value.id))?.complete('a-retry-ok');
388
+ expect(
389
+ (await waitForEvent(clientA, (event) => event.activityId === retryA.value.id))?.type,
390
+ ).toBe('completed');
391
+
392
+ expect(activityCallsA).toBe(2);
393
+ expect(sessionCallsA).toBe(1);
394
+ expect(activityCallsB).toBe(1);
395
+ expect(sessionCallsB).toBe(1);
396
+ expect(submittedA).toEqual(['a-first', 'a-retry']);
397
+ expect(submittedB).toEqual(['b-valid']);
398
+ } finally {
399
+ await Promise.all([clientA.close(), clientB.close()]);
400
+ await Promise.all([hostA.close(), hostB.close()]);
401
+ }
402
+
403
+ expect(providerA.closeCalls).toBe(1);
404
+ expect(providerB.closeCalls).toBe(1);
405
+ expect(providerA.sinks).toHaveLength(0);
406
+ expect(providerB.sinks).toHaveLength(0);
407
+ expect(providerA.closed).toBe(true);
408
+ expect(providerB.closed).toBe(true);
409
+ });
410
+
411
+ it('drains an interleaved backlog with one poll credit and releases terminal capacity', async () => {
412
+ const sinks = new Map<string, ActivitySink>();
413
+ const starts: string[] = [];
414
+ const provider: IntelligenceProvider = {
415
+ id: 'port.poll.credit.provider',
416
+ start(submission, sink) {
417
+ starts.push(submission.input);
418
+ sinks.set(String(submission.id), sink);
419
+ return ok(undefined);
420
+ },
421
+ cancel(id) {
422
+ sinks.get(String(id))?.cancelled();
423
+ return ok(undefined);
424
+ },
425
+ async close() {},
426
+ };
427
+ const runtime = createIntelligenceRuntime(provider, {
428
+ limits: { maxConcurrentActivities: 2, maxPollEvents: 1 },
429
+ });
430
+ const channel = new MessageChannel();
431
+ const rawHostPort = channel.port1;
432
+ let pollRequests = 0;
433
+ let pollResponses = 0;
434
+ let pollOutstanding = 0;
435
+ let maxPollOutstanding = 0;
436
+ rawHostPort.addEventListener('message', (event: MessageEvent<unknown>) => {
437
+ const message = event.data;
438
+ if (typeof message !== 'object' || message === null) return;
439
+ if ((message as { readonly kind?: unknown }).kind !== 'intelligence-poll') return;
440
+ pollRequests += 1;
441
+ pollOutstanding += 1;
442
+ maxPollOutstanding = Math.max(maxPollOutstanding, pollOutstanding);
443
+ });
444
+ const hostPort: IntelligenceMessagePort = {
445
+ postMessage(message) {
446
+ if (message.kind === 'intelligence-events') {
447
+ pollResponses += 1;
448
+ pollOutstanding -= 1;
449
+ }
450
+ rawHostPort.postMessage(message);
451
+ },
452
+ addEventListener(type, listener) {
453
+ rawHostPort.addEventListener(type, listener as EventListener);
454
+ },
455
+ removeEventListener(type, listener) {
456
+ rawHostPort.removeEventListener(type, listener as EventListener);
457
+ },
458
+ start() {
459
+ rawHostPort.start();
460
+ },
461
+ close() {
462
+ rawHostPort.close();
463
+ },
464
+ };
465
+ const host = bindIntelligencePort(hostPort, runtime);
466
+ const client = createIntelligencePortClient(
467
+ provider.id,
468
+ channel.port2 as unknown as IntelligenceMessagePort,
469
+ {
470
+ limits: { maxConcurrentActivities: 2, maxPollEvents: 1 },
471
+ createActivityId: (() => {
472
+ let identity = 0;
473
+ return () => activityId(`poll-credit-${++identity}`);
474
+ })(),
475
+ },
476
+ );
477
+
478
+ try {
479
+ const first = client.submit({ input: 'first' });
480
+ const second = client.submit({ input: 'second' });
481
+ expect(first.ok).toBe(true);
482
+ expect(second.ok).toBe(true);
483
+ if (!first.ok || !second.ok) return;
484
+
485
+ for (let attempt = 0; attempt < 40 && sinks.size < 2; attempt += 1) {
486
+ await new Promise((resolve) => setTimeout(resolve, 1));
487
+ }
488
+ expect(starts).toEqual(['first', 'second']);
489
+ const firstSink = sinks.get(String(first.value.id));
490
+ const secondSink = sinks.get(String(second.value.id));
491
+ expect(firstSink).toBeDefined();
492
+ expect(secondSink).toBeDefined();
493
+ if (firstSink === undefined || secondSink === undefined) return;
494
+
495
+ const emitted: string[] = [];
496
+ emitted.push('first:text-1');
497
+ firstSink.text('first-1');
498
+ emitted.push('second:text-1');
499
+ secondSink.text('second-1');
500
+ emitted.push('first:text-2');
501
+ firstSink.text('first-2');
502
+ emitted.push('second:text-2');
503
+ secondSink.text('second-2');
504
+ emitted.push('first:terminal');
505
+ firstSink.complete('first-ok');
506
+ emitted.push('second:terminal');
507
+ secondSink.complete('second-ok');
508
+ expect(emitted).toEqual([
509
+ 'first:text-1',
510
+ 'second:text-1',
511
+ 'first:text-2',
512
+ 'second:text-2',
513
+ 'first:terminal',
514
+ 'second:terminal',
515
+ ]);
516
+
517
+ client.poll();
518
+ client.poll();
519
+ for (let attempt = 0; attempt < 40 && pollRequests < 1; attempt += 1) {
520
+ await new Promise((resolve) => setTimeout(resolve, 1));
521
+ }
522
+ expect(pollRequests).toBe(1);
523
+ expect(maxPollOutstanding).toBe(1);
524
+
525
+ const received: ReturnType<typeof client.poll>[number][] = [];
526
+ for (let attempt = 0; attempt < 200 && received.length < 6; attempt += 1) {
527
+ received.push(...client.poll());
528
+ await new Promise((resolve) => setTimeout(resolve, 1));
529
+ }
530
+ expect(received).toHaveLength(6);
531
+ for (const id of [first.value.id, second.value.id]) {
532
+ const activityEvents = received.filter((event) => event.activityId === id);
533
+ expect(activityEvents.map((event) => event.sequence)).toEqual([1, 2, 3]);
534
+ expect(activityEvents.at(-1)?.type).toBe('completed');
535
+ }
536
+ expect(new Set(received.map((event) => String(event.activityId)))).toEqual(
537
+ new Set([String(first.value.id), String(second.value.id)]),
538
+ );
539
+ for (let attempt = 0; attempt < 40 && pollOutstanding !== 0; attempt += 1) {
540
+ await new Promise((resolve) => setTimeout(resolve, 1));
541
+ }
542
+ expect(pollOutstanding).toBe(0);
543
+ expect(pollResponses).toBe(pollRequests);
544
+ expect(maxPollOutstanding).toBe(1);
545
+
546
+ const requestsBeforeRecovery = pollRequests;
547
+ const after = client.submit({ input: 'after terminal' });
548
+ expect(after.ok).toBe(true);
549
+ if (!after.ok) return;
550
+ for (let attempt = 0; attempt < 40 && !starts.includes('after terminal'); attempt += 1) {
551
+ await new Promise((resolve) => setTimeout(resolve, 1));
552
+ }
553
+ expect(starts).toEqual(['first', 'second', 'after terminal']);
554
+ sinks.get(String(after.value.id))?.complete('after-ok');
555
+
556
+ const afterEvents: ReturnType<typeof client.poll>[number][] = [];
557
+ for (let attempt = 0; attempt < 100 && afterEvents.length === 0; attempt += 1) {
558
+ afterEvents.push(...client.poll());
559
+ await new Promise((resolve) => setTimeout(resolve, 1));
560
+ }
561
+ expect(afterEvents).toEqual([
562
+ expect.objectContaining({
563
+ type: 'completed',
564
+ activityId: after.value.id,
565
+ sequence: 1,
566
+ output: 'after-ok',
567
+ }),
568
+ ]);
569
+ for (let attempt = 0; attempt < 40 && pollOutstanding !== 0; attempt += 1) {
570
+ await new Promise((resolve) => setTimeout(resolve, 1));
571
+ }
572
+ expect(pollOutstanding).toBe(0);
573
+ expect(pollRequests - requestsBeforeRecovery).toBe(2);
574
+ expect(maxPollOutstanding).toBe(1);
575
+ } finally {
576
+ await client.close();
577
+ await host.close();
578
+ }
579
+ });
580
+
581
+ it('rejects invalid input synchronously before identity, capacity, port dispatch, or provider start', async () => {
582
+ const sinks = new Map<string, ActivitySink>();
583
+ const submittedInputs: string[] = [];
584
+ let activityIdentityCalls = 0;
585
+ let sessionIdentityCalls = 0;
586
+ let providerStarts = 0;
587
+ const provider: IntelligenceProvider = {
588
+ id: 'port.input.boundary.provider',
589
+ start(submission, sink) {
590
+ providerStarts += 1;
591
+ sinks.set(submission.id, sink);
592
+ return ok(undefined);
593
+ },
594
+ cancel() {
595
+ return ok(undefined);
596
+ },
597
+ async close() {},
598
+ };
599
+ const runtime = createIntelligenceRuntime(provider, {
600
+ limits: { maxInputChars: 4, maxConcurrentActivities: 1 },
601
+ });
602
+ const channel = new MessageChannel();
603
+ channel.port1.addEventListener('message', (event: MessageEvent<unknown>) => {
604
+ const message = event.data;
605
+ if (typeof message !== 'object' || message === null) return;
606
+ const value = message as {
607
+ readonly kind?: unknown;
608
+ readonly submission?: { readonly input?: unknown };
609
+ };
610
+ if (value.kind === 'intelligence-submit' && typeof value.submission?.input === 'string') {
611
+ submittedInputs.push(value.submission.input);
612
+ }
613
+ });
614
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
615
+ const client = createIntelligencePortClient(
616
+ 'port.input.boundary.provider',
617
+ channel.port2 as unknown as IntelligenceMessagePort,
618
+ {
619
+ limits: { maxInputChars: 4, maxConcurrentActivities: 1 },
620
+ createActivityId: () => {
621
+ activityIdentityCalls += 1;
622
+ return activityId('port-input-boundary-activity');
623
+ },
624
+ createSessionId: () => {
625
+ sessionIdentityCalls += 1;
626
+ return 'port-input-boundary-session';
627
+ },
628
+ },
629
+ );
630
+
631
+ const exact = client.submit({ input: 'abcd' });
632
+ expect(exact.ok).toBe(true);
633
+ if (!exact.ok) return;
634
+ expect(activityIdentityCalls).toBe(1);
635
+ expect(sessionIdentityCalls).toBe(1);
636
+ expect(providerStarts).toBe(0);
637
+
638
+ const empty = client.submit({ input: '' });
639
+ expect(empty.ok).toBe(false);
640
+ if (!empty.ok) {
641
+ expect(empty.error.code).toBe('intelligence-invalid-request');
642
+ expect(empty.error.detail).toEqual({ field: 'input', reason: 'input is empty' });
643
+ }
644
+
645
+ const overLimit = client.submit({ input: 'abcde' });
646
+ expect(overLimit.ok).toBe(false);
647
+ if (!overLimit.ok) {
648
+ expect(overLimit.error.code).toBe('intelligence-invalid-request');
649
+ expect(overLimit.error.detail).toEqual({
650
+ field: 'input',
651
+ reason: 'input exceeds 4 characters',
652
+ });
653
+ }
654
+
655
+ expect(activityIdentityCalls).toBe(1);
656
+ expect(sessionIdentityCalls).toBe(1);
657
+ expect(providerStarts).toBe(0);
658
+
659
+ for (let attempt = 0; attempt < 10 && providerStarts === 0; attempt += 1) {
660
+ await new Promise((resolve) => setTimeout(resolve, 5));
661
+ }
662
+ expect(providerStarts).toBe(1);
663
+ expect(submittedInputs).toEqual(['abcd']);
664
+ sinks.get(exact.value.id)?.complete('done');
665
+
666
+ let received: readonly ReturnType<typeof client.poll>[number][] = [];
667
+ for (let attempt = 0; attempt < 10 && received.length === 0; attempt += 1) {
668
+ received = client.poll();
669
+ await new Promise((resolve) => setTimeout(resolve, 5));
670
+ }
671
+ expect(received).toEqual([
672
+ {
673
+ type: 'completed',
674
+ activityId: exact.value.id,
675
+ sequence: 1,
676
+ session: exact.value.session,
677
+ output: 'done',
678
+ },
679
+ ]);
680
+
681
+ await client.close();
682
+ await host.close();
683
+ });
684
+
685
+ it('settles client close after a rejecting Host provider and permits a fresh binding', async () => {
686
+ let closeCalls = 0;
687
+ let starts = 0;
688
+ const provider: IntelligenceProvider = {
689
+ id: 'port.close.rejection.provider',
690
+ start(_submission, sink) {
691
+ starts += 1;
692
+ sink.complete('closed-run');
693
+ return ok(undefined);
694
+ },
695
+ cancel() {
696
+ return ok(undefined);
697
+ },
698
+ async close() {
699
+ closeCalls += 1;
700
+ throw new Error('sentinel Host close rejection');
701
+ },
702
+ };
703
+ const runtime = createIntelligenceRuntime(provider);
704
+ const channel = new MessageChannel();
705
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
706
+ const client = createIntelligencePortClient(
707
+ 'port.close.rejection.provider',
708
+ channel.port2 as unknown as IntelligenceMessagePort,
709
+ );
710
+
711
+ try {
712
+ expect(client.submit({ input: 'first binding' }).ok).toBe(true);
713
+ for (let attempt = 0; attempt < 20 && starts === 0; attempt += 1) {
714
+ await new Promise((resolve) => setTimeout(resolve, 5));
715
+ }
716
+ expect(starts).toBe(1);
717
+
718
+ const closeSettled = await Promise.race([
719
+ client.close().then(
720
+ () => true,
721
+ () => false,
722
+ ),
723
+ new Promise<false>((resolve) => setTimeout(() => resolve(false), 250)),
724
+ ]);
725
+ expect(closeSettled).toBe(true);
726
+ expect(closeCalls).toBe(1);
727
+
728
+ const freshChannel = new MessageChannel();
729
+ const freshProvider: IntelligenceProvider = {
730
+ id: 'port.close.recovery.provider',
731
+ start(_submission, sink) {
732
+ sink.complete('fresh-run');
733
+ return ok(undefined);
734
+ },
735
+ cancel() {
736
+ return ok(undefined);
737
+ },
738
+ async close() {},
739
+ };
740
+ const freshHost = bindIntelligencePort(
741
+ freshChannel.port1 as unknown as IntelligenceMessagePort,
742
+ createIntelligenceRuntime(freshProvider),
743
+ );
744
+ const freshClient = createIntelligencePortClient(
745
+ 'port.close.recovery.provider',
746
+ freshChannel.port2 as unknown as IntelligenceMessagePort,
747
+ );
748
+ try {
749
+ const freshStarted = freshClient.submit({ input: 'fresh binding' });
750
+ expect(freshStarted.ok).toBe(true);
751
+ let freshEvents: readonly ReturnType<typeof freshClient.poll>[number][] = [];
752
+ for (let attempt = 0; attempt < 20 && freshEvents.length === 0; attempt += 1) {
753
+ freshEvents = freshClient.poll();
754
+ await new Promise((resolve) => setTimeout(resolve, 5));
755
+ }
756
+ expect(freshEvents).toEqual([
757
+ expect.objectContaining({ type: 'completed', output: 'fresh-run' }),
758
+ ]);
759
+ await freshClient.close();
760
+ } finally {
761
+ await freshHost.close().catch(() => undefined);
762
+ }
763
+ } finally {
764
+ await host.close().catch(() => undefined);
765
+ }
766
+ });
767
+ });