@robota-sdk/agent-ui-terminal 3.0.0-beta.81
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/LICENSE +661 -0
- package/README.md +27 -0
- package/dist/node/index.cjs +32 -0
- package/dist/node/index.d.cts +1066 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1066 -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 +93 -0
- package/src/App.tsx +146 -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 +186 -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 +72 -0
- package/src/SessionStatusBar.tsx +76 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +284 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +455 -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 +567 -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 +24 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +629 -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 +284 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +642 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -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__/cjk-defer-submit.test.tsx +67 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +78 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +186 -0
- package/src/__tests__/cjk-text-input.test.ts +191 -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 +45 -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 +111 -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__/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 +473 -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 +330 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.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 +342 -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 +291 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +165 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +186 -0
- package/src/__tests__/pty/isolated-home.ts +64 -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 +153 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +134 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +138 -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/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/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +263 -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__/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-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 +282 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +177 -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 +62 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +139 -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 +451 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +136 -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 +92 -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 +87 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +124 -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-loop-escape.test.tsx +74 -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 +267 -0
- package/src/hooks/useAppInputBindings.ts +145 -0
- package/src/hooks/useAppInteractionState.ts +81 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +74 -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 +76 -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 +129 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +56 -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 +111 -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 +544 -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/shutdown-signal.ts +34 -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 +83 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +172 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +163 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +140 -0
- package/src/tui-session-events.ts +82 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +118 -0
- package/src/tui-state-manager.ts +327 -0
- package/src/types.ts +19 -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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEHAVIOR-2437 TC-07 — `/git status`, `/git diff` and `/git commit` on the BUILT robota binary, in
|
|
3
|
+
* a real PTY, over a throwaway git repository. The four User Execution scenarios of the spec, as a
|
|
4
|
+
* machine runs them: read path (S1), commit path (S2), nothing staged (S3), headless cancel (S4).
|
|
5
|
+
*
|
|
6
|
+
* Runs under `test:pty` against the built CLI (`pnpm --filter @robota-sdk/agent-cli build` first).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import { spawnTui } from './pty-driver.js';
|
|
18
|
+
|
|
19
|
+
import type { IPtySession } from './pty-driver.js';
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
22
|
+
const ROBOTA_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/robota.cjs');
|
|
23
|
+
|
|
24
|
+
const WAIT_MS = 20_000;
|
|
25
|
+
const CASE_TIMEOUT_MS = 120_000;
|
|
26
|
+
const DOWN = '\x1b[B';
|
|
27
|
+
const UP = '\x1b[A';
|
|
28
|
+
|
|
29
|
+
interface IFixture {
|
|
30
|
+
root: string;
|
|
31
|
+
repo: string;
|
|
32
|
+
home: string;
|
|
33
|
+
env: NodeJS.ProcessEnv;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A hermetic git environment: isolated HOME, no system config, signing off. */
|
|
37
|
+
function gitEnv(home: string): NodeJS.ProcessEnv {
|
|
38
|
+
const globalConfig = join(home, 'gitconfig');
|
|
39
|
+
writeFileSync(globalConfig, '[commit]\n\tgpgsign = false\n');
|
|
40
|
+
return {
|
|
41
|
+
PATH: process.env['PATH'] ?? '',
|
|
42
|
+
HOME: home,
|
|
43
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
44
|
+
GIT_CONFIG_GLOBAL: globalConfig,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function git(fixture: IFixture, ...args: string[]): string {
|
|
49
|
+
return execFileSync('git', args, {
|
|
50
|
+
cwd: fixture.repo,
|
|
51
|
+
env: fixture.env,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The dummy provider profile under the isolated HOME — boot/slash/exit make zero model calls. */
|
|
58
|
+
function writeProviderSettings(home: string): void {
|
|
59
|
+
const dir = join(home, '.robota');
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
writeFileSync(
|
|
62
|
+
join(dir, 'settings.json'),
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
currentProvider: 'anthropic',
|
|
65
|
+
providers: {
|
|
66
|
+
anthropic: { type: 'anthropic', model: 'claude-test-model', apiKey: 'pty-dummy-key' },
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
'utf8',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One commit holding `greeting.txt` and `notes.txt`; then `greeting.txt` edited and STAGED (when
|
|
75
|
+
* `stage`), `notes.txt` edited and left unstaged, `scratch.log` created untracked.
|
|
76
|
+
*/
|
|
77
|
+
function makeFixture(stage: boolean): IFixture {
|
|
78
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'robota-git-pty-')));
|
|
79
|
+
const home = join(root, 'home');
|
|
80
|
+
const repo = join(root, 'repo');
|
|
81
|
+
mkdirSync(home, { recursive: true });
|
|
82
|
+
mkdirSync(repo, { recursive: true });
|
|
83
|
+
writeProviderSettings(home);
|
|
84
|
+
const fixture: IFixture = { root, repo, home, env: gitEnv(home) };
|
|
85
|
+
git(fixture, 'init', '-q', '-b', 'main');
|
|
86
|
+
git(fixture, 'config', 'user.name', 'Robota Scenario');
|
|
87
|
+
git(fixture, 'config', 'user.email', 'scenario@example.com');
|
|
88
|
+
git(fixture, 'config', 'commit.gpgsign', 'false');
|
|
89
|
+
writeFileSync(join(repo, 'greeting.txt'), 'hello\n');
|
|
90
|
+
writeFileSync(join(repo, 'notes.txt'), 'note one\n');
|
|
91
|
+
git(fixture, 'add', '--', 'greeting.txt', 'notes.txt');
|
|
92
|
+
git(fixture, 'commit', '-q', '-m', 'chore: base');
|
|
93
|
+
if (stage) {
|
|
94
|
+
writeFileSync(join(repo, 'greeting.txt'), 'hello\nhello world\n');
|
|
95
|
+
git(fixture, 'add', '--', 'greeting.txt');
|
|
96
|
+
}
|
|
97
|
+
writeFileSync(join(repo, 'notes.txt'), 'note one\nnote two\n');
|
|
98
|
+
writeFileSync(join(repo, 'scratch.log'), 'scratch\n');
|
|
99
|
+
return fixture;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function commitCount(fixture: IFixture): number {
|
|
103
|
+
return git(fixture, 'log', '--oneline').trim().split('\n').length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function porcelain(fixture: IFixture): string[] {
|
|
107
|
+
return git(fixture, 'status', '--porcelain')
|
|
108
|
+
.split('\n')
|
|
109
|
+
.filter((line) => line.length > 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Type a slash command, press Enter, and return the transcript offset taken BEFORE typing. */
|
|
113
|
+
async function run(session: IPtySession, command: string): Promise<number> {
|
|
114
|
+
const since = session.outputOffset();
|
|
115
|
+
await session.sendKeys(command);
|
|
116
|
+
await session.pressEnter();
|
|
117
|
+
return since;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The confirmation's current highlight, from the newest `> Yes` / `> No` in the transcript. */
|
|
121
|
+
function currentHighlight(session: IPtySession): string | undefined {
|
|
122
|
+
const matches = session.snapshot().match(/> (Yes|No)\b/g);
|
|
123
|
+
return matches?.at(-1)?.slice(2);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Move the highlight to `label` by the rendered label, not by an assumed starting index — the
|
|
128
|
+
* options are `Yes` then `No`, so `No` is one Down away and `Yes` one Up.
|
|
129
|
+
*/
|
|
130
|
+
async function highlightOption(session: IPtySession, label: 'Yes' | 'No'): Promise<void> {
|
|
131
|
+
if (currentHighlight(session) === label) return;
|
|
132
|
+
const since = session.outputOffset();
|
|
133
|
+
session.writeRaw(label === 'No' ? DOWN : UP);
|
|
134
|
+
await session.waitForSince(since, new RegExp(`> ${label}\\b`), WAIT_MS);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Scenario 1's read steps: /help lists /git, /status stays unclaimed, /git status reads the fixture. */
|
|
138
|
+
async function expectReadSteps(session: IPtySession): Promise<void> {
|
|
139
|
+
let since = await run(session, '/help');
|
|
140
|
+
await session.waitForSince(since, /\/git/, WAIT_MS);
|
|
141
|
+
await session.waitForSince(since, /Type a message or \/help/, WAIT_MS);
|
|
142
|
+
expect(session.snapshotSince(since)).toMatch(/\/git\b/);
|
|
143
|
+
|
|
144
|
+
// The autocomplete popup runs its highlighted entry on Enter — `/status` alone would execute
|
|
145
|
+
// `/statusline`. A trailing space closes the popup, so the literal `/status` is what submits.
|
|
146
|
+
since = await run(session, '/status ');
|
|
147
|
+
await session.waitForSince(since, /Unknown command "\/status"/, WAIT_MS);
|
|
148
|
+
|
|
149
|
+
since = await run(session, '/git status');
|
|
150
|
+
await session.waitForSince(since, /untracked \(1\): scratch\.log/, WAIT_MS);
|
|
151
|
+
const status = session.snapshotSince(since);
|
|
152
|
+
expect(status).toContain('On branch main');
|
|
153
|
+
expect(status).toMatch(/staged \(1\): greeting\.txt/);
|
|
154
|
+
expect(status).toMatch(/unstaged \(1\): notes\.txt/);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Scenario 1's diff forms: each reads the fixture, none changes it. */
|
|
158
|
+
async function expectDiffForms(session: IPtySession, fixture: IFixture): Promise<void> {
|
|
159
|
+
let since = await run(session, '/git diff');
|
|
160
|
+
await session.waitForSince(since, /\+note two/, WAIT_MS);
|
|
161
|
+
expect(session.snapshotSince(since)).not.toContain('+hello world');
|
|
162
|
+
|
|
163
|
+
since = await run(session, '/git diff --staged');
|
|
164
|
+
await session.waitForSince(since, /\+hello world/, WAIT_MS);
|
|
165
|
+
expect(session.snapshotSince(since)).not.toContain('+note two');
|
|
166
|
+
|
|
167
|
+
since = await run(session, '/git diff -- notes.txt');
|
|
168
|
+
await session.waitForSince(since, /\+note two/, WAIT_MS);
|
|
169
|
+
expect(session.snapshotSince(since)).not.toMatch(/diff --git a\/greeting/);
|
|
170
|
+
|
|
171
|
+
since = await run(session, '/git diff HEAD~1');
|
|
172
|
+
await session.waitForSince(since, /Unknown revision: HEAD~1/, WAIT_MS);
|
|
173
|
+
expect(session.snapshotSince(since)).not.toContain('diff --git');
|
|
174
|
+
|
|
175
|
+
since = await run(session, '/git diff nosuchrev');
|
|
176
|
+
await session.waitForSince(since, /Unknown revision: nosuchrev/, WAIT_MS);
|
|
177
|
+
expect(session.snapshotSince(since)).not.toContain('diff --git');
|
|
178
|
+
|
|
179
|
+
// A short relative path: a long token wraps in the PTY and splits the line under test.
|
|
180
|
+
const outputPath = join(fixture.repo, 'x');
|
|
181
|
+
since = await run(session, '/git diff --output=x');
|
|
182
|
+
await session.waitForSince(since, /is not accepted/, WAIT_MS);
|
|
183
|
+
const usage = session.snapshotSince(since);
|
|
184
|
+
for (const form of ['--staged', '<rev>', '<a>..<b>', '-- <path>']) expect(usage).toContain(form);
|
|
185
|
+
expect(usage).not.toContain('diff --git');
|
|
186
|
+
expect(existsSync(outputPath)).toBe(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Scenario 2, step 1: a MUST-rule violation is refused before any dialog. */
|
|
190
|
+
async function expectSubjectRefusal(session: IPtySession, fixture: IFixture): Promise<void> {
|
|
191
|
+
const since = await run(session, '/git commit add greeting');
|
|
192
|
+
await session.waitForSince(since, /Commit refused/, WAIT_MS);
|
|
193
|
+
const refusal = session.snapshotSince(since);
|
|
194
|
+
expect(refusal).toContain('`: `');
|
|
195
|
+
expect(refusal).not.toContain('staged file(s)?');
|
|
196
|
+
expect(commitCount(fixture)).toBe(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Scenario 2, step 2: the confirmation lists exactly the staged file; No commits nothing. */
|
|
200
|
+
async function expectCancelOnNo(session: IPtySession, fixture: IFixture): Promise<void> {
|
|
201
|
+
const since = await run(session, '/git commit feat: add greeting');
|
|
202
|
+
await session.waitForSince(since, /Commit 1 staged file\(s\)\?/, WAIT_MS);
|
|
203
|
+
const dialog = session.snapshotSince(since);
|
|
204
|
+
expect(dialog).toContain('Message: feat: add greeting');
|
|
205
|
+
expect(dialog).toContain('M greeting.txt');
|
|
206
|
+
expect(dialog).not.toContain('notes.txt');
|
|
207
|
+
expect(dialog).not.toContain('scratch.log');
|
|
208
|
+
await highlightOption(session, 'No');
|
|
209
|
+
await session.pressEnter();
|
|
210
|
+
await session.waitForSince(since, /Commit cancelled\./, WAIT_MS);
|
|
211
|
+
expect(commitCount(fixture)).toBe(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Scenario 2, step 3: outer quotes stripped; Yes commits with the short hash reported. */
|
|
215
|
+
async function expectCommitOnYes(session: IPtySession): Promise<void> {
|
|
216
|
+
const since = await run(session, '/git commit "feat: add greeting"');
|
|
217
|
+
await session.waitForSince(since, /Commit 1 staged file\(s\)\?/, WAIT_MS);
|
|
218
|
+
const quoted = session.snapshotSince(since);
|
|
219
|
+
expect(quoted).toContain('Message: feat: add greeting');
|
|
220
|
+
expect(quoted).not.toContain('Message: "feat: add greeting"');
|
|
221
|
+
expect(quoted).toContain('M greeting.txt');
|
|
222
|
+
await highlightOption(session, 'Yes');
|
|
223
|
+
await session.pressEnter();
|
|
224
|
+
await session.waitForSince(since, /Committed: \[main [0-9a-f]{7}\] feat: add greeting/, WAIT_MS);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function exitTui(session: IPtySession): Promise<number> {
|
|
228
|
+
await run(session, '/exit');
|
|
229
|
+
await session.waitFor(/Exit the session\?/, WAIT_MS);
|
|
230
|
+
await session.pressEnter();
|
|
231
|
+
return session.expectExit(WAIT_MS);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
describe('/git through the real binary (BEHAVIOR-2437 TC-07)', () => {
|
|
235
|
+
let fixture: IFixture | undefined;
|
|
236
|
+
let session: IPtySession | undefined;
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
fixture = undefined;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
afterEach(async () => {
|
|
243
|
+
await session?.disposeAsync();
|
|
244
|
+
session = undefined;
|
|
245
|
+
if (fixture) rmSync(fixture.root, { recursive: true, force: true });
|
|
246
|
+
fixture = undefined;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it(
|
|
250
|
+
'Scenario 1: /help lists /git, /status stays unclaimed, status and every diff form read the fixture',
|
|
251
|
+
async () => {
|
|
252
|
+
fixture = makeFixture(true);
|
|
253
|
+
session = spawnTui({ projectDir: fixture.repo, homeDir: fixture.home });
|
|
254
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
255
|
+
await session.waitFor(/Idle/, WAIT_MS);
|
|
256
|
+
|
|
257
|
+
await expectReadSteps(session);
|
|
258
|
+
|
|
259
|
+
await expectDiffForms(session, fixture);
|
|
260
|
+
|
|
261
|
+
expect(await exitTui(session)).toBe(0);
|
|
262
|
+
expect(commitCount(fixture)).toBe(1);
|
|
263
|
+
expect(porcelain(fixture).sort()).toEqual(
|
|
264
|
+
['M greeting.txt', ' M notes.txt', '?? scratch.log'].sort(),
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
CASE_TIMEOUT_MS,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
it(
|
|
271
|
+
'Scenario 2: /git commit refuses a bad subject, commits nothing on No, and on Yes commits exactly the staged file',
|
|
272
|
+
async () => {
|
|
273
|
+
fixture = makeFixture(true);
|
|
274
|
+
session = spawnTui({ projectDir: fixture.repo, homeDir: fixture.home });
|
|
275
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
276
|
+
await session.waitFor(/Idle/, WAIT_MS);
|
|
277
|
+
|
|
278
|
+
await expectSubjectRefusal(session, fixture);
|
|
279
|
+
await expectCancelOnNo(session, fixture);
|
|
280
|
+
await expectCommitOnYes(session);
|
|
281
|
+
|
|
282
|
+
expect(await exitTui(session)).toBe(0);
|
|
283
|
+
expect(commitCount(fixture)).toBe(2);
|
|
284
|
+
expect(git(fixture, 'log', '-1', '--format=%s').trim()).toBe('feat: add greeting');
|
|
285
|
+
const stat = git(fixture, 'show', '--stat', '--format=%s', 'HEAD');
|
|
286
|
+
expect(stat).toContain('greeting.txt');
|
|
287
|
+
expect(stat).toContain('1 file changed');
|
|
288
|
+
expect(stat).not.toContain('notes.txt');
|
|
289
|
+
expect(porcelain(fixture).sort()).toEqual([' M notes.txt', '?? scratch.log'].sort());
|
|
290
|
+
},
|
|
291
|
+
CASE_TIMEOUT_MS,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
it(
|
|
295
|
+
'Scenario 3: with nothing staged, /git commit prints guidance with the counts and commits nothing',
|
|
296
|
+
async () => {
|
|
297
|
+
fixture = makeFixture(false);
|
|
298
|
+
session = spawnTui({ projectDir: fixture.repo, homeDir: fixture.home });
|
|
299
|
+
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
300
|
+
await session.waitFor(/Idle/, WAIT_MS);
|
|
301
|
+
|
|
302
|
+
const since = await run(session, '/git commit feat: nothing to commit');
|
|
303
|
+
await session.waitForSince(since, /Nothing is staged \(1 unstaged, 1 untracked\)/, WAIT_MS);
|
|
304
|
+
const guidance = session.snapshotSince(since);
|
|
305
|
+
expect(guidance).toContain('git add');
|
|
306
|
+
expect(guidance).toContain('/shell git add');
|
|
307
|
+
expect(guidance).not.toContain('staged file(s)?');
|
|
308
|
+
|
|
309
|
+
expect(await exitTui(session)).toBe(0);
|
|
310
|
+
expect(commitCount(fixture)).toBe(1);
|
|
311
|
+
expect(porcelain(fixture).sort()).toEqual([' M notes.txt', '?? scratch.log'].sort());
|
|
312
|
+
},
|
|
313
|
+
CASE_TIMEOUT_MS,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
it(
|
|
317
|
+
'Scenario 4: headless /git commit cancels (no confirmation can be asked), exits 1, commits nothing',
|
|
318
|
+
() => {
|
|
319
|
+
fixture = makeFixture(true);
|
|
320
|
+
const env: NodeJS.ProcessEnv = { ...fixture.env, TERM: 'dumb' };
|
|
321
|
+
const trust = spawnSync(process.execPath, [ROBOTA_BIN, 'trust', '--yes'], {
|
|
322
|
+
cwd: fixture.repo,
|
|
323
|
+
env,
|
|
324
|
+
encoding: 'utf8',
|
|
325
|
+
});
|
|
326
|
+
expect(trust.status).toBe(0);
|
|
327
|
+
const result = spawnSync(
|
|
328
|
+
process.execPath,
|
|
329
|
+
[ROBOTA_BIN, '-p', '/git commit feat: add greeting', '--bare', '--no-session-persistence'],
|
|
330
|
+
{ cwd: fixture.repo, env, encoding: 'utf8', timeout: 60_000 },
|
|
331
|
+
);
|
|
332
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
333
|
+
expect(result.status).toBe(1);
|
|
334
|
+
expect(output).toContain('cancelled');
|
|
335
|
+
expect(output).toContain('confirmation');
|
|
336
|
+
expect(output).not.toMatch(/\bYes\b/);
|
|
337
|
+
expect(commitCount(fixture)).toBe(1);
|
|
338
|
+
expect(porcelain(fixture)).toContain('M greeting.txt');
|
|
339
|
+
},
|
|
340
|
+
CASE_TIMEOUT_MS,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"timestamp":"2026-07-02T00:00:00.000Z","sessionId":"ask-fixture","event":"provider_request","executionId":"e1","round":0,"schemaVersion":1,"provider":"replay","model":"fixture-model","messages":[]}
|
|
2
|
+
{"timestamp":"2026-07-02T00:00:01.000Z","sessionId":"ask-fixture","event":"provider_response_normalized","executionId":"e1","round":0,"response":{"role":"assistant","content":null,"id":"a1","state":"complete","timestamp":"2026-07-02T00:00:01.000Z","toolCalls":[{"id":"call_ask_1","type":"function","function":{"name":"AskUserQuestion","arguments":"{\"questions\":[{\"question\":\"PICK_A_COLOR\",\"options\":[{\"label\":\"Red\"},{\"label\":\"Blue\"}]}]}"}}]},"schemaVersion":1}
|
|
3
|
+
{"timestamp":"2026-07-02T00:00:02.000Z","sessionId":"ask-fixture","event":"provider_response_normalized","executionId":"e2","round":0,"response":{"role":"assistant","content":"FINAL_AFTER_ANSWER","id":"a2","state":"complete","timestamp":"2026-07-02T00:00:02.000Z"},"schemaVersion":1}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"timestamp":"2026-06-28T00:00:00.000Z","sessionId":"replay-fixture","event":"provider_request","executionId":"e1","round":0,"schemaVersion":1,"provider":"replay","model":"fixture-model","messages":[]}
|
|
2
|
+
{"timestamp":"2026-06-28T00:00:01.000Z","sessionId":"replay-fixture","event":"provider_response_normalized","executionId":"e1","round":0,"response":{"role":"assistant","content":"REPLAYED_ANSWER_42","id":"a1","timestamp":"2026-06-28T00:00:01.000Z","state":"complete"},"schemaVersion":1}
|
|
3
|
+
{"timestamp":"2026-06-28T00:00:02.000Z","sessionId":"replay-fixture","event":"provider_response_normalized","executionId":"e2","round":0,"response":{"role":"assistant","content":"REPLAYED_ANSWER_42","id":"a2","timestamp":"2026-06-28T00:00:02.000Z","state":"complete"},"schemaVersion":1}
|
|
4
|
+
{"timestamp":"2026-06-28T00:00:03.000Z","sessionId":"replay-fixture","event":"provider_response_normalized","executionId":"e3","round":0,"response":{"role":"assistant","content":"REPLAYED_ANSWER_42","id":"a3","timestamp":"2026-06-28T00:00:03.000Z","state":"complete"},"schemaVersion":1}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"timestamp":"2026-07-24T00:00:00.000Z","sessionId":"shapes-fixture","event":"provider_request","executionId":"e1","round":0,"schemaVersion":1,"provider":"replay","model":"fixture-model","messages":[]}
|
|
2
|
+
{"timestamp":"2026-07-24T00:00:01.000Z","sessionId":"shapes-fixture","event":"provider_response_normalized","executionId":"e1","round":0,"response":{"role":"assistant","content":null,"id":"a1","state":"complete","timestamp":"2026-07-24T00:00:01.000Z","toolCalls":[{"id":"call_ask_single","type":"function","function":{"name":"AskUserQuestion","arguments":"{\"questions\":[{\"question\":\"PICK_SINGLE\",\"options\":[{\"label\":\"Red\"},{\"label\":\"Blue\"}]}]}"}}]},"schemaVersion":1}
|
|
3
|
+
{"timestamp":"2026-07-24T00:00:02.000Z","sessionId":"shapes-fixture","event":"provider_response_normalized","executionId":"e2","round":0,"response":{"role":"assistant","content":null,"id":"a2","state":"complete","timestamp":"2026-07-24T00:00:02.000Z","toolCalls":[{"id":"call_ask_multi","type":"function","function":{"name":"AskUserQuestion","arguments":"{\"questions\":[{\"question\":\"PICK_MULTI\",\"options\":[{\"label\":\"Alpha\"},{\"label\":\"Beta\"}],\"multiSelect\":true}]}"}}]},"schemaVersion":1}
|
|
4
|
+
{"timestamp":"2026-07-24T00:00:03.000Z","sessionId":"shapes-fixture","event":"provider_response_normalized","executionId":"e3","round":0,"response":{"role":"assistant","content":null,"id":"a3","state":"complete","timestamp":"2026-07-24T00:00:03.000Z","toolCalls":[{"id":"call_ask_text","type":"function","function":{"name":"AskUserQuestion","arguments":"{\"questions\":[{\"question\":\"TYPE_TEXT\"}]}"}}]},"schemaVersion":1}
|
|
5
|
+
{"timestamp":"2026-07-24T00:00:04.000Z","sessionId":"shapes-fixture","event":"provider_response_normalized","executionId":"e4","round":0,"response":{"role":"assistant","content":"ALL_SHAPES_DONE","id":"a4","state":"complete","timestamp":"2026-07-24T00:00:04.000Z"},"schemaVersion":1}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"timestamp":"2026-07-24T00:00:00.000Z","sessionId":"perm-fixture","event":"provider_request","executionId":"e1","round":0,"schemaVersion":1,"provider":"replay","model":"fixture-model","messages":[]}
|
|
2
|
+
{"timestamp":"2026-07-24T00:00:01.000Z","sessionId":"perm-fixture","event":"provider_response_normalized","executionId":"e1","round":0,"response":{"role":"assistant","content":null,"id":"a1","state":"complete","timestamp":"2026-07-24T00:00:01.000Z","toolCalls":[{"id":"call_shell_1","type":"function","function":{"name":"Shell","arguments":"{\"command\":\"printf PERM_TEST\\\\n\"}"}}]},"schemaVersion":1}
|
|
3
|
+
{"timestamp":"2026-07-24T00:00:02.000Z","sessionId":"perm-fixture","event":"provider_response_normalized","executionId":"e2","round":0,"response":{"role":"assistant","content":"AFTER_PERMISSION_FLOW","id":"a2","state":"complete","timestamp":"2026-07-24T00:00:02.000Z"},"schemaVersion":1}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"timestamp":"2026-07-25T00:00:00.000Z","sessionId":"screen-006-fixture","event":"provider_request","executionId":"e1","round":0,"schemaVersion":1,"provider":"replay","model":"fixture-model","messages":[]}
|
|
2
|
+
{"timestamp":"2026-07-25T00:00:01.000Z","sessionId":"screen-006-fixture","event":"provider_response_normalized","executionId":"e1","round":0,"response":{"role":"assistant","content":null,"id":"a1","state":"complete","timestamp":"2026-07-25T00:00:01.000Z","toolCalls":[{"id":"call_shell_1","type":"function","function":{"name":"Shell","arguments":"{\"command\":\"printf SCREEN006_TOOL\\\\n\"}"}}]},"schemaVersion":1}
|
|
3
|
+
{"timestamp":"2026-07-25T00:00:02.000Z","sessionId":"screen-006-fixture","event":"provider_response_normalized","executionId":"e1","round":1,"response":{"role":"assistant","content":"Here is the change:\n\n```diff\n+ added line\n- removed line\n```\n\nSCREEN006_DIFF_DONE","id":"a2","state":"complete","timestamp":"2026-07-25T00:00:02.000Z"},"schemaVersion":1}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"timestamp":"2026-09-20T00:00:00.000Z","sessionId":"screen-2002-fixture","event":"provider_request","executionId":"e1","round":0,"schemaVersion":1,"provider":"replay","model":"fixture-model","messages":[]}
|
|
2
|
+
{"timestamp":"2026-09-20T00:00:01.000Z","sessionId":"screen-2002-fixture","event":"provider_response_normalized","executionId":"e1","round":0,"response":{"role":"assistant","content":"Here is a snippet:\n\n```ts\nconst answer = 42;\n```\n\nSCREEN2002_SNIPPET_DONE","id":"a1","timestamp":"2026-09-20T00:00:01.000Z","state":"complete"},"schemaVersion":1}
|
|
3
|
+
{"timestamp":"2026-09-20T00:00:02.000Z","sessionId":"screen-2002-fixture","event":"provider_response_normalized","executionId":"e2","round":0,"response":{"role":"assistant","content":"Here is a snippet:\n\n```ts\nconst answer = 42;\n```\n\nSCREEN2002_SNIPPET_DONE","id":"a2","timestamp":"2026-09-20T00:00:02.000Z","state":"complete"},"schemaVersion":1}
|
|
4
|
+
{"timestamp":"2026-09-20T00:00:03.000Z","sessionId":"screen-2002-fixture","event":"provider_response_normalized","executionId":"e3","round":0,"response":{"role":"assistant","content":"Here is a snippet:\n\n```ts\nconst answer = 42;\n```\n\nSCREEN2002_SNIPPET_DONE","id":"a3","timestamp":"2026-09-20T00:00:03.000Z","state":"complete"},"schemaVersion":1}
|
|
5
|
+
{"timestamp":"2026-09-20T00:00:04.000Z","sessionId":"screen-2002-fixture","event":"provider_response_normalized","executionId":"e4","round":0,"response":{"role":"assistant","content":"Here is a snippet:\n\n```ts\nconst answer = 42;\n```\n\nSCREEN2002_SNIPPET_DONE","id":"a4","timestamp":"2026-09-20T00:00:04.000Z","state":"complete"},"schemaVersion":1}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST-009 Phase 3: a CLI flag's interactive-TUI effect, observed through a real PTY.
|
|
3
|
+
*
|
|
4
|
+
* The headless flag matrix (Phase 1) asserts request/output effects; this asserts the *rendered*
|
|
5
|
+
* effect a flag has on the live TUI. `--permission-mode <mode>` is shown in the status bar whenever it
|
|
6
|
+
* is not the default — so booting with `plan` / `acceptEdits` must render that mode. Runs in the
|
|
7
|
+
* dedicated PTY project against the BUILT robota binary (`pnpm --filter @robota-sdk/agent-cli build`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
15
|
+
|
|
16
|
+
import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
|
|
17
|
+
|
|
18
|
+
import type { IPtySession } from './pty-driver.js';
|
|
19
|
+
|
|
20
|
+
describe('CLI flag → TUI rendering through a real PTY (TEST-009 Phase 3)', () => {
|
|
21
|
+
let projectDir: string;
|
|
22
|
+
let session: IPtySession | undefined;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-flagtui-')));
|
|
26
|
+
writeTuiProviderSettings(projectDir);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await session?.disposeAsync();
|
|
31
|
+
session = undefined;
|
|
32
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('--permission-mode plan renders the plan mode in the status bar', async () => {
|
|
36
|
+
session = spawnTui({
|
|
37
|
+
projectDir,
|
|
38
|
+
homeDir: join(projectDir, 'home'),
|
|
39
|
+
args: ['--permission-mode', 'plan'],
|
|
40
|
+
});
|
|
41
|
+
await session.waitFor(/Type a message or \/help/);
|
|
42
|
+
await session.waitFor(/plan/, 15_000);
|
|
43
|
+
expect(session.snapshot()).toContain('plan');
|
|
44
|
+
}, 60_000);
|
|
45
|
+
|
|
46
|
+
it('--permission-mode acceptEdits renders that mode in the status bar', async () => {
|
|
47
|
+
session = spawnTui({
|
|
48
|
+
projectDir,
|
|
49
|
+
homeDir: join(projectDir, 'home'),
|
|
50
|
+
args: ['--permission-mode', 'acceptEdits'],
|
|
51
|
+
});
|
|
52
|
+
await session.waitFor(/Type a message or \/help/);
|
|
53
|
+
await session.waitFor(/acceptEdits/, 15_000);
|
|
54
|
+
expect(session.snapshot()).toContain('acceptEdits');
|
|
55
|
+
}, 60_000);
|
|
56
|
+
});
|