@robota-sdk/agent-transport 3.0.0-beta.75 → 3.0.0-beta.76
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/README.md +10 -10
- package/dist/node/headless/index.cjs +1 -1
- package/dist/node/{headless-CT2ibQnr.cjs → headless-OnpVk4-k.cjs} +7 -7
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.ts +1 -6
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +7 -75
- package/src/index.ts +1 -5
- package/src/transport-registry.ts +0 -9
- package/dist/node/http/index.cjs +0 -1
- package/dist/node/http/index.d.ts +0 -2
- package/dist/node/http/index.js +0 -1
- package/dist/node/http-2Jiuflc1.js +0 -2
- package/dist/node/http-2Jiuflc1.js.map +0 -1
- package/dist/node/http-CBAvefLw.cjs +0 -1
- package/dist/node/index-BNccqSpv.d.ts +0 -86
- package/dist/node/index-BNccqSpv.d.ts.map +0 -1
- package/dist/node/index-BUhHIf7X.d.ts +0 -86
- package/dist/node/index-BUhHIf7X.d.ts.map +0 -1
- package/dist/node/index-BnAGE-u9.d.ts +0 -33
- package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
- package/dist/node/index-BrQ4gGw0.d.ts +0 -213
- package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
- package/dist/node/index-CoeBF21y.d.ts +0 -213
- package/dist/node/index-CoeBF21y.d.ts.map +0 -1
- package/dist/node/index-DHt-2VQ-.d.ts +0 -46
- package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
- package/dist/node/index-DMwKN5Le.d.ts +0 -33
- package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
- package/dist/node/index-c0M42fsA.d.ts +0 -46
- package/dist/node/index-c0M42fsA.d.ts.map +0 -1
- package/dist/node/mcp/index.cjs +0 -1
- package/dist/node/mcp/index.d.ts +0 -2
- package/dist/node/mcp/index.js +0 -1
- package/dist/node/mcp-BOglBJNy.cjs +0 -1
- package/dist/node/mcp-D3BBVK7C.js +0 -2
- package/dist/node/mcp-D3BBVK7C.js.map +0 -1
- package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
- package/dist/node/tui/index.cjs +0 -1
- package/dist/node/tui/index.d.ts +0 -2
- package/dist/node/tui/index.js +0 -1
- package/dist/node/tui-CcH5EsQh.js +0 -25
- package/dist/node/tui-CcH5EsQh.js.map +0 -1
- package/dist/node/tui-DznRbcku.cjs +0 -24
- package/dist/node/ws/index.cjs +0 -1
- package/dist/node/ws/index.d.ts +0 -2
- package/dist/node/ws/index.js +0 -1
- package/dist/node/ws-Dc2RUwVs.js +0 -2
- package/dist/node/ws-Dc2RUwVs.js.map +0 -1
- package/dist/node/ws-QNMQn5kg.cjs +0 -1
- package/src/http/__tests__/http-transport.test.ts +0 -55
- package/src/http/__tests__/routes.test.ts +0 -168
- package/src/http/http-transport.ts +0 -41
- package/src/http/index.ts +0 -4
- package/src/http/routes.ts +0 -152
- package/src/mcp/__tests__/mcp-server.test.ts +0 -66
- package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
- package/src/mcp/index.ts +0 -4
- package/src/mcp/mcp-server.ts +0 -163
- package/src/mcp/mcp-transport.ts +0 -48
- package/src/tui/App.tsx +0 -491
- package/src/tui/BackgroundTaskPanel.tsx +0 -36
- package/src/tui/CjkTextInput.tsx +0 -199
- package/src/tui/ConfirmPrompt.tsx +0 -70
- package/src/tui/ContextWarningBanner.tsx +0 -34
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
- package/src/tui/InputArea.tsx +0 -310
- package/src/tui/InteractivePrompt.tsx +0 -59
- package/src/tui/ListPicker.tsx +0 -95
- package/src/tui/MenuSelect.tsx +0 -104
- package/src/tui/MessageList.tsx +0 -284
- package/src/tui/PermissionPrompt.tsx +0 -86
- package/src/tui/PluginTUI.tsx +0 -258
- package/src/tui/SessionPicker.tsx +0 -68
- package/src/tui/SessionStatusBar.tsx +0 -73
- package/src/tui/SlashAutocomplete.tsx +0 -110
- package/src/tui/StatusBar.tsx +0 -236
- package/src/tui/StreamingIndicator.tsx +0 -93
- package/src/tui/TextPrompt.tsx +0 -81
- package/src/tui/ToolCommandOutput.tsx +0 -39
- package/src/tui/ToolDiffBlock.tsx +0 -32
- package/src/tui/TransportTUI.tsx +0 -117
- package/src/tui/TuiInteractionChannel.ts +0 -495
- package/src/tui/UpdateNotice.tsx +0 -14
- package/src/tui/UsageSummaryEntry.tsx +0 -39
- package/src/tui/WaveText.tsx +0 -44
- package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
- package/src/tui/__tests__/ListPicker.test.tsx +0 -159
- package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
- package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
- package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
- package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
- package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
- package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
- package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
- package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
- package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
- package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
- package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
- package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
- package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
- package/src/tui/__tests__/command-output-summary.test.ts +0 -95
- package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
- package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
- package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
- package/src/tui/__tests__/input-area-flow.test.ts +0 -164
- package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
- package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
- package/src/tui/__tests__/pty/pty-driver.ts +0 -135
- package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
- package/src/tui/__tests__/render-channel-options.test.ts +0 -32
- package/src/tui/__tests__/render-markdown.test.ts +0 -72
- package/src/tui/__tests__/selection-flow.test.ts +0 -61
- package/src/tui/__tests__/session-init-poller.test.ts +0 -102
- package/src/tui/__tests__/session-naming.test.ts +0 -64
- package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
- package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
- package/src/tui/__tests__/status-activity.test.ts +0 -71
- package/src/tui/__tests__/status-bar.test.tsx +0 -177
- package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
- package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
- package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
- package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
- package/src/tui/background-task-row-format.ts +0 -53
- package/src/tui/command-interaction.ts +0 -9
- package/src/tui/command-output-summary.ts +0 -122
- package/src/tui/create-default-tui-cli-adapter.ts +0 -41
- package/src/tui/execution-workspace-view-model.ts +0 -123
- package/src/tui/flows/cjk-text-input-flow.ts +0 -285
- package/src/tui/flows/confirm-prompt-flow.ts +0 -45
- package/src/tui/flows/input-area-flow.ts +0 -189
- package/src/tui/flows/permission-prompt-flow.ts +0 -85
- package/src/tui/flows/selection-flow.ts +0 -126
- package/src/tui/flows/session-init-poller.ts +0 -77
- package/src/tui/flows/text-prompt-flow.ts +0 -98
- package/src/tui/hooks/command-effect-handler.ts +0 -97
- package/src/tui/hooks/command-effect-queue.ts +0 -39
- package/src/tui/hooks/side-effects-types.ts +0 -35
- package/src/tui/hooks/useAutocomplete.ts +0 -87
- package/src/tui/hooks/usePluginCallbacks.ts +0 -31
- package/src/tui/hooks/usePluginScreenData.ts +0 -85
- package/src/tui/hooks/useSideEffects.ts +0 -175
- package/src/tui/hooks/useSlashRouting.ts +0 -118
- package/src/tui/hooks/useStatusLineSettings.ts +0 -37
- package/src/tui/hooks/useTuiChannel.ts +0 -95
- package/src/tui/index.ts +0 -14
- package/src/tui/interactions/CommandConfirm.tsx +0 -36
- package/src/tui/interactions/CommandPicker.tsx +0 -77
- package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
- package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
- package/src/tui/plugin-tui-handlers.ts +0 -163
- package/src/tui/render-markdown.ts +0 -130
- package/src/tui/render.tsx +0 -129
- package/src/tui/session-naming.ts +0 -33
- package/src/tui/status-activity.ts +0 -63
- package/src/tui/tui-cli-adapter-context.tsx +0 -13
- package/src/tui/tui-cli-adapter.ts +0 -25
- package/src/tui/tui-state-manager.ts +0 -226
- package/src/tui/tui-transport.ts +0 -35
- package/src/tui/types.ts +0 -15
- package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
- package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
- package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
- package/src/tui/utils/edit-diff.ts +0 -153
- package/src/tui/utils/paste-labels.ts +0 -9
- package/src/tui/utils/tool-call-extractor.ts +0 -92
- package/src/tui/utils/tool-diff-summary.ts +0 -75
- package/src/ws/__tests__/ws-handler.test.ts +0 -409
- package/src/ws/__tests__/ws-transport.test.ts +0 -53
- package/src/ws/index.ts +0 -13
- package/src/ws/ws-background-messages.ts +0 -170
- package/src/ws/ws-handler.ts +0 -280
- package/src/ws/ws-protocol.ts +0 -78
- package/src/ws/ws-transport-configurable.ts +0 -128
- package/src/ws/ws-transport.ts +0 -42
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from 'ink-testing-library';
|
|
3
|
-
import { describe, it, expect } from 'vitest';
|
|
4
|
-
import TextPrompt from '../TextPrompt.js';
|
|
5
|
-
|
|
6
|
-
describe('TextPrompt', () => {
|
|
7
|
-
it('renders title', () => {
|
|
8
|
-
const { lastFrame } = render(
|
|
9
|
-
<TextPrompt title="Enter URL" onSubmit={() => {}} onCancel={() => {}} />,
|
|
10
|
-
);
|
|
11
|
-
expect(lastFrame()!).toContain('Enter URL');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('renders placeholder when provided', () => {
|
|
15
|
-
const { lastFrame } = render(
|
|
16
|
-
<TextPrompt title="Enter" placeholder="owner/repo" onSubmit={() => {}} onCancel={() => {}} />,
|
|
17
|
-
);
|
|
18
|
-
expect(lastFrame()!).toContain('owner/repo');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('calls onCancel on Escape', async () => {
|
|
22
|
-
let cancelled = false;
|
|
23
|
-
const { stdin } = render(
|
|
24
|
-
<TextPrompt
|
|
25
|
-
title="Enter"
|
|
26
|
-
onSubmit={() => {}}
|
|
27
|
-
onCancel={() => {
|
|
28
|
-
cancelled = true;
|
|
29
|
-
}}
|
|
30
|
-
/>,
|
|
31
|
-
);
|
|
32
|
-
stdin.write('\x1B');
|
|
33
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
34
|
-
expect(cancelled).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('calls onSubmit with value on Enter', () => {
|
|
38
|
-
let submitted = '';
|
|
39
|
-
const { stdin } = render(
|
|
40
|
-
<TextPrompt
|
|
41
|
-
title="Enter"
|
|
42
|
-
onSubmit={(v) => {
|
|
43
|
-
submitted = v;
|
|
44
|
-
}}
|
|
45
|
-
onCancel={() => {}}
|
|
46
|
-
/>,
|
|
47
|
-
);
|
|
48
|
-
stdin.write('hello');
|
|
49
|
-
stdin.write('\r');
|
|
50
|
-
expect(submitted).toBe('hello');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('can submit an empty value when allowed', () => {
|
|
54
|
-
let submitted = 'not-called';
|
|
55
|
-
const { stdin } = render(
|
|
56
|
-
<TextPrompt
|
|
57
|
-
title="Enter"
|
|
58
|
-
allowEmpty
|
|
59
|
-
onSubmit={(v) => {
|
|
60
|
-
submitted = v;
|
|
61
|
-
}}
|
|
62
|
-
onCancel={() => {}}
|
|
63
|
-
/>,
|
|
64
|
-
);
|
|
65
|
-
stdin.write('\r');
|
|
66
|
-
expect(submitted).toBe('');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('masks typed values when requested', async () => {
|
|
70
|
-
const { stdin, lastFrame } = render(
|
|
71
|
-
<TextPrompt title="Secret" masked onSubmit={() => {}} onCancel={() => {}} />,
|
|
72
|
-
);
|
|
73
|
-
stdin.write('abc');
|
|
74
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
75
|
-
expect(lastFrame()!).toContain('***');
|
|
76
|
-
expect(lastFrame()!).not.toContain('abc');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('shows validation error and blocks submit', async () => {
|
|
80
|
-
let submitted = false;
|
|
81
|
-
const validate = (v: string) => (v.length < 3 ? 'Too short' : undefined);
|
|
82
|
-
const { stdin, lastFrame } = render(
|
|
83
|
-
<TextPrompt
|
|
84
|
-
title="Enter"
|
|
85
|
-
onSubmit={() => {
|
|
86
|
-
submitted = true;
|
|
87
|
-
}}
|
|
88
|
-
onCancel={() => {}}
|
|
89
|
-
validate={validate}
|
|
90
|
-
/>,
|
|
91
|
-
);
|
|
92
|
-
stdin.write('ab');
|
|
93
|
-
stdin.write('\r');
|
|
94
|
-
expect(submitted).toBe(false);
|
|
95
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
96
|
-
expect(lastFrame()!).toContain('Too short');
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Display-contract tests for TuiInteractionChannel.
|
|
3
|
-
*
|
|
4
|
-
* Verifies user-visible state: which entries appear in stateManager.history,
|
|
5
|
-
* with which roles, and in which order.
|
|
6
|
-
*
|
|
7
|
-
* These are deliberately separate from lifecycle.test.ts:
|
|
8
|
-
* lifecycle → event routing / onChange propagation (mechanism layer)
|
|
9
|
-
* this file → what the user sees on screen (display contract layer)
|
|
10
|
-
*
|
|
11
|
-
* Design principles:
|
|
12
|
-
* - user_message assertions do NOT require getFullHistory injection
|
|
13
|
-
* - every history assertion checks entry.type (= role) explicitly
|
|
14
|
-
* - timing: user message visible BEFORE complete fires
|
|
15
|
-
*/
|
|
16
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
17
|
-
|
|
18
|
-
vi.mock('@robota-sdk/agent-framework', async () => {
|
|
19
|
-
const actual = await vi.importActual<typeof import('@robota-sdk/agent-framework')>(
|
|
20
|
-
'@robota-sdk/agent-framework',
|
|
21
|
-
);
|
|
22
|
-
return {
|
|
23
|
-
...actual,
|
|
24
|
-
InteractiveSession: vi.fn().mockImplementation(() => {
|
|
25
|
-
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
26
|
-
return {
|
|
27
|
-
getFullHistory: vi.fn().mockReturnValue([]),
|
|
28
|
-
setName: vi.fn(),
|
|
29
|
-
getName: vi.fn().mockReturnValue(undefined),
|
|
30
|
-
getPermissionMode: vi.fn().mockReturnValue('default'),
|
|
31
|
-
isInitialized: false,
|
|
32
|
-
on: vi.fn().mockImplementation((event: string, handler: (...args: unknown[]) => void) => {
|
|
33
|
-
if (!handlers.has(event)) handlers.set(event, []);
|
|
34
|
-
handlers.get(event)!.push(handler);
|
|
35
|
-
}),
|
|
36
|
-
off: vi.fn(),
|
|
37
|
-
emit: (event: string, ...args: unknown[]) => {
|
|
38
|
-
(handlers.get(event) ?? []).forEach((h) => h(...args));
|
|
39
|
-
},
|
|
40
|
-
submit: vi.fn().mockResolvedValue(undefined),
|
|
41
|
-
executeCommand: vi.fn().mockResolvedValue(null),
|
|
42
|
-
getPendingPrompt: vi.fn().mockReturnValue(null),
|
|
43
|
-
abort: vi.fn(),
|
|
44
|
-
cancelQueue: vi.fn(),
|
|
45
|
-
getContextState: vi.fn().mockReturnValue({
|
|
46
|
-
usedPercentage: 0,
|
|
47
|
-
usedTokens: 0,
|
|
48
|
-
maxTokens: 100_000,
|
|
49
|
-
}),
|
|
50
|
-
getExecutionWorkspaceSnapshot: vi.fn().mockReturnValue({ entries: [] }),
|
|
51
|
-
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
52
|
-
sendAgentJob: vi.fn().mockResolvedValue(undefined),
|
|
53
|
-
readExecutionWorkspaceDetail: vi.fn().mockResolvedValue({}),
|
|
54
|
-
};
|
|
55
|
-
}),
|
|
56
|
-
CommandRegistry: vi.fn().mockImplementation(() => ({
|
|
57
|
-
addModule: vi.fn(),
|
|
58
|
-
})),
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
import {
|
|
63
|
-
createAssistantMessage,
|
|
64
|
-
createSystemMessage,
|
|
65
|
-
createUserMessage,
|
|
66
|
-
messageToHistoryEntry,
|
|
67
|
-
} from '@robota-sdk/agent-core';
|
|
68
|
-
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
69
|
-
|
|
70
|
-
import type { IAIProvider, IHistoryEntry } from '@robota-sdk/agent-core';
|
|
71
|
-
import type { IExecutionResult } from '@robota-sdk/agent-interface-transport';
|
|
72
|
-
|
|
73
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
type MockSession = {
|
|
76
|
-
getFullHistory: ReturnType<typeof vi.fn>;
|
|
77
|
-
on: ReturnType<typeof vi.fn>;
|
|
78
|
-
emit: (event: string, ...args: unknown[]) => void;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
function getMockSession(channel: TuiInteractionChannel): MockSession {
|
|
82
|
-
return (channel as unknown as { interactiveSession: MockSession }).interactiveSession;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function emitSessionEvent(channel: TuiInteractionChannel, event: string, ...args: unknown[]): void {
|
|
86
|
-
getMockSession(channel).emit(event, ...args);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function makeChannel(): TuiInteractionChannel {
|
|
90
|
-
return new TuiInteractionChannel({
|
|
91
|
-
cwd: '/tmp/test',
|
|
92
|
-
provider: {} as IAIProvider,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function makeResult(overrides?: Partial<IExecutionResult>): IExecutionResult {
|
|
97
|
-
return {
|
|
98
|
-
contextState: { usedPercentage: 5, usedTokens: 500, maxTokens: 100_000 },
|
|
99
|
-
response: 'done',
|
|
100
|
-
...overrides,
|
|
101
|
-
} as unknown as IExecutionResult;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const MOCK_TOOL_RUNNING = {
|
|
105
|
-
toolName: 'bash',
|
|
106
|
-
isRunning: true,
|
|
107
|
-
input: '{}',
|
|
108
|
-
startTime: Date.now(),
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const MOCK_TOOL_DONE = {
|
|
112
|
-
toolName: 'bash',
|
|
113
|
-
isRunning: false,
|
|
114
|
-
input: '{}',
|
|
115
|
-
startTime: Date.now(),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
function entryRole(entry: IHistoryEntry): string {
|
|
119
|
-
return entry.type;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
beforeEach(() => {
|
|
123
|
-
vi.useFakeTimers();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
afterEach(() => {
|
|
127
|
-
vi.useRealTimers();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ── Group D: display contract (what the user sees) ────────────────────────────
|
|
131
|
-
|
|
132
|
-
describe('Group D — display contract: history entries and active tools', () => {
|
|
133
|
-
it('D1 (CLI-B05): user_message event immediately adds role=user entry before complete', async () => {
|
|
134
|
-
const channel = makeChannel();
|
|
135
|
-
await channel.start();
|
|
136
|
-
|
|
137
|
-
// user message fires BEFORE complete — must be in history immediately
|
|
138
|
-
emitSessionEvent(channel, 'user_message', 'hello');
|
|
139
|
-
|
|
140
|
-
expect(channel.stateManager.history).toHaveLength(1);
|
|
141
|
-
expect(entryRole(channel.stateManager.history[0])).toBe('user');
|
|
142
|
-
expect((channel.stateManager.history[0].data as { content: string }).content).toBe('hello');
|
|
143
|
-
|
|
144
|
-
await channel.stop();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('D2: complete syncs assistant entry into history', async () => {
|
|
148
|
-
const channel = makeChannel();
|
|
149
|
-
const mockSession = getMockSession(channel);
|
|
150
|
-
const assistantEntry = messageToHistoryEntry(createAssistantMessage('Hi!'));
|
|
151
|
-
mockSession.getFullHistory.mockReturnValue([assistantEntry]);
|
|
152
|
-
await channel.start();
|
|
153
|
-
|
|
154
|
-
emitSessionEvent(channel, 'complete', makeResult());
|
|
155
|
-
|
|
156
|
-
const roles = channel.stateManager.history.map(entryRole);
|
|
157
|
-
expect(roles).toContain('assistant');
|
|
158
|
-
await channel.stop();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('D3 (CLI-B06): error event syncs error entry into history — no silent failure', async () => {
|
|
162
|
-
const channel = makeChannel();
|
|
163
|
-
const mockSession = getMockSession(channel);
|
|
164
|
-
const userEntry = messageToHistoryEntry(createUserMessage('hello'));
|
|
165
|
-
const errorEntry = messageToHistoryEntry(createSystemMessage('Error: network failure'));
|
|
166
|
-
mockSession.getFullHistory.mockReturnValue([userEntry, errorEntry]);
|
|
167
|
-
await channel.start();
|
|
168
|
-
|
|
169
|
-
emitSessionEvent(channel, 'user_message', 'hello');
|
|
170
|
-
emitSessionEvent(channel, 'error');
|
|
171
|
-
|
|
172
|
-
// error entry from getFullHistory must be visible
|
|
173
|
-
const roles = channel.stateManager.history.map(entryRole);
|
|
174
|
-
expect(roles).toContain('system');
|
|
175
|
-
await channel.stop();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('D4 (CLI-B07): tool_end marks tool isRunning=false; complete clears all (no stale spinner after complete)', async () => {
|
|
179
|
-
const channel = makeChannel();
|
|
180
|
-
await channel.start();
|
|
181
|
-
|
|
182
|
-
emitSessionEvent(channel, 'tool_start', MOCK_TOOL_RUNNING);
|
|
183
|
-
expect(channel.stateManager.activeTools).toHaveLength(1);
|
|
184
|
-
|
|
185
|
-
// After tool_end: tool stays in activeTools with isRunning:false (shows "ran" status during streaming)
|
|
186
|
-
emitSessionEvent(channel, 'tool_end', MOCK_TOOL_DONE);
|
|
187
|
-
expect(channel.stateManager.activeTools).toHaveLength(1);
|
|
188
|
-
expect(channel.stateManager.activeTools[0]!.isRunning).toBe(false);
|
|
189
|
-
|
|
190
|
-
// After complete: all tools cleared — StreamingIndicator must be gone
|
|
191
|
-
emitSessionEvent(channel, 'complete', makeResult());
|
|
192
|
-
expect(channel.stateManager.activeTools).toHaveLength(0);
|
|
193
|
-
|
|
194
|
-
await channel.stop();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('D5 (CLI-B08): thinking(false) clears activeTools even without complete', async () => {
|
|
198
|
-
const channel = makeChannel();
|
|
199
|
-
await channel.start();
|
|
200
|
-
|
|
201
|
-
emitSessionEvent(channel, 'thinking', true);
|
|
202
|
-
emitSessionEvent(channel, 'tool_start', MOCK_TOOL_RUNNING);
|
|
203
|
-
expect(channel.stateManager.activeTools).toHaveLength(1);
|
|
204
|
-
|
|
205
|
-
// abort path: thinking(false) without complete
|
|
206
|
-
emitSessionEvent(channel, 'thinking', false);
|
|
207
|
-
expect(channel.stateManager.activeTools).toHaveLength(0);
|
|
208
|
-
|
|
209
|
-
await channel.stop();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('D6: full turn — user entry appears first, assistant entry follows after complete', async () => {
|
|
213
|
-
const channel = makeChannel();
|
|
214
|
-
const mockSession = getMockSession(channel);
|
|
215
|
-
const userEntry = messageToHistoryEntry(createUserMessage('hello'));
|
|
216
|
-
const assistantEntry = messageToHistoryEntry(createAssistantMessage('world'));
|
|
217
|
-
// session history after complete includes both
|
|
218
|
-
mockSession.getFullHistory.mockReturnValue([userEntry, assistantEntry]);
|
|
219
|
-
await channel.start();
|
|
220
|
-
|
|
221
|
-
// 1. user message fires — visible immediately
|
|
222
|
-
emitSessionEvent(channel, 'user_message', 'hello');
|
|
223
|
-
expect(entryRole(channel.stateManager.history[0])).toBe('user');
|
|
224
|
-
|
|
225
|
-
// 2. streaming
|
|
226
|
-
emitSessionEvent(channel, 'text_delta', 'world');
|
|
227
|
-
expect(channel.stateManager.streamingText).toBe('world');
|
|
228
|
-
|
|
229
|
-
// 3. complete — syncHistory replaces with authoritative session history
|
|
230
|
-
emitSessionEvent(channel, 'complete', makeResult());
|
|
231
|
-
expect(channel.stateManager.streamingText).toBe('');
|
|
232
|
-
|
|
233
|
-
const roles = channel.stateManager.history.map(entryRole);
|
|
234
|
-
expect(roles[0]).toBe('user');
|
|
235
|
-
expect(roles[1]).toBe('assistant');
|
|
236
|
-
|
|
237
|
-
await channel.stop();
|
|
238
|
-
});
|
|
239
|
-
});
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for TuiInteractionChannel lifecycle:
|
|
3
|
-
* session event wiring, handleInput roundtrip, onChange propagation.
|
|
4
|
-
*
|
|
5
|
-
* No Ink rendering, no PTY — pure TypeScript.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
-
|
|
9
|
-
vi.mock('@robota-sdk/agent-framework', async () => {
|
|
10
|
-
const actual = await vi.importActual<typeof import('@robota-sdk/agent-framework')>(
|
|
11
|
-
'@robota-sdk/agent-framework',
|
|
12
|
-
);
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
InteractiveSession: vi.fn().mockImplementation(() => {
|
|
16
|
-
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
17
|
-
return {
|
|
18
|
-
getFullHistory: vi.fn().mockReturnValue([]),
|
|
19
|
-
setName: vi.fn(),
|
|
20
|
-
getName: vi.fn().mockReturnValue(undefined),
|
|
21
|
-
getPermissionMode: vi.fn().mockReturnValue('default'),
|
|
22
|
-
isInitialized: false,
|
|
23
|
-
on: vi.fn().mockImplementation((event: string, handler: (...args: unknown[]) => void) => {
|
|
24
|
-
if (!handlers.has(event)) handlers.set(event, []);
|
|
25
|
-
handlers.get(event)!.push(handler);
|
|
26
|
-
}),
|
|
27
|
-
off: vi.fn(),
|
|
28
|
-
emit: (event: string, ...args: unknown[]) => {
|
|
29
|
-
(handlers.get(event) ?? []).forEach((h) => h(...args));
|
|
30
|
-
},
|
|
31
|
-
submit: vi.fn().mockResolvedValue(undefined),
|
|
32
|
-
executeCommand: vi.fn().mockResolvedValue(null),
|
|
33
|
-
getPendingPrompt: vi.fn().mockReturnValue(null),
|
|
34
|
-
abort: vi.fn(),
|
|
35
|
-
cancelQueue: vi.fn(),
|
|
36
|
-
getContextState: vi.fn().mockReturnValue({
|
|
37
|
-
usedPercentage: 0,
|
|
38
|
-
usedTokens: 0,
|
|
39
|
-
maxTokens: 100_000,
|
|
40
|
-
}),
|
|
41
|
-
getExecutionWorkspaceSnapshot: vi.fn().mockReturnValue({ entries: [] }),
|
|
42
|
-
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
43
|
-
sendAgentJob: vi.fn().mockResolvedValue(undefined),
|
|
44
|
-
readExecutionWorkspaceDetail: vi.fn().mockResolvedValue({}),
|
|
45
|
-
};
|
|
46
|
-
}),
|
|
47
|
-
CommandRegistry: vi.fn().mockImplementation(() => ({
|
|
48
|
-
addModule: vi.fn(),
|
|
49
|
-
})),
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
54
|
-
|
|
55
|
-
import type { IAIProvider } from '@robota-sdk/agent-core';
|
|
56
|
-
import type {
|
|
57
|
-
IExecutionResult,
|
|
58
|
-
IInteractiveSession,
|
|
59
|
-
ITransportRegistryView,
|
|
60
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
61
|
-
|
|
62
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
type MockSession = {
|
|
65
|
-
getFullHistory: ReturnType<typeof vi.fn>;
|
|
66
|
-
submit: ReturnType<typeof vi.fn>;
|
|
67
|
-
executeCommand: ReturnType<typeof vi.fn>;
|
|
68
|
-
on: ReturnType<typeof vi.fn>;
|
|
69
|
-
emit: (event: string, ...args: unknown[]) => void;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
function getMockSession(channel: TuiInteractionChannel): MockSession {
|
|
73
|
-
return (channel as unknown as { interactiveSession: MockSession }).interactiveSession;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function emitSessionEvent(channel: TuiInteractionChannel, event: string, ...args: unknown[]): void {
|
|
77
|
-
getMockSession(channel).emit(event, ...args);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function makeMockTransportRegistry(): {
|
|
81
|
-
registry: ITransportRegistryView<IInteractiveSession>;
|
|
82
|
-
startAll: ReturnType<typeof vi.fn>;
|
|
83
|
-
stopAll: ReturnType<typeof vi.fn>;
|
|
84
|
-
} {
|
|
85
|
-
const startAll = vi.fn().mockResolvedValue(undefined);
|
|
86
|
-
const stopAll = vi.fn().mockResolvedValue(undefined);
|
|
87
|
-
return {
|
|
88
|
-
registry: { startAll, stopAll } as unknown as ITransportRegistryView<IInteractiveSession>,
|
|
89
|
-
startAll,
|
|
90
|
-
stopAll,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function makeChannel(opts?: {
|
|
95
|
-
transportRegistry?: ITransportRegistryView<IInteractiveSession>;
|
|
96
|
-
}): TuiInteractionChannel {
|
|
97
|
-
return new TuiInteractionChannel({
|
|
98
|
-
cwd: '/tmp/test',
|
|
99
|
-
provider: {} as IAIProvider,
|
|
100
|
-
...opts,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const MOCK_RESULT = {
|
|
105
|
-
contextState: { usedPercentage: 10, usedTokens: 1_000, maxTokens: 100_000 },
|
|
106
|
-
response: 'Hello!',
|
|
107
|
-
} as unknown as IExecutionResult;
|
|
108
|
-
|
|
109
|
-
const MOCK_TOOL = {
|
|
110
|
-
toolName: 'bash',
|
|
111
|
-
isRunning: true,
|
|
112
|
-
input: '{}',
|
|
113
|
-
startTime: Date.now(),
|
|
114
|
-
} as unknown as Parameters<
|
|
115
|
-
InstanceType<typeof TuiInteractionChannel>['stateManager']['onToolStart']
|
|
116
|
-
>[0];
|
|
117
|
-
|
|
118
|
-
beforeEach(() => {
|
|
119
|
-
vi.useFakeTimers();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
afterEach(() => {
|
|
123
|
-
vi.useRealTimers();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ── Group A: channel.start() / channel.stop() lifecycle ───────────────────────
|
|
127
|
-
|
|
128
|
-
describe('Group A — channel.start() / channel.stop() lifecycle', () => {
|
|
129
|
-
it('A1: text_delta after start() updates stateManager.streamingText', async () => {
|
|
130
|
-
const channel = makeChannel();
|
|
131
|
-
await channel.start();
|
|
132
|
-
|
|
133
|
-
emitSessionEvent(channel, 'text_delta', 'Hello!');
|
|
134
|
-
|
|
135
|
-
expect(channel.stateManager.streamingText).toBe('Hello!');
|
|
136
|
-
await channel.stop();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('A2: complete after start() clears streaming state and updates contextState', async () => {
|
|
140
|
-
const channel = makeChannel();
|
|
141
|
-
await channel.start();
|
|
142
|
-
|
|
143
|
-
emitSessionEvent(channel, 'text_delta', 'streaming...');
|
|
144
|
-
emitSessionEvent(channel, 'complete', MOCK_RESULT);
|
|
145
|
-
|
|
146
|
-
expect(channel.stateManager.streamingText).toBe('');
|
|
147
|
-
expect(channel.stateManager.contextState.percentage).toBe(10);
|
|
148
|
-
expect(channel.stateManager.contextState.usedTokens).toBe(1_000);
|
|
149
|
-
await channel.stop();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('A3: tool_start after start() adds entry to stateManager.activeTools', async () => {
|
|
153
|
-
const channel = makeChannel();
|
|
154
|
-
await channel.start();
|
|
155
|
-
|
|
156
|
-
emitSessionEvent(channel, 'tool_start', MOCK_TOOL);
|
|
157
|
-
|
|
158
|
-
expect(channel.stateManager.activeTools).toHaveLength(1);
|
|
159
|
-
expect(channel.stateManager.activeTools[0]).toMatchObject({ toolName: 'bash' });
|
|
160
|
-
await channel.stop();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('A4: error after start() clears stateManager.streamingText', async () => {
|
|
164
|
-
const channel = makeChannel();
|
|
165
|
-
await channel.start();
|
|
166
|
-
|
|
167
|
-
emitSessionEvent(channel, 'text_delta', 'partial...');
|
|
168
|
-
emitSessionEvent(channel, 'error');
|
|
169
|
-
|
|
170
|
-
expect(channel.stateManager.streamingText).toBe('');
|
|
171
|
-
await channel.stop();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('A5: calling start() twice does not duplicate subscriptions', async () => {
|
|
175
|
-
const channel = makeChannel();
|
|
176
|
-
await channel.start();
|
|
177
|
-
await channel.start(); // second call is a no-op (sessionStarted guard)
|
|
178
|
-
|
|
179
|
-
emitSessionEvent(channel, 'text_delta', 'hi');
|
|
180
|
-
|
|
181
|
-
expect(channel.stateManager.streamingText).toBe('hi'); // not 'hihi'
|
|
182
|
-
await channel.stop();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('A6: stop() calls transportRegistry.stopAll exactly once', async () => {
|
|
186
|
-
const { registry, stopAll } = makeMockTransportRegistry();
|
|
187
|
-
const channel = makeChannel({ transportRegistry: registry });
|
|
188
|
-
await channel.start();
|
|
189
|
-
await channel.stop();
|
|
190
|
-
|
|
191
|
-
expect(stopAll).toHaveBeenCalledOnce();
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// ── Group B: handleInput() AI-response roundtrip ──────────────────────────────
|
|
196
|
-
|
|
197
|
-
describe('Group B — handleInput() roundtrip', () => {
|
|
198
|
-
it('B1: handleInput("hello") calls session.submit with "hello"', async () => {
|
|
199
|
-
const channel = makeChannel();
|
|
200
|
-
await channel.start();
|
|
201
|
-
|
|
202
|
-
await channel.handleInput('hello');
|
|
203
|
-
|
|
204
|
-
const mockSession = getMockSession(channel);
|
|
205
|
-
expect(mockSession.submit).toHaveBeenCalledWith('hello');
|
|
206
|
-
await channel.stop();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('B2: text_delta + complete syncs history to stateManager', async () => {
|
|
210
|
-
const channel = makeChannel();
|
|
211
|
-
const mockSession = getMockSession(channel);
|
|
212
|
-
const historyEntry = {
|
|
213
|
-
role: 'assistant',
|
|
214
|
-
content: [{ type: 'text', text: 'Hi!' }],
|
|
215
|
-
timestamp: Date.now(),
|
|
216
|
-
};
|
|
217
|
-
mockSession.getFullHistory.mockReturnValue([historyEntry]);
|
|
218
|
-
await channel.start();
|
|
219
|
-
|
|
220
|
-
await channel.handleInput('hello');
|
|
221
|
-
emitSessionEvent(channel, 'text_delta', 'Hi!');
|
|
222
|
-
expect(channel.stateManager.streamingText).toBe('Hi!');
|
|
223
|
-
|
|
224
|
-
emitSessionEvent(channel, 'complete', MOCK_RESULT);
|
|
225
|
-
expect(channel.stateManager.streamingText).toBe('');
|
|
226
|
-
expect(channel.stateManager.history).toHaveLength(1);
|
|
227
|
-
|
|
228
|
-
await channel.stop();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('B3: handleInput("/help") calls executeCommand, not session.submit', async () => {
|
|
232
|
-
const channel = makeChannel();
|
|
233
|
-
await channel.start();
|
|
234
|
-
|
|
235
|
-
await channel.handleInput('/help');
|
|
236
|
-
|
|
237
|
-
const mockSession = getMockSession(channel);
|
|
238
|
-
expect(mockSession.submit).not.toHaveBeenCalled();
|
|
239
|
-
expect(mockSession.executeCommand).toHaveBeenCalledWith('help', '');
|
|
240
|
-
await channel.stop();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('B4: handleInput("hello") triggers channel.onChange at least once', async () => {
|
|
244
|
-
const channel = makeChannel();
|
|
245
|
-
const onChange = vi.fn();
|
|
246
|
-
channel.onChange = onChange;
|
|
247
|
-
await channel.start();
|
|
248
|
-
onChange.mockClear();
|
|
249
|
-
|
|
250
|
-
await channel.handleInput('hello');
|
|
251
|
-
emitSessionEvent(channel, 'text_delta', 'hey');
|
|
252
|
-
|
|
253
|
-
expect(onChange).toHaveBeenCalled();
|
|
254
|
-
await channel.stop();
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ── Group C: onChange propagation invariant ───────────────────────────────────
|
|
259
|
-
|
|
260
|
-
describe('Group C — onChange propagation invariant', () => {
|
|
261
|
-
it('C1: session event after start() causes channel.onChange to fire', async () => {
|
|
262
|
-
const channel = makeChannel();
|
|
263
|
-
const onChange = vi.fn();
|
|
264
|
-
channel.onChange = onChange;
|
|
265
|
-
await channel.start();
|
|
266
|
-
onChange.mockClear();
|
|
267
|
-
|
|
268
|
-
// tool_start calls notify() directly (not debounced), so onChange fires immediately
|
|
269
|
-
emitSessionEvent(channel, 'tool_start', MOCK_TOOL);
|
|
270
|
-
|
|
271
|
-
expect(onChange).toHaveBeenCalled();
|
|
272
|
-
await channel.stop();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('C2: channel.onChange does not fire for events before start()', () => {
|
|
276
|
-
const channel = makeChannel();
|
|
277
|
-
const onChange = vi.fn();
|
|
278
|
-
channel.onChange = onChange;
|
|
279
|
-
// Do NOT call channel.start() — handlers not registered yet
|
|
280
|
-
emitSessionEvent(channel, 'text_delta', 'hello'); // no-op: no handlers
|
|
281
|
-
|
|
282
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('C3: channel.onChange does not fire for events after stop()', async () => {
|
|
286
|
-
const channel = makeChannel();
|
|
287
|
-
const onChange = vi.fn();
|
|
288
|
-
channel.onChange = onChange;
|
|
289
|
-
await channel.start();
|
|
290
|
-
await channel.stop(); // sets this.onChange = null
|
|
291
|
-
onChange.mockClear();
|
|
292
|
-
|
|
293
|
-
emitSessionEvent(channel, 'text_delta', 'hello');
|
|
294
|
-
|
|
295
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
296
|
-
});
|
|
297
|
-
});
|