@robota-sdk/agent-transport 3.0.0-beta.79 → 3.0.0-beta.82

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.
Files changed (113) hide show
  1. package/README.md +84 -26
  2. package/dist/browser/client.d.ts +2 -0
  3. package/dist/browser/client.js +1 -0
  4. package/dist/browser/index.d.ts +520 -0
  5. package/dist/browser/index.d.ts.map +1 -0
  6. package/dist/browser/index.js +2 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/browser/message-decoders-BB8n-H2X.d.ts +252 -0
  9. package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +1 -0
  10. package/dist/browser/message-decoders-CDmDoZPR.js +2 -0
  11. package/dist/browser/message-decoders-CDmDoZPR.js.map +1 -0
  12. package/dist/node/index.cjs +1 -1
  13. package/dist/node/index.d.cts +766 -0
  14. package/dist/node/index.d.cts.map +1 -0
  15. package/dist/node/index.d.ts +765 -27
  16. package/dist/node/index.d.ts.map +1 -1
  17. package/dist/node/index.js +1 -1
  18. package/dist/node/index.js.map +1 -1
  19. package/dist/node/node.cjs +1 -0
  20. package/dist/node/node.d.cts +216 -0
  21. package/dist/node/node.d.cts.map +1 -0
  22. package/dist/node/node.d.ts +216 -0
  23. package/dist/node/node.d.ts.map +1 -0
  24. package/dist/node/node.js +2 -0
  25. package/dist/node/node.js.map +1 -0
  26. package/package.json +68 -47
  27. package/src/__tests__/access-token-verifier.test.ts +672 -0
  28. package/src/__tests__/admission.test.ts +130 -0
  29. package/src/__tests__/channel-frames.test.ts +214 -0
  30. package/src/__tests__/driver-attribution-capability.test.ts +83 -0
  31. package/src/__tests__/file-transfer.test.ts +281 -0
  32. package/src/__tests__/handoff-chunking.test.ts +215 -0
  33. package/src/__tests__/handoff-manifest.test.ts +52 -0
  34. package/src/__tests__/message-decoders.test.ts +249 -0
  35. package/src/__tests__/outbound-delivery.test.ts +558 -0
  36. package/src/__tests__/peer-message-ledger.test.ts +200 -0
  37. package/src/__tests__/personal-usage-report.test.ts +193 -0
  38. package/src/__tests__/resume-buffer.test.ts +93 -0
  39. package/src/__tests__/session-event-delivery.test.ts +101 -0
  40. package/src/__tests__/session-message-handler-e5.test.ts +99 -0
  41. package/src/__tests__/session-message-handler.test.ts +590 -0
  42. package/src/__tests__/session-resume-bridge.test.ts +406 -0
  43. package/src/__tests__/submit-prompt-shape.test.ts +78 -0
  44. package/src/__tests__/usage-report-carrier.test.ts +98 -0
  45. package/src/__tests__/ws-broadcast-events.test.ts +84 -0
  46. package/src/background-messages.ts +169 -0
  47. package/src/channel-frames.ts +182 -0
  48. package/src/client.ts +9 -0
  49. package/src/handoff-chunking.ts +235 -0
  50. package/src/index.ts +112 -3
  51. package/src/message-decoders.ts +229 -0
  52. package/src/message-parser.ts +33 -0
  53. package/src/node/access-token-verifier.ts +363 -0
  54. package/src/node/admission.ts +153 -0
  55. package/src/node/file-transfer.ts +530 -0
  56. package/src/node/handoff-manifest.ts +77 -0
  57. package/src/node/index.ts +27 -0
  58. package/src/outbound-delivery.ts +231 -0
  59. package/src/peer-message-ledger.ts +193 -0
  60. package/src/protocol-session.ts +28 -0
  61. package/src/resume-buffer.ts +123 -0
  62. package/src/session-events.ts +198 -0
  63. package/src/session-message-handler.ts +282 -0
  64. package/src/session-query-messages.ts +42 -0
  65. package/src/session-resume-bridge.ts +252 -0
  66. package/src/usage-messages.ts +195 -0
  67. package/src/wire-messages.ts +165 -0
  68. package/dist/node/headless/index.cjs +0 -1
  69. package/dist/node/headless/index.d.ts +0 -2
  70. package/dist/node/headless/index.js +0 -1
  71. package/dist/node/headless-DG_MEbf0.js +0 -19
  72. package/dist/node/headless-DG_MEbf0.js.map +0 -1
  73. package/dist/node/headless-xq77JBq0.cjs +0 -18
  74. package/dist/node/index-CrJ2Lhn3.d.ts +0 -96
  75. package/dist/node/index-CrJ2Lhn3.d.ts.map +0 -1
  76. package/dist/node/index-JIvzrtaA.d.ts +0 -52
  77. package/dist/node/index-JIvzrtaA.d.ts.map +0 -1
  78. package/dist/node/index-JTD_xtEQ.d.ts +0 -96
  79. package/dist/node/index-JTD_xtEQ.d.ts.map +0 -1
  80. package/dist/node/index-y_HBOfzO.d.ts +0 -52
  81. package/dist/node/index-y_HBOfzO.d.ts.map +0 -1
  82. package/dist/node/programmatic/index.cjs +0 -1
  83. package/dist/node/programmatic/index.d.ts +0 -2
  84. package/dist/node/programmatic/index.js +0 -1
  85. package/dist/node/programmatic-6wLf68qJ.cjs +0 -1
  86. package/dist/node/programmatic-70dxIXtS.js +0 -2
  87. package/dist/node/programmatic-70dxIXtS.js.map +0 -1
  88. package/dist/node/testing/index.cjs +0 -1
  89. package/dist/node/testing/index.d.ts +0 -2
  90. package/dist/node/testing/index.js +0 -1
  91. package/src/__tests__/programmatic/programmatic-ask-user.test.ts +0 -32
  92. package/src/__tests__/programmatic/programmatic-driver.test.ts +0 -107
  93. package/src/__tests__/programmatic/programmatic-flows.test.ts +0 -140
  94. package/src/__tests__/transport-registry.stop-all.test.ts +0 -53
  95. package/src/headless/HeadlessInteractionChannel.ts +0 -129
  96. package/src/headless/__tests__/headless-channel-options.test.ts +0 -106
  97. package/src/headless/__tests__/headless-goal-runner.test.ts +0 -108
  98. package/src/headless/__tests__/headless-provider-failure.integration.test.ts +0 -143
  99. package/src/headless/__tests__/headless-runner-initialization.test.ts +0 -45
  100. package/src/headless/__tests__/headless-runner.test.ts +0 -508
  101. package/src/headless/__tests__/headless-skill-activation.integration.test.ts +0 -430
  102. package/src/headless/__tests__/headless-transport.test.ts +0 -267
  103. package/src/headless/cli-input.ts +0 -50
  104. package/src/headless/headless-runner.ts +0 -230
  105. package/src/headless/headless-stream-json.ts +0 -148
  106. package/src/headless/headless-transport.ts +0 -43
  107. package/src/headless/index.ts +0 -8
  108. package/src/headless/print-terminal.ts +0 -57
  109. package/src/programmatic/ProgrammaticInteractionChannel.ts +0 -82
  110. package/src/programmatic/createProgrammaticAgent.ts +0 -71
  111. package/src/programmatic/index.ts +0 -5
  112. package/src/testing/index.ts +0 -10
  113. package/src/transport-registry.ts +0 -103
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
3
- import { createHeadlessRunner } from '../headless-runner.js';
4
-
5
- describe('createHeadlessRunner initialization', () => {
6
- it('stream-json reads the session id after submit initializes the session', async () => {
7
- const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
8
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
9
- let initialized = false;
10
- const session = {
11
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
12
- if (!listeners.has(event)) listeners.set(event, []);
13
- listeners.get(event)!.push(handler);
14
- }),
15
- off: vi.fn(),
16
- submit: vi.fn(async () => {
17
- initialized = true;
18
- for (const h of listeners.get('text_delta') ?? []) {
19
- h('Hello');
20
- }
21
- for (const h of listeners.get('complete') ?? []) {
22
- h({ response: 'Hello', history: [], toolSummaries: [], contextState: {} });
23
- }
24
- }),
25
- getSession: vi.fn(() => {
26
- if (!initialized) {
27
- throw new Error('InteractiveSession not initialized');
28
- }
29
- return { getSessionId: () => 'initialized-session' };
30
- }),
31
- } as unknown as IInteractiveSession;
32
-
33
- try {
34
- const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
35
- const exitCode = await runner.run('test prompt');
36
-
37
- expect(exitCode).toBe(0);
38
- expect(stdoutWriteSpy).toHaveBeenCalledWith(
39
- expect.stringContaining('"session_id":"initialized-session"'),
40
- );
41
- } finally {
42
- stdoutWriteSpy.mockRestore();
43
- }
44
- });
45
- });
@@ -1,508 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import type {
3
- IExecutionResult,
4
- IInteractiveSession,
5
- TBackgroundJobGroupEvent,
6
- } from '@robota-sdk/agent-interface-transport';
7
-
8
- import { createHeadlessRunner } from '../headless-runner.js';
9
- import type { TBackgroundTaskEvent } from '@robota-sdk/agent-interface-transport';
10
-
11
- function createMockSession(behavior: 'complete' | 'interrupted' | 'error', response = '') {
12
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
13
- return {
14
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
15
- if (!listeners.has(event)) listeners.set(event, []);
16
- listeners.get(event)!.push(handler);
17
- }),
18
- off: vi.fn(),
19
- submit: vi.fn(async () => {
20
- if (behavior === 'complete') {
21
- const result: IExecutionResult = {
22
- response,
23
- history: [],
24
- toolSummaries: [],
25
- contextState: {} as IExecutionResult['contextState'],
26
- };
27
- for (const h of listeners.get('complete') ?? []) {
28
- h(result);
29
- }
30
- } else if (behavior === 'interrupted') {
31
- const result: IExecutionResult = {
32
- response,
33
- history: [],
34
- toolSummaries: [],
35
- contextState: {} as IExecutionResult['contextState'],
36
- };
37
- for (const h of listeners.get('interrupted') ?? []) {
38
- h(result);
39
- }
40
- } else if (behavior === 'error') {
41
- for (const h of listeners.get('error') ?? []) {
42
- h(new Error('test error'));
43
- }
44
- }
45
- }),
46
- executeCommand: vi.fn().mockResolvedValue(null),
47
- getSession: vi.fn(() => ({ getSessionId: () => 'test-session-id' })),
48
- } as unknown as IInteractiveSession;
49
- }
50
-
51
- describe('createHeadlessRunner (text format)', () => {
52
- let stdoutWriteSpy: any; // allow-any: vi.spyOn process.stdout.write has incompatible MockInstance generic bounds
53
-
54
- beforeEach(() => {
55
- stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
56
- });
57
-
58
- afterEach(() => {
59
- stdoutWriteSpy.mockRestore();
60
- });
61
-
62
- it('writes response + newline to stdout and returns exit code 0 on complete', async () => {
63
- const session = createMockSession('complete', 'Hello, world!');
64
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
65
-
66
- const exitCode = await runner.run('test prompt');
67
-
68
- expect(exitCode).toBe(0);
69
- expect(stdoutWriteSpy).toHaveBeenCalledWith('Hello, world!\n');
70
- });
71
-
72
- it('writes partial response on interrupted and returns exit code 0', async () => {
73
- const session = createMockSession('interrupted', 'partial output');
74
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
75
-
76
- const exitCode = await runner.run('test prompt');
77
-
78
- expect(exitCode).toBe(0);
79
- expect(stdoutWriteSpy).toHaveBeenCalledWith('partial output\n');
80
- });
81
-
82
- it('does not write to stdout on interrupted with empty response', async () => {
83
- const session = createMockSession('interrupted', '');
84
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
85
-
86
- const exitCode = await runner.run('test prompt');
87
-
88
- expect(exitCode).toBe(0);
89
- expect(stdoutWriteSpy).not.toHaveBeenCalled();
90
- });
91
-
92
- it('returns exit code 1 on error', async () => {
93
- const session = createMockSession('error');
94
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
95
-
96
- const exitCode = await runner.run('test prompt');
97
-
98
- expect(exitCode).toBe(1);
99
- expect(stdoutWriteSpy).not.toHaveBeenCalled();
100
- });
101
-
102
- it('TC-02 (CLI-064): text format writes the error message to stderr on error', async () => {
103
- const stderrWrites: string[] = [];
104
- const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
105
- stderrWrites.push(String(chunk));
106
- return true;
107
- }) as never);
108
- try {
109
- const session = createMockSession('error');
110
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
111
-
112
- const exitCode = await runner.run('test prompt');
113
-
114
- expect(exitCode).toBe(1);
115
- expect(stderrWrites.join('')).toContain('test error');
116
- } finally {
117
- stderrSpy.mockRestore();
118
- }
119
- });
120
-
121
- it('passes the prompt to session.submit', async () => {
122
- const session = createMockSession('complete', 'ok');
123
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
124
-
125
- await runner.run('my prompt');
126
-
127
- expect(session.submit).toHaveBeenCalledWith('my prompt');
128
- });
129
-
130
- it('executes /agent as a command without submitting to the model', async () => {
131
- const session = {
132
- ...createMockSession('complete', 'unused'),
133
- executeCommand: vi.fn().mockResolvedValue({
134
- message: 'Started agent job: agent_1',
135
- success: true,
136
- data: { agentId: 'agent_1' },
137
- }),
138
- } as unknown as IInteractiveSession;
139
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
140
-
141
- const exitCode = await runner.run('/agent run Plan --background "draft architecture"');
142
-
143
- expect(exitCode).toBe(0);
144
- expect(session.executeCommand).toHaveBeenCalledWith(
145
- 'agent',
146
- 'run Plan --background "draft architecture"',
147
- );
148
- expect(session.submit).not.toHaveBeenCalled();
149
- expect(stdoutWriteSpy).toHaveBeenCalledWith('Started agent job: agent_1\n');
150
- });
151
-
152
- it('executes /skill through SDK command routing without submitting the raw slash prompt', async () => {
153
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
154
- const session = {
155
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
156
- if (!listeners.has(event)) listeners.set(event, []);
157
- listeners.get(event)!.push(handler);
158
- }),
159
- off: vi.fn(),
160
- submit: vi.fn(),
161
- executeCommand: vi.fn().mockImplementation(async () => {
162
- const result: IExecutionResult = {
163
- response: 'Skill response',
164
- history: [],
165
- toolSummaries: [],
166
- contextState: {} as IExecutionResult['contextState'],
167
- };
168
- for (const handler of listeners.get('complete') ?? []) {
169
- handler(result);
170
- }
171
- return {
172
- success: true,
173
- message: '',
174
- effects: [{ type: 'session-execution-started' }],
175
- data: { skill: 'audit', sessionExecution: true },
176
- };
177
- }),
178
- getSession: vi.fn(() => ({ getSessionId: () => 'test-session-id' })),
179
- } as unknown as IInteractiveSession;
180
- const runner = createHeadlessRunner({ session, outputFormat: 'text' });
181
-
182
- const exitCode = await runner.run('/audit src/index.ts');
183
-
184
- expect(exitCode).toBe(0);
185
- expect(session.executeCommand).toHaveBeenCalledWith('audit', 'src/index.ts');
186
- expect(session.submit).not.toHaveBeenCalled();
187
- expect(stdoutWriteSpy).toHaveBeenCalledWith('Skill response\n');
188
- });
189
- });
190
-
191
- describe('createHeadlessRunner (json format)', () => {
192
- let stdoutWriteSpy: any; // allow-any: vi.spyOn process.stdout.write has incompatible MockInstance generic bounds
193
-
194
- beforeEach(() => {
195
- stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
196
- });
197
-
198
- afterEach(() => {
199
- stdoutWriteSpy.mockRestore();
200
- });
201
-
202
- it('json format outputs { type, result, session_id, subtype: success }', async () => {
203
- const session = createMockSession('complete', 'JSON response');
204
- const runner = createHeadlessRunner({ session, outputFormat: 'json' });
205
-
206
- const exitCode = await runner.run('test prompt');
207
-
208
- expect(exitCode).toBe(0);
209
- expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
210
- const output = (stdoutWriteSpy.mock.calls[0] as [string])[0];
211
- const parsed: unknown = JSON.parse(output.trim());
212
- expect(parsed).toEqual({
213
- type: 'result',
214
- result: 'JSON response',
215
- session_id: 'test-session-id',
216
- subtype: 'success',
217
- });
218
- });
219
-
220
- it('json format outputs subtype error on failure', async () => {
221
- const session = createMockSession('error');
222
- const runner = createHeadlessRunner({ session, outputFormat: 'json' });
223
-
224
- const exitCode = await runner.run('test prompt');
225
-
226
- expect(exitCode).toBe(1);
227
- expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
228
- const output = (stdoutWriteSpy.mock.calls[0] as [string])[0];
229
- const parsed: unknown = JSON.parse(output.trim());
230
- expect(parsed).toEqual({
231
- type: 'result',
232
- result: '',
233
- session_id: 'test-session-id',
234
- subtype: 'error',
235
- error_code: 'api_error',
236
- });
237
- });
238
-
239
- it('json format outputs subtype success with partial response on interrupted', async () => {
240
- const session = createMockSession('interrupted', 'partial');
241
- const runner = createHeadlessRunner({ session, outputFormat: 'json' });
242
-
243
- const exitCode = await runner.run('test prompt');
244
-
245
- expect(exitCode).toBe(0);
246
- expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
247
- const output = (stdoutWriteSpy.mock.calls[0] as [string])[0];
248
- const parsed: unknown = JSON.parse(output.trim());
249
- expect(parsed).toEqual({
250
- type: 'result',
251
- result: 'partial',
252
- session_id: 'test-session-id',
253
- subtype: 'success',
254
- });
255
- });
256
- });
257
-
258
- describe('createHeadlessRunner (stream-json format)', () => {
259
- let stdoutWriteSpy: any; // allow-any: vi.spyOn process.stdout.write has incompatible MockInstance generic bounds
260
-
261
- beforeEach(() => {
262
- stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
263
- });
264
-
265
- afterEach(() => {
266
- stdoutWriteSpy.mockRestore();
267
- });
268
-
269
- it('stream-json emits content_block_delta events and final result', async () => {
270
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
271
- const session = {
272
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
273
- if (!listeners.has(event)) listeners.set(event, []);
274
- listeners.get(event)!.push(handler);
275
- }),
276
- off: vi.fn(),
277
- submit: vi.fn(async () => {
278
- for (const h of listeners.get('text_delta') ?? []) {
279
- h('Hello');
280
- h(' world');
281
- }
282
- for (const h of listeners.get('complete') ?? []) {
283
- h({ response: 'Hello world', history: [], toolSummaries: [], contextState: {} });
284
- }
285
- }),
286
- getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
287
- } as unknown as IInteractiveSession;
288
-
289
- const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
290
- const exitCode = await runner.run('test prompt');
291
-
292
- expect(exitCode).toBe(0);
293
-
294
- const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
295
- const parsed: Array<Record<string, unknown>> = lines.map(
296
- (line: string) => JSON.parse(line) as Record<string, unknown>,
297
- );
298
-
299
- // 2 stream_event lines + 1 final result line
300
- expect(parsed).toHaveLength(3);
301
-
302
- // First two are stream events with content_block_delta
303
- const streamEvents = parsed.filter(
304
- (p: Record<string, unknown>) => p['type'] === 'stream_event',
305
- );
306
- expect(streamEvents).toHaveLength(2);
307
-
308
- for (const evt of streamEvents) {
309
- expect(evt['session_id']).toBe('stream-session');
310
- expect(evt['uuid']).toBeDefined();
311
- const inner = evt['event'] as Record<string, unknown>;
312
- expect(inner['type']).toBe('content_block_delta');
313
- const delta = inner['delta'] as Record<string, unknown>;
314
- expect(delta['type']).toBe('text_delta');
315
- }
316
-
317
- const firstDelta = (streamEvents[0]!['event'] as Record<string, unknown>)['delta'] as Record<
318
- string,
319
- unknown
320
- >;
321
- const secondDelta = (streamEvents[1]!['event'] as Record<string, unknown>)['delta'] as Record<
322
- string,
323
- unknown
324
- >;
325
- expect(firstDelta['text']).toBe('Hello');
326
- expect(secondDelta['text']).toBe(' world');
327
-
328
- // Final result line
329
- const resultLine = parsed.find((p: Record<string, unknown>) => p['type'] === 'result');
330
- expect(resultLine).toEqual({
331
- type: 'result',
332
- result: 'Hello world',
333
- session_id: 'stream-session',
334
- subtype: 'success',
335
- });
336
- });
337
-
338
- it('stream-json emits error result on error', async () => {
339
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
340
- const session = {
341
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
342
- if (!listeners.has(event)) listeners.set(event, []);
343
- listeners.get(event)!.push(handler);
344
- }),
345
- off: vi.fn(),
346
- submit: vi.fn(async () => {
347
- for (const h of listeners.get('error') ?? []) {
348
- h(new Error('stream error'));
349
- }
350
- }),
351
- getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
352
- } as unknown as IInteractiveSession;
353
-
354
- const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
355
- const exitCode = await runner.run('test prompt');
356
-
357
- expect(exitCode).toBe(1);
358
-
359
- const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
360
- const parsed: Array<Record<string, unknown>> = lines.map(
361
- (line: string) => JSON.parse(line) as Record<string, unknown>,
362
- );
363
-
364
- expect(parsed).toHaveLength(1);
365
- expect(parsed[0]).toEqual({
366
- type: 'result',
367
- result: '',
368
- session_id: 'stream-session',
369
- subtype: 'error',
370
- error_code: 'api_error',
371
- });
372
- });
373
-
374
- it('stream-json emits background task events before the final result', async () => {
375
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
376
- const session = {
377
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
378
- if (!listeners.has(event)) listeners.set(event, []);
379
- listeners.get(event)!.push(handler);
380
- }),
381
- off: vi.fn(),
382
- submit: vi.fn(),
383
- executeCommand: vi.fn().mockImplementation(async () => {
384
- for (const h of listeners.get('background_task_event') ?? []) {
385
- h({
386
- type: 'background_task_created',
387
- task: {
388
- id: 'agent_1',
389
- kind: 'agent',
390
- label: 'Plan',
391
- status: 'running',
392
- mode: 'background',
393
- parentSessionId: 'stream-session',
394
- depth: 1,
395
- cwd: '/workspace',
396
- updatedAt: '2026-05-01T00:00:00.000Z',
397
- unread: false,
398
- promptPreview: 'draft architecture',
399
- },
400
- } satisfies TBackgroundTaskEvent);
401
- }
402
- return {
403
- message: 'Started agent job: agent_1',
404
- success: true,
405
- data: { agentId: 'agent_1' },
406
- };
407
- }),
408
- getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
409
- } as unknown as IInteractiveSession;
410
-
411
- const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
412
- const exitCode = await runner.run('/agent run Plan --background "draft architecture"');
413
-
414
- expect(exitCode).toBe(0);
415
-
416
- const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
417
- const parsed: Array<Record<string, unknown>> = lines.map(
418
- (line: string) => JSON.parse(line) as Record<string, unknown>,
419
- );
420
-
421
- expect(parsed).toHaveLength(2);
422
- expect(session.submit).not.toHaveBeenCalled();
423
- expect(session.executeCommand).toHaveBeenCalledWith(
424
- 'agent',
425
- 'run Plan --background "draft architecture"',
426
- );
427
- expect(parsed[0]).toMatchObject({
428
- type: 'stream_event',
429
- session_id: 'stream-session',
430
- event: {
431
- type: 'background_task_event',
432
- background_task_event: {
433
- type: 'background_task_created',
434
- },
435
- },
436
- });
437
- expect(parsed[1]).toEqual({
438
- type: 'result',
439
- result: 'Started agent job: agent_1',
440
- session_id: 'stream-session',
441
- subtype: 'success',
442
- });
443
- });
444
-
445
- it('stream-json emits background job group events before the final result', async () => {
446
- const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
447
- const session = {
448
- on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
449
- if (!listeners.has(event)) listeners.set(event, []);
450
- listeners.get(event)!.push(handler);
451
- }),
452
- off: vi.fn(),
453
- submit: vi.fn(),
454
- executeCommand: vi.fn().mockImplementation(async () => {
455
- for (const h of listeners.get('background_job_group_event') ?? []) {
456
- h({
457
- type: 'background_job_group_completed',
458
- group: {
459
- id: 'group_1',
460
- parentSessionId: 'stream-session',
461
- waitPolicy: 'wait_all',
462
- taskIds: ['agent_1'],
463
- status: 'completed',
464
- createdAt: '2026-05-01T00:00:00.000Z',
465
- updatedAt: '2026-05-01T00:00:01.000Z',
466
- completedAt: '2026-05-01T00:00:01.000Z',
467
- results: [{ taskId: 'agent_1', label: 'Plan', status: 'completed' }],
468
- },
469
- } satisfies TBackgroundJobGroupEvent);
470
- }
471
- return {
472
- message: 'Background job group group_1: completed',
473
- success: true,
474
- data: { groupId: 'group_1' },
475
- };
476
- }),
477
- getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
478
- } as unknown as IInteractiveSession;
479
-
480
- const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
481
- const exitCode = await runner.run('/agent wait group_1');
482
-
483
- expect(exitCode).toBe(0);
484
-
485
- const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
486
- const parsed: Array<Record<string, unknown>> = lines.map(
487
- (line: string) => JSON.parse(line) as Record<string, unknown>,
488
- );
489
-
490
- expect(parsed).toHaveLength(2);
491
- expect(parsed[0]).toMatchObject({
492
- type: 'stream_event',
493
- session_id: 'stream-session',
494
- event: {
495
- type: 'background_job_group_event',
496
- background_job_group_event: {
497
- type: 'background_job_group_completed',
498
- },
499
- },
500
- });
501
- expect(parsed[1]).toEqual({
502
- type: 'result',
503
- result: 'Background job group group_1: completed',
504
- session_id: 'stream-session',
505
- subtype: 'success',
506
- });
507
- });
508
- });