@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,32 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import type { IAIProvider } from '@robota-sdk/agent-core';
3
- import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
4
- import { toChannelOptions } from '../render.js';
5
- import type { IRenderOptions } from '../render.js';
6
-
7
- describe('toChannelOptions', () => {
8
- it('TC-02: threads allowedTools and deniedTools into the channel options', () => {
9
- const renderOptions: IRenderOptions = {
10
- cwd: '/tmp/project',
11
- provider: {} as IAIProvider,
12
- cliAdapter: {} as ITuiCliAdapter,
13
- allowedTools: ['Read'],
14
- deniedTools: ['Bash'],
15
- };
16
- const channelOptions = toChannelOptions(renderOptions, 'session-1');
17
- expect(channelOptions.allowedTools).toEqual(['Read']);
18
- expect(channelOptions.deniedTools).toEqual(['Bash']);
19
- expect(channelOptions.resumeSessionId).toBe('session-1');
20
- expect(channelOptions.cwd).toBe('/tmp/project');
21
- });
22
-
23
- it('leaves tool filters undefined when not provided', () => {
24
- const channelOptions = toChannelOptions({
25
- cwd: '/tmp/project',
26
- provider: {} as IAIProvider,
27
- cliAdapter: {} as ITuiCliAdapter,
28
- });
29
- expect(channelOptions.allowedTools).toBeUndefined();
30
- expect(channelOptions.deniedTools).toBeUndefined();
31
- });
32
- });
@@ -1,72 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { renderMarkdown } from '../render-markdown.js';
3
-
4
- const ANSI_LIGHT_RED = '\u001b[38;5;210m';
5
- const ANSI_LIGHT_GREEN = '\u001b[38;5;120m';
6
- const ANSI_DARK_RED_BACKGROUND = '\u001b[48;5;52m';
7
- const ANSI_DARK_GREEN_BACKGROUND = '\u001b[48;5;22m';
8
- const ANSI_RESET = '\u001b[0m';
9
- const CODE_BLOCK_INDENT = ' ';
10
-
11
- describe('renderMarkdown', () => {
12
- it('renders diff fenced code blocks with addition and removal colors', () => {
13
- const output = renderMarkdown(
14
- ['Before', '', '```diff', '- const oldValue = true;', '+ const newValue = true;', '```'].join(
15
- '\n',
16
- ),
17
- { color: true },
18
- );
19
-
20
- expect(output).toContain(`${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}`);
21
- expect(output).toContain(`${CODE_BLOCK_INDENT}- const oldValue = true;`);
22
- expect(output).toContain(`${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}`);
23
- expect(output).toContain(`${CODE_BLOCK_INDENT}+ const newValue = true;`);
24
- });
25
-
26
- it('pads added and removed diff rows before applying background colors', () => {
27
- const codeBlockWidth = 24;
28
- const removedRow = `${CODE_BLOCK_INDENT}- removed`.padEnd(codeBlockWidth);
29
- const addedRow = `${CODE_BLOCK_INDENT}+ added`.padEnd(codeBlockWidth);
30
- const output = renderMarkdown(['```diff', '- removed', '+ added', '```'].join('\n'), {
31
- color: true,
32
- codeBlockWidth,
33
- });
34
-
35
- expect(output).toContain(
36
- `${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}${removedRow}${ANSI_RESET}`,
37
- );
38
- expect(output).toContain(
39
- `${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}${addedRow}${ANSI_RESET}`,
40
- );
41
- });
42
-
43
- it('keeps diff fenced code block content readable when color is disabled', () => {
44
- const output = renderMarkdown(
45
- ['```diff', '- removed line', '+ added line', ' unchanged line', '```'].join('\n'),
46
- { color: false },
47
- );
48
-
49
- expect(output).toContain('- removed line');
50
- expect(output).toContain('+ added line');
51
- expect(output).toContain(' unchanged line');
52
- expect(output).not.toContain(ANSI_LIGHT_RED);
53
- expect(output).not.toContain(ANSI_LIGHT_GREEN);
54
- expect(output).not.toContain(ANSI_DARK_RED_BACKGROUND);
55
- expect(output).not.toContain(ANSI_DARK_GREEN_BACKGROUND);
56
- });
57
-
58
- it('keeps regular fenced code blocks as code output', () => {
59
- const output = renderMarkdown(['```ts', 'const value: string = "ok";', '```'].join('\n'), {
60
- color: false,
61
- });
62
-
63
- expect(output).toContain('const value: string = "ok";');
64
- });
65
-
66
- it('keeps inline markdown formatting readable', () => {
67
- const output = renderMarkdown('Use **bold** and `code` here.', { color: false });
68
-
69
- expect(output).toContain('bold');
70
- expect(output).toContain('code');
71
- });
72
- });
@@ -1,61 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- applySelectionInput,
4
- createSelectionFlowState,
5
- getDirectionalSelectionInputAction,
6
- getVerticalSelectionInputAction,
7
- } from '../flows/selection-flow.js';
8
-
9
- describe('selection flow', () => {
10
- it('Given first item selected When previous is applied Then selection stays bounded', () => {
11
- const result = applySelectionInput(createSelectionFlowState(), 'previous', { itemCount: 3 });
12
-
13
- expect(result.state.selectedIndex).toBe(0);
14
- expect(result.effect).toEqual({ type: 'none' });
15
- });
16
-
17
- it('Given last item selected When next is applied Then selection stays bounded', () => {
18
- const state = { selectedIndex: 2, scrollOffset: 0, resolved: false };
19
-
20
- const result = applySelectionInput(state, 'next', { itemCount: 3 });
21
-
22
- expect(result.state.selectedIndex).toBe(2);
23
- });
24
-
25
- it('Given wrapping selection When previous from first is applied Then it wraps to last', () => {
26
- const result = applySelectionInput(createSelectionFlowState(), 'previous', {
27
- itemCount: 3,
28
- wrap: true,
29
- });
30
-
31
- expect(result.state.selectedIndex).toBe(2);
32
- });
33
-
34
- it('Given max visible window When moving below viewport Then scroll offset follows', () => {
35
- const state = applySelectionInput(createSelectionFlowState(), 'next', {
36
- itemCount: 4,
37
- maxVisible: 2,
38
- }).state;
39
-
40
- const result = applySelectionInput(state, 'next', { itemCount: 4, maxVisible: 2 });
41
-
42
- expect(result.state).toMatchObject({ selectedIndex: 2, scrollOffset: 1 });
43
- });
44
-
45
- it('Given selected item When select is applied Then selected index is emitted once', () => {
46
- const state = { selectedIndex: 1, scrollOffset: 0, resolved: false };
47
-
48
- const selected = applySelectionInput(state, 'select', { itemCount: 3 });
49
- const ignored = applySelectionInput(selected.state, 'select', { itemCount: 3 });
50
-
51
- expect(selected.effect).toEqual({ type: 'select', index: 1 });
52
- expect(ignored.effect).toEqual({ type: 'none' });
53
- });
54
-
55
- it('Given raw key info When mapped Then vertical and directional actions are produced', () => {
56
- expect(getVerticalSelectionInputAction({ downArrow: true })).toBe('next');
57
- expect(getVerticalSelectionInputAction({ escape: true })).toBe('cancel');
58
- expect(getDirectionalSelectionInputAction({ leftArrow: true })).toBe('previous');
59
- expect(getDirectionalSelectionInputAction({ rightArrow: true })).toBe('next');
60
- });
61
- });
@@ -1,102 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- import { createSessionInitPoller } from '../flows/session-init-poller.js';
4
-
5
- describe('createSessionInitPoller', () => {
6
- beforeEach(() => {
7
- vi.useFakeTimers();
8
- });
9
-
10
- afterEach(() => {
11
- vi.useRealTimers();
12
- });
13
-
14
- it('TC-04: calls onReady and stops once the check succeeds', () => {
15
- const onReady = vi.fn();
16
- const onFailure = vi.fn();
17
- let ready = false;
18
- const poller = createSessionInitPoller({
19
- check: () => {
20
- if (!ready) throw new Error('InteractiveSession not initialized. Call submit().');
21
- },
22
- intervalMs: 200,
23
- timeoutMs: 15000,
24
- onReady,
25
- onFailure,
26
- });
27
- poller.start();
28
- vi.advanceTimersByTime(600);
29
- expect(onReady).not.toHaveBeenCalled();
30
- ready = true;
31
- vi.advanceTimersByTime(200);
32
- expect(onReady).toHaveBeenCalledTimes(1);
33
- vi.advanceTimersByTime(2000);
34
- expect(onReady).toHaveBeenCalledTimes(1);
35
- expect(onFailure).not.toHaveBeenCalled();
36
- });
37
-
38
- it('TC-04: benign not-initialized errors poll until timeout, then fail with timeout kind', () => {
39
- const onReady = vi.fn();
40
- const onFailure = vi.fn();
41
- const poller = createSessionInitPoller({
42
- check: () => {
43
- throw new Error('InteractiveSession not initialized. Call submit().');
44
- },
45
- intervalMs: 200,
46
- timeoutMs: 1000,
47
- onReady,
48
- onFailure,
49
- });
50
- poller.start();
51
- vi.advanceTimersByTime(900);
52
- expect(onFailure).not.toHaveBeenCalled();
53
- vi.advanceTimersByTime(400);
54
- expect(onFailure).toHaveBeenCalledTimes(1);
55
- expect(onFailure.mock.calls[0]?.[0]).toMatchObject({ kind: 'timeout' });
56
- expect(onReady).not.toHaveBeenCalled();
57
- vi.advanceTimersByTime(2000);
58
- expect(onFailure).toHaveBeenCalledTimes(1);
59
- });
60
-
61
- it('TC-04: a real error fails immediately with the error attached', () => {
62
- const onFailure = vi.fn();
63
- const poller = createSessionInitPoller({
64
- check: () => {
65
- throw new Error('ENOENT: session store unreadable');
66
- },
67
- intervalMs: 200,
68
- timeoutMs: 15000,
69
- onReady: vi.fn(),
70
- onFailure,
71
- });
72
- poller.start();
73
- vi.advanceTimersByTime(200);
74
- expect(onFailure).toHaveBeenCalledTimes(1);
75
- expect(onFailure.mock.calls[0]?.[0]).toMatchObject({ kind: 'error' });
76
- expect(String((onFailure.mock.calls[0]?.[0] as { error: Error }).error.message)).toContain(
77
- 'ENOENT',
78
- );
79
- vi.advanceTimersByTime(2000);
80
- expect(onFailure).toHaveBeenCalledTimes(1);
81
- });
82
-
83
- it('stop() cancels polling without callbacks', () => {
84
- const onReady = vi.fn();
85
- const onFailure = vi.fn();
86
- const poller = createSessionInitPoller({
87
- check: () => {
88
- throw new Error('InteractiveSession not initialized.');
89
- },
90
- intervalMs: 200,
91
- timeoutMs: 1000,
92
- onReady,
93
- onFailure,
94
- });
95
- poller.start();
96
- vi.advanceTimersByTime(400);
97
- poller.stop();
98
- vi.advanceTimersByTime(5000);
99
- expect(onReady).not.toHaveBeenCalled();
100
- expect(onFailure).not.toHaveBeenCalled();
101
- });
102
- });
@@ -1,64 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { generateSessionName } from '../session-naming.js';
3
- import type { IAIProvider } from '@robota-sdk/agent-core';
4
-
5
- function makeProvider(responseContent: string): IAIProvider {
6
- return {
7
- name: 'mock',
8
- version: '1.0.0',
9
- chat: vi.fn().mockResolvedValue({ role: 'assistant', content: responseContent }),
10
- generateResponse: vi.fn(),
11
- supportsTools: () => false,
12
- } as unknown as IAIProvider;
13
- }
14
-
15
- describe('generateSessionName', () => {
16
- it('returns sanitized name from provider response', async () => {
17
- const provider = makeProvider('refactor-auth-middleware');
18
- const name = await generateSessionName(provider, 'Refactor the auth middleware');
19
- expect(name).toBe('refactor-auth-middleware');
20
- });
21
-
22
- it('lowercases and strips special characters', async () => {
23
- const provider = makeProvider('Fix: Database Connection!');
24
- const name = await generateSessionName(provider, 'Fix database connection');
25
- expect(name).toBe('fix-database-connection');
26
- });
27
-
28
- it('collapses multiple spaces to single hyphen', async () => {
29
- const provider = makeProvider('write api docs');
30
- const name = await generateSessionName(provider, 'Write API docs');
31
- expect(name).toBe('write-api-docs');
32
- });
33
-
34
- it('falls back to sanitized first message when response is too short', async () => {
35
- const provider = makeProvider('ok');
36
- const name = await generateSessionName(provider, 'Fix login bug');
37
- expect(name).toBe('fix-login-bug');
38
- });
39
-
40
- it('truncates long names to 60 chars', async () => {
41
- const long = 'a'.repeat(100);
42
- const provider = makeProvider(long);
43
- const name = await generateSessionName(provider, 'something');
44
- expect(name.length).toBeLessThanOrEqual(60);
45
- });
46
-
47
- it('passes maxTokens: 20 to provider', async () => {
48
- const provider = makeProvider('short-name');
49
- await generateSessionName(provider, 'test');
50
- expect(provider.chat).toHaveBeenCalledWith(
51
- expect.any(Array),
52
- expect.objectContaining({ maxTokens: 20 }),
53
- );
54
- });
55
-
56
- it('truncates first message to 200 chars before sending', async () => {
57
- const long = 'x'.repeat(500);
58
- const provider = makeProvider('short-name');
59
- await generateSessionName(provider, long);
60
- const messages = (provider.chat as ReturnType<typeof vi.fn>).mock.calls[0][0];
61
- const userMsg = messages.find((m: { role: string }) => m.role === 'user');
62
- expect(userMsg.content.length).toBeLessThanOrEqual(200);
63
- });
64
- });
@@ -1,307 +0,0 @@
1
- /**
2
- * CLI-B11 TC-01/03/05 + CLI-B12 TC-01/02/04: session-switch channel ownership
3
- * at the App boundary.
4
- *
5
- * The 2026-05-31 context-loss bug lived between render.tsx, App.tsx and
6
- * TuiInteractionChannel — InteractiveSession-level tests stayed green through it.
7
- * These tests render the REAL App with a mocked createChannel factory and drive
8
- * switches through the real SessionPicker, pinning the factory-call contract.
9
- * Since CLI-B12 the factory is the SOLE channel source: App creates the initial
10
- * channel in its useState initializer and replaces it on every switch.
11
- */
12
-
13
- import { mkdtempSync, rmSync } from 'node:fs';
14
- import { tmpdir } from 'node:os';
15
- import { join } from 'node:path';
16
-
17
- import { render } from 'ink-testing-library';
18
- import React from 'react';
19
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
20
-
21
- import App from '../App.js';
22
- import { CommandEffectQueue } from '../hooks/command-effect-queue.js';
23
- import { TuiStateManager } from '../tui-state-manager.js';
24
-
25
- import type { ICommandEffectQueue } from '../hooks/command-effect-queue.js';
26
- import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
27
- import type { TuiInteractionChannel } from '../TuiInteractionChannel.js';
28
- import type {
29
- IInteractiveSessionRecord,
30
- IInteractiveSessionStore,
31
- } from '@robota-sdk/agent-interface-transport';
32
-
33
- const TICK_MS = 30;
34
- const FRAME_DEADLINE_MS = 3000;
35
-
36
- function tick(ms = TICK_MS): Promise<void> {
37
- return new Promise((resolve) => setTimeout(resolve, ms));
38
- }
39
-
40
- async function waitForFrame(
41
- lastFrame: () => string | undefined,
42
- predicate: (frame: string) => boolean,
43
- ): Promise<void> {
44
- const deadline = Date.now() + FRAME_DEADLINE_MS;
45
- while (Date.now() < deadline) {
46
- const frame = lastFrame();
47
- if (frame !== undefined && predicate(frame)) return;
48
- await tick(10);
49
- }
50
- throw new Error(`waitForFrame timeout\n--- frame ---\n${lastFrame() ?? '<none>'}`);
51
- }
52
-
53
- interface IFakeChannel {
54
- sessionName: string | undefined;
55
- stateManager: TuiStateManager;
56
- onChange: (() => void) | null;
57
- isShuttingDown: boolean;
58
- permissionRequest: null;
59
- start: ReturnType<typeof vi.fn>;
60
- stop: ReturnType<typeof vi.fn>;
61
- handleInput: ReturnType<typeof vi.fn>;
62
- abort: ReturnType<typeof vi.fn>;
63
- cancelQueue: ReturnType<typeof vi.fn>;
64
- shutdown: ReturnType<typeof vi.fn>;
65
- selectExecutionWorkspaceEntry: ReturnType<typeof vi.fn>;
66
- readExecutionWorkspaceDetail: ReturnType<typeof vi.fn>;
67
- getSession: () => unknown;
68
- getRegistry: () => unknown;
69
- getCommandEffectQueue: () => ICommandEffectQueue;
70
- /** Test handle: the queue backing getCommandEffectQueue. */
71
- effectQueue: CommandEffectQueue;
72
- /** Test handle: which resumeSessionId this channel was created for. */
73
- createdFor: string | undefined;
74
- }
75
-
76
- function createFakeChannel(createdFor: string | undefined): IFakeChannel {
77
- const effectQueue = new CommandEffectQueue();
78
- const fakeSession = {
79
- getName: (): string | undefined => undefined,
80
- getSession: (): never => {
81
- throw new Error('session not initialized (test fake)');
82
- },
83
- getFullHistory: (): never[] => [],
84
- setName: vi.fn(),
85
- shutdown: vi.fn(async () => {}),
86
- sendAgentJob: vi.fn(async () => {}),
87
- };
88
- const fakeRegistry = {
89
- getCommands: (): never[] => [],
90
- getSubcommands: (): never[] => [],
91
- };
92
- return {
93
- sessionName: undefined,
94
- stateManager: new TuiStateManager(),
95
- onChange: null,
96
- isShuttingDown: false,
97
- permissionRequest: null,
98
- start: vi.fn(async () => {}),
99
- stop: vi.fn(async () => {}),
100
- handleInput: vi.fn(async () => {}),
101
- abort: vi.fn(),
102
- cancelQueue: vi.fn(),
103
- shutdown: vi.fn(async () => {}),
104
- selectExecutionWorkspaceEntry: vi.fn(),
105
- readExecutionWorkspaceDetail: vi.fn(async () => ({ lines: [], title: '' })),
106
- getSession: () => fakeSession,
107
- getRegistry: () => fakeRegistry,
108
- getCommandEffectQueue: () => effectQueue,
109
- effectQueue,
110
- createdFor,
111
- };
112
- }
113
-
114
- function asChannel(fake: IFakeChannel): TuiInteractionChannel {
115
- return fake as unknown as TuiInteractionChannel;
116
- }
117
-
118
- function createFakeStore(records: IInteractiveSessionRecord[]): IInteractiveSessionStore {
119
- return {
120
- save: () => undefined,
121
- load: (id) => records.find((r) => r.id === id),
122
- list: () => records,
123
- delete: () => undefined,
124
- };
125
- }
126
-
127
- function sessionRecord(
128
- id: string,
129
- cwd: string,
130
- updatedAt = '2026-06-13T00:00:00.000Z',
131
- ): IInteractiveSessionRecord {
132
- return {
133
- id,
134
- cwd,
135
- createdAt: '2026-06-13T00:00:00.000Z',
136
- updatedAt,
137
- messages: [
138
- { role: 'user', content: `hello from ${id}` },
139
- { role: 'assistant', content: `reply in ${id}` },
140
- ] as IInteractiveSessionRecord['messages'],
141
- };
142
- }
143
-
144
- /** The picker lists sessions newest-first; bumping updatedAt puts a record on top. */
145
- function touch(records: IInteractiveSessionRecord[], id: string, updatedAt: string): void {
146
- const record = records.find((r) => r.id === id);
147
- if (!record) throw new Error(`no record ${id}`);
148
- record.updatedAt = updatedAt;
149
- }
150
-
151
- function createCliAdapter(settingsPath: string): ITuiCliAdapter {
152
- return {
153
- getUserSettingsPath: () => settingsPath,
154
- readSettings: () => ({}),
155
- writeSettings: vi.fn(),
156
- deleteSettings: vi.fn().mockReturnValue(false),
157
- applyStatusLineSettings: vi.fn(),
158
- reloadPluginCommandSource: vi.fn(),
159
- applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
160
- getGitBranch: vi.fn().mockReturnValue(undefined),
161
- getProviderDisplayName: vi.fn((type: string) => type),
162
- };
163
- }
164
-
165
- describe('App session-switch channel ownership (CLI-B11)', () => {
166
- let cwd: string;
167
- let created: IFakeChannel[];
168
- let createChannel: ReturnType<typeof vi.fn>;
169
-
170
- beforeEach(() => {
171
- cwd = mkdtempSync(join(tmpdir(), 'robota-b11-'));
172
- created = [];
173
- createChannel = vi.fn((resumeSessionId?: string) => {
174
- const fake = createFakeChannel(resumeSessionId);
175
- created.push(fake);
176
- return asChannel(fake);
177
- });
178
- });
179
-
180
- afterEach(() => {
181
- rmSync(cwd, { recursive: true, force: true });
182
- });
183
-
184
- function renderApp(options?: { sessionIds?: string[] }) {
185
- const ids = options?.sessionIds ?? ['session-aaaaaaaa', 'session-bbbbbbbb'];
186
- const records = ids.map((id) => sessionRecord(id, cwd));
187
- const store = createFakeStore(records);
188
- const instance = render(
189
- <App
190
- cwd={cwd}
191
- createChannel={createChannel}
192
- sessionStore={store}
193
- showSessionPickerOnStart
194
- cliAdapter={createCliAdapter(join(cwd, 'settings.json'))}
195
- />,
196
- );
197
- return { ...instance, records };
198
- }
199
-
200
- it('TC-01 (B11) / TC-01 (B12): the factory is the sole channel source — once at mount, once per switch with the selected sessionId', async () => {
201
- const { stdin, lastFrame } = renderApp();
202
- await tick();
203
- expect(lastFrame()).toContain('Select a session to resume');
204
-
205
- // CLI-B12 TC-01: initial channel from the useState initializer, exactly once.
206
- expect(createChannel).toHaveBeenCalledTimes(1);
207
- expect(createChannel).toHaveBeenNthCalledWith(1, undefined);
208
-
209
- stdin.write('\r'); // select first item (newest first — equal timestamps keep list order)
210
- await tick();
211
-
212
- // CLI-B11 TC-A: the switch asks the factory for exactly one channel with the id.
213
- expect(createChannel).toHaveBeenCalledTimes(2);
214
- expect(createChannel).toHaveBeenNthCalledWith(2, 'session-aaaaaaaa');
215
- });
216
-
217
- it('TC-03 (B11) / TC-02 (B12): the previous channel is stopped before the new one becomes active', async () => {
218
- const { stdin } = renderApp();
219
- await tick();
220
- const initialChannel = created[0]!;
221
- expect(initialChannel.start).toHaveBeenCalled();
222
-
223
- stdin.write('\r');
224
- await tick();
225
-
226
- // Old channel released: stopped by the switch handler and by the unmounting
227
- // AppInner's effect cleanup (stop() is idempotent by contract).
228
- expect(initialChannel.stop).toHaveBeenCalled();
229
- expect(created).toHaveLength(2);
230
- const newChannel = created[1]!;
231
- expect(newChannel.start).toHaveBeenCalled();
232
- expect(newChannel.stop).not.toHaveBeenCalled();
233
-
234
- // CLI-B12 TC-02 ordering: old stop() was invoked BEFORE the factory built
235
- // the replacement channel (stop-before-active contract).
236
- const stopOrder = initialChannel.stop.mock.invocationCallOrder[0]!;
237
- const replacementOrder = createChannel.mock.invocationCallOrder[1]!;
238
- expect(stopOrder).toBeLessThan(replacementOrder);
239
- });
240
-
241
- it('TC-04 (B12): App renders from the factory alone — no channel prop exists', async () => {
242
- // The old no-factory fallback (B11 TC-D) is deleted with CLI-B12: createChannel
243
- // is required and `channel` is no longer a prop (enforced at the type level —
244
- // passing one is a compile error). This pins the runtime half: a render with
245
- // only the factory boots, starts the initial channel, and keeps rendering.
246
- const { lastFrame } = renderApp();
247
- await tick();
248
-
249
- expect(lastFrame()).toBeTruthy();
250
- expect(createChannel).toHaveBeenCalledTimes(1);
251
- expect(created[0]!.start).toHaveBeenCalled();
252
- });
253
-
254
- it('TC-05: consecutive switches A→B→C create one channel per switch and stop each prior channel', async () => {
255
- // Selection always takes the top (newest) entry; arrow-key navigation itself
256
- // is covered by ListPicker.test.tsx. updatedAt ordering decides the target.
257
- const ids = ['aaaaaaaa-1111', 'bbbbbbbb-2222', 'cccccccc-3333'];
258
- const { stdin, lastFrame, records } = renderApp({ sessionIds: ids });
259
- touch(records, 'aaaaaaaa-1111', '2026-06-13T01:00:00.000Z'); // A on top
260
- await waitForFrame(lastFrame, (f) => f.includes('Select a session to resume'));
261
-
262
- // Mount creates the initial channel (factory call 1, undefined).
263
- expect(createChannel).toHaveBeenNthCalledWith(1, undefined);
264
- const channelInitial = created[0]!;
265
-
266
- // Switch 1: pick A (top) from the startup picker.
267
- stdin.write('\r');
268
- await waitForFrame(lastFrame, () => createChannel.mock.calls.length === 2);
269
- expect(createChannel).toHaveBeenNthCalledWith(2, 'aaaaaaaa-1111');
270
- expect(channelInitial.stop).toHaveBeenCalled();
271
- const channelA = created[1]!;
272
-
273
- // Switch 2: reopen the picker via a queued session-picker-requested effect,
274
- // drained by a submit on the active channel (real /resume drain path).
275
- touch(records, 'bbbbbbbb-2222', '2026-06-13T02:00:00.000Z'); // B on top
276
- channelA.effectQueue.enqueueEffects([{ type: 'session-picker-requested' }]);
277
- stdin.write('x');
278
- await tick();
279
- stdin.write('\r'); // submit input → drains queue → picker opens
280
- await waitForFrame(lastFrame, (f) => f.includes('> bbbbbbbb'));
281
- await tick(); // settle: let the reopened picker's useInput subscription attach
282
- stdin.write('\r');
283
- await waitForFrame(lastFrame, () => createChannel.mock.calls.length === 3);
284
- expect(createChannel).toHaveBeenNthCalledWith(3, 'bbbbbbbb-2222');
285
- expect(channelA.stop).toHaveBeenCalled();
286
- const channelB = created[2]!;
287
- expect(channelB.start).toHaveBeenCalled();
288
-
289
- // Switch 3: same drill from B to C.
290
- touch(records, 'cccccccc-3333', '2026-06-13T03:00:00.000Z'); // C on top
291
- channelB.effectQueue.enqueueEffects([{ type: 'session-picker-requested' }]);
292
- stdin.write('x');
293
- await tick();
294
- stdin.write('\r');
295
- await waitForFrame(lastFrame, (f) => f.includes('> cccccccc'));
296
- await tick(); // settle: let the reopened picker's useInput subscription attach
297
- stdin.write('\r');
298
- await waitForFrame(lastFrame, () => createChannel.mock.calls.length === 4);
299
- expect(createChannel).toHaveBeenNthCalledWith(4, 'cccccccc-3333');
300
- expect(channelB.stop).toHaveBeenCalled();
301
-
302
- const channelC = created[3]!;
303
- expect(channelC.start).toHaveBeenCalled();
304
- expect(channelC.stop).not.toHaveBeenCalled();
305
- expect(createChannel).toHaveBeenCalledTimes(4);
306
- });
307
- });