@robota-sdk/agent-transport 3.0.0-beta.74 → 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 (197) hide show
  1. package/README.md +10 -10
  2. package/dist/node/headless/index.cjs +1 -1
  3. package/dist/node/headless/index.d.ts +1 -1
  4. package/dist/node/headless/index.js +1 -1
  5. package/dist/node/headless-OnpVk4-k.cjs +15 -0
  6. package/dist/node/{headless-D02zUEGh.js → headless-mRYilLfC.js} +2 -2
  7. package/dist/node/{headless-D02zUEGh.js.map → headless-mRYilLfC.js.map} +1 -1
  8. package/dist/node/{index-DE3-dHqw.d.ts → index-CYl7ksS6.d.ts} +12 -2
  9. package/dist/node/{index-DE3-dHqw.d.ts.map → index-CYl7ksS6.d.ts.map} +1 -1
  10. package/dist/node/{index-WKTgvhlg.d.ts → index-E8Gx4-lc.d.ts} +12 -2
  11. package/dist/node/{index-WKTgvhlg.d.ts.map → index-E8Gx4-lc.d.ts.map} +1 -1
  12. package/dist/node/index.cjs +1 -1
  13. package/dist/node/index.d.ts +2 -7
  14. package/dist/node/index.d.ts.map +1 -1
  15. package/dist/node/index.js +1 -1
  16. package/dist/node/index.js.map +1 -1
  17. package/package.json +7 -75
  18. package/src/headless/HeadlessInteractionChannel.ts +21 -1
  19. package/src/index.ts +1 -5
  20. package/src/transport-registry.ts +0 -9
  21. package/dist/node/headless-BeHAOlIM.cjs +0 -15
  22. package/dist/node/http/index.cjs +0 -1
  23. package/dist/node/http/index.d.ts +0 -2
  24. package/dist/node/http/index.js +0 -1
  25. package/dist/node/http-2Jiuflc1.js +0 -2
  26. package/dist/node/http-2Jiuflc1.js.map +0 -1
  27. package/dist/node/http-CBAvefLw.cjs +0 -1
  28. package/dist/node/index-BQLN_Lc9.d.ts +0 -78
  29. package/dist/node/index-BQLN_Lc9.d.ts.map +0 -1
  30. package/dist/node/index-BnAGE-u9.d.ts +0 -33
  31. package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
  32. package/dist/node/index-BrQ4gGw0.d.ts +0 -213
  33. package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
  34. package/dist/node/index-CoeBF21y.d.ts +0 -213
  35. package/dist/node/index-CoeBF21y.d.ts.map +0 -1
  36. package/dist/node/index-DHt-2VQ-.d.ts +0 -46
  37. package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
  38. package/dist/node/index-DMwKN5Le.d.ts +0 -33
  39. package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
  40. package/dist/node/index-IvYaYY6v.d.ts +0 -78
  41. package/dist/node/index-IvYaYY6v.d.ts.map +0 -1
  42. package/dist/node/index-c0M42fsA.d.ts +0 -46
  43. package/dist/node/index-c0M42fsA.d.ts.map +0 -1
  44. package/dist/node/mcp/index.cjs +0 -1
  45. package/dist/node/mcp/index.d.ts +0 -2
  46. package/dist/node/mcp/index.js +0 -1
  47. package/dist/node/mcp-BOglBJNy.cjs +0 -1
  48. package/dist/node/mcp-D3BBVK7C.js +0 -2
  49. package/dist/node/mcp-D3BBVK7C.js.map +0 -1
  50. package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
  51. package/dist/node/tui/index.cjs +0 -1
  52. package/dist/node/tui/index.d.ts +0 -2
  53. package/dist/node/tui/index.js +0 -1
  54. package/dist/node/tui-Btb1q88j.js +0 -25
  55. package/dist/node/tui-Btb1q88j.js.map +0 -1
  56. package/dist/node/tui-SbUT7Zlt.cjs +0 -24
  57. package/dist/node/ws/index.cjs +0 -1
  58. package/dist/node/ws/index.d.ts +0 -2
  59. package/dist/node/ws/index.js +0 -1
  60. package/dist/node/ws-Dc2RUwVs.js +0 -2
  61. package/dist/node/ws-Dc2RUwVs.js.map +0 -1
  62. package/dist/node/ws-QNMQn5kg.cjs +0 -1
  63. package/src/http/__tests__/http-transport.test.ts +0 -55
  64. package/src/http/__tests__/routes.test.ts +0 -168
  65. package/src/http/http-transport.ts +0 -41
  66. package/src/http/index.ts +0 -4
  67. package/src/http/routes.ts +0 -152
  68. package/src/mcp/__tests__/mcp-server.test.ts +0 -66
  69. package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
  70. package/src/mcp/index.ts +0 -4
  71. package/src/mcp/mcp-server.ts +0 -163
  72. package/src/mcp/mcp-transport.ts +0 -48
  73. package/src/tui/App.tsx +0 -488
  74. package/src/tui/BackgroundTaskPanel.tsx +0 -36
  75. package/src/tui/CjkTextInput.tsx +0 -199
  76. package/src/tui/ConfirmPrompt.tsx +0 -70
  77. package/src/tui/ContextWarningBanner.tsx +0 -34
  78. package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
  79. package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
  80. package/src/tui/InputArea.tsx +0 -310
  81. package/src/tui/InteractivePrompt.tsx +0 -59
  82. package/src/tui/ListPicker.tsx +0 -95
  83. package/src/tui/MenuSelect.tsx +0 -104
  84. package/src/tui/MessageList.tsx +0 -284
  85. package/src/tui/PermissionPrompt.tsx +0 -86
  86. package/src/tui/PluginTUI.tsx +0 -258
  87. package/src/tui/SessionPicker.tsx +0 -68
  88. package/src/tui/SessionStatusBar.tsx +0 -70
  89. package/src/tui/SlashAutocomplete.tsx +0 -110
  90. package/src/tui/StatusBar.tsx +0 -209
  91. package/src/tui/StreamingIndicator.tsx +0 -93
  92. package/src/tui/TextPrompt.tsx +0 -81
  93. package/src/tui/ToolCommandOutput.tsx +0 -39
  94. package/src/tui/ToolDiffBlock.tsx +0 -32
  95. package/src/tui/TransportTUI.tsx +0 -117
  96. package/src/tui/TuiInteractionChannel.ts +0 -483
  97. package/src/tui/UpdateNotice.tsx +0 -14
  98. package/src/tui/UsageSummaryEntry.tsx +0 -39
  99. package/src/tui/WaveText.tsx +0 -44
  100. package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
  101. package/src/tui/__tests__/ListPicker.test.tsx +0 -159
  102. package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
  103. package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
  104. package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
  105. package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
  106. package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
  107. package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
  108. package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
  109. package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
  110. package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
  111. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
  112. package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
  113. package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
  114. package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
  115. package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
  116. package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
  117. package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
  118. package/src/tui/__tests__/command-output-summary.test.ts +0 -95
  119. package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
  120. package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
  121. package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
  122. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
  123. package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
  124. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
  125. package/src/tui/__tests__/input-area-flow.test.ts +0 -164
  126. package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
  127. package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
  128. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
  129. package/src/tui/__tests__/pty/pty-driver.ts +0 -135
  130. package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
  131. package/src/tui/__tests__/render-channel-options.test.ts +0 -32
  132. package/src/tui/__tests__/render-markdown.test.ts +0 -72
  133. package/src/tui/__tests__/selection-flow.test.ts +0 -61
  134. package/src/tui/__tests__/session-init-poller.test.ts +0 -102
  135. package/src/tui/__tests__/session-naming.test.ts +0 -64
  136. package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
  137. package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
  138. package/src/tui/__tests__/status-activity.test.ts +0 -71
  139. package/src/tui/__tests__/status-bar.test.tsx +0 -158
  140. package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
  141. package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
  142. package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
  143. package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
  144. package/src/tui/background-task-row-format.ts +0 -53
  145. package/src/tui/command-interaction.ts +0 -9
  146. package/src/tui/command-output-summary.ts +0 -122
  147. package/src/tui/create-default-tui-cli-adapter.ts +0 -41
  148. package/src/tui/execution-workspace-view-model.ts +0 -123
  149. package/src/tui/flows/cjk-text-input-flow.ts +0 -285
  150. package/src/tui/flows/confirm-prompt-flow.ts +0 -45
  151. package/src/tui/flows/input-area-flow.ts +0 -189
  152. package/src/tui/flows/permission-prompt-flow.ts +0 -85
  153. package/src/tui/flows/selection-flow.ts +0 -126
  154. package/src/tui/flows/session-init-poller.ts +0 -77
  155. package/src/tui/flows/text-prompt-flow.ts +0 -98
  156. package/src/tui/hooks/command-effect-handler.ts +0 -97
  157. package/src/tui/hooks/command-effect-queue.ts +0 -39
  158. package/src/tui/hooks/side-effects-types.ts +0 -35
  159. package/src/tui/hooks/useAutocomplete.ts +0 -87
  160. package/src/tui/hooks/usePluginCallbacks.ts +0 -31
  161. package/src/tui/hooks/usePluginScreenData.ts +0 -85
  162. package/src/tui/hooks/useSideEffects.ts +0 -175
  163. package/src/tui/hooks/useSlashRouting.ts +0 -118
  164. package/src/tui/hooks/useStatusLineSettings.ts +0 -37
  165. package/src/tui/hooks/useTuiChannel.ts +0 -95
  166. package/src/tui/index.ts +0 -14
  167. package/src/tui/interactions/CommandConfirm.tsx +0 -36
  168. package/src/tui/interactions/CommandPicker.tsx +0 -77
  169. package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
  170. package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
  171. package/src/tui/plugin-tui-handlers.ts +0 -163
  172. package/src/tui/render-markdown.ts +0 -130
  173. package/src/tui/render.tsx +0 -117
  174. package/src/tui/session-naming.ts +0 -33
  175. package/src/tui/status-activity.ts +0 -63
  176. package/src/tui/tui-cli-adapter-context.tsx +0 -13
  177. package/src/tui/tui-cli-adapter.ts +0 -25
  178. package/src/tui/tui-state-manager.ts +0 -226
  179. package/src/tui/tui-transport.ts +0 -35
  180. package/src/tui/types.ts +0 -15
  181. package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
  182. package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
  183. package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
  184. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
  185. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
  186. package/src/tui/utils/edit-diff.ts +0 -153
  187. package/src/tui/utils/paste-labels.ts +0 -9
  188. package/src/tui/utils/tool-call-extractor.ts +0 -92
  189. package/src/tui/utils/tool-diff-summary.ts +0 -75
  190. package/src/ws/__tests__/ws-handler.test.ts +0 -409
  191. package/src/ws/__tests__/ws-transport.test.ts +0 -53
  192. package/src/ws/index.ts +0 -13
  193. package/src/ws/ws-background-messages.ts +0 -170
  194. package/src/ws/ws-handler.ts +0 -280
  195. package/src/ws/ws-protocol.ts +0 -78
  196. package/src/ws/ws-transport-configurable.ts +0 -128
  197. package/src/ws/ws-transport.ts +0 -42
