@robota-sdk/agent-transport 3.0.0-beta.73 → 3.0.0-beta.74
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/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-DCtHvyVf.cjs → headless-BeHAOlIM.cjs} +4 -3
- package/dist/node/{headless-C6tj35h3.js → headless-D02zUEGh.js} +4 -3
- package/dist/node/headless-D02zUEGh.js.map +1 -0
- package/dist/node/http/index.cjs +1 -1
- package/dist/node/http/index.d.ts +1 -1
- package/dist/node/http/index.js +1 -1
- package/dist/node/{http-Br10Ps8m.js → http-2Jiuflc1.js} +1 -1
- package/dist/node/http-2Jiuflc1.js.map +1 -0
- package/dist/node/http-CBAvefLw.cjs +1 -0
- package/dist/node/{index-BVNhOeeU.d.ts → index-BQLN_Lc9.d.ts} +5 -3
- package/dist/node/index-BQLN_Lc9.d.ts.map +1 -0
- package/dist/node/{index-C9LWCL4l.d.ts → index-BnAGE-u9.d.ts} +2 -3
- package/dist/node/index-BnAGE-u9.d.ts.map +1 -0
- package/dist/node/{index-COWvtBa2.d.ts → index-BrQ4gGw0.d.ts} +3 -3
- package/dist/node/index-BrQ4gGw0.d.ts.map +1 -0
- package/dist/node/{index-X2Zg8FEY.d.ts → index-CoeBF21y.d.ts} +3 -3
- package/dist/node/index-CoeBF21y.d.ts.map +1 -0
- package/dist/node/{index-27HV5PJB.d.ts → index-DE3-dHqw.d.ts} +8 -3
- package/dist/node/index-DE3-dHqw.d.ts.map +1 -0
- package/dist/node/{index-BRgV_MPB.d.ts → index-DHt-2VQ-.d.ts} +2 -3
- package/dist/node/index-DHt-2VQ-.d.ts.map +1 -0
- package/dist/node/{index-nBlMTFkZ.d.ts → index-DMwKN5Le.d.ts} +2 -3
- package/dist/node/index-DMwKN5Le.d.ts.map +1 -0
- package/dist/node/{index-TMAlNHuM.d.ts → index-IvYaYY6v.d.ts} +5 -3
- package/dist/node/index-IvYaYY6v.d.ts.map +1 -0
- package/dist/node/{index-BRchlFBE.d.ts → index-WKTgvhlg.d.ts} +8 -3
- package/dist/node/index-WKTgvhlg.d.ts.map +1 -0
- package/dist/node/{index-C5KNEBO9.d.ts → index-c0M42fsA.d.ts} +2 -3
- package/dist/node/index-c0M42fsA.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.ts +6 -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/dist/node/mcp/index.cjs +1 -1
- package/dist/node/mcp/index.d.ts +1 -1
- package/dist/node/mcp/index.js +1 -1
- package/dist/node/mcp-BOglBJNy.cjs +1 -0
- package/dist/node/{mcp-BAujHOMr.js → mcp-D3BBVK7C.js} +1 -1
- package/dist/node/mcp-D3BBVK7C.js.map +1 -0
- package/dist/node/{chunk-Bmb41Sf3.cjs → rolldown-runtime-CMqjfN_6.cjs} +1 -1
- package/dist/node/testing/index.cjs +1 -0
- package/dist/node/testing/index.d.ts +21 -0
- package/dist/node/testing/index.d.ts.map +1 -0
- package/dist/node/testing/index.js +2 -0
- package/dist/node/testing/index.js.map +1 -0
- package/dist/node/tui/index.cjs +1 -1
- package/dist/node/tui/index.d.ts +1 -1
- package/dist/node/tui/index.js +1 -1
- package/dist/node/{tui-DIdvTeiT.js → tui-Btb1q88j.js} +4 -4
- package/dist/node/tui-Btb1q88j.js.map +1 -0
- package/dist/node/tui-SbUT7Zlt.cjs +24 -0
- package/dist/node/ws/index.cjs +1 -1
- package/dist/node/ws/index.d.ts +1 -1
- package/dist/node/ws/index.js +1 -1
- package/dist/node/{ws-BWel8nzl.js → ws-Dc2RUwVs.js} +1 -1
- package/dist/node/ws-Dc2RUwVs.js.map +1 -0
- package/dist/node/ws-QNMQn5kg.cjs +1 -0
- package/package.json +35 -22
- package/src/headless/HeadlessInteractionChannel.ts +9 -1
- package/src/headless/__tests__/headless-channel-options.test.ts +106 -0
- package/src/headless/__tests__/headless-provider-failure.integration.test.ts +143 -0
- package/src/headless/__tests__/headless-runner-initialization.test.ts +1 -1
- package/src/headless/__tests__/headless-runner.test.ts +24 -3
- package/src/headless/__tests__/headless-transport.test.ts +1 -2
- package/src/headless/headless-runner.ts +3 -2
- package/src/headless/headless-stream-json.ts +5 -5
- package/src/headless/headless-transport.ts +1 -2
- package/src/http/__tests__/http-transport.test.ts +1 -1
- package/src/http/__tests__/routes.test.ts +1 -1
- package/src/http/http-transport.ts +1 -2
- package/src/http/routes.ts +1 -1
- package/src/mcp/__tests__/mcp-server.test.ts +1 -1
- package/src/mcp/__tests__/mcp-transport.test.ts +1 -1
- package/src/mcp/mcp-server.ts +1 -1
- package/src/mcp/mcp-transport.ts +1 -2
- package/src/testing/__tests__/scripted-provider.test.ts +73 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/scripted-provider.ts +73 -0
- package/src/transport-registry.ts +1 -1
- package/src/tui/App.tsx +22 -11
- package/src/tui/BackgroundTaskPanel.tsx +1 -1
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +1 -1
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +1 -1
- package/src/tui/InputArea.tsx +2 -1
- package/src/tui/InteractivePrompt.tsx +2 -2
- package/src/tui/PluginTUI.tsx +1 -1
- package/src/tui/SessionPicker.tsx +1 -1
- package/src/tui/SessionStatusBar.tsx +1 -1
- package/src/tui/SlashAutocomplete.tsx +1 -1
- package/src/tui/StreamingIndicator.tsx +1 -1
- package/src/tui/TransportTUI.tsx +1 -1
- package/src/tui/TuiInteractionChannel.ts +60 -38
- package/src/tui/UsageSummaryEntry.tsx +1 -1
- package/src/tui/__tests__/PluginTUI.test.tsx +1 -1
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +1 -1
- package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
- package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +5 -2
- package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +1 -1
- package/src/tui/__tests__/background-task-panel.test.tsx +1 -1
- package/src/tui/__tests__/background-task-row-format.test.ts +1 -1
- package/src/tui/__tests__/channel-factory-integration.test.ts +138 -0
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +1 -1
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +1 -1
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +1 -1
- package/src/tui/__tests__/input-area-flow.test.ts +1 -1
- package/src/tui/__tests__/pty/pty-driver.ts +135 -0
- package/src/tui/__tests__/pty/tui-pty.ptytest.ts +61 -0
- package/src/tui/__tests__/render-channel-options.test.ts +32 -0
- package/src/tui/__tests__/session-init-poller.test.ts +102 -0
- package/src/tui/__tests__/session-switch-channel.test.tsx +307 -0
- package/src/tui/__tests__/slash-routing-effects.test.ts +4 -1
- package/src/tui/__tests__/status-activity.test.ts +3 -3
- package/src/tui/__tests__/status-bar.test.tsx +6 -5
- package/src/tui/__tests__/tui-channel-init-failure.test.ts +57 -0
- package/src/tui/__tests__/tui-state-manager.test.ts +1 -1
- package/src/tui/background-task-row-format.ts +1 -1
- package/src/tui/execution-workspace-view-model.ts +1 -1
- package/src/tui/flows/input-area-flow.ts +1 -1
- package/src/tui/flows/permission-prompt-flow.ts +1 -1
- package/src/tui/flows/session-init-poller.ts +77 -0
- package/src/tui/hooks/command-effect-handler.ts +4 -1
- package/src/tui/hooks/command-effect-queue.ts +1 -1
- package/src/tui/hooks/side-effects-types.ts +2 -2
- package/src/tui/hooks/useAutocomplete.ts +3 -2
- package/src/tui/hooks/usePluginCallbacks.ts +1 -1
- package/src/tui/hooks/usePluginScreenData.ts +1 -1
- package/src/tui/hooks/useSideEffects.ts +1 -1
- package/src/tui/hooks/useSlashRouting.ts +3 -3
- package/src/tui/hooks/useStatusLineSettings.ts +1 -1
- package/src/tui/hooks/useTuiChannel.ts +3 -3
- package/src/tui/plugin-tui-handlers.ts +1 -1
- package/src/tui/render.tsx +38 -25
- package/src/tui/status-activity.ts +2 -2
- package/src/tui/tui-cli-adapter.ts +3 -3
- package/src/tui/tui-state-manager.ts +2 -2
- package/src/tui/tui-transport.ts +4 -2
- package/src/ws/__tests__/ws-handler.test.ts +6 -4
- package/src/ws/__tests__/ws-transport.test.ts +1 -1
- package/src/ws/ws-background-messages.ts +1 -1
- package/src/ws/ws-handler.ts +4 -4
- package/src/ws/ws-protocol.ts +6 -4
- package/src/ws/ws-transport-configurable.ts +4 -2
- package/src/ws/ws-transport.ts +1 -2
- package/dist/node/headless-C6tj35h3.js.map +0 -1
- package/dist/node/http-Br10Ps8m.js.map +0 -1
- package/dist/node/http-Da6Kw4oy.cjs +0 -1
- package/dist/node/index-27HV5PJB.d.ts.map +0 -1
- package/dist/node/index-BRchlFBE.d.ts.map +0 -1
- package/dist/node/index-BRgV_MPB.d.ts.map +0 -1
- package/dist/node/index-BVNhOeeU.d.ts.map +0 -1
- package/dist/node/index-C5KNEBO9.d.ts.map +0 -1
- package/dist/node/index-C9LWCL4l.d.ts.map +0 -1
- package/dist/node/index-COWvtBa2.d.ts.map +0 -1
- package/dist/node/index-TMAlNHuM.d.ts.map +0 -1
- package/dist/node/index-X2Zg8FEY.d.ts.map +0 -1
- package/dist/node/index-nBlMTFkZ.d.ts.map +0 -1
- package/dist/node/mcp-BAujHOMr.js.map +0 -1
- package/dist/node/mcp-Bl8jUfev.cjs +0 -1
- package/dist/node/tui-D30s8S5f.cjs +0 -24
- package/dist/node/tui-DIdvTeiT.js.map +0 -1
- package/dist/node/ws-BWel8nzl.js.map +0 -1
- package/dist/node/ws-tCjj2gPu.cjs +0 -1
- package/src/tui/InkTerminal.ts +0 -42
- package/src/tui/hooks/use-interactive-session-init.ts +0 -91
- package/src/tui/hooks/usePermissionQueue.ts +0 -52
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box, Text, useStdout } from 'ink';
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
3
|
|
|
4
|
-
import type { ICommand } from '@robota-sdk/agent-
|
|
4
|
+
import type { ICommand } from '@robota-sdk/agent-interface-transport';
|
|
5
5
|
|
|
6
6
|
interface IProps {
|
|
7
7
|
/** Filtered list of commands to display */
|
|
@@ -9,7 +9,7 @@ import React from 'react';
|
|
|
9
9
|
import { renderMarkdown } from './render-markdown.js';
|
|
10
10
|
import ToolDiffBlock from './ToolDiffBlock.js';
|
|
11
11
|
|
|
12
|
-
import type { IToolState } from '@robota-sdk/agent-
|
|
12
|
+
import type { IToolState } from '@robota-sdk/agent-interface-transport';
|
|
13
13
|
|
|
14
14
|
function getToolStyle(t: IToolState): {
|
|
15
15
|
color: string;
|
package/src/tui/TransportTUI.tsx
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { Box, Text, useInput } from 'ink';
|
|
8
8
|
import React, { useState, useCallback } from 'react';
|
|
9
9
|
|
|
10
|
-
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
11
10
|
import type {
|
|
11
|
+
IInteractiveSession,
|
|
12
12
|
ITransportEntry,
|
|
13
13
|
ITransportRegistryView,
|
|
14
14
|
} from '@robota-sdk/agent-interface-transport';
|
|
@@ -12,37 +12,40 @@ import {
|
|
|
12
12
|
} from '@robota-sdk/agent-core';
|
|
13
13
|
import { InteractiveSession, CommandRegistry } from '@robota-sdk/agent-framework';
|
|
14
14
|
|
|
15
|
+
import { createSessionInitPoller } from './flows/session-init-poller.js';
|
|
15
16
|
import { CommandEffectQueue, type ICommandEffectQueue } from './hooks/command-effect-queue.js';
|
|
16
17
|
import { applySystemCommandResult } from './hooks/useSlashRouting.js';
|
|
17
18
|
import { generateSessionName } from './session-naming.js';
|
|
18
19
|
import { TuiStateManager } from './tui-state-manager.js';
|
|
19
20
|
|
|
21
|
+
import type { ISessionInitPoller, TSessionInitFailure } from './flows/session-init-poller.js';
|
|
20
22
|
import type { IPermissionRequest } from './types.js';
|
|
21
23
|
import type { IAIProvider, TPermissionMode, TSessionEndReason } from '@robota-sdk/agent-core';
|
|
22
24
|
import type { TToolArgs } from '@robota-sdk/agent-core';
|
|
23
|
-
import type { IInteractionChannel } from '@robota-sdk/agent-framework';
|
|
24
|
-
import type {
|
|
25
|
-
InteractionEvent,
|
|
26
|
-
IActionRequest,
|
|
27
|
-
IActionResponse,
|
|
28
|
-
ICommandInfo,
|
|
29
|
-
} from '@robota-sdk/agent-framework';
|
|
30
25
|
import type {
|
|
31
26
|
IBackgroundTaskRunner,
|
|
32
27
|
ICommandHostAdapters,
|
|
33
28
|
ICommandModule,
|
|
34
|
-
IInteractiveSession,
|
|
35
|
-
IInteractiveSessionStore,
|
|
36
29
|
TSubagentRunnerFactory,
|
|
37
|
-
IExecutionWorkspaceEvent,
|
|
38
|
-
IExecutionDetailPage,
|
|
39
|
-
IExecutionResult,
|
|
40
30
|
TShellExecFn,
|
|
41
31
|
} from '@robota-sdk/agent-framework';
|
|
42
|
-
import type {
|
|
43
|
-
|
|
32
|
+
import type {
|
|
33
|
+
IActionRequest,
|
|
34
|
+
IActionResponse,
|
|
35
|
+
ICommandInfo,
|
|
36
|
+
IExecutionDetailPage,
|
|
37
|
+
IExecutionResult,
|
|
38
|
+
IExecutionWorkspaceEvent,
|
|
39
|
+
IInteractionChannel,
|
|
40
|
+
IInteractiveSession,
|
|
41
|
+
IInteractiveSessionStore,
|
|
42
|
+
ITransportRegistryView,
|
|
43
|
+
InteractionEvent,
|
|
44
|
+
TPermissionResultValue,
|
|
45
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
44
46
|
|
|
45
47
|
const SESSION_INIT_POLL_MS = 200;
|
|
48
|
+
const SESSION_INIT_TIMEOUT_MS = 15000;
|
|
46
49
|
|
|
47
50
|
export interface ITuiInteractionChannelOptions {
|
|
48
51
|
cwd: string;
|
|
@@ -92,7 +95,7 @@ export class TuiInteractionChannel implements IInteractionChannel {
|
|
|
92
95
|
|
|
93
96
|
private autoNameTriggered = false;
|
|
94
97
|
private sessionStarted = false;
|
|
95
|
-
private
|
|
98
|
+
private initPoller: ISessionInitPoller | null = null;
|
|
96
99
|
private permissionQueue: Array<{
|
|
97
100
|
toolName: string;
|
|
98
101
|
toolArgs: TToolArgs;
|
|
@@ -369,6 +372,9 @@ export class TuiInteractionChannel implements IInteractionChannel {
|
|
|
369
372
|
const onSkillActivation = (): void => {
|
|
370
373
|
manager.syncHistory(session.getFullHistory());
|
|
371
374
|
};
|
|
375
|
+
const onMemoryEvent = (): void => {
|
|
376
|
+
manager.syncHistory(session.getFullHistory());
|
|
377
|
+
};
|
|
372
378
|
const onExecutionWorkspaceEvent = (event: IExecutionWorkspaceEvent): void => {
|
|
373
379
|
manager.syncExecutionWorkspaceSnapshot(event.snapshot);
|
|
374
380
|
};
|
|
@@ -384,6 +390,7 @@ export class TuiInteractionChannel implements IInteractionChannel {
|
|
|
384
390
|
session.on('context_update', manager.onContextUpdate);
|
|
385
391
|
session.on('compact', onCompact);
|
|
386
392
|
session.on('skill_activation', onSkillActivation);
|
|
393
|
+
session.on('memory_event', onMemoryEvent);
|
|
387
394
|
session.on('execution_workspace_event', onExecutionWorkspaceEvent);
|
|
388
395
|
}
|
|
389
396
|
|
|
@@ -413,36 +420,51 @@ export class TuiInteractionChannel implements IInteractionChannel {
|
|
|
413
420
|
}
|
|
414
421
|
|
|
415
422
|
private startInitCheck(): void {
|
|
416
|
-
this.
|
|
417
|
-
this.runInitCheck()
|
|
418
|
-
|
|
423
|
+
this.initPoller = createSessionInitPoller({
|
|
424
|
+
check: () => this.runInitCheck(),
|
|
425
|
+
intervalMs: SESSION_INIT_POLL_MS,
|
|
426
|
+
timeoutMs: SESSION_INIT_TIMEOUT_MS,
|
|
427
|
+
onReady: () => undefined,
|
|
428
|
+
onFailure: (failure) => this.onInitFailure(failure),
|
|
429
|
+
});
|
|
430
|
+
this.initPoller.start();
|
|
419
431
|
}
|
|
420
432
|
|
|
433
|
+
/** Throws while the session is not ready; the init poller classifies the error. */
|
|
421
434
|
private runInitCheck(): void {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.stateManager.syncHistory(restored);
|
|
432
|
-
}
|
|
433
|
-
this.syncExecutionWorkspace();
|
|
434
|
-
this.stopInitCheck();
|
|
435
|
-
} catch {
|
|
436
|
-
// allow-fallback: session initializes asynchronously; poll until ready
|
|
437
|
-
/* Not yet initialized */
|
|
435
|
+
const ctx = this.interactiveSession.getContextState();
|
|
436
|
+
this.stateManager.setContextState({
|
|
437
|
+
percentage: ctx.usedPercentage,
|
|
438
|
+
usedTokens: ctx.usedTokens,
|
|
439
|
+
maxTokens: ctx.maxTokens,
|
|
440
|
+
});
|
|
441
|
+
const restored = this.interactiveSession.getFullHistory();
|
|
442
|
+
if (restored.length > 0) {
|
|
443
|
+
this.stateManager.syncHistory(restored);
|
|
438
444
|
}
|
|
445
|
+
this.syncExecutionWorkspace();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private onInitFailure(failure: TSessionInitFailure): void {
|
|
449
|
+
const message =
|
|
450
|
+
failure.kind === 'timeout'
|
|
451
|
+
? `Session initialization timed out after ${SESSION_INIT_TIMEOUT_MS / 1000}s${
|
|
452
|
+
failure.lastError ? ` (last error: ${failure.lastError.message})` : ''
|
|
453
|
+
}`
|
|
454
|
+
: `Session initialization failed: ${failure.error.message}`;
|
|
455
|
+
this.stateManager.onError();
|
|
456
|
+
this.stateManager.addEntry({
|
|
457
|
+
id: `session-init-error-${Date.now()}`,
|
|
458
|
+
timestamp: new Date(),
|
|
459
|
+
category: 'event',
|
|
460
|
+
type: 'session-init-error',
|
|
461
|
+
data: { message },
|
|
462
|
+
});
|
|
439
463
|
}
|
|
440
464
|
|
|
441
465
|
private stopInitCheck(): void {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
this.initCheckInterval = null;
|
|
445
|
-
}
|
|
466
|
+
this.initPoller?.stop();
|
|
467
|
+
this.initPoller = null;
|
|
446
468
|
}
|
|
447
469
|
|
|
448
470
|
private syncExecutionWorkspace(): void {
|
|
@@ -3,7 +3,7 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
5
|
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
6
|
-
import type { IUsageSnapshot } from '@robota-sdk/agent-
|
|
6
|
+
import type { IUsageSnapshot } from '@robota-sdk/agent-interface-transport';
|
|
7
7
|
|
|
8
8
|
const TOKEN_COMPACT_THRESHOLD = 1000;
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { render } from 'ink-testing-library';
|
|
4
4
|
import { describe, it, expect, vi } from 'vitest';
|
|
5
5
|
import PluginTUI from '../PluginTUI.js';
|
|
6
|
-
import type { ICommandPluginAdapter } from '@robota-sdk/agent-
|
|
6
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-transport';
|
|
7
7
|
|
|
8
8
|
function mockCallbacks(): ICommandPluginAdapter {
|
|
9
9
|
return {
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { render } from 'ink-testing-library';
|
|
3
3
|
import { describe, it, expect } from 'vitest';
|
|
4
4
|
import SlashAutocomplete from '../SlashAutocomplete.js';
|
|
5
|
-
import type { ICommand } from '@robota-sdk/agent-
|
|
5
|
+
import type { ICommand } from '@robota-sdk/agent-interface-transport';
|
|
6
6
|
|
|
7
7
|
// ink-testing-library fixes stdout.columns = 100
|
|
8
8
|
// outer box chrome = 4 → rowWidth = 96 in tests
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
69
69
|
|
|
70
70
|
import type { IAIProvider, IHistoryEntry } from '@robota-sdk/agent-core';
|
|
71
|
-
import type { IExecutionResult } from '@robota-sdk/agent-
|
|
71
|
+
import type { IExecutionResult } from '@robota-sdk/agent-interface-transport';
|
|
72
72
|
|
|
73
73
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
74
74
|
|
|
@@ -53,8 +53,11 @@ vi.mock('@robota-sdk/agent-framework', async () => {
|
|
|
53
53
|
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
54
54
|
|
|
55
55
|
import type { IAIProvider } from '@robota-sdk/agent-core';
|
|
56
|
-
import type {
|
|
57
|
-
|
|
56
|
+
import type {
|
|
57
|
+
IExecutionResult,
|
|
58
|
+
IInteractiveSession,
|
|
59
|
+
ITransportRegistryView,
|
|
60
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
58
61
|
|
|
59
62
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
60
63
|
|
|
@@ -29,7 +29,7 @@ vi.mock('@robota-sdk/agent-framework', async () => {
|
|
|
29
29
|
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
30
30
|
|
|
31
31
|
import type { IAIProvider } from '@robota-sdk/agent-core';
|
|
32
|
-
import type { IActionRequest } from '@robota-sdk/agent-
|
|
32
|
+
import type { IActionRequest } from '@robota-sdk/agent-interface-transport';
|
|
33
33
|
|
|
34
34
|
function makeChannel(): TuiInteractionChannel {
|
|
35
35
|
return new TuiInteractionChannel({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import { render } from 'ink-testing-library';
|
|
4
|
-
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-
|
|
4
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
|
|
5
5
|
import BackgroundTaskPanel from '../BackgroundTaskPanel.js';
|
|
6
6
|
|
|
7
7
|
function makeEntry(overrides: Partial<IExecutionWorkspaceEntry>): IExecutionWorkspaceEntry {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-
|
|
2
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
|
|
3
3
|
import { formatBackgroundTaskRow } from '../background-task-row-format.js';
|
|
4
4
|
|
|
5
5
|
function makeEntry(overrides: Partial<IExecutionWorkspaceEntry>): IExecutionWorkspaceEntry {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-B11 TC-02: real-store channel factory integration.
|
|
3
|
+
*
|
|
4
|
+
* The official CI equivalent of real-resume-verify-v3.mjs: build the channel
|
|
5
|
+
* exactly the way render.tsx does (toChannelOptions + TuiInteractionChannel)
|
|
6
|
+
* over a REAL project session store with a persisted conversation, and assert
|
|
7
|
+
* the restored model context is non-empty. No store/session mocks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { createProjectSessionStore } from '@robota-sdk/agent-framework';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import { createScriptedProvider } from '../../testing/scripted-provider.js';
|
|
18
|
+
import { toChannelOptions, type IRenderOptions } from '../render.js';
|
|
19
|
+
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
20
|
+
|
|
21
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
22
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
23
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-transport';
|
|
24
|
+
|
|
25
|
+
const RESTORE_DEADLINE_MS = 10_000;
|
|
26
|
+
const POLL_MS = 50;
|
|
27
|
+
|
|
28
|
+
function fakeCliAdapter(settingsPath: string): ITuiCliAdapter {
|
|
29
|
+
return {
|
|
30
|
+
getUserSettingsPath: () => settingsPath,
|
|
31
|
+
readSettings: () => ({}),
|
|
32
|
+
writeSettings: vi.fn(),
|
|
33
|
+
deleteSettings: vi.fn().mockReturnValue(false),
|
|
34
|
+
applyStatusLineSettings: vi.fn(),
|
|
35
|
+
reloadPluginCommandSource: vi.fn(),
|
|
36
|
+
applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
|
|
37
|
+
getGitBranch: vi.fn().mockReturnValue(undefined),
|
|
38
|
+
getProviderDisplayName: vi.fn((type: string) => type),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function persistConversation(store: IInteractiveSessionStore, id: string, cwd: string): void {
|
|
43
|
+
const messages: TUniversalMessage[] = [
|
|
44
|
+
{ role: 'user', content: 'Remember the number 42.' } as TUniversalMessage,
|
|
45
|
+
{ role: 'assistant', content: 'Noted: 42.' } as TUniversalMessage,
|
|
46
|
+
{ role: 'user', content: 'And the city is Busan.' } as TUniversalMessage,
|
|
47
|
+
{ role: 'assistant', content: 'Noted: Busan.' } as TUniversalMessage,
|
|
48
|
+
];
|
|
49
|
+
store.save({
|
|
50
|
+
id,
|
|
51
|
+
cwd,
|
|
52
|
+
createdAt: '2026-06-13T00:00:00.000Z',
|
|
53
|
+
updatedAt: '2026-06-13T00:00:00.000Z',
|
|
54
|
+
messages,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('channel factory restores persisted context (CLI-B11 TC-02)', () => {
|
|
59
|
+
let cwd: string;
|
|
60
|
+
let channel: TuiInteractionChannel | undefined;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
cwd = mkdtempSync(join(tmpdir(), 'robota-b11-int-'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(async () => {
|
|
67
|
+
await channel?.stop();
|
|
68
|
+
channel = undefined;
|
|
69
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('createChannel(sessionId) over a real FileSessionStore yields usedTokens > 0', async () => {
|
|
73
|
+
const store = createProjectSessionStore(cwd);
|
|
74
|
+
const sessionId = 'b11-restore-session';
|
|
75
|
+
persistConversation(store, sessionId, cwd);
|
|
76
|
+
|
|
77
|
+
// Exactly the render.tsx factory: toChannelOptions(options, resumeSessionId).
|
|
78
|
+
const scripted = createScriptedProvider([{ text: 'unused in this test' }]);
|
|
79
|
+
const options: IRenderOptions = {
|
|
80
|
+
cwd,
|
|
81
|
+
provider: scripted.provider,
|
|
82
|
+
sessionStore: store,
|
|
83
|
+
cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
|
|
84
|
+
};
|
|
85
|
+
channel = new TuiInteractionChannel(toChannelOptions(options, sessionId));
|
|
86
|
+
await channel.start();
|
|
87
|
+
|
|
88
|
+
// Restoration is asynchronous (pendingRestoreMessages inject after init).
|
|
89
|
+
const deadline = Date.now() + RESTORE_DEADLINE_MS;
|
|
90
|
+
let usedTokens = 0;
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
try {
|
|
93
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
94
|
+
usedTokens = channel.getSession().getContextState().usedTokens;
|
|
95
|
+
if (usedTokens > 0) break;
|
|
96
|
+
} catch {
|
|
97
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
98
|
+
}
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The persisted messages were injected into the model context — the exact
|
|
103
|
+
// signal that was 0 in the 2026-05-31 bug. (getFullHistory() is the display
|
|
104
|
+
// log restored from record.history, which this record intentionally omits.)
|
|
105
|
+
expect(usedTokens).toBeGreaterThan(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('a channel created WITHOUT resumeSessionId starts with an empty context (control)', async () => {
|
|
109
|
+
const store = createProjectSessionStore(cwd);
|
|
110
|
+
persistConversation(store, 'b11-other-session', cwd);
|
|
111
|
+
|
|
112
|
+
const scripted = createScriptedProvider([{ text: 'unused' }]);
|
|
113
|
+
const options: IRenderOptions = {
|
|
114
|
+
cwd,
|
|
115
|
+
provider: scripted.provider,
|
|
116
|
+
sessionStore: store,
|
|
117
|
+
cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
|
|
118
|
+
};
|
|
119
|
+
channel = new TuiInteractionChannel(toChannelOptions(options, undefined));
|
|
120
|
+
await channel.start();
|
|
121
|
+
|
|
122
|
+
const deadline = Date.now() + RESTORE_DEADLINE_MS;
|
|
123
|
+
let ready = false;
|
|
124
|
+
while (Date.now() < deadline && !ready) {
|
|
125
|
+
try {
|
|
126
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
127
|
+
channel.getSession().getContextState();
|
|
128
|
+
ready = true;
|
|
129
|
+
} catch {
|
|
130
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(ready).toBe(true);
|
|
136
|
+
expect(channel.getSession().getFullHistory()).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -4,7 +4,7 @@ import { render } from 'ink-testing-library';
|
|
|
4
4
|
import type {
|
|
5
5
|
IExecutionWorkspaceEntry,
|
|
6
6
|
IExecutionWorkspaceSnapshot,
|
|
7
|
-
} from '@robota-sdk/agent-
|
|
7
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
8
8
|
import ExecutionWorkspaceSwitcher from '../ExecutionWorkspaceSwitcher.js';
|
|
9
9
|
import ExecutionWorkspaceDetailPane from '../ExecutionWorkspaceDetailPane.js';
|
|
10
10
|
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import type {
|
|
3
3
|
IExecutionWorkspaceEntry,
|
|
4
4
|
IExecutionWorkspaceSnapshot,
|
|
5
|
-
} from '@robota-sdk/agent-
|
|
5
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
6
6
|
import {
|
|
7
7
|
countActiveBackgroundWorkspaceEntries,
|
|
8
8
|
formatExecutionDetailRecord,
|
|
@@ -15,7 +15,7 @@ import React from 'react';
|
|
|
15
15
|
import InteractivePrompt from '../../InteractivePrompt.js';
|
|
16
16
|
|
|
17
17
|
import type { IAIProvider, IProviderDefinition } from '@robota-sdk/agent-core';
|
|
18
|
-
import type { TCommandInteractionPrompt as TInteractivePrompt } from '@robota-sdk/agent-
|
|
18
|
+
import type { TCommandInteractionPrompt as TInteractivePrompt } from '@robota-sdk/agent-interface-transport';
|
|
19
19
|
|
|
20
20
|
const openaiDefaults = {
|
|
21
21
|
apiKey: '$ENV:OPENAI_API_KEY',
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
resolveTabCompletion,
|
|
14
14
|
shouldSubmitInput,
|
|
15
15
|
} from '../flows/input-area-flow.js';
|
|
16
|
-
import type { ICommand } from '@robota-sdk/agent-
|
|
16
|
+
import type { ICommand } from '@robota-sdk/agent-interface-transport';
|
|
17
17
|
import {
|
|
18
18
|
createAssistantMessage,
|
|
19
19
|
createSystemMessage,
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
});
|