@robota-sdk/agent-ui-terminal 3.0.0-beta.100
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/CHANGELOG.md +1204 -0
- package/LICENSE +661 -0
- package/README.md +50 -0
- package/dist/node/index.d.ts +1224 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +88 -0
- package/src/App.tsx +157 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +236 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +92 -0
- package/src/SessionStatusBar.tsx +80 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +308 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +872 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +530 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/__fixtures__/korean-text.json +17 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
- package/src/__tests__/cjk-text-input.test.ts +196 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +474 -0
- package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +329 -0
- package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
- package/src/__tests__/pty/isolated-home.ts +97 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +155 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-picker.test.tsx +93 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +290 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +178 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +149 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +482 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/__tests__/wire-tui-channel.test.ts +1457 -0
- package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +140 -0
- package/src/attached-session-connection.ts +14 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +88 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +271 -0
- package/src/hooks/useAppInputBindings.ts +163 -0
- package/src/hooks/useAppInteractionState.ts +84 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +90 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +93 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +141 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +72 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +746 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/short-session-id.ts +9 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/slash-command-input.ts +11 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +111 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +184 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +171 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +149 -0
- package/src/tui-session-events.ts +89 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +134 -0
- package/src/tui-state-manager.ts +347 -0
- package/src/types.ts +21 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +963 -0
- package/src/wire-tui-client-commands.ts +142 -0
- package/src/wire-tui-projection.ts +136 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 TC-11 / TC-12 — screen-reader mode on the BUILT CLI binary, in a real PTY.
|
|
3
|
+
*
|
|
4
|
+
* This is the only level at which the claim can be checked. A screen reader has no `aria-live`: the
|
|
5
|
+
* ONLY channel to it is which bytes land in the terminal buffer, so a component test asserting on a
|
|
6
|
+
* frame is asserting on something the reader never sees. Here the transcript is the evidence.
|
|
7
|
+
*
|
|
8
|
+
* TC-11 pairs the positive with the DEFAULT-OFF proof deliberately: the same fixture, the same
|
|
9
|
+
* binary, one flag apart. Without the negative half, "no box-drawing characters" would also pass if
|
|
10
|
+
* the fixture happened to render none.
|
|
11
|
+
*
|
|
12
|
+
* Runs under `test:pty` against the built CLI (`pnpm build` first).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mkdtempSync, rmSync, realpathSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
21
|
+
|
|
22
|
+
import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
|
|
23
|
+
|
|
24
|
+
import type { IPtySession } from './pty-driver.js';
|
|
25
|
+
|
|
26
|
+
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures');
|
|
27
|
+
const REPLAY_FIXTURE = join(FIXTURES, 'replay-conversation.jsonl');
|
|
28
|
+
|
|
29
|
+
/** Every box-drawing glyph the TUI draws chrome with, in one class. */
|
|
30
|
+
const BOX_DRAWING = /[│─╭╮╰╯┌┐└┘]/u;
|
|
31
|
+
|
|
32
|
+
/** OSC 133 prompt-start — the mark a terminal turns into jump-to-previous-turn. */
|
|
33
|
+
const OSC_133_PROMPT_START = '\x1b]133;A';
|
|
34
|
+
|
|
35
|
+
/** How long one `waitFor` may block, and how long a whole case may take, on a loaded CI runner. */
|
|
36
|
+
const WAIT_MS = 30_000;
|
|
37
|
+
const CASE_TIMEOUT_MS = 90_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mark the isolated HOME as already onboarded. TC-12 asserts on the FIRST line the process writes,
|
|
41
|
+
* and the first-run welcome is a separate concern with its own onboarding copy — a returning user
|
|
42
|
+
* is the case where "the first line confirms the mode" is the claim being made.
|
|
43
|
+
*/
|
|
44
|
+
function markOnboarded(homeDir: string): void {
|
|
45
|
+
writeFileSync(join(homeDir, 'state', 'onboarded'), new Date().toISOString());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('CLI-2004 screen-reader mode through the real binary', () => {
|
|
49
|
+
let projectDir: string;
|
|
50
|
+
let session: IPtySession | undefined;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screenreader-pty-')));
|
|
54
|
+
writeTuiProviderSettings(projectDir);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
await session?.disposeAsync();
|
|
59
|
+
session = undefined;
|
|
60
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it(
|
|
64
|
+
'TC-11: --screen-reader emits no box-drawing chrome, role labels, and OSC 133 marks',
|
|
65
|
+
async () => {
|
|
66
|
+
session = spawnTui({
|
|
67
|
+
projectDir,
|
|
68
|
+
homeDir: join(projectDir, 'home'),
|
|
69
|
+
args: ['--session-log', REPLAY_FIXTURE, '--screen-reader'],
|
|
70
|
+
// The waits are exercised by their own unit test; a real one here only slows the fixture.
|
|
71
|
+
env: { PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
75
|
+
await session.sendKeys('hello');
|
|
76
|
+
await session.pressEnter();
|
|
77
|
+
await session.waitFor(/REPLAYED_ANSWER_42/, WAIT_MS);
|
|
78
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
79
|
+
|
|
80
|
+
const snapshot = session.snapshot();
|
|
81
|
+
const raw = session.raw();
|
|
82
|
+
|
|
83
|
+
// The transcript is labelled by ROLE, not by vendor.
|
|
84
|
+
expect(snapshot).toContain('assistant:');
|
|
85
|
+
expect(snapshot).toContain('you:');
|
|
86
|
+
expect(snapshot).toContain('REPLAYED_ANSWER_42');
|
|
87
|
+
|
|
88
|
+
// Not one box-drawing character reaches the terminal.
|
|
89
|
+
expect(snapshot).not.toMatch(BOX_DRAWING);
|
|
90
|
+
|
|
91
|
+
// The turn boundary is marked for terminals that navigate by it.
|
|
92
|
+
expect(raw).toContain(OSC_133_PROMPT_START);
|
|
93
|
+
},
|
|
94
|
+
CASE_TIMEOUT_MS,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('CLI-2004 the same fixture with the mode off', () => {
|
|
99
|
+
let projectDir: string;
|
|
100
|
+
let session: IPtySession | undefined;
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screenreader-pty-')));
|
|
104
|
+
writeTuiProviderSettings(projectDir);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(async () => {
|
|
108
|
+
await session?.disposeAsync();
|
|
109
|
+
session = undefined;
|
|
110
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it(
|
|
114
|
+
'TC-11: the SAME fixture without the flag still renders the bordered UI (default-off)',
|
|
115
|
+
async () => {
|
|
116
|
+
session = spawnTui({
|
|
117
|
+
projectDir,
|
|
118
|
+
homeDir: join(projectDir, 'home'),
|
|
119
|
+
args: ['--session-log', REPLAY_FIXTURE],
|
|
120
|
+
env: { PRODUCT_TURN_MARKS: '1' },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
124
|
+
await session.sendKeys('hello');
|
|
125
|
+
await session.pressEnter();
|
|
126
|
+
await session.waitFor(/REPLAYED_ANSWER_42/, WAIT_MS);
|
|
127
|
+
|
|
128
|
+
const snapshot = session.snapshot();
|
|
129
|
+
|
|
130
|
+
expect(snapshot).toMatch(BOX_DRAWING);
|
|
131
|
+
expect(snapshot).toContain('You:');
|
|
132
|
+
expect(snapshot).not.toContain('[Screen reader mode:');
|
|
133
|
+
expect(session.raw()).not.toContain(OSC_133_PROMPT_START);
|
|
134
|
+
},
|
|
135
|
+
CASE_TIMEOUT_MS,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('configured turn-mark override through the real binary', () => {
|
|
140
|
+
let projectDir: string;
|
|
141
|
+
let session: IPtySession | undefined;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-turn-marks-pty-')));
|
|
145
|
+
writeTuiProviderSettings(projectDir);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(async () => {
|
|
149
|
+
await session?.disposeAsync();
|
|
150
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
for (const [label, env, expected] of [
|
|
154
|
+
['WezTerm default', { TERM_PROGRAM: 'WezTerm' }, false],
|
|
155
|
+
['WezTerm opt-in', { TERM_PROGRAM: 'WezTerm', PRODUCT_TURN_MARKS: '1' }, true],
|
|
156
|
+
['TTY kill switch', { PRODUCT_TURN_MARKS: '0' }, false],
|
|
157
|
+
] as const) {
|
|
158
|
+
it(
|
|
159
|
+
`${label} ${expected ? 'emits' : 'withholds'} OSC 133`,
|
|
160
|
+
async () => {
|
|
161
|
+
session = spawnTui({
|
|
162
|
+
projectDir,
|
|
163
|
+
homeDir: join(projectDir, 'home'),
|
|
164
|
+
args: ['--screen-reader'],
|
|
165
|
+
env: { ...env, PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
166
|
+
});
|
|
167
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
168
|
+
expect(session.raw().includes(OSC_133_PROMPT_START)).toBe(expected);
|
|
169
|
+
},
|
|
170
|
+
CASE_TIMEOUT_MS,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('CLI-2004 the channel the mode was enabled through', () => {
|
|
176
|
+
let projectDir: string;
|
|
177
|
+
let session: IPtySession | undefined;
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screenreader-pty-')));
|
|
181
|
+
writeTuiProviderSettings(projectDir);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterEach(async () => {
|
|
185
|
+
await session?.disposeAsync();
|
|
186
|
+
session = undefined;
|
|
187
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it(
|
|
191
|
+
'TC-12: PRODUCT_SCREEN_READER=1 with no flag announces the env channel first',
|
|
192
|
+
async () => {
|
|
193
|
+
markOnboarded(join(projectDir, 'home'));
|
|
194
|
+
session = spawnTui({
|
|
195
|
+
projectDir,
|
|
196
|
+
homeDir: join(projectDir, 'home'),
|
|
197
|
+
env: { PRODUCT_SCREEN_READER: '1', PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
201
|
+
|
|
202
|
+
expect(session.snapshot().trimStart().startsWith('[Screen reader mode: on via env]')).toBe(
|
|
203
|
+
true,
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
CASE_TIMEOUT_MS,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
it(
|
|
210
|
+
'TC-12: --screen-reader beats PRODUCT_SCREEN_READER=0 and says so',
|
|
211
|
+
async () => {
|
|
212
|
+
markOnboarded(join(projectDir, 'home'));
|
|
213
|
+
session = spawnTui({
|
|
214
|
+
projectDir,
|
|
215
|
+
homeDir: join(projectDir, 'home'),
|
|
216
|
+
args: ['--screen-reader'],
|
|
217
|
+
env: { PRODUCT_SCREEN_READER: '0', PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
221
|
+
|
|
222
|
+
expect(session.snapshot().trimStart().startsWith('[Screen reader mode: on via flag]')).toBe(
|
|
223
|
+
true,
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
CASE_TIMEOUT_MS,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `the CLI session attach command` end to end on the BUILT binary, in a real terminal: start a supervised
|
|
3
|
+
* session, attach and confirm on the controlling terminal, submit, detach with `/exit`, see it still
|
|
4
|
+
* running, attach again read-only and find the earlier prompt, detach with Ctrl-C, peek from
|
|
5
|
+
* `session view` and land back in the view, then stop it.
|
|
6
|
+
*
|
|
7
|
+
* The provider points at a closed local port, so the submitted turn fails without any network call
|
|
8
|
+
* leaving this machine. `XDG_RUNTIME_DIR` is a short directory so the control socket path fits.
|
|
9
|
+
*
|
|
10
|
+
* Runs under `test:pty` against the built CLI (`pnpm --filter @robota-sdk/agent-cli build` first).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
14
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
20
|
+
|
|
21
|
+
import { createIsolatedHome, createPtyEnv } from './isolated-home.js';
|
|
22
|
+
import { spawnPty } from './spawn-pty.js';
|
|
23
|
+
|
|
24
|
+
import type { IPtyRunSession } from './spawn-pty.js';
|
|
25
|
+
|
|
26
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
27
|
+
const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
|
|
28
|
+
const WAIT_MS = 20_000;
|
|
29
|
+
|
|
30
|
+
const cleanups: (() => void)[] = [];
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function agent(
|
|
36
|
+
args: readonly string[],
|
|
37
|
+
cwd: string,
|
|
38
|
+
env: NodeJS.ProcessEnv,
|
|
39
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
40
|
+
const result = spawnSync(process.execPath, [AGENT_BIN, ...args], {
|
|
41
|
+
cwd,
|
|
42
|
+
env,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: WAIT_MS,
|
|
45
|
+
});
|
|
46
|
+
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function attach(id: string, cwd: string, env: NodeJS.ProcessEnv, observe = false): IPtyRunSession {
|
|
50
|
+
const session = spawnPty({
|
|
51
|
+
command: process.execPath,
|
|
52
|
+
args: [AGENT_BIN, 'session', 'attach', id, ...(observe ? ['--observe'] : [])],
|
|
53
|
+
cwd,
|
|
54
|
+
env,
|
|
55
|
+
});
|
|
56
|
+
cleanups.push(() => session.dispose());
|
|
57
|
+
return session;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('the CLI session attach command on the built binary', () => {
|
|
61
|
+
it('attaches, submits, detaches without stopping the session, and re-attaches', async () => {
|
|
62
|
+
const project = realpathSync(mkdtempSync(join(tmpdir(), 'agent-attach-pty-')));
|
|
63
|
+
// `/tmp` rather than the platform temp directory: the control socket path must stay short.
|
|
64
|
+
const runtime = mkdtempSync('/tmp/rsa-');
|
|
65
|
+
cleanups.push(() => rmSync(project, { recursive: true, force: true }));
|
|
66
|
+
cleanups.push(() => rmSync(runtime, { recursive: true, force: true }));
|
|
67
|
+
const home = createIsolatedHome();
|
|
68
|
+
const stateRoot = join(runtime, 's');
|
|
69
|
+
mkdirSync(stateRoot, { recursive: true });
|
|
70
|
+
writeFileSync(join(stateRoot, 'onboarded'), new Date().toISOString(), 'utf8');
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(stateRoot, 'settings.json'),
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
currentProvider: 'anthropic',
|
|
75
|
+
providers: {
|
|
76
|
+
anthropic: {
|
|
77
|
+
type: 'anthropic',
|
|
78
|
+
model: 'claude-test-model',
|
|
79
|
+
apiKey: 'pty-dummy-key',
|
|
80
|
+
baseURL: 'http://127.0.0.1:9',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
'utf8',
|
|
85
|
+
);
|
|
86
|
+
const env = createPtyEnv({
|
|
87
|
+
HOME: home,
|
|
88
|
+
XDG_RUNTIME_DIR: runtime,
|
|
89
|
+
PRODUCT_USER_STATE_DIR: stateRoot,
|
|
90
|
+
PRODUCT_CACHE_DIR: join(runtime, 'c'),
|
|
91
|
+
PRODUCT_LOG_DIR: join(runtime, 'l'),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Workspace trust is recorded for a repository, as a user would grant it before a background start.
|
|
95
|
+
const git = (...args: string[]): void => {
|
|
96
|
+
execFileSync('git', args, { cwd: project, env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
97
|
+
};
|
|
98
|
+
git('init', '-q', '-b', 'main');
|
|
99
|
+
git('config', 'user.name', 'Attach Fixture');
|
|
100
|
+
git('config', 'user.email', 'attach@example.invalid');
|
|
101
|
+
writeFileSync(join(project, 'README.md'), 'fixture\n');
|
|
102
|
+
git('add', '--', 'README.md');
|
|
103
|
+
git('commit', '-q', '-m', 'chore: fixture');
|
|
104
|
+
const trusted = agent(['trust', '--yes'], project, env);
|
|
105
|
+
expect(trusted.status, trusted.stderr).toBe(0);
|
|
106
|
+
const started = agent(
|
|
107
|
+
['session', 'start', '--background', '--name', 'pty-attach'],
|
|
108
|
+
project,
|
|
109
|
+
env,
|
|
110
|
+
);
|
|
111
|
+
expect(started.status, started.stderr).toBe(0);
|
|
112
|
+
const id = /Supervised session: ([0-9a-f-]{36})/u.exec(started.stdout)?.[1];
|
|
113
|
+
expect(id).toBeDefined();
|
|
114
|
+
cleanups.push(() => {
|
|
115
|
+
agent(['session', 'stop', id!], project, env);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const drive = attach(id!, project, env);
|
|
119
|
+
await drive.waitFor('Attach? Type yes', WAIT_MS);
|
|
120
|
+
expect(drive.snapshot()).toContain('drive (send prompts, answer its questions)');
|
|
121
|
+
await drive.sendKeys('yes');
|
|
122
|
+
await drive.pressEnter();
|
|
123
|
+
await drive.waitFor('Type a message or /help', WAIT_MS);
|
|
124
|
+
await drive.sendKeys('hello from the pty');
|
|
125
|
+
await drive.pressEnter();
|
|
126
|
+
await drive.waitFor('Last turn failed', WAIT_MS);
|
|
127
|
+
await drive.sendKeys('/exit');
|
|
128
|
+
await drive.pressEnter();
|
|
129
|
+
expect(await drive.expectExit(WAIT_MS)).toBe(0);
|
|
130
|
+
|
|
131
|
+
const listed = agent(['session', 'list', '--format', 'json'], project, env);
|
|
132
|
+
const supervised = (
|
|
133
|
+
JSON.parse(listed.stdout) as {
|
|
134
|
+
supervised: { sessions: { id: string; liveness: string; control: string }[] };
|
|
135
|
+
}
|
|
136
|
+
).supervised.sessions;
|
|
137
|
+
expect(supervised).toContainEqual(
|
|
138
|
+
expect.objectContaining({ id, liveness: 'alive', control: 'available' }),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const observe = attach(id!, project, env, true);
|
|
142
|
+
await observe.waitFor('Attach? Type yes', WAIT_MS);
|
|
143
|
+
expect(observe.snapshot()).toContain('observe (read only)');
|
|
144
|
+
await observe.sendKeys('yes');
|
|
145
|
+
await observe.pressEnter();
|
|
146
|
+
await observe.waitFor('Observing — read only', WAIT_MS);
|
|
147
|
+
await observe.waitFor('hello from the pty', WAIT_MS);
|
|
148
|
+
observe.write('\x03');
|
|
149
|
+
expect(await observe.expectExit(WAIT_MS)).toBe(0);
|
|
150
|
+
const afterObserve = agent(['session', 'list', '--format', 'json'], project, env);
|
|
151
|
+
const remaining = (
|
|
152
|
+
JSON.parse(afterObserve.stdout) as {
|
|
153
|
+
supervised: { sessions: { id: string; liveness: string; control: string }[] };
|
|
154
|
+
}
|
|
155
|
+
).supervised.sessions;
|
|
156
|
+
expect(remaining).toContainEqual(
|
|
157
|
+
expect.objectContaining({ id, liveness: 'alive', control: 'available' }),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// From the session view: peek at the row, confirm there, detach, and land back in the view.
|
|
161
|
+
const view = spawnPty({
|
|
162
|
+
command: process.execPath,
|
|
163
|
+
args: [AGENT_BIN, 'session', 'view'],
|
|
164
|
+
cwd: project,
|
|
165
|
+
env,
|
|
166
|
+
});
|
|
167
|
+
cleanups.push(() => view.dispose());
|
|
168
|
+
await view.waitFor('a Attach p Peek', WAIT_MS);
|
|
169
|
+
view.write('p');
|
|
170
|
+
await view.waitFor('to observe (read only)?', WAIT_MS);
|
|
171
|
+
view.write('y');
|
|
172
|
+
await view.waitFor('Observing — read only', WAIT_MS);
|
|
173
|
+
await view.waitFor('hello from the pty', WAIT_MS);
|
|
174
|
+
const beforeDetach = view.outputOffset();
|
|
175
|
+
view.write('\x1d');
|
|
176
|
+
await view.waitForSince(beforeDetach, 'Background sessions across projects', WAIT_MS);
|
|
177
|
+
view.write('q');
|
|
178
|
+
expect(await view.expectExit(WAIT_MS)).toBe(0);
|
|
179
|
+
|
|
180
|
+
expect(agent(['session', 'stop', id!], project, env).status).toBe(0);
|
|
181
|
+
}, 120_000);
|
|
182
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2 Stage C (TC-04): `/settings` still opens the settings screen — now rendered from
|
|
3
|
+
* the requester-routed `ui_intent` session event (the legacy `settings-tui-requested` effect
|
|
4
|
+
* branch is deleted from the TUI). Observed through a real PTY against the BUILT CLI binary
|
|
5
|
+
* (`pnpm --filter @robota-sdk/agent-cli build`).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtempSync, rmSync, realpathSync } 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('/settings opens the settings screen via ui_intent (CMD-004 Stage C)', () => {
|
|
19
|
+
let projectDir: string;
|
|
20
|
+
let session: IPtySession | undefined;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-settings-pty-')));
|
|
24
|
+
writeTuiProviderSettings(projectDir);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await session?.disposeAsync();
|
|
29
|
+
session = undefined;
|
|
30
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders the transports settings screen and Esc returns to the prompt', async () => {
|
|
34
|
+
session = spawnTui({ projectDir, homeDir: join(projectDir, 'home') });
|
|
35
|
+
await session.waitFor(/Type a message or \/help/);
|
|
36
|
+
|
|
37
|
+
await session.sendKeys('/settings');
|
|
38
|
+
await session.pressEnter();
|
|
39
|
+
|
|
40
|
+
// The settings screen opened — from the ui_intent event, not a legacy effect branch.
|
|
41
|
+
await session.waitFor(/Settings › Transports/, 20_000);
|
|
42
|
+
expect(session.snapshot()).toContain('Settings › Transports');
|
|
43
|
+
|
|
44
|
+
// Esc closes the screen and returns to the idle prompt.
|
|
45
|
+
const mark = session.outputOffset();
|
|
46
|
+
session.pressEscape();
|
|
47
|
+
await session.waitForSince(mark, /Type a message or \/help/, 20_000);
|
|
48
|
+
}, 60_000);
|
|
49
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST-007: self-test for the TUI-owned PTY harness (relocated internally by BOUNDARY-2655).
|
|
3
|
+
*
|
|
4
|
+
* Drives a trivial process (no fixture needed) through `spawnPty` to pin the harness contract:
|
|
5
|
+
* `waitFor` resolves on a printed marker, `expectExit` returns the real exit code, and
|
|
6
|
+
* `snapshot()`/`raw()` capture output. The terminal-handoff suites (in agent-ui-terminal) are the
|
|
7
|
+
* harness's real-world consumers; this guards the harness itself.
|
|
8
|
+
*
|
|
9
|
+
* HARNESS-025 adds the HOME-isolation contract: a PTY child must never receive the developer's real
|
|
10
|
+
* `HOME`, or the suite silently depends on whatever is in that home directory.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import { createIsolatedHome, createPtyEnv } from './isolated-home.js';
|
|
19
|
+
import { spawnPty } from './spawn-pty.js';
|
|
20
|
+
|
|
21
|
+
import type { IPtyRunSession } from './spawn-pty.js';
|
|
22
|
+
|
|
23
|
+
const PACKAGE_DIR = fileURLToPath(new URL('../../..', import.meta.url));
|
|
24
|
+
const sessions: IPtyRunSession[] = [];
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
for (const session of sessions.splice(0)) session.dispose();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('spawnPty harness self-test', () => {
|
|
31
|
+
it('waits for a marker and reports the real exit code', async () => {
|
|
32
|
+
const session = spawnPty({
|
|
33
|
+
command: process.execPath,
|
|
34
|
+
args: ['-e', 'process.stdout.write("MARKER_OK\\n"); setTimeout(() => process.exit(3), 50);'],
|
|
35
|
+
cwd: PACKAGE_DIR,
|
|
36
|
+
env: createPtyEnv(),
|
|
37
|
+
});
|
|
38
|
+
sessions.push(session);
|
|
39
|
+
|
|
40
|
+
await session.waitFor('MARKER_OK', 5_000);
|
|
41
|
+
expect(session.snapshot()).toContain('MARKER_OK');
|
|
42
|
+
expect(await session.expectExit(5_000)).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('paces sent keys and reads them back from the child', async () => {
|
|
46
|
+
const session = spawnPty({
|
|
47
|
+
command: 'sh',
|
|
48
|
+
args: ['-c', 'IFS= read -r line; printf "ECHO:[%s]\\n" "$line"'],
|
|
49
|
+
cwd: PACKAGE_DIR,
|
|
50
|
+
env: createPtyEnv(),
|
|
51
|
+
});
|
|
52
|
+
sessions.push(session);
|
|
53
|
+
|
|
54
|
+
await session.sendKeys('abc', 10);
|
|
55
|
+
session.write('\r');
|
|
56
|
+
await session.waitFor('ECHO:[abc]', 5_000);
|
|
57
|
+
expect(await session.expectExit(5_000)).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('PTY HOME isolation', () => {
|
|
62
|
+
/** Ask the child to report the HOME it actually sees, so the assertion is on the child's view. */
|
|
63
|
+
function spawnHomeReporter(env?: NodeJS.ProcessEnv): IPtyRunSession {
|
|
64
|
+
const session = spawnPty({
|
|
65
|
+
command: 'sh',
|
|
66
|
+
args: ['-c', 'printf "CHILD_HOME:[%s]\\n" "$HOME"'],
|
|
67
|
+
cwd: PACKAGE_DIR,
|
|
68
|
+
...(env ? { env } : {}),
|
|
69
|
+
});
|
|
70
|
+
sessions.push(session);
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readReportedHome(snapshot: string): string {
|
|
75
|
+
const match = /CHILD_HOME:\[([^\]]*)\]/.exec(snapshot);
|
|
76
|
+
if (match?.[1] === undefined) throw new Error(`child never reported HOME:\n${snapshot}`);
|
|
77
|
+
return match[1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('does not hand the real HOME to a child when no env is supplied', async () => {
|
|
81
|
+
const realHome = process.env['HOME'];
|
|
82
|
+
expect(
|
|
83
|
+
realHome,
|
|
84
|
+
'the test host must have a HOME for this assertion to mean anything',
|
|
85
|
+
).toBeTruthy();
|
|
86
|
+
|
|
87
|
+
const session = spawnHomeReporter();
|
|
88
|
+
await session.waitFor('CHILD_HOME:[', 5_000);
|
|
89
|
+
await session.expectExit(5_000);
|
|
90
|
+
|
|
91
|
+
const childHome = readReportedHome(session.snapshot());
|
|
92
|
+
expect(childHome).not.toBe(realHome);
|
|
93
|
+
expect(childHome.startsWith(tmpdir())).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('hands the child the isolated HOME built by createPtyEnv', async () => {
|
|
97
|
+
const env = createPtyEnv();
|
|
98
|
+
const session = spawnHomeReporter(env);
|
|
99
|
+
await session.waitFor('CHILD_HOME:[', 5_000);
|
|
100
|
+
await session.expectExit(5_000);
|
|
101
|
+
|
|
102
|
+
expect(readReportedHome(session.snapshot())).toBe(env['HOME']);
|
|
103
|
+
expect(env['HOME']).not.toBe(process.env['HOME']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('creates an EMPTY home directory, so no user config can leak in', () => {
|
|
107
|
+
const home = createIsolatedHome();
|
|
108
|
+
expect(existsSync(home)).toBe(true);
|
|
109
|
+
expect(readdirSync(home)).toEqual([]);
|
|
110
|
+
expect(home).not.toBe(process.env['HOME']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('lets an override customize the env without reintroducing the real HOME', () => {
|
|
114
|
+
const env = createPtyEnv({ EDITOR: 'sh /tmp/fake-editor.sh' });
|
|
115
|
+
expect(env['EDITOR']).toBe('sh /tmp/fake-editor.sh');
|
|
116
|
+
expect(env['TERM']).toBe('xterm-256color');
|
|
117
|
+
expect(env['PATH']).toBe(process.env['PATH']);
|
|
118
|
+
expect(env['HOME']).not.toBe(process.env['HOME']);
|
|
119
|
+
});
|
|
120
|
+
});
|