@@ -1,255 +0,0 @@
1
- /**
2
- * Tests for prompt queue — submit during execution queues prompt,
3
- * auto-executes after completion, backspace cancels queue, ESC aborts.
4
- */
5
-
6
- import React, { useState, useCallback, useEffect, useRef } from 'react';
7
- import { cleanup, render } from 'ink-testing-library';
8
- import { Box, Text, useInput } from 'ink';
9
- import { afterEach, describe, it, expect, vi } from 'vitest';
10
-
11
- interface IQueueTestController {
12
- completeCurrent?: () => void;
13
- }
14
-
15
- const WAIT_FOR_ASSERTION_TIMEOUT_MS = 10000;
16
- const WAIT_FOR_ASSERTION_INTERVAL_MS = 20;
17
-
18
- async function waitForAssertion(
19
- assertion: () => void,
20
- timeoutMs = WAIT_FOR_ASSERTION_TIMEOUT_MS,
21
- ): Promise<void> {
22
- const startedAt = Date.now();
23
- let lastError: Error | undefined;
24
-
25
- while (Date.now() - startedAt < timeoutMs) {
26
- try {
27
- assertion();
28
- return;
29
- } catch (error) {
30
- lastError = error instanceof Error ? error : new Error(String(error));
31
- await new Promise((resolve) => setTimeout(resolve, WAIT_FOR_ASSERTION_INTERVAL_MS));
32
- }
33
- }
34
-
35
- if (lastError) throw lastError;
36
- throw new Error('Timed out waiting for assertion');
37
- }
38
-
39
- /**
40
- * Minimal App that simulates the prompt queue behavior.
41
- */
42
- function QueueTestApp({
43
- onExecute,
44
- onAbort,
45
- controller,
46
- }: {
47
- onExecute: (prompt: string) => void;
48
- onAbort?: () => void;
49
- controller?: IQueueTestController;
50
- }): React.ReactElement {
51
- const [isThinking, setIsThinking] = useState(false);
52
- const thinkingRef = useRef(false);
53
- const [pendingPrompt, setPendingPrompt] = useState<string | null>(null);
54
- const pendingRef = useRef<string | null>(null);
55
- const [isAborting, setIsAborting] = useState(false);
56
- const [log, setLog] = useState<string[]>([]);
57
-
58
- const executePrompt = useCallback(
59
- async (input: string) => {
60
- thinkingRef.current = true;
61
- setIsThinking(true);
62
- setLog((prev) => [...prev, `exec:${input}`]);
63
- onExecute(input);
64
- await new Promise<void>((resolve) => {
65
- if (controller) {
66
- controller.completeCurrent = () => {
67
- controller.completeCurrent = undefined;
68
- resolve();
69
- };
70
- return;
71
- }
72
- setTimeout(resolve, 300);
73
- });
74
- thinkingRef.current = false;
75
- setIsThinking(false);
76
- },
77
- [controller, onExecute],
78
- );
79
-
80
- const handleSubmit = useCallback(
81
- async (input: string) => {
82
- if (thinkingRef.current) {
83
- setPendingPrompt(input);
84
- pendingRef.current = input;
85
- setLog((prev) => [...prev, `queued:${input}`]);
86
- return;
87
- }
88
- await executePrompt(input);
89
- },
90
- [executePrompt],
91
- );
92
-
93
- useInput((_input, key) => {
94
- // ESC always aborts + clears queue
95
- if (key.escape && thinkingRef.current) {
96
- setIsAborting(true);
97
- setPendingPrompt(null);
98
- pendingRef.current = null;
99
- onAbort?.();
100
- }
101
- // Backspace cancels queue only
102
- if ((key.backspace || key.delete) && pendingRef.current) {
103
- setPendingPrompt(null);
104
- pendingRef.current = null;
105
- setLog((prev) => [...prev, 'queue-cleared']);
106
- }
107
- });
108
-
109
- // Auto-execute queued prompt when thinking ends
110
- useEffect(() => {
111
- if (!isThinking) {
112
- setIsAborting(false);
113
- if (pendingRef.current) {
114
- const prompt = pendingRef.current;
115
- setPendingPrompt(null);
116
- pendingRef.current = null;
117
- setTimeout(() => executePrompt(prompt), 0);
118
- }
119
- }
120
- }, [isThinking, executePrompt]);
121
-
122
- return (
123
- <Box flexDirection="column">
124
- <Text>thinking={String(isThinking)}</Text>
125
- <Text>pending={pendingPrompt ?? 'none'}</Text>
126
- <Text>aborting={String(isAborting)}</Text>
127
- <Text>log={log.join(',')}</Text>
128
- <SubmitTrigger onSubmit={handleSubmit} />
129
- </Box>
130
- );
131
- }
132
-
133
- function SubmitTrigger({
134
- onSubmit,
135
- }: {
136
- onSubmit: (input: string) => Promise<void>;
137
- }): React.ReactElement {
138
- useInput((input) => {
139
- if (input.startsWith('s:')) {
140
- onSubmit(input.slice(2));
141
- }
142
- });
143
- return <></>;
144
- }
145
-
146
- describe('Prompt Queue', () => {
147
- afterEach(() => {
148
- cleanup();
149
- vi.clearAllMocks();
150
- });
151
-
152
- it('executes prompt normally when not thinking', async () => {
153
- const onExecute = vi.fn();
154
- const { stdin } = render(<QueueTestApp onExecute={onExecute} />);
155
-
156
- stdin.write('s:hello');
157
- await waitForAssertion(() => expect(onExecute).toHaveBeenCalledWith('hello'));
158
-
159
- expect(onExecute).toHaveBeenCalledWith('hello');
160
- });
161
-
162
- it('queues prompt when thinking, auto-executes after completion', async () => {
163
- const onExecute = vi.fn();
164
- const controller: IQueueTestController = {};
165
- const { stdin, lastFrame } = render(
166
- <QueueTestApp onExecute={onExecute} controller={controller} />,
167
- );
168
-
169
- stdin.write('s:first');
170
- await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
171
-
172
- stdin.write('s:second');
173
- await waitForAssertion(() => expect(lastFrame()!).toContain('pending=second'));
174
-
175
- controller.completeCurrent?.();
176
- await waitForAssertion(() => expect(onExecute).toHaveBeenCalledWith('second'));
177
-
178
- expect(onExecute).toHaveBeenCalledWith('first');
179
- expect(onExecute).toHaveBeenCalledWith('second');
180
- });
181
-
182
- it('only queues 1 prompt — last one wins', async () => {
183
- const onExecute = vi.fn();
184
- const controller: IQueueTestController = {};
185
- const { stdin, lastFrame } = render(
186
- <QueueTestApp onExecute={onExecute} controller={controller} />,
187
- );
188
-
189
- stdin.write('s:first');
190
- await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
191
-
192
- stdin.write('s:second');
193
- await new Promise((r) => setTimeout(r, 5));
194
- stdin.write('s:third');
195
- await waitForAssertion(() => expect(lastFrame()!).toContain('pending=third'));
196
-
197
- controller.completeCurrent?.();
198
- await waitForAssertion(() => expect(onExecute).toHaveBeenCalledWith('third'));
199
-
200
- expect(onExecute).toHaveBeenCalledWith('first');
201
- expect(onExecute).toHaveBeenCalledWith('third');
202
- expect(onExecute).not.toHaveBeenCalledWith('second');
203
- });
204
-
205
- it('ESC aborts execution and clears queue', async () => {
206
- const onExecute = vi.fn();
207
- const onAbort = vi.fn();
208
- const controller: IQueueTestController = {};
209
- const { stdin, lastFrame } = render(
210
- <QueueTestApp onExecute={onExecute} onAbort={onAbort} controller={controller} />,
211
- );
212
-
213
- stdin.write('s:first');
214
- await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
215
-
216
- stdin.write('s:queued');
217
- await waitForAssertion(() => expect(lastFrame()!).toContain('pending=queued'));
218
-
219
- stdin.write('\x1B');
220
- await waitForAssertion(() => expect(lastFrame()!).toContain('pending=none'));
221
-
222
- expect(onAbort).toHaveBeenCalled();
223
-
224
- controller.completeCurrent?.();
225
- await new Promise((r) => setTimeout(r, 50));
226
- expect(onExecute).not.toHaveBeenCalledWith('queued');
227
- });
228
-
229
- it('Backspace cancels queue without aborting', async () => {
230
- const onExecute = vi.fn();
231
- const onAbort = vi.fn();
232
- const controller: IQueueTestController = {};
233
- const { stdin, lastFrame } = render(
234
- <QueueTestApp onExecute={onExecute} onAbort={onAbort} controller={controller} />,
235
- );
236
-
237
- stdin.write('s:first');
238
- await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
239
-
240
- stdin.write('s:queued');
241
- await waitForAssertion(() => expect(lastFrame()!).toContain('pending=queued'));
242
-
243
- stdin.write('\x7F'); // backspace
244
- await waitForAssertion(() => expect(lastFrame()!).toContain('pending=none'));
245
-
246
- expect(lastFrame()!).toContain('queue-cleared');
247
- expect(onAbort).not.toHaveBeenCalled();
248
-
249
- // Execution continues normally
250
- controller.completeCurrent?.();
251
- await new Promise((r) => setTimeout(r, 50));
252
- expect(onExecute).toHaveBeenCalledWith('first');
253
- expect(onExecute).not.toHaveBeenCalledWith('queued');
254
- });
255
- });
@@ -1,233 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'vitest';
2
- import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
- import { createRequire } from 'node:module';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
8
-
9
- const require = createRequire(import.meta.url);
10
- const TSX_ESM_HOOK_PATH: string = require.resolve('tsx/esm');
11
-
12
- const openaiDefaults = {
13
- model: 'gpt-4o',
14
- apiKey: '$ENV:OPENAI_API_KEY',
15
- };
16
-
17
- const DRIVER_PATH = fileURLToPath(
18
- new URL('./fixtures/provider-setup-prompt-driver.tsx', import.meta.url),
19
- );
20
- const TEST_TIMEOUT_MS = 30000;
21
- const WAIT_TIMEOUT_MS = 15000;
22
- const INPUT_SETTLE_MS = 75;
23
- const OUTPUT_TAIL_LENGTH = 2000;
24
-
25
- interface IPtyHarness {
26
- submit(input?: string): Promise<void>;
27
- waitFor(text: string): Promise<void>;
28
- waitForExit(): Promise<number>;
29
- dispose(): void;
30
- }
31
-
32
- const tempDirs: string[] = [];
33
- const activeHarnesses: IPtyHarness[] = [];
34
-
35
- afterEach(() => {
36
- for (const harness of activeHarnesses.splice(0)) {
37
- harness.dispose();
38
- }
39
- for (const dir of tempDirs.splice(0)) {
40
- rmSync(dir, { recursive: true, force: true });
41
- }
42
- });
43
-
44
- describe('provider setup interaction PTY E2E', () => {
45
- it(
46
- 'submits OpenAI values through a real pseudo terminal',
47
- async () => {
48
- const { harness, outputPath } = spawnProviderSetupDriver('openai');
49
-
50
- await harness.waitFor('OpenAI model');
51
- await harness.waitFor('https://platform.openai.com/api-keys');
52
- await harness.submit(openaiDefaults.model);
53
- await harness.waitFor('OpenAI API key');
54
- await harness.submit();
55
-
56
- expect(await harness.waitForExit()).toBe(0);
57
- harness.dispose();
58
- expect(readResult(outputPath)).toEqual({
59
- profile: 'openai',
60
- type: 'openai',
61
- model: openaiDefaults.model,
62
- apiKey: openaiDefaults.apiKey,
63
- setCurrent: true,
64
- });
65
- },
66
- TEST_TIMEOUT_MS,
67
- );
68
-
69
- it(
70
- 'submits Anthropic values typed through a real pseudo terminal',
71
- async () => {
72
- const { harness, outputPath } = spawnProviderSetupDriver('anthropic');
73
-
74
- await harness.waitFor('Anthropic API key');
75
- await harness.submit('sk-test');
76
- await harness.waitFor('Anthropic model');
77
- await harness.submit('claude-test');
78
-
79
- expect(await harness.waitForExit()).toBe(0);
80
- harness.dispose();
81
- expect(readResult(outputPath)).toEqual({
82
- profile: 'anthropic',
83
- type: 'anthropic',
84
- model: 'claude-test',
85
- apiKey: 'sk-test',
86
- setCurrent: true,
87
- });
88
- },
89
- TEST_TIMEOUT_MS,
90
- );
91
- });
92
-
93
- function spawnProviderSetupDriver(type: 'openai' | 'anthropic'): {
94
- harness: IPtyHarness;
95
- outputPath: string;
96
- } {
97
- const dir = mkdtempSync(join(tmpdir(), 'robota-provider-pty-'));
98
- tempDirs.push(dir);
99
- const outputPath = join(dir, 'result.json');
100
- const proc = pty.spawn(
101
- process.execPath,
102
- ['--import', TSX_ESM_HOOK_PATH, DRIVER_PATH, outputPath, type],
103
- {
104
- cols: 120,
105
- rows: 40,
106
- cwd: fileURLToPath(new URL('../../../../..', import.meta.url)),
107
- env: createPtyEnv(),
108
- },
109
- );
110
- const harness = createHarness(proc);
111
- activeHarnesses.push(harness);
112
- return { harness, outputPath };
113
- }
114
-
115
- function createHarness(proc: pty.IPty): IPtyHarness {
116
- let output = '';
117
- let exitCode: number | undefined;
118
- const waiters: Array<() => void> = [];
119
-
120
- proc.onData((data) => {
121
- output += data;
122
- notifyWaiters(waiters);
123
- });
124
- proc.onExit((event) => {
125
- exitCode = event.exitCode;
126
- notifyWaiters(waiters);
127
- });
128
-
129
- return {
130
- async submit(input = '') {
131
- await sleep(INPUT_SETTLE_MS);
132
- if (input) {
133
- proc.write(input);
134
- await sleep(INPUT_SETTLE_MS);
135
- }
136
- proc.write('\r');
137
- },
138
- waitFor(text: string) {
139
- return waitUntil(
140
- () => output.includes(text),
141
- waiters,
142
- () => {
143
- return new Error(`Timed out waiting for "${text}". Output:\n${tail(output)}`);
144
- },
145
- );
146
- },
147
- waitForExit() {
148
- return waitUntil(
149
- () => exitCode !== undefined,
150
- waiters,
151
- () => {
152
- return new Error(`Timed out waiting for PTY exit. Output:\n${tail(output)}`);
153
- },
154
- ).then(() => exitCode ?? -1);
155
- },
156
- dispose() {
157
- if (exitCode === undefined) {
158
- proc.kill();
159
- }
160
- },
161
- };
162
- }
163
-
164
- function sleep(ms: number): Promise<void> {
165
- return new Promise((resolve) => {
166
- setTimeout(resolve, ms);
167
- });
168
- }
169
-
170
- function createPtyEnv(): NodeJS.ProcessEnv {
171
- const env: NodeJS.ProcessEnv = {
172
- ...process.env,
173
- FORCE_COLOR: '0',
174
- TERM: 'xterm-256color',
175
- };
176
- delete env.CI;
177
- return env;
178
- }
179
-
180
- function waitUntil(
181
- predicate: () => boolean,
182
- waiters: Array<() => void>,
183
- createTimeoutError: () => Error,
184
- ): Promise<void> {
185
- if (predicate()) {
186
- return Promise.resolve();
187
- }
188
- return new Promise((resolve, reject) => {
189
- let settled = false;
190
- const removeWaiter = (waiter: () => void): void => {
191
- const index = waiters.indexOf(waiter);
192
- if (index >= 0) {
193
- waiters.splice(index, 1);
194
- }
195
- };
196
- const check = () => {
197
- if (settled) {
198
- return;
199
- }
200
- if (!predicate()) {
201
- return;
202
- }
203
- settled = true;
204
- clearTimeout(deadline);
205
- removeWaiter(check);
206
- resolve();
207
- };
208
- const deadline = setTimeout(() => {
209
- if (settled) {
210
- return;
211
- }
212
- settled = true;
213
- removeWaiter(check);
214
- reject(createTimeoutError());
215
- }, WAIT_TIMEOUT_MS);
216
- waiters.push(check);
217
- });
218
- }
219
-
220
- function notifyWaiters(waiters: Array<() => void>): void {
221
- for (const waiter of [...waiters]) {
222
- waiter();
223
- }
224
- }
225
-
226
- function readResult(outputPath: string): Record<string, string | boolean> {
227
- expect(existsSync(outputPath)).toBe(true);
228
- return JSON.parse(readFileSync(outputPath, 'utf8')) as Record<string, string | boolean>;
229
- }
230
-
231
- function tail(output: string): string {
232
- return output.slice(-OUTPUT_TAIL_LENGTH);
233
- }
@@ -1,135 +0,0 @@
1
- /**
2
- * PTY TUI driver (CLI-074 TC-07/08).
3
- *
4
- * Spawns the built robota CLI in a real pseudo-terminal so Ink renders exactly
5
- * as in a user terminal, with per-key paced input (expect(1)-style burst input
6
- * gets bundled as a bracketed paste — the failure mode this driver exists to
7
- * avoid). Test-only; lives in a dedicated vitest project (*.ptytest.ts).
8
- */
9
-
10
- import { mkdirSync, writeFileSync } from 'node:fs';
11
- import { join, resolve } from 'node:path';
12
-
13
- import { spawn } from '@homebridge/node-pty-prebuilt-multiarch';
14
-
15
- import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch';
16
-
17
- const REPO_ROOT = resolve(__dirname, '../../../../../..');
18
- const ROBOTA_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/robota.cjs');
19
-
20
- // eslint-disable-next-line no-control-regex
21
- const ANSI_PATTERN = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][B0]|[\x00-\x08\x0b-\x1f]/g;
22
-
23
- export interface IPtySession {
24
- /** Type text one key at a time (default 35ms/key — human-ish, avoids paste bundling). */
25
- sendKeys(text: string, perKeyDelayMs?: number): Promise<void>;
26
- /** Press Enter as a single keystroke. */
27
- pressEnter(): Promise<void>;
28
- /** Wait until the ANSI-stripped output matches; throws with a snapshot on timeout. */
29
- waitFor(pattern: RegExp, timeoutMs?: number): Promise<void>;
30
- /** Current ANSI-stripped output. */
31
- snapshot(): string;
32
- /** Wait for process exit; throws with a snapshot on timeout. */
33
- expectExit(timeoutMs?: number): Promise<number>;
34
- /** Force-kill (cleanup). */
35
- kill(): void;
36
- }
37
-
38
- export interface ISpawnTuiOptions {
39
- /** Project cwd (a provider profile settings.json is written here). */
40
- projectDir: string;
41
- /** Isolated HOME directory. */
42
- homeDir: string;
43
- cols?: number;
44
- rows?: number;
45
- }
46
-
47
- export function writeTuiProviderSettings(projectDir: string): void {
48
- const settingsDir = join(projectDir, '.robota');
49
- mkdirSync(settingsDir, { recursive: true });
50
- writeFileSync(
51
- join(settingsDir, 'settings.json'),
52
- JSON.stringify({
53
- currentProvider: 'anthropic',
54
- providers: {
55
- // Boot/slash/exit make zero model calls — the key is never used.
56
- anthropic: { type: 'anthropic', model: 'claude-test-model', apiKey: 'pty-dummy-key' },
57
- },
58
- }),
59
- 'utf8',
60
- );
61
- }
62
-
63
- function sleep(ms: number): Promise<void> {
64
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
65
- }
66
-
67
- export function spawnTui(options: ISpawnTuiOptions): IPtySession {
68
- mkdirSync(options.homeDir, { recursive: true });
69
- let output = '';
70
- let exitCode: number | undefined;
71
-
72
- const pty: IPty = spawn(process.execPath, [ROBOTA_BIN], {
73
- name: 'xterm-256color',
74
- cols: options.cols ?? 100,
75
- rows: options.rows ?? 32,
76
- cwd: options.projectDir,
77
- env: {
78
- PATH: process.env['PATH'] ?? '',
79
- HOME: options.homeDir,
80
- TERM: 'xterm-256color',
81
- // Never inherit real provider keys into PTY runs.
82
- },
83
- });
84
-
85
- pty.onData((data) => {
86
- output += data;
87
- });
88
- pty.onExit(({ exitCode: code }) => {
89
- exitCode = code;
90
- });
91
-
92
- const stripped = (): string => output.replace(ANSI_PATTERN, '');
93
-
94
- return {
95
- async sendKeys(text: string, perKeyDelayMs = 35): Promise<void> {
96
- for (const ch of text) {
97
- pty.write(ch);
98
- await sleep(perKeyDelayMs);
99
- }
100
- },
101
- async pressEnter(): Promise<void> {
102
- await sleep(120);
103
- pty.write('\r');
104
- await sleep(120);
105
- },
106
- async waitFor(pattern: RegExp, timeoutMs = 15_000): Promise<void> {
107
- const deadline = Date.now() + timeoutMs;
108
- while (Date.now() < deadline) {
109
- if (pattern.test(stripped())) return;
110
- await sleep(100);
111
- }
112
- throw new Error(
113
- `PTY waitFor timeout (${timeoutMs}ms) for ${String(pattern)}\n--- snapshot ---\n${stripped().slice(-2000)}`,
114
- );
115
- },
116
- snapshot: stripped,
117
- async expectExit(timeoutMs = 10_000): Promise<number> {
118
- const deadline = Date.now() + timeoutMs;
119
- while (Date.now() < deadline) {
120
- if (exitCode !== undefined) return exitCode;
121
- await sleep(100);
122
- }
123
- throw new Error(
124
- `PTY process did not exit within ${timeoutMs}ms\n--- snapshot ---\n${stripped().slice(-2000)}`,
125
- );
126
- },
127
- kill(): void {
128
- try {
129
- pty.kill();
130
- } catch {
131
- // allow-fallback: process already exited — kill on a dead pty is a no-op by design
132
- }
133
- },
134
- };
135
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Real-PTY TUI suites (CLI-074 TC-07/08).
3
- *
4
- * Runs in the dedicated PTY vitest project (vitest.pty.config.ts) against the
5
- * BUILT robota binary — `pnpm --filter @robota-sdk/agent-cli build` first.
6
- */
7
-
8
- import { mkdtempSync, rmSync } from 'node:fs';
9
- import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
11
-
12
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
13
-
14
- import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
15
-
16
- import type { IPtySession } from './pty-driver.js';
17
-
18
- describe('TUI through a real PTY (CLI-074)', () => {
19
- let projectDir: string;
20
- let session: IPtySession | undefined;
21
-
22
- beforeEach(() => {
23
- projectDir = mkdtempSync(join(tmpdir(), 'robota-pty-'));
24
- writeTuiProviderSettings(projectDir);
25
- });
26
-
27
- afterEach(() => {
28
- session?.kill();
29
- session = undefined;
30
- rmSync(projectDir, { recursive: true, force: true });
31
- });
32
-
33
- it('TC-07: boots, opens slash autocomplete, and executes /help as a command', async () => {
34
- session = spawnTui({ projectDir, homeDir: join(projectDir, 'home') });
35
-
36
- // Boot: prompt + status bar render.
37
- await session.waitFor(/Type a message or \/help/);
38
- await session.waitFor(/Idle/);
39
-
40
- // '/' opens the autocomplete dropdown listing commands.
41
- await session.sendKeys('/');
42
- await session.waitFor(/\/help\s+Show available commands/);
43
-
44
- // Typing the rest at human key rate must stay a command, not a paste.
45
- await session.sendKeys('help');
46
- await session.pressEnter();
47
- await session.waitFor(/Available commands|\/cost|\/clear/i, 20_000);
48
- expect(session.snapshot()).not.toContain('[Pasted text');
49
- }, 60_000);
50
-
51
- it('TC-08: /exit reaches process exit within 10s', async () => {
52
- session = spawnTui({ projectDir, homeDir: join(projectDir, 'home') });
53
- await session.waitFor(/Type a message or \/help/);
54
-
55
- await session.sendKeys('/exit');
56
- await session.pressEnter();
57
-
58
- const exitCode = await session.expectExit(10_000);
59
- expect(exitCode).toBe(0);
60
- }, 60_000);
61
- });