@robota-sdk/agent-transport 3.0.0-beta.75 → 3.0.0-beta.77

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