@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.
Files changed (187) hide show
  1. package/README.md +10 -10
  2. package/dist/node/headless/index.cjs +1 -1
  3. package/dist/node/{headless-CT2ibQnr.cjs → headless-OnpVk4-k.cjs} +7 -7
  4. package/dist/node/index.cjs +1 -1
  5. package/dist/node/index.d.ts +1 -6
  6. package/dist/node/index.d.ts.map +1 -1
  7. package/dist/node/index.js +1 -1
  8. package/dist/node/index.js.map +1 -1
  9. package/package.json +7 -75
  10. package/src/index.ts +1 -5
  11. package/src/transport-registry.ts +0 -9
  12. package/dist/node/http/index.cjs +0 -1
  13. package/dist/node/http/index.d.ts +0 -2
  14. package/dist/node/http/index.js +0 -1
  15. package/dist/node/http-2Jiuflc1.js +0 -2
  16. package/dist/node/http-2Jiuflc1.js.map +0 -1
  17. package/dist/node/http-CBAvefLw.cjs +0 -1
  18. package/dist/node/index-BNccqSpv.d.ts +0 -86
  19. package/dist/node/index-BNccqSpv.d.ts.map +0 -1
  20. package/dist/node/index-BUhHIf7X.d.ts +0 -86
  21. package/dist/node/index-BUhHIf7X.d.ts.map +0 -1
  22. package/dist/node/index-BnAGE-u9.d.ts +0 -33
  23. package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
  24. package/dist/node/index-BrQ4gGw0.d.ts +0 -213
  25. package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
  26. package/dist/node/index-CoeBF21y.d.ts +0 -213
  27. package/dist/node/index-CoeBF21y.d.ts.map +0 -1
  28. package/dist/node/index-DHt-2VQ-.d.ts +0 -46
  29. package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
  30. package/dist/node/index-DMwKN5Le.d.ts +0 -33
  31. package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
  32. package/dist/node/index-c0M42fsA.d.ts +0 -46
  33. package/dist/node/index-c0M42fsA.d.ts.map +0 -1
  34. package/dist/node/mcp/index.cjs +0 -1
  35. package/dist/node/mcp/index.d.ts +0 -2
  36. package/dist/node/mcp/index.js +0 -1
  37. package/dist/node/mcp-BOglBJNy.cjs +0 -1
  38. package/dist/node/mcp-D3BBVK7C.js +0 -2
  39. package/dist/node/mcp-D3BBVK7C.js.map +0 -1
  40. package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
  41. package/dist/node/tui/index.cjs +0 -1
  42. package/dist/node/tui/index.d.ts +0 -2
  43. package/dist/node/tui/index.js +0 -1
  44. package/dist/node/tui-CcH5EsQh.js +0 -25
  45. package/dist/node/tui-CcH5EsQh.js.map +0 -1
  46. package/dist/node/tui-DznRbcku.cjs +0 -24
  47. package/dist/node/ws/index.cjs +0 -1
  48. package/dist/node/ws/index.d.ts +0 -2
  49. package/dist/node/ws/index.js +0 -1
  50. package/dist/node/ws-Dc2RUwVs.js +0 -2
  51. package/dist/node/ws-Dc2RUwVs.js.map +0 -1
  52. package/dist/node/ws-QNMQn5kg.cjs +0 -1
  53. package/src/http/__tests__/http-transport.test.ts +0 -55
  54. package/src/http/__tests__/routes.test.ts +0 -168
  55. package/src/http/http-transport.ts +0 -41
  56. package/src/http/index.ts +0 -4
  57. package/src/http/routes.ts +0 -152
  58. package/src/mcp/__tests__/mcp-server.test.ts +0 -66
  59. package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
  60. package/src/mcp/index.ts +0 -4
  61. package/src/mcp/mcp-server.ts +0 -163
  62. package/src/mcp/mcp-transport.ts +0 -48
  63. package/src/tui/App.tsx +0 -491
  64. package/src/tui/BackgroundTaskPanel.tsx +0 -36
  65. package/src/tui/CjkTextInput.tsx +0 -199
  66. package/src/tui/ConfirmPrompt.tsx +0 -70
  67. package/src/tui/ContextWarningBanner.tsx +0 -34
  68. package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
  69. package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
  70. package/src/tui/InputArea.tsx +0 -310
  71. package/src/tui/InteractivePrompt.tsx +0 -59
  72. package/src/tui/ListPicker.tsx +0 -95
  73. package/src/tui/MenuSelect.tsx +0 -104
  74. package/src/tui/MessageList.tsx +0 -284
  75. package/src/tui/PermissionPrompt.tsx +0 -86
  76. package/src/tui/PluginTUI.tsx +0 -258
  77. package/src/tui/SessionPicker.tsx +0 -68
  78. package/src/tui/SessionStatusBar.tsx +0 -73
  79. package/src/tui/SlashAutocomplete.tsx +0 -110
  80. package/src/tui/StatusBar.tsx +0 -236
  81. package/src/tui/StreamingIndicator.tsx +0 -93
  82. package/src/tui/TextPrompt.tsx +0 -81
  83. package/src/tui/ToolCommandOutput.tsx +0 -39
  84. package/src/tui/ToolDiffBlock.tsx +0 -32
  85. package/src/tui/TransportTUI.tsx +0 -117
  86. package/src/tui/TuiInteractionChannel.ts +0 -495
  87. package/src/tui/UpdateNotice.tsx +0 -14
  88. package/src/tui/UsageSummaryEntry.tsx +0 -39
  89. package/src/tui/WaveText.tsx +0 -44
  90. package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
  91. package/src/tui/__tests__/ListPicker.test.tsx +0 -159
  92. package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
  93. package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
  94. package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
  95. package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
  96. package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
  97. package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
  98. package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
  99. package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
  100. package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
  101. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
  102. package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
  103. package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
  104. package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
  105. package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
  106. package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
  107. package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
  108. package/src/tui/__tests__/command-output-summary.test.ts +0 -95
  109. package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
  110. package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
  111. package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
  112. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
  113. package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
  114. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
  115. package/src/tui/__tests__/input-area-flow.test.ts +0 -164
  116. package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
  117. package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
  118. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
  119. package/src/tui/__tests__/pty/pty-driver.ts +0 -135
  120. package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
  121. package/src/tui/__tests__/render-channel-options.test.ts +0 -32
  122. package/src/tui/__tests__/render-markdown.test.ts +0 -72
  123. package/src/tui/__tests__/selection-flow.test.ts +0 -61
  124. package/src/tui/__tests__/session-init-poller.test.ts +0 -102
  125. package/src/tui/__tests__/session-naming.test.ts +0 -64
  126. package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
  127. package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
  128. package/src/tui/__tests__/status-activity.test.ts +0 -71
  129. package/src/tui/__tests__/status-bar.test.tsx +0 -177
  130. package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
  131. package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
  132. package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
  133. package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
  134. package/src/tui/background-task-row-format.ts +0 -53
  135. package/src/tui/command-interaction.ts +0 -9
  136. package/src/tui/command-output-summary.ts +0 -122
  137. package/src/tui/create-default-tui-cli-adapter.ts +0 -41
  138. package/src/tui/execution-workspace-view-model.ts +0 -123
  139. package/src/tui/flows/cjk-text-input-flow.ts +0 -285
  140. package/src/tui/flows/confirm-prompt-flow.ts +0 -45
  141. package/src/tui/flows/input-area-flow.ts +0 -189
  142. package/src/tui/flows/permission-prompt-flow.ts +0 -85
  143. package/src/tui/flows/selection-flow.ts +0 -126
  144. package/src/tui/flows/session-init-poller.ts +0 -77
  145. package/src/tui/flows/text-prompt-flow.ts +0 -98
  146. package/src/tui/hooks/command-effect-handler.ts +0 -97
  147. package/src/tui/hooks/command-effect-queue.ts +0 -39
  148. package/src/tui/hooks/side-effects-types.ts +0 -35
  149. package/src/tui/hooks/useAutocomplete.ts +0 -87
  150. package/src/tui/hooks/usePluginCallbacks.ts +0 -31
  151. package/src/tui/hooks/usePluginScreenData.ts +0 -85
  152. package/src/tui/hooks/useSideEffects.ts +0 -175
  153. package/src/tui/hooks/useSlashRouting.ts +0 -118
  154. package/src/tui/hooks/useStatusLineSettings.ts +0 -37
  155. package/src/tui/hooks/useTuiChannel.ts +0 -95
  156. package/src/tui/index.ts +0 -14
  157. package/src/tui/interactions/CommandConfirm.tsx +0 -36
  158. package/src/tui/interactions/CommandPicker.tsx +0 -77
  159. package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
  160. package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
  161. package/src/tui/plugin-tui-handlers.ts +0 -163
  162. package/src/tui/render-markdown.ts +0 -130
  163. package/src/tui/render.tsx +0 -129
  164. package/src/tui/session-naming.ts +0 -33
  165. package/src/tui/status-activity.ts +0 -63
  166. package/src/tui/tui-cli-adapter-context.tsx +0 -13
  167. package/src/tui/tui-cli-adapter.ts +0 -25
  168. package/src/tui/tui-state-manager.ts +0 -226
  169. package/src/tui/tui-transport.ts +0 -35
  170. package/src/tui/types.ts +0 -15
  171. package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
  172. package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
  173. package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
  174. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
  175. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
  176. package/src/tui/utils/edit-diff.ts +0 -153
  177. package/src/tui/utils/paste-labels.ts +0 -9
  178. package/src/tui/utils/tool-call-extractor.ts +0 -92
  179. package/src/tui/utils/tool-diff-summary.ts +0 -75
  180. package/src/ws/__tests__/ws-handler.test.ts +0 -409
  181. package/src/ws/__tests__/ws-transport.test.ts +0 -53
  182. package/src/ws/index.ts +0 -13
  183. package/src/ws/ws-background-messages.ts +0 -170
  184. package/src/ws/ws-handler.ts +0 -280
  185. package/src/ws/ws-protocol.ts +0 -78
  186. package/src/ws/ws-transport-configurable.ts +0 -128
  187. 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
- });