@robota-sdk/agent-ui-terminal 3.0.0-beta.85 → 3.0.0-beta.87
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 +68 -14
- package/README.md +2 -2
- package/dist/node/index.d.ts +8 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +14 -14
- package/dist/node/index.js.map +1 -1
- package/package.json +19 -20
- package/src/SupervisedSessionView.tsx +12 -2
- package/src/__tests__/SupervisedSessionView.test.tsx +9 -5
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +1 -1
- package/src/__tests__/UpdateNotice.test.tsx +2 -2
- package/src/__tests__/attached-app-session-switch.test.tsx +1 -1
- package/src/__tests__/attention-bell-and-marks.test.ts +5 -5
- package/src/__tests__/channel-factory-integration.test.ts +3 -3
- package/src/__tests__/checkpoint-store-per-channel.test.ts +1 -1
- package/src/__tests__/cjk-fallback-render.test.tsx +2 -2
- package/src/__tests__/command-handoff-pty-e2e.test.ts +2 -2
- package/src/__tests__/command-result-handler.test.ts +3 -3
- package/src/__tests__/fork-attach.test.tsx +3 -3
- package/src/__tests__/helpers/terminal-profiles.ts +4 -4
- package/src/__tests__/humanize-tool-name.test.ts +1 -1
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +5 -5
- package/src/__tests__/product-display-name.test.tsx +5 -5
- package/src/__tests__/pty/ask-user-question.ptytest.ts +2 -2
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +3 -3
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +9 -9
- package/src/__tests__/pty/flag-tui.ptytest.ts +2 -2
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +53 -24
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +59 -14
- package/src/__tests__/pty/ime-cursor.ptytest.ts +4 -4
- package/src/__tests__/pty/isolated-home.ts +37 -4
- package/src/__tests__/pty/org-policy.ptytest.ts +3 -3
- package/src/__tests__/pty/provider-setup.ptytest.ts +2 -2
- package/src/__tests__/pty/pty-driver.ts +8 -6
- package/src/__tests__/pty/replay-conversation.ptytest.ts +2 -2
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +2 -2
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +4 -4
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +3 -3
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +2 -2
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +3 -3
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +3 -3
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +16 -16
- package/src/__tests__/pty/session-attach.ptytest.ts +66 -28
- package/src/__tests__/pty/settings-screen.ptytest.ts +2 -2
- package/src/__tests__/pty/spawn-pty.ts +3 -3
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +2 -2
- package/src/__tests__/pty/trust-prompt.ptytest.ts +5 -5
- package/src/__tests__/pty/tui-pty.ptytest.ts +2 -2
- package/src/__tests__/screen-reader-labels.test.ts +1 -1
- package/src/__tests__/screen-reader-pacing.test.ts +3 -3
- package/src/__tests__/screen-reader-render-options.test.ts +3 -3
- package/src/__tests__/session-switch-channel.test.tsx +1 -1
- package/src/__tests__/terminal-capabilities.test.ts +9 -9
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +1 -1
- package/src/__tests__/tui-channel-init-failure.test.ts +1 -1
- package/src/__tests__/tui-state-manager.test.ts +2 -2
- package/src/__tests__/wire-tui-channel.test.ts +2 -2
- package/src/keybindings/__tests__/keybinding-registry.test.ts +2 -2
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +8 -1
- package/src/keybindings/node-keybindings-source.ts +6 -6
- package/src/render.tsx +7 -1
- package/src/terminal-capabilities.ts +1 -1
- package/src/tui-channel-options.ts +2 -0
- package/src/tui-session-options.ts +4 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SCREEN-010 TC-05: chat-window scrollback layout, on the real binary.
|
|
3
3
|
*
|
|
4
|
-
* Runs in the PTY vitest project against the BUILT
|
|
4
|
+
* Runs in the PTY vitest project against the BUILT CLI. Automates the former "manual" smoke
|
|
5
5
|
* (per the never-ask-the-user-to-test rule): generates committed output larger than a small viewport
|
|
6
6
|
* and asserts the committed history is emitted to the terminal's native scrollback while the input
|
|
7
7
|
* prompt + status bar stay pinned at the bottom. Once committed content is in scrollback and the input
|
|
@@ -47,7 +47,7 @@ describe('SCREEN-010 chat-window scrollback layout (real binary)', () => {
|
|
|
47
47
|
let session: IPtySession | undefined;
|
|
48
48
|
|
|
49
49
|
beforeEach(() => {
|
|
50
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
50
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-scrollback-pty-')));
|
|
51
51
|
writeTuiProviderSettings(projectDir);
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -97,7 +97,7 @@ describe('SCREEN-010 chat-window scrollback layout (real binary)', () => {
|
|
|
97
97
|
homeDir: join(projectDir, 'home'),
|
|
98
98
|
rows: 16,
|
|
99
99
|
args: ['--screen-reader'],
|
|
100
|
-
env: {
|
|
100
|
+
env: { PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
await session.waitFor(/Type a message or \/help/, PROMPT_WAIT_MS);
|
|
@@ -56,7 +56,7 @@ function seedSession(homeDir: string, cwd: string): void {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
const sessionsDir = join(homeDir, '
|
|
59
|
+
const sessionsDir = join(homeDir, 'state', 'sessions');
|
|
60
60
|
mkdirSync(sessionsDir, { recursive: true });
|
|
61
61
|
writeFileSync(
|
|
62
62
|
join(sessionsDir, `${SESSION_ID}.json`),
|
|
@@ -81,7 +81,7 @@ describe('SCREEN-1993 TC-10: the resumed transcript lives in native scrollback',
|
|
|
81
81
|
let session: IPtySession | undefined;
|
|
82
82
|
|
|
83
83
|
beforeEach(() => {
|
|
84
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
84
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-1993-scrollback-')));
|
|
85
85
|
writeTuiProviderSettings(projectDir);
|
|
86
86
|
seedSession(join(projectDir, 'home'), projectDir);
|
|
87
87
|
});
|
|
@@ -50,7 +50,7 @@ function snippetLine(raw: string): string {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function seedThemeFiles(homeDir: string): void {
|
|
53
|
-
const themes = join(homeDir, '
|
|
53
|
+
const themes = join(homeDir, 'state', 'themes');
|
|
54
54
|
mkdirSync(themes, { recursive: true });
|
|
55
55
|
writeFileSync(
|
|
56
56
|
join(themes, 'mine.json'),
|
|
@@ -76,7 +76,7 @@ function seedThemeFiles(homeDir: string): void {
|
|
|
76
76
|
|
|
77
77
|
const pluginVersionDir = join(
|
|
78
78
|
homeDir,
|
|
79
|
-
'
|
|
79
|
+
'state',
|
|
80
80
|
'plugins',
|
|
81
81
|
'cache',
|
|
82
82
|
'fixtures',
|
|
@@ -106,7 +106,7 @@ describe('SCREEN-2002 themes through the real binary', () => {
|
|
|
106
106
|
let session: IPtySession | undefined;
|
|
107
107
|
|
|
108
108
|
beforeEach(() => {
|
|
109
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
109
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen2002-pty-')));
|
|
110
110
|
writeTuiProviderSettings(projectDir);
|
|
111
111
|
homeDir = join(projectDir, 'home');
|
|
112
112
|
seedThemeFiles(homeDir);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SCREEN-2670 TC-09 — the pre-write park on the BUILT
|
|
2
|
+
* SCREEN-2670 TC-09 — the pre-write park on the BUILT CLI binary, in a real PTY.
|
|
3
3
|
*
|
|
4
4
|
* A screen reader sees bytes and time, nothing else, so the claim is checked where both exist: the
|
|
5
5
|
* raw PTY stream is sampled every few milliseconds and each growth is stamped. A COMMIT the park
|
|
@@ -157,7 +157,7 @@ function spawnScenario(projectDir: string, mode: 'park' | 'zero' | 'off'): IPtyS
|
|
|
157
157
|
projectDir,
|
|
158
158
|
homeDir: join(projectDir, 'home'),
|
|
159
159
|
args: ['--session-log', REPLAY_FIXTURE, ...screenReader, '--name', 'prepark-scenario'],
|
|
160
|
-
env: {
|
|
160
|
+
env: { PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0', PRODUCT_SCREEN_READER_PREPARK_MS: prepark },
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -171,7 +171,7 @@ describe('SCREEN-2670 the pre-write park through the real binary', () => {
|
|
|
171
171
|
let session: IPtySession | undefined;
|
|
172
172
|
|
|
173
173
|
beforeEach(() => {
|
|
174
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
174
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-prepark-pty-')));
|
|
175
175
|
writeTuiProviderSettings(projectDir);
|
|
176
176
|
});
|
|
177
177
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-2004 TC-11 / TC-12 — screen-reader mode on the BUILT
|
|
2
|
+
* CLI-2004 TC-11 / TC-12 — screen-reader mode on the BUILT CLI binary, in a real PTY.
|
|
3
3
|
*
|
|
4
4
|
* This is the only level at which the claim can be checked. A screen reader has no `aria-live`: the
|
|
5
5
|
* ONLY channel to it is which bytes land in the terminal buffer, so a component test asserting on a
|
|
@@ -42,7 +42,7 @@ const CASE_TIMEOUT_MS = 90_000;
|
|
|
42
42
|
* is the case where "the first line confirms the mode" is the claim being made.
|
|
43
43
|
*/
|
|
44
44
|
function markOnboarded(homeDir: string): void {
|
|
45
|
-
writeFileSync(join(homeDir, '
|
|
45
|
+
writeFileSync(join(homeDir, 'state', 'onboarded'), new Date().toISOString());
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe('CLI-2004 screen-reader mode through the real binary', () => {
|
|
@@ -50,7 +50,7 @@ describe('CLI-2004 screen-reader mode through the real binary', () => {
|
|
|
50
50
|
let session: IPtySession | undefined;
|
|
51
51
|
|
|
52
52
|
beforeEach(() => {
|
|
53
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
53
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screenreader-pty-')));
|
|
54
54
|
writeTuiProviderSettings(projectDir);
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -68,7 +68,7 @@ describe('CLI-2004 screen-reader mode through the real binary', () => {
|
|
|
68
68
|
homeDir: join(projectDir, 'home'),
|
|
69
69
|
args: ['--session-log', REPLAY_FIXTURE, '--screen-reader'],
|
|
70
70
|
// The waits are exercised by their own unit test; a real one here only slows the fixture.
|
|
71
|
-
env: {
|
|
71
|
+
env: { PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
@@ -100,7 +100,7 @@ describe('CLI-2004 the same fixture with the mode off', () => {
|
|
|
100
100
|
let session: IPtySession | undefined;
|
|
101
101
|
|
|
102
102
|
beforeEach(() => {
|
|
103
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
103
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screenreader-pty-')));
|
|
104
104
|
writeTuiProviderSettings(projectDir);
|
|
105
105
|
});
|
|
106
106
|
|
|
@@ -117,7 +117,7 @@ describe('CLI-2004 the same fixture with the mode off', () => {
|
|
|
117
117
|
projectDir,
|
|
118
118
|
homeDir: join(projectDir, 'home'),
|
|
119
119
|
args: ['--session-log', REPLAY_FIXTURE],
|
|
120
|
-
env: {
|
|
120
|
+
env: { PRODUCT_TURN_MARKS: '1' },
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
@@ -136,12 +136,12 @@ describe('CLI-2004 the same fixture with the mode off', () => {
|
|
|
136
136
|
);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
describe('
|
|
139
|
+
describe('configured turn-mark override through the real binary', () => {
|
|
140
140
|
let projectDir: string;
|
|
141
141
|
let session: IPtySession | undefined;
|
|
142
142
|
|
|
143
143
|
beforeEach(() => {
|
|
144
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
144
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-turn-marks-pty-')));
|
|
145
145
|
writeTuiProviderSettings(projectDir);
|
|
146
146
|
});
|
|
147
147
|
|
|
@@ -152,8 +152,8 @@ describe('Robota turn-mark override through the real binary', () => {
|
|
|
152
152
|
|
|
153
153
|
for (const [label, env, expected] of [
|
|
154
154
|
['WezTerm default', { TERM_PROGRAM: 'WezTerm' }, false],
|
|
155
|
-
['WezTerm opt-in', { TERM_PROGRAM: 'WezTerm',
|
|
156
|
-
['TTY kill switch', {
|
|
155
|
+
['WezTerm opt-in', { TERM_PROGRAM: 'WezTerm', PRODUCT_TURN_MARKS: '1' }, true],
|
|
156
|
+
['TTY kill switch', { PRODUCT_TURN_MARKS: '0' }, false],
|
|
157
157
|
] as const) {
|
|
158
158
|
it(
|
|
159
159
|
`${label} ${expected ? 'emits' : 'withholds'} OSC 133`,
|
|
@@ -162,7 +162,7 @@ describe('Robota turn-mark override through the real binary', () => {
|
|
|
162
162
|
projectDir,
|
|
163
163
|
homeDir: join(projectDir, 'home'),
|
|
164
164
|
args: ['--screen-reader'],
|
|
165
|
-
env: { ...env,
|
|
165
|
+
env: { ...env, PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
166
166
|
});
|
|
167
167
|
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
168
168
|
expect(session.raw().includes(OSC_133_PROMPT_START)).toBe(expected);
|
|
@@ -177,7 +177,7 @@ describe('CLI-2004 the channel the mode was enabled through', () => {
|
|
|
177
177
|
let session: IPtySession | undefined;
|
|
178
178
|
|
|
179
179
|
beforeEach(() => {
|
|
180
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
180
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screenreader-pty-')));
|
|
181
181
|
writeTuiProviderSettings(projectDir);
|
|
182
182
|
});
|
|
183
183
|
|
|
@@ -188,13 +188,13 @@ describe('CLI-2004 the channel the mode was enabled through', () => {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
it(
|
|
191
|
-
'TC-12:
|
|
191
|
+
'TC-12: PRODUCT_SCREEN_READER=1 with no flag announces the env channel first',
|
|
192
192
|
async () => {
|
|
193
193
|
markOnboarded(join(projectDir, 'home'));
|
|
194
194
|
session = spawnTui({
|
|
195
195
|
projectDir,
|
|
196
196
|
homeDir: join(projectDir, 'home'),
|
|
197
|
-
env: {
|
|
197
|
+
env: { PRODUCT_SCREEN_READER: '1', PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
@@ -207,14 +207,14 @@ describe('CLI-2004 the channel the mode was enabled through', () => {
|
|
|
207
207
|
);
|
|
208
208
|
|
|
209
209
|
it(
|
|
210
|
-
'TC-12: --screen-reader beats
|
|
210
|
+
'TC-12: --screen-reader beats PRODUCT_SCREEN_READER=0 and says so',
|
|
211
211
|
async () => {
|
|
212
212
|
markOnboarded(join(projectDir, 'home'));
|
|
213
213
|
session = spawnTui({
|
|
214
214
|
projectDir,
|
|
215
215
|
homeDir: join(projectDir, 'home'),
|
|
216
216
|
args: ['--screen-reader'],
|
|
217
|
-
env: {
|
|
217
|
+
env: { PRODUCT_SCREEN_READER: '0', PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
|
|
218
218
|
});
|
|
219
219
|
|
|
220
220
|
await session.waitFor(/Type a message or \/help/, WAIT_MS);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `the CLI session attach command` end to end on the BUILT binary, in a real terminal: start a supervised
|
|
3
3
|
* session, attach and confirm on the controlling terminal, submit, detach with `/exit`, see it still
|
|
4
4
|
* running, attach again read-only and find the earlier prompt, detach with Ctrl-C, peek from
|
|
5
5
|
* `session view` and land back in the view, then stop it.
|
|
@@ -24,7 +24,7 @@ import { spawnPty } from './spawn-pty.js';
|
|
|
24
24
|
import type { IPtyRunSession } from './spawn-pty.js';
|
|
25
25
|
|
|
26
26
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
27
|
-
const
|
|
27
|
+
const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
|
|
28
28
|
const WAIT_MS = 20_000;
|
|
29
29
|
|
|
30
30
|
const cleanups: (() => void)[] = [];
|
|
@@ -32,15 +32,24 @@ afterEach(() => {
|
|
|
32
32
|
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
function
|
|
36
|
-
|
|
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
|
+
});
|
|
37
46
|
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
function attach(id: string, cwd: string, env: NodeJS.ProcessEnv, observe = false): IPtyRunSession {
|
|
41
50
|
const session = spawnPty({
|
|
42
51
|
command: process.execPath,
|
|
43
|
-
args: [
|
|
52
|
+
args: [AGENT_BIN, 'session', 'attach', id, ...(observe ? ['--observe'] : [])],
|
|
44
53
|
cwd,
|
|
45
54
|
env,
|
|
46
55
|
});
|
|
@@ -48,18 +57,19 @@ function attach(id: string, cwd: string, env: NodeJS.ProcessEnv, observe = false
|
|
|
48
57
|
return session;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
describe('
|
|
60
|
+
describe('the CLI session attach command on the built binary', () => {
|
|
52
61
|
it('attaches, submits, detaches without stopping the session, and re-attaches', async () => {
|
|
53
|
-
const project = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
62
|
+
const project = realpathSync(mkdtempSync(join(tmpdir(), 'agent-attach-pty-')));
|
|
54
63
|
// `/tmp` rather than the platform temp directory: the control socket path must stay short.
|
|
55
64
|
const runtime = mkdtempSync('/tmp/rsa-');
|
|
56
65
|
cleanups.push(() => rmSync(project, { recursive: true, force: true }));
|
|
57
66
|
cleanups.push(() => rmSync(runtime, { recursive: true, force: true }));
|
|
58
67
|
const home = createIsolatedHome();
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
const stateRoot = join(runtime, 's');
|
|
69
|
+
mkdirSync(stateRoot, { recursive: true });
|
|
70
|
+
writeFileSync(join(stateRoot, 'onboarded'), new Date().toISOString(), 'utf8');
|
|
61
71
|
writeFileSync(
|
|
62
|
-
join(
|
|
72
|
+
join(stateRoot, 'settings.json'),
|
|
63
73
|
JSON.stringify({
|
|
64
74
|
currentProvider: 'anthropic',
|
|
65
75
|
providers: {
|
|
@@ -73,65 +83,93 @@ describe('robota session attach on the built binary', () => {
|
|
|
73
83
|
}),
|
|
74
84
|
'utf8',
|
|
75
85
|
);
|
|
76
|
-
const env = createPtyEnv({
|
|
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
|
+
});
|
|
77
93
|
|
|
78
94
|
// Workspace trust is recorded for a repository, as a user would grant it before a background start.
|
|
79
95
|
const git = (...args: string[]): void => {
|
|
80
96
|
execFileSync('git', args, { cwd: project, env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
81
97
|
};
|
|
82
98
|
git('init', '-q', '-b', 'main');
|
|
83
|
-
git('config', 'user.name', '
|
|
99
|
+
git('config', 'user.name', 'Attach Fixture');
|
|
84
100
|
git('config', 'user.email', 'attach@example.invalid');
|
|
85
101
|
writeFileSync(join(project, 'README.md'), 'fixture\n');
|
|
86
102
|
git('add', '--', 'README.md');
|
|
87
103
|
git('commit', '-q', '-m', 'chore: fixture');
|
|
88
|
-
const trusted =
|
|
104
|
+
const trusted = agent(['trust', '--yes'], project, env);
|
|
89
105
|
expect(trusted.status, trusted.stderr).toBe(0);
|
|
90
|
-
const started =
|
|
106
|
+
const started = agent(
|
|
107
|
+
['session', 'start', '--background', '--name', 'pty-attach'],
|
|
108
|
+
project,
|
|
109
|
+
env,
|
|
110
|
+
);
|
|
91
111
|
expect(started.status, started.stderr).toBe(0);
|
|
92
112
|
const id = /Supervised session: ([0-9a-f-]{36})/u.exec(started.stdout)?.[1];
|
|
93
113
|
expect(id).toBeDefined();
|
|
94
|
-
cleanups.push(() => {
|
|
114
|
+
cleanups.push(() => {
|
|
115
|
+
agent(['session', 'stop', id!], project, env);
|
|
116
|
+
});
|
|
95
117
|
|
|
96
118
|
const drive = attach(id!, project, env);
|
|
97
119
|
await drive.waitFor('Attach? Type yes', WAIT_MS);
|
|
98
120
|
expect(drive.snapshot()).toContain('drive (send prompts, answer its questions)');
|
|
99
121
|
await drive.sendKeys('yes');
|
|
100
122
|
await drive.pressEnter();
|
|
101
|
-
await drive.waitFor('
|
|
123
|
+
await drive.waitFor('Type a message or /help', WAIT_MS);
|
|
102
124
|
await drive.sendKeys('hello from the pty');
|
|
103
125
|
await drive.pressEnter();
|
|
104
|
-
await drive.waitFor('
|
|
126
|
+
await drive.waitFor('Last turn failed', WAIT_MS);
|
|
105
127
|
await drive.sendKeys('/exit');
|
|
106
128
|
await drive.pressEnter();
|
|
107
|
-
await drive.waitFor('keeps running', WAIT_MS);
|
|
108
129
|
expect(await drive.expectExit(WAIT_MS)).toBe(0);
|
|
109
130
|
|
|
110
|
-
const listed =
|
|
111
|
-
const supervised = (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
);
|
|
115
140
|
|
|
116
141
|
const observe = attach(id!, project, env, true);
|
|
117
142
|
await observe.waitFor('Attach? Type yes', WAIT_MS);
|
|
118
143
|
expect(observe.snapshot()).toContain('observe (read only)');
|
|
119
144
|
await observe.sendKeys('yes');
|
|
120
145
|
await observe.pressEnter();
|
|
121
|
-
await observe.waitFor('Observing
|
|
146
|
+
await observe.waitFor('Observing — read only', WAIT_MS);
|
|
122
147
|
await observe.waitFor('hello from the pty', WAIT_MS);
|
|
123
148
|
observe.write('\x03');
|
|
124
|
-
await observe.waitFor('keeps running', WAIT_MS);
|
|
125
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
|
+
);
|
|
126
159
|
|
|
127
160
|
// From the session view: peek at the row, confirm there, detach, and land back in the view.
|
|
128
|
-
const view = spawnPty({
|
|
161
|
+
const view = spawnPty({
|
|
162
|
+
command: process.execPath,
|
|
163
|
+
args: [AGENT_BIN, 'session', 'view'],
|
|
164
|
+
cwd: project,
|
|
165
|
+
env,
|
|
166
|
+
});
|
|
129
167
|
cleanups.push(() => view.dispose());
|
|
130
168
|
await view.waitFor('a Attach p Peek', WAIT_MS);
|
|
131
169
|
view.write('p');
|
|
132
170
|
await view.waitFor('to observe (read only)?', WAIT_MS);
|
|
133
171
|
view.write('y');
|
|
134
|
-
await view.waitFor('Observing
|
|
172
|
+
await view.waitFor('Observing — read only', WAIT_MS);
|
|
135
173
|
await view.waitFor('hello from the pty', WAIT_MS);
|
|
136
174
|
const beforeDetach = view.outputOffset();
|
|
137
175
|
view.write('\x1d');
|
|
@@ -139,6 +177,6 @@ describe('robota session attach on the built binary', () => {
|
|
|
139
177
|
view.write('q');
|
|
140
178
|
expect(await view.expectExit(WAIT_MS)).toBe(0);
|
|
141
179
|
|
|
142
|
-
expect(
|
|
180
|
+
expect(agent(['session', 'stop', id!], project, env).status).toBe(0);
|
|
143
181
|
}, 120_000);
|
|
144
182
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CMD-004 Phase 2 Stage C (TC-04): `/settings` still opens the settings screen — now rendered from
|
|
3
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
|
|
4
|
+
* branch is deleted from the TUI). Observed through a real PTY against the BUILT CLI binary
|
|
5
5
|
* (`pnpm --filter @robota-sdk/agent-cli build`).
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -20,7 +20,7 @@ describe('/settings opens the settings screen via ui_intent (CMD-004 Stage C)',
|
|
|
20
20
|
let session: IPtySession | undefined;
|
|
21
21
|
|
|
22
22
|
beforeEach(() => {
|
|
23
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
23
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-settings-pty-')));
|
|
24
24
|
writeTuiProviderSettings(projectDir);
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* in a user terminal) and exposes per-key paced input plus marker/exit waiting. Two convenience
|
|
7
7
|
* spawners build on the core:
|
|
8
8
|
*
|
|
9
|
-
* - `spawnPty` — drive any command (e.g. the built
|
|
9
|
+
* - `spawnPty` — drive any command (e.g. the built CLI binary).
|
|
10
10
|
* - `spawnPtyFixture` — drive a TSX fixture through the `tsx/esm` import hook (no build step),
|
|
11
11
|
* the pattern used for focused source-level E2E.
|
|
12
12
|
*
|
|
@@ -96,8 +96,8 @@ export function spawnPty(options: IPtyRunOptions): IPtyRunSession {
|
|
|
96
96
|
rows: options.rows ?? 32,
|
|
97
97
|
cwd: options.cwd,
|
|
98
98
|
// HARNESS-025: the default HOME is a throwaway directory, never the developer's real one — a
|
|
99
|
-
//
|
|
100
|
-
env: options.env ??
|
|
99
|
+
// Configured state and HOME are both isolated so the run cannot depend on the developer profile.
|
|
100
|
+
env: createPtyEnv(options.env ?? {}),
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
pty.onData((data) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TERM-003 / TEST-007: real-binary terminal-handoff evidence.
|
|
3
3
|
*
|
|
4
|
-
* Runs in the dedicated PTY vitest project (vitest.pty.config.ts) against the BUILT
|
|
4
|
+
* Runs in the dedicated PTY vitest project (vitest.pty.config.ts) against the BUILT CLI binary
|
|
5
5
|
* (`pnpm --filter @robota-sdk/agent-cli build` first). Unlike the source-level fixture E2E
|
|
6
6
|
* (`terminal-handoff-pty-e2e` / `command-handoff-pty-e2e`), this drives the WHOLE user path —
|
|
7
7
|
* real CLI → real command pipeline → real `TerminalHandoffController` injected by `render.tsx` →
|
|
@@ -25,7 +25,7 @@ describe('terminal handoff through the real binary (TERM-003)', () => {
|
|
|
25
25
|
let session: IPtySession | undefined;
|
|
26
26
|
|
|
27
27
|
beforeEach(() => {
|
|
28
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
28
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-handoff-pty-')));
|
|
29
29
|
writeTuiProviderSettings(projectDir);
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -37,14 +37,14 @@ interface IFixture {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function makeFixture(): IFixture {
|
|
40
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
40
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'agent-trust-prompt-')));
|
|
41
41
|
const home = join(root, 'home');
|
|
42
42
|
const repo = join(root, 'repo');
|
|
43
|
-
mkdirSync(join(home, '
|
|
43
|
+
mkdirSync(join(home, 'state'), { recursive: true });
|
|
44
44
|
// Skip the first-run welcome and provider setup: neither is what this scenario observes.
|
|
45
|
-
writeFileSync(join(home, '
|
|
45
|
+
writeFileSync(join(home, 'state', 'onboarded'), new Date().toISOString(), 'utf8');
|
|
46
46
|
writeFileSync(
|
|
47
|
-
join(home, '
|
|
47
|
+
join(home, 'state', 'settings.json'),
|
|
48
48
|
JSON.stringify({
|
|
49
49
|
currentProvider: 'anthropic',
|
|
50
50
|
providers: {
|
|
@@ -58,7 +58,7 @@ function makeFixture(): IFixture {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function trustedGrants(fixture: IFixture): number {
|
|
61
|
-
const store = join(fixture.home, '
|
|
61
|
+
const store = join(fixture.home, 'state', 'workspace-trust.json');
|
|
62
62
|
if (!existsSync(store)) return 0;
|
|
63
63
|
const parsed = JSON.parse(readFileSync(store, 'utf8')) as { grants: { state: string }[] };
|
|
64
64
|
return parsed.grants.filter((grant) => grant.state === 'trusted').length;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Real-PTY TUI suites (CLI-074 TC-07/08).
|
|
3
3
|
*
|
|
4
4
|
* Runs in the dedicated PTY vitest project (vitest.pty.config.ts) against the
|
|
5
|
-
* BUILT
|
|
5
|
+
* BUILT CLI binary — `pnpm --filter @robota-sdk/agent-cli build` first.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
|
|
@@ -20,7 +20,7 @@ describe('TUI through a real PTY (CLI-074)', () => {
|
|
|
20
20
|
let session: IPtySession | undefined;
|
|
21
21
|
|
|
22
22
|
beforeEach(() => {
|
|
23
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
23
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-')));
|
|
24
24
|
writeTuiProviderSettings(projectDir);
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
22
22
|
|
|
23
23
|
/** Every provider package this repo ships, plus the vendor names behind them. */
|
|
24
|
-
const VENDOR_WORDS = /(claude|anthropic|openai|gpt|gemini|google|
|
|
24
|
+
const VENDOR_WORDS = /(claude|anthropic|openai|gpt|gemini|google|llama|mistral|copilot)/i;
|
|
25
25
|
|
|
26
26
|
function renderLabel(role: TUniversalMessage['role'], enabled: boolean, driverId?: string): string {
|
|
27
27
|
const label = React.createElement(
|
|
@@ -39,9 +39,9 @@ function collectWarnings(): { notes: string[]; warn: (message: string) => void }
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
describe('TC-20: resolvePacing', () => {
|
|
42
|
-
it('ignores ambient
|
|
43
|
-
vi.stubEnv('
|
|
44
|
-
vi.stubEnv('
|
|
42
|
+
it('ignores ambient Fixture Agent pacing variables without a host choice', () => {
|
|
43
|
+
vi.stubEnv('PRODUCT_SCREEN_READER_STARTUP_QUIET_MS', '0');
|
|
44
|
+
vi.stubEnv('PRODUCT_SCREEN_READER_PREPARK_MS', '0');
|
|
45
45
|
try {
|
|
46
46
|
expect(resolvePacing({ enabled: true })).toEqual({
|
|
47
47
|
startupQuietMs: DEFAULT_STARTUP_QUIET_MS,
|
|
@@ -44,7 +44,7 @@ vi.mock('ink', async (importOriginal) => {
|
|
|
44
44
|
|
|
45
45
|
// The App boots a channel and starts I/O; the mode's threading is what is under test, not the App.
|
|
46
46
|
vi.mock('../App.js', () => ({
|
|
47
|
-
default: (): React.ReactElement => React.createElement('
|
|
47
|
+
default: (): React.ReactElement => React.createElement('agent-test-app'),
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
50
|
function baseOptions(): IRenderOptions {
|
|
@@ -231,8 +231,8 @@ describe('SCREEN-2670 TC-05: with the mode off, render() is called with no stdou
|
|
|
231
231
|
expect(Object.keys(inkRender.mock.calls[0]?.[1] ?? {})).not.toContain('stdout');
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
it('ignores an ambient
|
|
235
|
-
vi.stubEnv('
|
|
234
|
+
it('ignores an ambient Fixture Agent park override when the host supplies no park choice', async () => {
|
|
235
|
+
vi.stubEnv('PRODUCT_SCREEN_READER_PREPARK_MS', '0');
|
|
236
236
|
const { renderApp } = await import('../render.js');
|
|
237
237
|
await renderApp({
|
|
238
238
|
...baseOptions(),
|
|
@@ -220,7 +220,7 @@ describe('App session-switch channel ownership (CLI-B11)', () => {
|
|
|
220
220
|
let initialStartIntent: boolean;
|
|
221
221
|
|
|
222
222
|
beforeEach(() => {
|
|
223
|
-
cwd = mkdtempSync(join(tmpdir(), '
|
|
223
|
+
cwd = mkdtempSync(join(tmpdir(), 'agent-b11-'));
|
|
224
224
|
created = [];
|
|
225
225
|
initialStartError = undefined;
|
|
226
226
|
initialStartIntent = false;
|
|
@@ -58,7 +58,7 @@ describe('supportsImeCursorPositioning (CLI-062)', () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it('on for a plain interactive TTY, off for a non-TTY', () => {
|
|
61
|
-
vi.stubEnv('
|
|
61
|
+
vi.stubEnv('PRODUCT_IME_CURSOR', undefined);
|
|
62
62
|
vi.stubEnv('TERM_PROGRAM', undefined);
|
|
63
63
|
setTty(true);
|
|
64
64
|
expect(supportsImeCursorPositioning()).toBe(true);
|
|
@@ -67,30 +67,30 @@ describe('supportsImeCursorPositioning (CLI-062)', () => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('I5: Apple_Terminal is OFF by default (Korean-IME SIGSEGV is an Apple-side bug)', () => {
|
|
70
|
-
vi.stubEnv('
|
|
70
|
+
vi.stubEnv('PRODUCT_IME_CURSOR', undefined);
|
|
71
71
|
setTty(true);
|
|
72
72
|
vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
|
|
73
73
|
expect(supportsImeCursorPositioning()).toBe(false);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
it('I5:
|
|
76
|
+
it('I5: PRODUCT_IME_CURSOR=1 opts Apple_Terminal in explicitly', () => {
|
|
77
77
|
setTty(true);
|
|
78
78
|
vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
|
|
79
|
-
vi.stubEnv('
|
|
79
|
+
vi.stubEnv('PRODUCT_IME_CURSOR', '1');
|
|
80
80
|
expect(supportsImeCursorPositioning({ override: true })).toBe(true);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it('ignores an ambient
|
|
83
|
+
it('ignores an ambient Fixture Agent override without a host choice', () => {
|
|
84
84
|
setTty(true);
|
|
85
85
|
vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
|
|
86
|
-
vi.stubEnv('
|
|
86
|
+
vi.stubEnv('PRODUCT_IME_CURSOR', '1');
|
|
87
87
|
expect(supportsImeCursorPositioning()).toBe(false);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
it('
|
|
90
|
+
it('PRODUCT_IME_CURSOR=0 is a kill switch even on a capable terminal', () => {
|
|
91
91
|
setTty(true);
|
|
92
92
|
vi.stubEnv('TERM_PROGRAM', undefined);
|
|
93
|
-
vi.stubEnv('
|
|
93
|
+
vi.stubEnv('PRODUCT_IME_CURSOR', '0');
|
|
94
94
|
expect(supportsImeCursorPositioning({ override: false })).toBe(false);
|
|
95
95
|
});
|
|
96
96
|
});
|
|
@@ -122,7 +122,7 @@ describe('CLI-062 terminal matrix — supportsImeCursorPositioning per terminal'
|
|
|
122
122
|
vi.stubEnv(key, undefined);
|
|
123
123
|
}
|
|
124
124
|
for (const [key, value] of Object.entries(profile.env)) vi.stubEnv(key, value);
|
|
125
|
-
vi.stubEnv('
|
|
125
|
+
vi.stubEnv('PRODUCT_IME_CURSOR', override);
|
|
126
126
|
|
|
127
127
|
expect(
|
|
128
128
|
supportsImeCursorPositioning({
|
|
@@ -39,7 +39,7 @@ describe('terminal handoff PTY E2E', () => {
|
|
|
39
39
|
it(
|
|
40
40
|
'hands the real terminal to a child, receives its input, and resumes the TUI',
|
|
41
41
|
async () => {
|
|
42
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
42
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-handoff-pty-')));
|
|
43
43
|
tempDirs.push(dir);
|
|
44
44
|
const outputPath = join(dir, 'result.json');
|
|
45
45
|
|