@lumenflow/surfaces 5.1.0
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 +190 -0
- package/README.md +40 -0
- package/cli/__tests__/gates.test.ts +97 -0
- package/cli/__tests__/inspect.test.ts +184 -0
- package/cli/__tests__/task-lifecycle.test.ts +203 -0
- package/cli/gates.ts +46 -0
- package/cli/index.ts +6 -0
- package/cli/inspect.ts +138 -0
- package/cli/task-lifecycle.ts +46 -0
- package/http/__tests__/agent-runtime-remote-controls.test.ts +249 -0
- package/http/__tests__/auth-boundary.test.ts +57 -0
- package/http/__tests__/channel-send-governance.test.ts +158 -0
- package/http/__tests__/event-stream.test.ts +340 -0
- package/http/__tests__/phone-device-tool-api.test.ts +177 -0
- package/http/__tests__/remote-exposure.test.ts +212 -0
- package/http/__tests__/run-agent.test.ts +447 -0
- package/http/__tests__/scope-enforcement.test.ts +349 -0
- package/http/__tests__/sidecar-entry.test.ts +158 -0
- package/http/__tests__/tool-api-schema-validation.test.ts +213 -0
- package/http/__tests__/tool-api.test.ts +491 -0
- package/http/__tests__/tool-discovery.test.ts +384 -0
- package/http/ag-ui-adapter.ts +352 -0
- package/http/auth.ts +294 -0
- package/http/control-plane-event-subscriber.ts +233 -0
- package/http/event-stream.ts +216 -0
- package/http/index.ts +10 -0
- package/http/run-agent.ts +416 -0
- package/http/server.ts +329 -0
- package/http/sidecar-entry.ts +218 -0
- package/http/task-api.ts +307 -0
- package/http/tool-api.ts +373 -0
- package/http/tool-discovery.ts +159 -0
- package/mcp/__tests__/server.test.ts +554 -0
- package/mcp/index.ts +4 -0
- package/mcp/server.ts +250 -0
- package/package.json +51 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { PassThrough } from 'node:stream';
|
|
7
|
+
import type {
|
|
8
|
+
Disposable,
|
|
9
|
+
KernelEvent,
|
|
10
|
+
KernelRuntime,
|
|
11
|
+
ReplayFilter,
|
|
12
|
+
TaskSpec,
|
|
13
|
+
} from '@lumenflow/kernel';
|
|
14
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
15
|
+
import { createRunAgentRouter, type RunAgentConfig, type RunAgentInput } from '../run-agent.js';
|
|
16
|
+
import { AG_UI_EVENT_TYPES } from '../ag-ui-adapter.js';
|
|
17
|
+
import type { EventSubscriber } from '../event-stream.js';
|
|
18
|
+
|
|
19
|
+
// --- Constants ---
|
|
20
|
+
|
|
21
|
+
const WORKSPACE_ID = 'ws-test-1923';
|
|
22
|
+
|
|
23
|
+
const HTTP_METHOD = {
|
|
24
|
+
POST: 'POST',
|
|
25
|
+
GET: 'GET',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
const HTTP_STATUS = {
|
|
29
|
+
OK: 200,
|
|
30
|
+
BAD_REQUEST: 400,
|
|
31
|
+
METHOD_NOT_ALLOWED: 405,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const HTTP_HEADERS = {
|
|
35
|
+
CONTENT_TYPE: 'content-type',
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const CONTENT_TYPE = {
|
|
39
|
+
JSON: 'application/json; charset=utf-8',
|
|
40
|
+
EVENT_STREAM: 'text/event-stream; charset=utf-8',
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const TIMESTAMP = {
|
|
44
|
+
ZERO: '2026-02-20T00:00:00.000Z',
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
const RUN_ID = 'run-task-1923-1';
|
|
48
|
+
|
|
49
|
+
// --- Test helpers ---
|
|
50
|
+
|
|
51
|
+
class MockResponse extends EventEmitter {
|
|
52
|
+
statusCode = HTTP_STATUS.OK;
|
|
53
|
+
body = '';
|
|
54
|
+
readonly headers = new Map<string, string>();
|
|
55
|
+
ended = false;
|
|
56
|
+
|
|
57
|
+
setHeader(name: string, value: string | number | readonly string[]): this {
|
|
58
|
+
this.headers.set(name.toLowerCase(), String(value));
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
write(chunk: string | Buffer): boolean {
|
|
63
|
+
this.body += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
end(chunk?: string | Buffer): this {
|
|
68
|
+
if (chunk !== undefined) {
|
|
69
|
+
this.write(chunk);
|
|
70
|
+
}
|
|
71
|
+
this.ended = true;
|
|
72
|
+
this.emit('finish');
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createRequest(options: { method: string; url: string; body?: unknown }): IncomingMessage {
|
|
78
|
+
const request = new PassThrough() as unknown as IncomingMessage & {
|
|
79
|
+
method: string;
|
|
80
|
+
url: string;
|
|
81
|
+
headers: IncomingHttpHeaders;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
request.method = options.method;
|
|
85
|
+
request.url = options.url;
|
|
86
|
+
request.headers = {
|
|
87
|
+
[HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPE.JSON,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const payload = options.body === undefined ? '' : JSON.stringify(options.body);
|
|
91
|
+
(request as unknown as PassThrough).end(payload);
|
|
92
|
+
return request;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createRunAgentInput(): RunAgentInput {
|
|
96
|
+
return {
|
|
97
|
+
threadId: 'thread-1923',
|
|
98
|
+
runId: 'copilot-run-1923',
|
|
99
|
+
messages: [
|
|
100
|
+
{
|
|
101
|
+
id: 'msg-1',
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: 'List all tasks',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
tools: [
|
|
107
|
+
{
|
|
108
|
+
name: 'task.list',
|
|
109
|
+
description: 'Lists available tasks',
|
|
110
|
+
parameters: { type: 'object', properties: {} },
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createRuntimeStub(): Pick<
|
|
117
|
+
KernelRuntime,
|
|
118
|
+
'createTask' | 'claimTask' | 'completeTask' | 'inspectTask'
|
|
119
|
+
> {
|
|
120
|
+
return {
|
|
121
|
+
createTask: vi.fn(async (taskSpec: TaskSpec) => ({
|
|
122
|
+
task: taskSpec,
|
|
123
|
+
task_spec_path: `/tmp/${taskSpec.id}.yaml`,
|
|
124
|
+
event: {
|
|
125
|
+
schema_version: 1,
|
|
126
|
+
kind: 'task_created' as const,
|
|
127
|
+
task_id: taskSpec.id,
|
|
128
|
+
timestamp: TIMESTAMP.ZERO,
|
|
129
|
+
spec_hash: 'spec-hash',
|
|
130
|
+
},
|
|
131
|
+
})),
|
|
132
|
+
claimTask: vi.fn(async (input) => ({
|
|
133
|
+
task_id: input.task_id,
|
|
134
|
+
run: {
|
|
135
|
+
run_id: RUN_ID,
|
|
136
|
+
task_id: input.task_id,
|
|
137
|
+
status: 'executing',
|
|
138
|
+
started_at: TIMESTAMP.ZERO,
|
|
139
|
+
by: input.by,
|
|
140
|
+
session_id: input.session_id,
|
|
141
|
+
},
|
|
142
|
+
events: [
|
|
143
|
+
{
|
|
144
|
+
schema_version: 1,
|
|
145
|
+
kind: 'task_claimed' as const,
|
|
146
|
+
task_id: input.task_id,
|
|
147
|
+
timestamp: TIMESTAMP.ZERO,
|
|
148
|
+
by: input.by,
|
|
149
|
+
session_id: input.session_id,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
schema_version: 1,
|
|
153
|
+
kind: 'run_started' as const,
|
|
154
|
+
task_id: input.task_id,
|
|
155
|
+
run_id: RUN_ID,
|
|
156
|
+
timestamp: TIMESTAMP.ZERO,
|
|
157
|
+
by: input.by,
|
|
158
|
+
session_id: input.session_id,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
policy: { decision: 'allow', decisions: [] },
|
|
162
|
+
})),
|
|
163
|
+
completeTask: vi.fn(async (input) => ({
|
|
164
|
+
task_id: input.task_id,
|
|
165
|
+
run_id: input.run_id ?? RUN_ID,
|
|
166
|
+
events: [],
|
|
167
|
+
policy: { decision: 'allow', decisions: [] },
|
|
168
|
+
})),
|
|
169
|
+
inspectTask: vi.fn(async (taskId: string) => ({ task_id: taskId })),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createEventSubscriberStub(): {
|
|
174
|
+
subscriber: EventSubscriber;
|
|
175
|
+
triggerEvent: (event: KernelEvent) => void;
|
|
176
|
+
dispose: ReturnType<typeof vi.fn>;
|
|
177
|
+
} {
|
|
178
|
+
const dispose = vi.fn();
|
|
179
|
+
let callback: ((event: KernelEvent) => void | Promise<void>) | null = null;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
subscriber: {
|
|
183
|
+
subscribe: vi.fn((_filter: ReplayFilter, cb: (event: KernelEvent) => void) => {
|
|
184
|
+
callback = cb;
|
|
185
|
+
return { dispose } satisfies Disposable;
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
triggerEvent: (event: KernelEvent) => {
|
|
189
|
+
callback?.(event);
|
|
190
|
+
},
|
|
191
|
+
dispose,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createDefaultConfig(): RunAgentConfig {
|
|
196
|
+
return {
|
|
197
|
+
workspaceId: WORKSPACE_ID,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Tests ---
|
|
202
|
+
|
|
203
|
+
describe('RunAgent workspace_id from config (B-WSVAL)', () => {
|
|
204
|
+
it('passes workspace_id from config to createTask, not hardcoded ag-ui', async () => {
|
|
205
|
+
const runtime = createRuntimeStub();
|
|
206
|
+
const eventStub = createEventSubscriberStub();
|
|
207
|
+
const config = createDefaultConfig();
|
|
208
|
+
|
|
209
|
+
const router = createRunAgentRouter(
|
|
210
|
+
runtime as unknown as KernelRuntime,
|
|
211
|
+
eventStub.subscriber,
|
|
212
|
+
config,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const request = createRequest({
|
|
216
|
+
method: HTTP_METHOD.POST,
|
|
217
|
+
url: '/ag-ui/v1/run',
|
|
218
|
+
body: createRunAgentInput(),
|
|
219
|
+
});
|
|
220
|
+
const response = new MockResponse();
|
|
221
|
+
|
|
222
|
+
await router.handleRequest(
|
|
223
|
+
request as IncomingMessage,
|
|
224
|
+
response as unknown as ServerResponse<IncomingMessage>,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Allow time for async event processing
|
|
228
|
+
const createTaskArg = (runtime.createTask as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
|
229
|
+
expect(createTaskArg).toBeDefined();
|
|
230
|
+
expect(createTaskArg.workspace_id).toBe(WORKSPACE_ID);
|
|
231
|
+
expect(createTaskArg.workspace_id).not.toBe('ag-ui');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('RunAgent acceptance non-empty (B-ACCEPT)', () => {
|
|
236
|
+
it('builds TaskSpec with at least one acceptance criterion', async () => {
|
|
237
|
+
const runtime = createRuntimeStub();
|
|
238
|
+
const eventStub = createEventSubscriberStub();
|
|
239
|
+
const config = createDefaultConfig();
|
|
240
|
+
|
|
241
|
+
const router = createRunAgentRouter(
|
|
242
|
+
runtime as unknown as KernelRuntime,
|
|
243
|
+
eventStub.subscriber,
|
|
244
|
+
config,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const request = createRequest({
|
|
248
|
+
method: HTTP_METHOD.POST,
|
|
249
|
+
url: '/ag-ui/v1/run',
|
|
250
|
+
body: createRunAgentInput(),
|
|
251
|
+
});
|
|
252
|
+
const response = new MockResponse();
|
|
253
|
+
|
|
254
|
+
await router.handleRequest(
|
|
255
|
+
request as IncomingMessage,
|
|
256
|
+
response as unknown as ServerResponse<IncomingMessage>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const createTaskArg = (runtime.createTask as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
|
|
260
|
+
expect(createTaskArg).toBeDefined();
|
|
261
|
+
expect(Array.isArray(createTaskArg.acceptance)).toBe(true);
|
|
262
|
+
expect(createTaskArg.acceptance.length).toBeGreaterThanOrEqual(1);
|
|
263
|
+
expect(typeof createTaskArg.acceptance[0]).toBe('string');
|
|
264
|
+
expect(createTaskArg.acceptance[0].length).toBeGreaterThan(0);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('RunAgent subscribes to events and streams them (B-RUNAG)', () => {
|
|
269
|
+
it('subscribes to task events via EventSubscriber', async () => {
|
|
270
|
+
const runtime = createRuntimeStub();
|
|
271
|
+
const eventStub = createEventSubscriberStub();
|
|
272
|
+
const config = createDefaultConfig();
|
|
273
|
+
|
|
274
|
+
const router = createRunAgentRouter(
|
|
275
|
+
runtime as unknown as KernelRuntime,
|
|
276
|
+
eventStub.subscriber,
|
|
277
|
+
config,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const request = createRequest({
|
|
281
|
+
method: HTTP_METHOD.POST,
|
|
282
|
+
url: '/ag-ui/v1/run',
|
|
283
|
+
body: createRunAgentInput(),
|
|
284
|
+
});
|
|
285
|
+
const response = new MockResponse();
|
|
286
|
+
|
|
287
|
+
await router.handleRequest(
|
|
288
|
+
request as IncomingMessage,
|
|
289
|
+
response as unknown as ServerResponse<IncomingMessage>,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(eventStub.subscriber.subscribe).toHaveBeenCalledTimes(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('streams kernel events as AG-UI events to the response', async () => {
|
|
296
|
+
const runtime = createRuntimeStub();
|
|
297
|
+
const eventStub = createEventSubscriberStub();
|
|
298
|
+
const config = createDefaultConfig();
|
|
299
|
+
|
|
300
|
+
const router = createRunAgentRouter(
|
|
301
|
+
runtime as unknown as KernelRuntime,
|
|
302
|
+
eventStub.subscriber,
|
|
303
|
+
config,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const request = createRequest({
|
|
307
|
+
method: HTTP_METHOD.POST,
|
|
308
|
+
url: '/ag-ui/v1/run',
|
|
309
|
+
body: createRunAgentInput(),
|
|
310
|
+
});
|
|
311
|
+
const response = new MockResponse();
|
|
312
|
+
|
|
313
|
+
await router.handleRequest(
|
|
314
|
+
request as IncomingMessage,
|
|
315
|
+
response as unknown as ServerResponse<IncomingMessage>,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Simulate a kernel event being emitted
|
|
319
|
+
const kernelEvent = {
|
|
320
|
+
schema_version: 1,
|
|
321
|
+
kind: 'task_claimed',
|
|
322
|
+
task_id: 'some-task-id',
|
|
323
|
+
timestamp: TIMESTAMP.ZERO,
|
|
324
|
+
by: 'test',
|
|
325
|
+
session_id: 'test-session',
|
|
326
|
+
} as unknown as KernelEvent;
|
|
327
|
+
|
|
328
|
+
eventStub.triggerEvent(kernelEvent);
|
|
329
|
+
|
|
330
|
+
// The streamed event should appear in the response body
|
|
331
|
+
const lines = response.body.split('\n').filter((line) => line.trim().length > 0);
|
|
332
|
+
// Should have at least RUN_STARTED + the streamed kernel event
|
|
333
|
+
expect(lines.length).toBeGreaterThanOrEqual(2);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('emits RUN_COMPLETED only when task_completed kernel event arrives', async () => {
|
|
337
|
+
const runtime = createRuntimeStub();
|
|
338
|
+
const eventStub = createEventSubscriberStub();
|
|
339
|
+
const config = createDefaultConfig();
|
|
340
|
+
|
|
341
|
+
const router = createRunAgentRouter(
|
|
342
|
+
runtime as unknown as KernelRuntime,
|
|
343
|
+
eventStub.subscriber,
|
|
344
|
+
config,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const request = createRequest({
|
|
348
|
+
method: HTTP_METHOD.POST,
|
|
349
|
+
url: '/ag-ui/v1/run',
|
|
350
|
+
body: createRunAgentInput(),
|
|
351
|
+
});
|
|
352
|
+
const response = new MockResponse();
|
|
353
|
+
|
|
354
|
+
// handleRequest returns but should NOT have ended the response yet
|
|
355
|
+
// (it stays open waiting for events)
|
|
356
|
+
const handlePromise = router.handleRequest(
|
|
357
|
+
request as IncomingMessage,
|
|
358
|
+
response as unknown as ServerResponse<IncomingMessage>,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Wait for the handler to set up
|
|
362
|
+
await handlePromise;
|
|
363
|
+
|
|
364
|
+
// Before task_completed, the response should NOT contain RUN_COMPLETED
|
|
365
|
+
const linesBeforeComplete = response.body.split('\n').filter((line) => line.trim().length > 0);
|
|
366
|
+
const hasRunCompletedBefore = linesBeforeComplete.some((line) => {
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(line);
|
|
369
|
+
return parsed.type === AG_UI_EVENT_TYPES.RUN_COMPLETED;
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
expect(hasRunCompletedBefore).toBe(false);
|
|
375
|
+
|
|
376
|
+
// Now emit the task_completed kernel event
|
|
377
|
+
const taskCompletedEvent = {
|
|
378
|
+
schema_version: 1,
|
|
379
|
+
kind: 'task_completed',
|
|
380
|
+
task_id: 'some-task-id',
|
|
381
|
+
timestamp: '2026-02-20T00:00:05.000Z',
|
|
382
|
+
} as unknown as KernelEvent;
|
|
383
|
+
|
|
384
|
+
eventStub.triggerEvent(taskCompletedEvent);
|
|
385
|
+
|
|
386
|
+
// Now RUN_COMPLETED should appear
|
|
387
|
+
const linesAfterComplete = response.body.split('\n').filter((line) => line.trim().length > 0);
|
|
388
|
+
const hasRunCompletedAfter = linesAfterComplete.some((line) => {
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(line);
|
|
391
|
+
return parsed.type === AG_UI_EVENT_TYPES.RUN_COMPLETED;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
expect(hasRunCompletedAfter).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('does NOT emit fake RUN_COMPLETED immediately after RUN_STARTED', async () => {
|
|
400
|
+
const runtime = createRuntimeStub();
|
|
401
|
+
const eventStub = createEventSubscriberStub();
|
|
402
|
+
const config = createDefaultConfig();
|
|
403
|
+
|
|
404
|
+
const router = createRunAgentRouter(
|
|
405
|
+
runtime as unknown as KernelRuntime,
|
|
406
|
+
eventStub.subscriber,
|
|
407
|
+
config,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const request = createRequest({
|
|
411
|
+
method: HTTP_METHOD.POST,
|
|
412
|
+
url: '/ag-ui/v1/run',
|
|
413
|
+
body: createRunAgentInput(),
|
|
414
|
+
});
|
|
415
|
+
const response = new MockResponse();
|
|
416
|
+
|
|
417
|
+
await router.handleRequest(
|
|
418
|
+
request as IncomingMessage,
|
|
419
|
+
response as unknown as ServerResponse<IncomingMessage>,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// Parse all events from the response body at this point (before any
|
|
423
|
+
// kernel events are triggered via the subscriber)
|
|
424
|
+
const lines = response.body.split('\n').filter((line) => line.trim().length > 0);
|
|
425
|
+
const events = lines
|
|
426
|
+
.map((line) => {
|
|
427
|
+
try {
|
|
428
|
+
return JSON.parse(line);
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
.filter(Boolean);
|
|
434
|
+
|
|
435
|
+
// RUN_STARTED should be the only (or first) event
|
|
436
|
+
const runStartedEvents = events.filter(
|
|
437
|
+
(e: Record<string, unknown>) => e.type === AG_UI_EVENT_TYPES.RUN_STARTED,
|
|
438
|
+
);
|
|
439
|
+
expect(runStartedEvents.length).toBe(1);
|
|
440
|
+
|
|
441
|
+
// RUN_COMPLETED should NOT be present yet (no execution happened)
|
|
442
|
+
const runCompletedEvents = events.filter(
|
|
443
|
+
(e: Record<string, unknown>) => e.type === AG_UI_EVENT_TYPES.RUN_COMPLETED,
|
|
444
|
+
);
|
|
445
|
+
expect(runCompletedEvents.length).toBe(0);
|
|
446
|
+
});
|
|
447
|
+
});
|