@robota-sdk/agent-transport 3.0.0-beta.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/node/headless/index.cjs +1 -0
- package/dist/node/headless/index.d.ts +2 -0
- package/dist/node/headless/index.js +1 -0
- package/dist/node/headless-CWEpJXFK.js +7 -0
- package/dist/node/headless-CWEpJXFK.js.map +1 -0
- package/dist/node/headless-CsZFelG9.cjs +6 -0
- package/dist/node/http/index.cjs +1 -0
- package/dist/node/http/index.d.ts +2 -0
- package/dist/node/http/index.js +1 -0
- package/dist/node/http-CM3TJhrF.cjs +1 -0
- package/dist/node/http-DwO1AHG-.js +2 -0
- package/dist/node/http-DwO1AHG-.js.map +1 -0
- package/dist/node/index--Ti9NzQX.d.ts +64 -0
- package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
- package/dist/node/index-B_rcr14p.d.ts +47 -0
- package/dist/node/index-B_rcr14p.d.ts.map +1 -0
- package/dist/node/index-C9LWCL4l.d.ts +34 -0
- package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
- package/dist/node/index-CAr3ioVh.d.ts +64 -0
- package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
- package/dist/node/index-CEs25wVk.d.ts +213 -0
- package/dist/node/index-CEs25wVk.d.ts.map +1 -0
- package/dist/node/index-CvXLpjJO.d.ts +213 -0
- package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
- package/dist/node/index-D34WUfFH.d.ts +26 -0
- package/dist/node/index-D34WUfFH.d.ts.map +1 -0
- package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
- package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
- package/dist/node/index-k3TUjA-T.d.ts +26 -0
- package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
- package/dist/node/index-nBlMTFkZ.d.ts +34 -0
- package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +1 -0
- package/dist/node/mcp/index.cjs +1 -0
- package/dist/node/mcp/index.d.ts +2 -0
- package/dist/node/mcp/index.js +1 -0
- package/dist/node/mcp-BXBwF6Wu.js +2 -0
- package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
- package/dist/node/mcp-DcHuGokt.cjs +1 -0
- package/dist/node/tui/index.cjs +1 -0
- package/dist/node/tui/index.d.ts +2 -0
- package/dist/node/tui/index.js +1 -0
- package/dist/node/tui-CeD_6rSo.cjs +24 -0
- package/dist/node/tui-zmDTPk4b.js +25 -0
- package/dist/node/tui-zmDTPk4b.js.map +1 -0
- package/dist/node/ws/index.cjs +1 -0
- package/dist/node/ws/index.d.ts +2 -0
- package/dist/node/ws/index.js +1 -0
- package/dist/node/ws-B-oRccFl.js +2 -0
- package/dist/node/ws-B-oRccFl.js.map +1 -0
- package/dist/node/ws-COnIgnmn.cjs +1 -0
- package/package.json +141 -0
- package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
- package/src/headless/__tests__/headless-runner.test.ts +484 -0
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
- package/src/headless/__tests__/headless-transport.test.ts +268 -0
- package/src/headless/headless-runner.ts +141 -0
- package/src/headless/headless-stream-json.ts +142 -0
- package/src/headless/headless-transport.ts +43 -0
- package/src/headless/index.ts +4 -0
- package/src/http/__tests__/http-transport.test.ts +55 -0
- package/src/http/__tests__/routes.test.ts +168 -0
- package/src/http/http-transport.ts +42 -0
- package/src/http/index.ts +4 -0
- package/src/http/routes.ts +151 -0
- package/src/index.ts +5 -0
- package/src/mcp/__tests__/mcp-server.test.ts +66 -0
- package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp-server.ts +162 -0
- package/src/mcp/mcp-transport.ts +48 -0
- package/src/tui/App.tsx +478 -0
- package/src/tui/BackgroundTaskPanel.tsx +34 -0
- package/src/tui/CjkTextInput.tsx +204 -0
- package/src/tui/ConfirmPrompt.tsx +69 -0
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
- package/src/tui/InkTerminal.ts +42 -0
- package/src/tui/InputArea.tsx +298 -0
- package/src/tui/InteractivePrompt.tsx +57 -0
- package/src/tui/ListPicker.tsx +94 -0
- package/src/tui/MenuSelect.tsx +103 -0
- package/src/tui/MessageList.tsx +282 -0
- package/src/tui/PermissionPrompt.tsx +84 -0
- package/src/tui/PluginTUI.tsx +256 -0
- package/src/tui/SessionPicker.tsx +66 -0
- package/src/tui/SessionStatusBar.tsx +66 -0
- package/src/tui/SlashAutocomplete.tsx +110 -0
- package/src/tui/StatusBar.tsx +213 -0
- package/src/tui/StreamingIndicator.tsx +91 -0
- package/src/tui/TextPrompt.tsx +80 -0
- package/src/tui/ToolCommandOutput.tsx +37 -0
- package/src/tui/ToolDiffBlock.tsx +30 -0
- package/src/tui/TransportTUI.tsx +116 -0
- package/src/tui/UpdateNotice.tsx +14 -0
- package/src/tui/UsageSummaryEntry.tsx +38 -0
- package/src/tui/WaveText.tsx +44 -0
- package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
- package/src/tui/__tests__/ListPicker.test.tsx +159 -0
- package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
- package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
- package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
- package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
- package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
- package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
- package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
- package/src/tui/__tests__/command-output-summary.test.ts +95 -0
- package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
- package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
- package/src/tui/__tests__/input-area-flow.test.ts +152 -0
- package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
- package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
- package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
- package/src/tui/__tests__/render-markdown.test.ts +72 -0
- package/src/tui/__tests__/selection-flow.test.ts +61 -0
- package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
- package/src/tui/__tests__/status-activity.test.ts +71 -0
- package/src/tui/__tests__/status-bar.test.tsx +157 -0
- package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
- package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
- package/src/tui/background-task-row-format.ts +52 -0
- package/src/tui/command-output-summary.ts +122 -0
- package/src/tui/execution-workspace-view-model.ts +123 -0
- package/src/tui/flows/cjk-text-input-flow.ts +285 -0
- package/src/tui/flows/confirm-prompt-flow.ts +45 -0
- package/src/tui/flows/input-area-flow.ts +186 -0
- package/src/tui/flows/permission-prompt-flow.ts +76 -0
- package/src/tui/flows/selection-flow.ts +126 -0
- package/src/tui/flows/text-prompt-flow.ts +98 -0
- package/src/tui/hooks/command-effect-handler.ts +98 -0
- package/src/tui/hooks/command-effect-queue.ts +39 -0
- package/src/tui/hooks/model-change-side-effect.ts +63 -0
- package/src/tui/hooks/side-effects-types.ts +38 -0
- package/src/tui/hooks/use-interactive-session-init.ts +50 -0
- package/src/tui/hooks/useAutocomplete.ts +85 -0
- package/src/tui/hooks/useInteractiveSession.ts +273 -0
- package/src/tui/hooks/usePermissionQueue.ts +51 -0
- package/src/tui/hooks/usePluginCallbacks.ts +30 -0
- package/src/tui/hooks/usePluginScreenData.ts +84 -0
- package/src/tui/hooks/useSideEffects.ts +210 -0
- package/src/tui/hooks/useSlashRouting.ts +117 -0
- package/src/tui/hooks/useStatusLineSettings.ts +35 -0
- package/src/tui/index.ts +3 -0
- package/src/tui/plugin-tui-handlers.ts +163 -0
- package/src/tui/render-markdown.ts +129 -0
- package/src/tui/render.tsx +60 -0
- package/src/tui/status-activity.ts +63 -0
- package/src/tui/tui-cli-adapter-context.tsx +12 -0
- package/src/tui/tui-cli-adapter.ts +25 -0
- package/src/tui/tui-state-manager.ts +225 -0
- package/src/tui/tui-transport.ts +32 -0
- package/src/tui/types.ts +14 -0
- package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/tui/utils/edit-diff.ts +152 -0
- package/src/tui/utils/paste-labels.ts +9 -0
- package/src/tui/utils/tool-call-extractor.ts +91 -0
- package/src/tui/utils/tool-diff-summary.ts +75 -0
- package/src/ws/__tests__/ws-handler.test.ts +407 -0
- package/src/ws/__tests__/ws-transport.test.ts +53 -0
- package/src/ws/index.ts +13 -0
- package/src/ws/ws-background-messages.ts +170 -0
- package/src/ws/ws-handler.ts +279 -0
- package/src/ws/ws-protocol.ts +76 -0
- package/src/ws/ws-transport-configurable.ts +123 -0
- package/src/ws/ws-transport.ts +42 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
3
|
+
import type { IExecutionResult } from '@robota-sdk/agent-framework';
|
|
4
|
+
import type { TBackgroundJobGroupEvent } from '@robota-sdk/agent-framework';
|
|
5
|
+
import type { TBackgroundTaskEvent } from '@robota-sdk/agent-framework';
|
|
6
|
+
import { createHeadlessRunner } from '../headless-runner.js';
|
|
7
|
+
|
|
8
|
+
function createMockSession(behavior: 'complete' | 'interrupted' | 'error', response = '') {
|
|
9
|
+
const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
10
|
+
return {
|
|
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
|
+
if (behavior === 'complete') {
|
|
18
|
+
const result: IExecutionResult = {
|
|
19
|
+
response,
|
|
20
|
+
history: [],
|
|
21
|
+
toolSummaries: [],
|
|
22
|
+
contextState: {} as IExecutionResult['contextState'],
|
|
23
|
+
};
|
|
24
|
+
for (const h of listeners.get('complete') ?? []) {
|
|
25
|
+
h(result);
|
|
26
|
+
}
|
|
27
|
+
} else if (behavior === 'interrupted') {
|
|
28
|
+
const result: IExecutionResult = {
|
|
29
|
+
response,
|
|
30
|
+
history: [],
|
|
31
|
+
toolSummaries: [],
|
|
32
|
+
contextState: {} as IExecutionResult['contextState'],
|
|
33
|
+
};
|
|
34
|
+
for (const h of listeners.get('interrupted') ?? []) {
|
|
35
|
+
h(result);
|
|
36
|
+
}
|
|
37
|
+
} else if (behavior === 'error') {
|
|
38
|
+
for (const h of listeners.get('error') ?? []) {
|
|
39
|
+
h(new Error('test error'));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
executeCommand: vi.fn().mockResolvedValue(null),
|
|
44
|
+
getSession: vi.fn(() => ({ getSessionId: () => 'test-session-id' })),
|
|
45
|
+
} as unknown as IInteractiveSession;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('createHeadlessRunner (text format)', () => {
|
|
49
|
+
let stdoutWriteSpy: any; // allow-any: vi.spyOn process.stdout.write has incompatible MockInstance generic bounds
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
stdoutWriteSpy.mockRestore();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('writes response + newline to stdout and returns exit code 0 on complete', async () => {
|
|
60
|
+
const session = createMockSession('complete', 'Hello, world!');
|
|
61
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
62
|
+
|
|
63
|
+
const exitCode = await runner.run('test prompt');
|
|
64
|
+
|
|
65
|
+
expect(exitCode).toBe(0);
|
|
66
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith('Hello, world!\n');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('writes partial response on interrupted and returns exit code 0', async () => {
|
|
70
|
+
const session = createMockSession('interrupted', 'partial output');
|
|
71
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
72
|
+
|
|
73
|
+
const exitCode = await runner.run('test prompt');
|
|
74
|
+
|
|
75
|
+
expect(exitCode).toBe(0);
|
|
76
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith('partial output\n');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not write to stdout on interrupted with empty response', async () => {
|
|
80
|
+
const session = createMockSession('interrupted', '');
|
|
81
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
82
|
+
|
|
83
|
+
const exitCode = await runner.run('test prompt');
|
|
84
|
+
|
|
85
|
+
expect(exitCode).toBe(0);
|
|
86
|
+
expect(stdoutWriteSpy).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns exit code 1 on error', async () => {
|
|
90
|
+
const session = createMockSession('error');
|
|
91
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
92
|
+
|
|
93
|
+
const exitCode = await runner.run('test prompt');
|
|
94
|
+
|
|
95
|
+
expect(exitCode).toBe(1);
|
|
96
|
+
expect(stdoutWriteSpy).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('passes the prompt to session.submit', async () => {
|
|
100
|
+
const session = createMockSession('complete', 'ok');
|
|
101
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
102
|
+
|
|
103
|
+
await runner.run('my prompt');
|
|
104
|
+
|
|
105
|
+
expect(session.submit).toHaveBeenCalledWith('my prompt');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('executes /agent as a command without submitting to the model', async () => {
|
|
109
|
+
const session = {
|
|
110
|
+
...createMockSession('complete', 'unused'),
|
|
111
|
+
executeCommand: vi.fn().mockResolvedValue({
|
|
112
|
+
message: 'Started agent job: agent_1',
|
|
113
|
+
success: true,
|
|
114
|
+
data: { agentId: 'agent_1' },
|
|
115
|
+
}),
|
|
116
|
+
} as unknown as IInteractiveSession;
|
|
117
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
118
|
+
|
|
119
|
+
const exitCode = await runner.run('/agent run Plan --background "draft architecture"');
|
|
120
|
+
|
|
121
|
+
expect(exitCode).toBe(0);
|
|
122
|
+
expect(session.executeCommand).toHaveBeenCalledWith(
|
|
123
|
+
'agent',
|
|
124
|
+
'run Plan --background "draft architecture"',
|
|
125
|
+
);
|
|
126
|
+
expect(session.submit).not.toHaveBeenCalled();
|
|
127
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith('Started agent job: agent_1\n');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('executes /skill through SDK command routing without submitting the raw slash prompt', async () => {
|
|
131
|
+
const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
132
|
+
const session = {
|
|
133
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
134
|
+
if (!listeners.has(event)) listeners.set(event, []);
|
|
135
|
+
listeners.get(event)!.push(handler);
|
|
136
|
+
}),
|
|
137
|
+
off: vi.fn(),
|
|
138
|
+
submit: vi.fn(),
|
|
139
|
+
executeCommand: vi.fn().mockImplementation(async () => {
|
|
140
|
+
const result: IExecutionResult = {
|
|
141
|
+
response: 'Skill response',
|
|
142
|
+
history: [],
|
|
143
|
+
toolSummaries: [],
|
|
144
|
+
contextState: {} as IExecutionResult['contextState'],
|
|
145
|
+
};
|
|
146
|
+
for (const handler of listeners.get('complete') ?? []) {
|
|
147
|
+
handler(result);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
message: '',
|
|
152
|
+
effects: [{ type: 'session-execution-started' }],
|
|
153
|
+
data: { skill: 'audit', sessionExecution: true },
|
|
154
|
+
};
|
|
155
|
+
}),
|
|
156
|
+
getSession: vi.fn(() => ({ getSessionId: () => 'test-session-id' })),
|
|
157
|
+
} as unknown as IInteractiveSession;
|
|
158
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'text' });
|
|
159
|
+
|
|
160
|
+
const exitCode = await runner.run('/audit src/index.ts');
|
|
161
|
+
|
|
162
|
+
expect(exitCode).toBe(0);
|
|
163
|
+
expect(session.executeCommand).toHaveBeenCalledWith('audit', 'src/index.ts');
|
|
164
|
+
expect(session.submit).not.toHaveBeenCalled();
|
|
165
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith('Skill response\n');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('createHeadlessRunner (json format)', () => {
|
|
170
|
+
let stdoutWriteSpy: any; // allow-any: vi.spyOn process.stdout.write has incompatible MockInstance generic bounds
|
|
171
|
+
|
|
172
|
+
beforeEach(() => {
|
|
173
|
+
stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
stdoutWriteSpy.mockRestore();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('json format outputs { type, result, session_id, subtype: success }', async () => {
|
|
181
|
+
const session = createMockSession('complete', 'JSON response');
|
|
182
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'json' });
|
|
183
|
+
|
|
184
|
+
const exitCode = await runner.run('test prompt');
|
|
185
|
+
|
|
186
|
+
expect(exitCode).toBe(0);
|
|
187
|
+
expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
|
|
188
|
+
const output = (stdoutWriteSpy.mock.calls[0] as [string])[0];
|
|
189
|
+
const parsed: unknown = JSON.parse(output.trim());
|
|
190
|
+
expect(parsed).toEqual({
|
|
191
|
+
type: 'result',
|
|
192
|
+
result: 'JSON response',
|
|
193
|
+
session_id: 'test-session-id',
|
|
194
|
+
subtype: 'success',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('json format outputs subtype error on failure', async () => {
|
|
199
|
+
const session = createMockSession('error');
|
|
200
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'json' });
|
|
201
|
+
|
|
202
|
+
const exitCode = await runner.run('test prompt');
|
|
203
|
+
|
|
204
|
+
expect(exitCode).toBe(1);
|
|
205
|
+
expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
|
|
206
|
+
const output = (stdoutWriteSpy.mock.calls[0] as [string])[0];
|
|
207
|
+
const parsed: unknown = JSON.parse(output.trim());
|
|
208
|
+
expect(parsed).toEqual({
|
|
209
|
+
type: 'result',
|
|
210
|
+
result: '',
|
|
211
|
+
session_id: 'test-session-id',
|
|
212
|
+
subtype: 'error',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('json format outputs subtype success with partial response on interrupted', async () => {
|
|
217
|
+
const session = createMockSession('interrupted', 'partial');
|
|
218
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'json' });
|
|
219
|
+
|
|
220
|
+
const exitCode = await runner.run('test prompt');
|
|
221
|
+
|
|
222
|
+
expect(exitCode).toBe(0);
|
|
223
|
+
expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
|
|
224
|
+
const output = (stdoutWriteSpy.mock.calls[0] as [string])[0];
|
|
225
|
+
const parsed: unknown = JSON.parse(output.trim());
|
|
226
|
+
expect(parsed).toEqual({
|
|
227
|
+
type: 'result',
|
|
228
|
+
result: 'partial',
|
|
229
|
+
session_id: 'test-session-id',
|
|
230
|
+
subtype: 'success',
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('createHeadlessRunner (stream-json format)', () => {
|
|
236
|
+
let stdoutWriteSpy: any; // allow-any: vi.spyOn process.stdout.write has incompatible MockInstance generic bounds
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
afterEach(() => {
|
|
243
|
+
stdoutWriteSpy.mockRestore();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('stream-json emits content_block_delta events and final result', async () => {
|
|
247
|
+
const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
248
|
+
const session = {
|
|
249
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
250
|
+
if (!listeners.has(event)) listeners.set(event, []);
|
|
251
|
+
listeners.get(event)!.push(handler);
|
|
252
|
+
}),
|
|
253
|
+
off: vi.fn(),
|
|
254
|
+
submit: vi.fn(async () => {
|
|
255
|
+
for (const h of listeners.get('text_delta') ?? []) {
|
|
256
|
+
h('Hello');
|
|
257
|
+
h(' world');
|
|
258
|
+
}
|
|
259
|
+
for (const h of listeners.get('complete') ?? []) {
|
|
260
|
+
h({ response: 'Hello world', history: [], toolSummaries: [], contextState: {} });
|
|
261
|
+
}
|
|
262
|
+
}),
|
|
263
|
+
getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
|
|
264
|
+
} as unknown as IInteractiveSession;
|
|
265
|
+
|
|
266
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
|
|
267
|
+
const exitCode = await runner.run('test prompt');
|
|
268
|
+
|
|
269
|
+
expect(exitCode).toBe(0);
|
|
270
|
+
|
|
271
|
+
const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
|
|
272
|
+
const parsed: Array<Record<string, unknown>> = lines.map(
|
|
273
|
+
(line: string) => JSON.parse(line) as Record<string, unknown>,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// 2 stream_event lines + 1 final result line
|
|
277
|
+
expect(parsed).toHaveLength(3);
|
|
278
|
+
|
|
279
|
+
// First two are stream events with content_block_delta
|
|
280
|
+
const streamEvents = parsed.filter(
|
|
281
|
+
(p: Record<string, unknown>) => p['type'] === 'stream_event',
|
|
282
|
+
);
|
|
283
|
+
expect(streamEvents).toHaveLength(2);
|
|
284
|
+
|
|
285
|
+
for (const evt of streamEvents) {
|
|
286
|
+
expect(evt['session_id']).toBe('stream-session');
|
|
287
|
+
expect(evt['uuid']).toBeDefined();
|
|
288
|
+
const inner = evt['event'] as Record<string, unknown>;
|
|
289
|
+
expect(inner['type']).toBe('content_block_delta');
|
|
290
|
+
const delta = inner['delta'] as Record<string, unknown>;
|
|
291
|
+
expect(delta['type']).toBe('text_delta');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const firstDelta = (streamEvents[0]!['event'] as Record<string, unknown>)['delta'] as Record<
|
|
295
|
+
string,
|
|
296
|
+
unknown
|
|
297
|
+
>;
|
|
298
|
+
const secondDelta = (streamEvents[1]!['event'] as Record<string, unknown>)['delta'] as Record<
|
|
299
|
+
string,
|
|
300
|
+
unknown
|
|
301
|
+
>;
|
|
302
|
+
expect(firstDelta['text']).toBe('Hello');
|
|
303
|
+
expect(secondDelta['text']).toBe(' world');
|
|
304
|
+
|
|
305
|
+
// Final result line
|
|
306
|
+
const resultLine = parsed.find((p: Record<string, unknown>) => p['type'] === 'result');
|
|
307
|
+
expect(resultLine).toEqual({
|
|
308
|
+
type: 'result',
|
|
309
|
+
result: 'Hello world',
|
|
310
|
+
session_id: 'stream-session',
|
|
311
|
+
subtype: 'success',
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('stream-json emits error result on error', async () => {
|
|
316
|
+
const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
317
|
+
const session = {
|
|
318
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
319
|
+
if (!listeners.has(event)) listeners.set(event, []);
|
|
320
|
+
listeners.get(event)!.push(handler);
|
|
321
|
+
}),
|
|
322
|
+
off: vi.fn(),
|
|
323
|
+
submit: vi.fn(async () => {
|
|
324
|
+
for (const h of listeners.get('error') ?? []) {
|
|
325
|
+
h(new Error('stream error'));
|
|
326
|
+
}
|
|
327
|
+
}),
|
|
328
|
+
getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
|
|
329
|
+
} as unknown as IInteractiveSession;
|
|
330
|
+
|
|
331
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
|
|
332
|
+
const exitCode = await runner.run('test prompt');
|
|
333
|
+
|
|
334
|
+
expect(exitCode).toBe(1);
|
|
335
|
+
|
|
336
|
+
const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
|
|
337
|
+
const parsed: Array<Record<string, unknown>> = lines.map(
|
|
338
|
+
(line: string) => JSON.parse(line) as Record<string, unknown>,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(parsed).toHaveLength(1);
|
|
342
|
+
expect(parsed[0]).toEqual({
|
|
343
|
+
type: 'result',
|
|
344
|
+
result: '',
|
|
345
|
+
session_id: 'stream-session',
|
|
346
|
+
subtype: 'error',
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('stream-json emits background task events before the final result', async () => {
|
|
351
|
+
const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
352
|
+
const session = {
|
|
353
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
354
|
+
if (!listeners.has(event)) listeners.set(event, []);
|
|
355
|
+
listeners.get(event)!.push(handler);
|
|
356
|
+
}),
|
|
357
|
+
off: vi.fn(),
|
|
358
|
+
submit: vi.fn(),
|
|
359
|
+
executeCommand: vi.fn().mockImplementation(async () => {
|
|
360
|
+
for (const h of listeners.get('background_task_event') ?? []) {
|
|
361
|
+
h({
|
|
362
|
+
type: 'background_task_created',
|
|
363
|
+
task: {
|
|
364
|
+
id: 'agent_1',
|
|
365
|
+
kind: 'agent',
|
|
366
|
+
label: 'Plan',
|
|
367
|
+
status: 'running',
|
|
368
|
+
mode: 'background',
|
|
369
|
+
parentSessionId: 'stream-session',
|
|
370
|
+
depth: 1,
|
|
371
|
+
cwd: '/workspace',
|
|
372
|
+
updatedAt: '2026-05-01T00:00:00.000Z',
|
|
373
|
+
unread: false,
|
|
374
|
+
promptPreview: 'draft architecture',
|
|
375
|
+
},
|
|
376
|
+
} satisfies TBackgroundTaskEvent);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
message: 'Started agent job: agent_1',
|
|
380
|
+
success: true,
|
|
381
|
+
data: { agentId: 'agent_1' },
|
|
382
|
+
};
|
|
383
|
+
}),
|
|
384
|
+
getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
|
|
385
|
+
} as unknown as IInteractiveSession;
|
|
386
|
+
|
|
387
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
|
|
388
|
+
const exitCode = await runner.run('/agent run Plan --background "draft architecture"');
|
|
389
|
+
|
|
390
|
+
expect(exitCode).toBe(0);
|
|
391
|
+
|
|
392
|
+
const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
|
|
393
|
+
const parsed: Array<Record<string, unknown>> = lines.map(
|
|
394
|
+
(line: string) => JSON.parse(line) as Record<string, unknown>,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
expect(parsed).toHaveLength(2);
|
|
398
|
+
expect(session.submit).not.toHaveBeenCalled();
|
|
399
|
+
expect(session.executeCommand).toHaveBeenCalledWith(
|
|
400
|
+
'agent',
|
|
401
|
+
'run Plan --background "draft architecture"',
|
|
402
|
+
);
|
|
403
|
+
expect(parsed[0]).toMatchObject({
|
|
404
|
+
type: 'stream_event',
|
|
405
|
+
session_id: 'stream-session',
|
|
406
|
+
event: {
|
|
407
|
+
type: 'background_task_event',
|
|
408
|
+
background_task_event: {
|
|
409
|
+
type: 'background_task_created',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
expect(parsed[1]).toEqual({
|
|
414
|
+
type: 'result',
|
|
415
|
+
result: 'Started agent job: agent_1',
|
|
416
|
+
session_id: 'stream-session',
|
|
417
|
+
subtype: 'success',
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('stream-json emits background job group events before the final result', async () => {
|
|
422
|
+
const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
423
|
+
const session = {
|
|
424
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
425
|
+
if (!listeners.has(event)) listeners.set(event, []);
|
|
426
|
+
listeners.get(event)!.push(handler);
|
|
427
|
+
}),
|
|
428
|
+
off: vi.fn(),
|
|
429
|
+
submit: vi.fn(),
|
|
430
|
+
executeCommand: vi.fn().mockImplementation(async () => {
|
|
431
|
+
for (const h of listeners.get('background_job_group_event') ?? []) {
|
|
432
|
+
h({
|
|
433
|
+
type: 'background_job_group_completed',
|
|
434
|
+
group: {
|
|
435
|
+
id: 'group_1',
|
|
436
|
+
parentSessionId: 'stream-session',
|
|
437
|
+
waitPolicy: 'wait_all',
|
|
438
|
+
taskIds: ['agent_1'],
|
|
439
|
+
status: 'completed',
|
|
440
|
+
createdAt: '2026-05-01T00:00:00.000Z',
|
|
441
|
+
updatedAt: '2026-05-01T00:00:01.000Z',
|
|
442
|
+
completedAt: '2026-05-01T00:00:01.000Z',
|
|
443
|
+
results: [{ taskId: 'agent_1', label: 'Plan', status: 'completed' }],
|
|
444
|
+
},
|
|
445
|
+
} satisfies TBackgroundJobGroupEvent);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
message: 'Background job group group_1: completed',
|
|
449
|
+
success: true,
|
|
450
|
+
data: { groupId: 'group_1' },
|
|
451
|
+
};
|
|
452
|
+
}),
|
|
453
|
+
getSession: vi.fn(() => ({ getSessionId: () => 'stream-session' })),
|
|
454
|
+
} as unknown as IInteractiveSession;
|
|
455
|
+
|
|
456
|
+
const runner = createHeadlessRunner({ session, outputFormat: 'stream-json' });
|
|
457
|
+
const exitCode = await runner.run('/agent wait group_1');
|
|
458
|
+
|
|
459
|
+
expect(exitCode).toBe(0);
|
|
460
|
+
|
|
461
|
+
const lines = stdoutWriteSpy.mock.calls.map((call: unknown[]) => (call as [string])[0].trim());
|
|
462
|
+
const parsed: Array<Record<string, unknown>> = lines.map(
|
|
463
|
+
(line: string) => JSON.parse(line) as Record<string, unknown>,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(parsed).toHaveLength(2);
|
|
467
|
+
expect(parsed[0]).toMatchObject({
|
|
468
|
+
type: 'stream_event',
|
|
469
|
+
session_id: 'stream-session',
|
|
470
|
+
event: {
|
|
471
|
+
type: 'background_job_group_event',
|
|
472
|
+
background_job_group_event: {
|
|
473
|
+
type: 'background_job_group_completed',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
expect(parsed[1]).toEqual({
|
|
478
|
+
type: 'result',
|
|
479
|
+
result: 'Background job group group_1: completed',
|
|
480
|
+
session_id: 'stream-session',
|
|
481
|
+
subtype: 'success',
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|