@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.
- package/README.md +10 -10
- package/dist/node/headless/index.cjs +1 -1
- package/dist/node/headless/index.d.ts +1 -1
- package/dist/node/headless/index.js +1 -1
- package/dist/node/headless-OnpVk4-k.cjs +15 -0
- package/dist/node/{headless-D02zUEGh.js → headless-mRYilLfC.js} +2 -2
- package/dist/node/{headless-D02zUEGh.js.map → headless-mRYilLfC.js.map} +1 -1
- package/dist/node/{index-DE3-dHqw.d.ts → index-CYl7ksS6.d.ts} +12 -2
- package/dist/node/{index-DE3-dHqw.d.ts.map → index-CYl7ksS6.d.ts.map} +1 -1
- package/dist/node/{index-WKTgvhlg.d.ts → index-E8Gx4-lc.d.ts} +12 -2
- package/dist/node/{index-WKTgvhlg.d.ts.map → index-E8Gx4-lc.d.ts.map} +1 -1
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.ts +2 -7
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +7 -75
- package/src/headless/HeadlessInteractionChannel.ts +21 -1
- package/src/index.ts +1 -5
- package/src/transport-registry.ts +0 -9
- package/dist/node/headless-BeHAOlIM.cjs +0 -15
- package/dist/node/http/index.cjs +0 -1
- package/dist/node/http/index.d.ts +0 -2
- package/dist/node/http/index.js +0 -1
- package/dist/node/http-2Jiuflc1.js +0 -2
- package/dist/node/http-2Jiuflc1.js.map +0 -1
- package/dist/node/http-CBAvefLw.cjs +0 -1
- package/dist/node/index-BQLN_Lc9.d.ts +0 -78
- package/dist/node/index-BQLN_Lc9.d.ts.map +0 -1
- package/dist/node/index-BnAGE-u9.d.ts +0 -33
- package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
- package/dist/node/index-BrQ4gGw0.d.ts +0 -213
- package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
- package/dist/node/index-CoeBF21y.d.ts +0 -213
- package/dist/node/index-CoeBF21y.d.ts.map +0 -1
- package/dist/node/index-DHt-2VQ-.d.ts +0 -46
- package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
- package/dist/node/index-DMwKN5Le.d.ts +0 -33
- package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
- package/dist/node/index-IvYaYY6v.d.ts +0 -78
- package/dist/node/index-IvYaYY6v.d.ts.map +0 -1
- package/dist/node/index-c0M42fsA.d.ts +0 -46
- package/dist/node/index-c0M42fsA.d.ts.map +0 -1
- package/dist/node/mcp/index.cjs +0 -1
- package/dist/node/mcp/index.d.ts +0 -2
- package/dist/node/mcp/index.js +0 -1
- package/dist/node/mcp-BOglBJNy.cjs +0 -1
- package/dist/node/mcp-D3BBVK7C.js +0 -2
- package/dist/node/mcp-D3BBVK7C.js.map +0 -1
- package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
- package/dist/node/tui/index.cjs +0 -1
- package/dist/node/tui/index.d.ts +0 -2
- package/dist/node/tui/index.js +0 -1
- package/dist/node/tui-Btb1q88j.js +0 -25
- package/dist/node/tui-Btb1q88j.js.map +0 -1
- package/dist/node/tui-SbUT7Zlt.cjs +0 -24
- package/dist/node/ws/index.cjs +0 -1
- package/dist/node/ws/index.d.ts +0 -2
- package/dist/node/ws/index.js +0 -1
- package/dist/node/ws-Dc2RUwVs.js +0 -2
- package/dist/node/ws-Dc2RUwVs.js.map +0 -1
- package/dist/node/ws-QNMQn5kg.cjs +0 -1
- package/src/http/__tests__/http-transport.test.ts +0 -55
- package/src/http/__tests__/routes.test.ts +0 -168
- package/src/http/http-transport.ts +0 -41
- package/src/http/index.ts +0 -4
- package/src/http/routes.ts +0 -152
- package/src/mcp/__tests__/mcp-server.test.ts +0 -66
- package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
- package/src/mcp/index.ts +0 -4
- package/src/mcp/mcp-server.ts +0 -163
- package/src/mcp/mcp-transport.ts +0 -48
- package/src/tui/App.tsx +0 -488
- package/src/tui/BackgroundTaskPanel.tsx +0 -36
- package/src/tui/CjkTextInput.tsx +0 -199
- package/src/tui/ConfirmPrompt.tsx +0 -70
- package/src/tui/ContextWarningBanner.tsx +0 -34
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
- package/src/tui/InputArea.tsx +0 -310
- package/src/tui/InteractivePrompt.tsx +0 -59
- package/src/tui/ListPicker.tsx +0 -95
- package/src/tui/MenuSelect.tsx +0 -104
- package/src/tui/MessageList.tsx +0 -284
- package/src/tui/PermissionPrompt.tsx +0 -86
- package/src/tui/PluginTUI.tsx +0 -258
- package/src/tui/SessionPicker.tsx +0 -68
- package/src/tui/SessionStatusBar.tsx +0 -70
- package/src/tui/SlashAutocomplete.tsx +0 -110
- package/src/tui/StatusBar.tsx +0 -209
- package/src/tui/StreamingIndicator.tsx +0 -93
- package/src/tui/TextPrompt.tsx +0 -81
- package/src/tui/ToolCommandOutput.tsx +0 -39
- package/src/tui/ToolDiffBlock.tsx +0 -32
- package/src/tui/TransportTUI.tsx +0 -117
- package/src/tui/TuiInteractionChannel.ts +0 -483
- package/src/tui/UpdateNotice.tsx +0 -14
- package/src/tui/UsageSummaryEntry.tsx +0 -39
- package/src/tui/WaveText.tsx +0 -44
- package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
- package/src/tui/__tests__/ListPicker.test.tsx +0 -159
- package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
- package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
- package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
- package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
- package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
- package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
- package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
- package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
- package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
- package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
- package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
- package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
- package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
- package/src/tui/__tests__/command-output-summary.test.ts +0 -95
- package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
- package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
- package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
- package/src/tui/__tests__/input-area-flow.test.ts +0 -164
- package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
- package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
- package/src/tui/__tests__/pty/pty-driver.ts +0 -135
- package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
- package/src/tui/__tests__/render-channel-options.test.ts +0 -32
- package/src/tui/__tests__/render-markdown.test.ts +0 -72
- package/src/tui/__tests__/selection-flow.test.ts +0 -61
- package/src/tui/__tests__/session-init-poller.test.ts +0 -102
- package/src/tui/__tests__/session-naming.test.ts +0 -64
- package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
- package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
- package/src/tui/__tests__/status-activity.test.ts +0 -71
- package/src/tui/__tests__/status-bar.test.tsx +0 -158
- package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
- package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
- package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
- package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
- package/src/tui/background-task-row-format.ts +0 -53
- package/src/tui/command-interaction.ts +0 -9
- package/src/tui/command-output-summary.ts +0 -122
- package/src/tui/create-default-tui-cli-adapter.ts +0 -41
- package/src/tui/execution-workspace-view-model.ts +0 -123
- package/src/tui/flows/cjk-text-input-flow.ts +0 -285
- package/src/tui/flows/confirm-prompt-flow.ts +0 -45
- package/src/tui/flows/input-area-flow.ts +0 -189
- package/src/tui/flows/permission-prompt-flow.ts +0 -85
- package/src/tui/flows/selection-flow.ts +0 -126
- package/src/tui/flows/session-init-poller.ts +0 -77
- package/src/tui/flows/text-prompt-flow.ts +0 -98
- package/src/tui/hooks/command-effect-handler.ts +0 -97
- package/src/tui/hooks/command-effect-queue.ts +0 -39
- package/src/tui/hooks/side-effects-types.ts +0 -35
- package/src/tui/hooks/useAutocomplete.ts +0 -87
- package/src/tui/hooks/usePluginCallbacks.ts +0 -31
- package/src/tui/hooks/usePluginScreenData.ts +0 -85
- package/src/tui/hooks/useSideEffects.ts +0 -175
- package/src/tui/hooks/useSlashRouting.ts +0 -118
- package/src/tui/hooks/useStatusLineSettings.ts +0 -37
- package/src/tui/hooks/useTuiChannel.ts +0 -95
- package/src/tui/index.ts +0 -14
- package/src/tui/interactions/CommandConfirm.tsx +0 -36
- package/src/tui/interactions/CommandPicker.tsx +0 -77
- package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
- package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
- package/src/tui/plugin-tui-handlers.ts +0 -163
- package/src/tui/render-markdown.ts +0 -130
- package/src/tui/render.tsx +0 -117
- package/src/tui/session-naming.ts +0 -33
- package/src/tui/status-activity.ts +0 -63
- package/src/tui/tui-cli-adapter-context.tsx +0 -13
- package/src/tui/tui-cli-adapter.ts +0 -25
- package/src/tui/tui-state-manager.ts +0 -226
- package/src/tui/tui-transport.ts +0 -35
- package/src/tui/types.ts +0 -15
- package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
- package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
- package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
- package/src/tui/utils/edit-diff.ts +0 -153
- package/src/tui/utils/paste-labels.ts +0 -9
- package/src/tui/utils/tool-call-extractor.ts +0 -92
- package/src/tui/utils/tool-diff-summary.ts +0 -75
- package/src/ws/__tests__/ws-handler.test.ts +0 -409
- package/src/ws/__tests__/ws-transport.test.ts +0 -53
- package/src/ws/index.ts +0 -13
- package/src/ws/ws-background-messages.ts +0 -170
- package/src/ws/ws-handler.ts +0 -280
- package/src/ws/ws-protocol.ts +0 -78
- package/src/ws/ws-transport-configurable.ts +0 -128
- package/src/ws/ws-transport.ts +0 -42
|
@@ -1,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
|
-
});
|