@robota-sdk/agent-ui-terminal 3.0.0-beta.86 → 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 +54 -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
|
@@ -31,7 +31,9 @@ import { spawnTui } from './pty-driver.js';
|
|
|
31
31
|
import type { IPtySession } from './pty-driver.js';
|
|
32
32
|
|
|
33
33
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
34
|
-
const
|
|
34
|
+
const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
|
|
35
|
+
const CLI_NAME = 'fixture-agent';
|
|
36
|
+
const PROTOCOL_SCHEME = 'fixture-agent';
|
|
35
37
|
const WAIT_MS = 20_000;
|
|
36
38
|
const CASE_TIMEOUT_MS = 120_000;
|
|
37
39
|
const PROMPT = 'Summarize the README in one sentence';
|
|
@@ -42,11 +44,12 @@ interface IFixture {
|
|
|
42
44
|
trusted: string;
|
|
43
45
|
untrusted: string;
|
|
44
46
|
elsewhere: string;
|
|
47
|
+
userStateRoot: string;
|
|
45
48
|
env: NodeJS.ProcessEnv;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
function writeProviderSettings(home: string): void {
|
|
49
|
-
const dir = join(home, '
|
|
52
|
+
const dir = join(home, 'state');
|
|
50
53
|
mkdirSync(dir, { recursive: true });
|
|
51
54
|
// Skip the first-run welcome: it is an onboarding screen, not what this scenario observes.
|
|
52
55
|
writeFileSync(join(dir, 'onboarded'), new Date().toISOString(), 'utf8');
|
|
@@ -68,7 +71,7 @@ function initRepo(path: string, env: NodeJS.ProcessEnv): void {
|
|
|
68
71
|
execFileSync('git', args, { cwd: path, env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
69
72
|
};
|
|
70
73
|
run('init', '-q', '-b', 'main');
|
|
71
|
-
run('config', 'user.name', '
|
|
74
|
+
run('config', 'user.name', 'Scenario Fixture');
|
|
72
75
|
run('config', 'user.email', 'scenario@example.invalid');
|
|
73
76
|
writeFileSync(join(path, 'README.md'), 'fixture\n');
|
|
74
77
|
run('add', '--', 'README.md');
|
|
@@ -76,13 +79,37 @@ function initRepo(path: string, env: NodeJS.ProcessEnv): void {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function makeFixture(): IFixture {
|
|
79
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
82
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'agent-flow2006-')));
|
|
80
83
|
const home = join(root, 'home');
|
|
81
84
|
mkdirSync(home, { recursive: true });
|
|
82
85
|
writeProviderSettings(home);
|
|
86
|
+
const userStateRoot = join(home, 'state');
|
|
83
87
|
const env: NodeJS.ProcessEnv = {
|
|
84
88
|
PATH: process.env['PATH'] ?? '',
|
|
85
89
|
HOME: home,
|
|
90
|
+
PRODUCT_ID: CLI_NAME,
|
|
91
|
+
PRODUCT_DISPLAY_NAME: 'Fixture Agent',
|
|
92
|
+
PRODUCT_CLI_NAME: CLI_NAME,
|
|
93
|
+
PRODUCT_ENV_PREFIX: 'FIXTURE_AGENT_',
|
|
94
|
+
PRODUCT_PACKAGE_SCOPE: '@fixture-agent',
|
|
95
|
+
PRODUCT_APP_ID: 'org.example.fixture-agent',
|
|
96
|
+
PRODUCT_PROTOCOL_SCHEME: PROTOCOL_SCHEME,
|
|
97
|
+
PRODUCT_TELEMETRY_SERVICE_NAME: 'fixture-agent',
|
|
98
|
+
PRODUCT_DAEMON_NAMESPACE: 'fixture-agent-daemon',
|
|
99
|
+
PRODUCT_DESKTOP_EXECUTABLE: 'fixture-robota',
|
|
100
|
+
PRODUCT_MCP_CLIENT_NAME: 'fixture-agent-client',
|
|
101
|
+
PRODUCT_MODEL_TOOL_PREFIX: 'fixture_agent_command_',
|
|
102
|
+
PRODUCT_PROMPT_TAG: 'fixture_agent_references',
|
|
103
|
+
PRODUCT_EDITOR_TEMP_PREFIX: 'fixture-agent-editor-',
|
|
104
|
+
PRODUCT_USER_STATE_DIR: userStateRoot,
|
|
105
|
+
PRODUCT_PROJECT_STATE_DIR: '.fixture-agent',
|
|
106
|
+
PRODUCT_CACHE_DIR: join(userStateRoot, 'cache'),
|
|
107
|
+
PRODUCT_LOG_DIR: join(userStateRoot, 'logs'),
|
|
108
|
+
PRODUCT_BROWSER_NAMESPACE: 'fixture-agent-browser',
|
|
109
|
+
PRODUCT_BROWSER_CREDENTIAL_DATABASE: 'fixture-agent-credentials',
|
|
110
|
+
PRODUCT_CREDENTIAL_SERVICE: 'org.example.fixture-agent.credentials',
|
|
111
|
+
PRODUCT_CRYPTO_NAMESPACE: 'fixture-agent-crypto',
|
|
112
|
+
SECURITY_MASTER_KEY_DERIVATION_PATH: '[123,0]',
|
|
86
113
|
TERM: 'xterm-256color',
|
|
87
114
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
88
115
|
GIT_CONFIG_GLOBAL: join(home, 'gitconfig'),
|
|
@@ -94,13 +121,14 @@ function makeFixture(): IFixture {
|
|
|
94
121
|
trusted: join(root, 'trusted'),
|
|
95
122
|
untrusted: join(root, 'untrusted'),
|
|
96
123
|
elsewhere: join(root, 'elsewhere'),
|
|
124
|
+
userStateRoot,
|
|
97
125
|
env,
|
|
98
126
|
};
|
|
99
127
|
initRepo(fixture.trusted, env);
|
|
100
128
|
initRepo(fixture.untrusted, env);
|
|
101
129
|
mkdirSync(fixture.elsewhere, { recursive: true });
|
|
102
130
|
// Grant trust to the one repository a link is allowed to open.
|
|
103
|
-
const granted = spawnSync(process.execPath, [
|
|
131
|
+
const granted = spawnSync(process.execPath, [AGENT_BIN, 'trust', '--yes'], {
|
|
104
132
|
cwd: fixture.trusted,
|
|
105
133
|
env,
|
|
106
134
|
encoding: 'utf8',
|
|
@@ -110,16 +138,16 @@ function makeFixture(): IFixture {
|
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
function link(fixture: IFixture, extra = ''): string {
|
|
113
|
-
return
|
|
141
|
+
return `${PROTOCOL_SCHEME}://open?v=1&prompt=${encodeURIComponent(PROMPT)}&cwd=${fixture.trusted}${extra}`;
|
|
114
142
|
}
|
|
115
143
|
|
|
116
|
-
/** Run `
|
|
144
|
+
/** Run the fixture CLI's `open` command as a process, headless — used for the refusal scenarios. */
|
|
117
145
|
function openHeadless(
|
|
118
146
|
fixture: IFixture,
|
|
119
147
|
url: string,
|
|
120
148
|
cwd = fixture.elsewhere,
|
|
121
149
|
): ReturnType<typeof spawnSync> {
|
|
122
|
-
return spawnSync(process.execPath, [
|
|
150
|
+
return spawnSync(process.execPath, [AGENT_BIN, 'open', url], {
|
|
123
151
|
cwd,
|
|
124
152
|
env: { ...fixture.env, TERM: 'dumb' },
|
|
125
153
|
encoding: 'utf8',
|
|
@@ -134,7 +162,7 @@ function openHeadless(
|
|
|
134
162
|
*/
|
|
135
163
|
function sessionRecords(home: string): readonly string[] {
|
|
136
164
|
try {
|
|
137
|
-
return readdirSync(join(home, '
|
|
165
|
+
return readdirSync(join(home, 'state', 'peers')).sort();
|
|
138
166
|
} catch {
|
|
139
167
|
return [];
|
|
140
168
|
}
|
|
@@ -143,7 +171,7 @@ function sessionRecords(home: string): readonly string[] {
|
|
|
143
171
|
/** The trust store's bytes — a refused link must not add, remove or touch a grant. */
|
|
144
172
|
function trustStoreDigest(home: string): string {
|
|
145
173
|
return createHash('sha256')
|
|
146
|
-
.update(readFileSync(join(home, '
|
|
174
|
+
.update(readFileSync(join(home, 'state', 'workspace-trust.json')))
|
|
147
175
|
.digest('hex');
|
|
148
176
|
}
|
|
149
177
|
|
|
@@ -155,19 +183,19 @@ function malformedLinks(fixture: IFixture): readonly (readonly [string, string])
|
|
|
155
183
|
return [
|
|
156
184
|
[link(fixture, '&provider=anthropic'), 'provider'],
|
|
157
185
|
[link(fixture, `&prompt=${encodeURIComponent('second')}`), 'more than once'],
|
|
158
|
-
[
|
|
159
|
-
[
|
|
186
|
+
[`${PROTOCOL_SCHEME}://open?prompt=hi&cwd=${fixture.trusted}`, 'no `v`'],
|
|
187
|
+
[`${PROTOCOL_SCHEME}://open?v=2&prompt=hi&cwd=${fixture.trusted}`, 'version 1'],
|
|
160
188
|
[
|
|
161
|
-
|
|
189
|
+
`${PROTOCOL_SCHEME}://open?v=1&prompt=${encodeURIComponent('/mode bypassPermissions')}&cwd=${fixture.trusted}`,
|
|
162
190
|
'not a command',
|
|
163
191
|
],
|
|
164
|
-
[
|
|
165
|
-
[
|
|
166
|
-
[
|
|
192
|
+
[`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=relative/path`, 'absolute'],
|
|
193
|
+
[`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=${fixture.trusted}/../untrusted`, '`..`'],
|
|
194
|
+
[`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=${join(fixture.root, 'absent')}`, 'does not exist'],
|
|
167
195
|
];
|
|
168
196
|
}
|
|
169
197
|
|
|
170
|
-
describe('
|
|
198
|
+
describe('configured open deep link through the CLI binary (FLOW-2006 TC-09)', () => {
|
|
171
199
|
let fixture: IFixture | undefined;
|
|
172
200
|
let session: IPtySession | undefined;
|
|
173
201
|
|
|
@@ -191,6 +219,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
|
|
|
191
219
|
projectDir: current.elsewhere,
|
|
192
220
|
homeDir: current.home,
|
|
193
221
|
args: ['open', link(current), '--name', 'flow2006'],
|
|
222
|
+
env: current.env as Record<string, string>,
|
|
194
223
|
});
|
|
195
224
|
|
|
196
225
|
// The PREFILLED TEXT is the arrival signal, not the empty-composer placeholder (which never
|
|
@@ -227,7 +256,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
|
|
|
227
256
|
const current = fixture;
|
|
228
257
|
if (!current) throw new Error('fixture');
|
|
229
258
|
const recordsBefore = sessionRecords(current.home);
|
|
230
|
-
const settingsBefore = readFileSync(join(current.
|
|
259
|
+
const settingsBefore = readFileSync(join(current.userStateRoot, 'settings.json'), 'utf8');
|
|
231
260
|
const cases = malformedLinks(current);
|
|
232
261
|
for (const [url, expected] of cases) {
|
|
233
262
|
const result = openHeadless(current, url);
|
|
@@ -238,7 +267,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
|
|
|
238
267
|
// A SECOND LINK after the first is refused — the argv-appending shape the platforms document.
|
|
239
268
|
const twoArgs = spawnSync(
|
|
240
269
|
process.execPath,
|
|
241
|
-
[
|
|
270
|
+
[AGENT_BIN, 'open', link(current), link(current)],
|
|
242
271
|
{
|
|
243
272
|
cwd: current.elsewhere,
|
|
244
273
|
env: { ...current.env, TERM: 'dumb' },
|
|
@@ -251,7 +280,7 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
|
|
|
251
280
|
// Nothing was started and nothing was configured: no session record appeared, and the
|
|
252
281
|
// settings the `/mode bypassPermissions` link tried to reach are byte-identical.
|
|
253
282
|
expect(sessionRecords(current.home)).toEqual(recordsBefore);
|
|
254
|
-
expect(readFileSync(join(current.
|
|
283
|
+
expect(readFileSync(join(current.userStateRoot, 'settings.json'), 'utf8')).toBe(
|
|
255
284
|
settingsBefore,
|
|
256
285
|
);
|
|
257
286
|
},
|
|
@@ -266,19 +295,19 @@ describe('robota open through the real binary (FLOW-2006 TC-09)', () => {
|
|
|
266
295
|
const digestBefore = trustStoreDigest(current.home);
|
|
267
296
|
const untrusted = openHeadless(
|
|
268
297
|
current,
|
|
269
|
-
|
|
298
|
+
`${PROTOCOL_SCHEME}://open?v=1&prompt=hi&cwd=${current.untrusted}`,
|
|
270
299
|
);
|
|
271
300
|
expect(untrusted.status).toBe(1);
|
|
272
|
-
expect(`${untrusted.stderr}`).toContain(
|
|
301
|
+
expect(`${untrusted.stderr}`).toContain(`${CLI_NAME} trust --yes`);
|
|
273
302
|
|
|
274
|
-
const slug = openHeadless(current,
|
|
303
|
+
const slug = openHeadless(current, `${PROTOCOL_SCHEME}://open?v=1&prompt=hi&repo=nobody/not-a-clone`);
|
|
275
304
|
expect(slug.status).toBe(1);
|
|
276
305
|
expect(`${slug.stderr}`).toContain('nobody/not-a-clone');
|
|
277
306
|
expect(`${slug.stderr}`).not.toContain('cloning');
|
|
278
307
|
|
|
279
308
|
// A refused link grants nothing and records nothing: the untrusted repository is still
|
|
280
309
|
// untrusted and the store is byte-identical to what it was before the two invocations.
|
|
281
|
-
const status = spawnSync(process.execPath, [
|
|
310
|
+
const status = spawnSync(process.execPath, [AGENT_BIN, 'trust', 'status'], {
|
|
282
311
|
cwd: current.untrusted,
|
|
283
312
|
env: current.env,
|
|
284
313
|
encoding: 'utf8',
|
|
@@ -3,31 +3,46 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every other cell reads the cursor out of the byte stream our own VT interpreter replays. tmux is
|
|
5
5
|
* a full, independent terminal emulator that can be driven headlessly AND queried: it runs the
|
|
6
|
-
* built
|
|
6
|
+
* built CLI binary in a real pane, receives the Korean keystrokes through its own input path,
|
|
7
7
|
* and then reports where ITS state machine put the hardware cursor
|
|
8
8
|
* (`#{cursor_x}` / `#{cursor_y}` / `#{cursor_flag}`). That makes this the strongest available
|
|
9
9
|
* confirmation short of macOS hardware: a second implementation agrees the cursor sits on the
|
|
10
10
|
* input row at the composition column — which is the cell an OS IME anchors its window to.
|
|
11
11
|
*
|
|
12
|
-
* Skipped when tmux is not installed; `
|
|
12
|
+
* Skipped when tmux is not installed; `PRODUCT_TMUX_BIN` overrides the binary. GitHub's ubuntu
|
|
13
13
|
* runner images ship tmux, so this runs in CI rather than quietly evaporating there.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
chmodSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
mkdtempSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
} from 'node:fs';
|
|
18
26
|
import { tmpdir } from 'node:os';
|
|
19
27
|
import { join, resolve } from 'node:path';
|
|
20
28
|
|
|
21
29
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
22
30
|
|
|
31
|
+
import { fixtureProductEnvironment } from './isolated-home.js';
|
|
32
|
+
|
|
23
33
|
const REPO_ROOT = resolve(__dirname, '../../../../..');
|
|
24
|
-
const
|
|
34
|
+
const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
|
|
35
|
+
const GIT_BIN =
|
|
36
|
+
(process.env['PATH'] ?? '/usr/bin:/bin')
|
|
37
|
+
.split(':')
|
|
38
|
+
.map((directory) => join(directory, 'git'))
|
|
39
|
+
.find((candidate) => existsSync(candidate)) ?? '/usr/bin/git';
|
|
25
40
|
const COLS = 80;
|
|
26
41
|
const COMPOSITION = '안녕';
|
|
27
42
|
const COMPOSITION_WIDTH = 4;
|
|
28
43
|
|
|
29
44
|
function resolveTmux(): string | undefined {
|
|
30
|
-
const override = process.env['
|
|
45
|
+
const override = process.env['PRODUCT_TMUX_BIN'];
|
|
31
46
|
if (override !== undefined && override !== '') return override;
|
|
32
47
|
const which = spawnSync('sh', ['-c', 'command -v tmux'], { encoding: 'utf8' });
|
|
33
48
|
const found = which.stdout.trim();
|
|
@@ -49,14 +64,16 @@ interface ITmuxRun {
|
|
|
49
64
|
socket: string;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
/** Run
|
|
67
|
+
/** Run the CLI inside a real tmux pane and ask tmux where the cursor ended up. */
|
|
53
68
|
function runInTmux(run: ITmuxRun, rows: number, imeCursor: string | undefined): ITmuxProbe {
|
|
54
69
|
const tmux = TMUX!;
|
|
55
70
|
const projectDir = join(run.root, 'proj');
|
|
56
71
|
const homeDir = join(run.root, 'home');
|
|
57
|
-
mkdirSync(
|
|
72
|
+
mkdirSync(projectDir, { recursive: true });
|
|
73
|
+
mkdirSync(join(homeDir, 'state'), { recursive: true });
|
|
74
|
+
writeFileSync(join(homeDir, 'state/onboarded'), new Date().toISOString(), 'utf8');
|
|
58
75
|
writeFileSync(
|
|
59
|
-
join(homeDir, '
|
|
76
|
+
join(homeDir, 'state/settings.json'),
|
|
60
77
|
JSON.stringify({
|
|
61
78
|
currentProvider: 'anthropic',
|
|
62
79
|
providers: {
|
|
@@ -69,14 +86,42 @@ function runInTmux(run: ITmuxRun, rows: number, imeCursor: string | undefined):
|
|
|
69
86
|
// A wrapper keeps the pane's env explicit (never inheriting real provider keys) while letting
|
|
70
87
|
// tmux itself set TERM/TERM_PROGRAM for the pane — the handshake under test.
|
|
71
88
|
const wrapper = join(run.root, 'wrap.sh');
|
|
72
|
-
const imeAssignment = imeCursor === undefined ? '' : `
|
|
89
|
+
const imeAssignment = imeCursor === undefined ? '' : `PRODUCT_IME_CURSOR=${imeCursor} `;
|
|
90
|
+
const productAssignments = Object.entries(fixtureProductEnvironment(homeDir))
|
|
91
|
+
.map(([key, value]) => `${key}='${value.replaceAll("'", "'\\''")}'`)
|
|
92
|
+
.join(' ');
|
|
93
|
+
const fixtureEnv = {
|
|
94
|
+
PATH: process.env['PATH'] || '/usr/bin:/bin:/opt/homebrew/bin',
|
|
95
|
+
HOME: homeDir,
|
|
96
|
+
...fixtureProductEnvironment(homeDir),
|
|
97
|
+
};
|
|
98
|
+
execFileSync(GIT_BIN, ['init', '-q', '-b', 'main'], { cwd: projectDir });
|
|
99
|
+
execFileSync(
|
|
100
|
+
GIT_BIN,
|
|
101
|
+
[
|
|
102
|
+
'-c',
|
|
103
|
+
'user.name=PTY Fixture',
|
|
104
|
+
'-c',
|
|
105
|
+
'user.email=pty@example.invalid',
|
|
106
|
+
'commit',
|
|
107
|
+
'--allow-empty',
|
|
108
|
+
'-q',
|
|
109
|
+
'-m',
|
|
110
|
+
'fixture',
|
|
111
|
+
],
|
|
112
|
+
{ cwd: projectDir, env: fixtureEnv },
|
|
113
|
+
);
|
|
114
|
+
execFileSync(process.execPath, [AGENT_BIN, 'trust', '--yes'], {
|
|
115
|
+
cwd: projectDir,
|
|
116
|
+
env: fixtureEnv,
|
|
117
|
+
});
|
|
73
118
|
writeFileSync(
|
|
74
119
|
wrapper,
|
|
75
120
|
[
|
|
76
121
|
'#!/bin/sh',
|
|
77
122
|
`cd "${projectDir}"`,
|
|
78
|
-
`exec env -i PATH="$PATH" HOME="${homeDir}" TERM="$TERM" TERM_PROGRAM="$TERM_PROGRAM" ${imeAssignment}` +
|
|
79
|
-
`"${process.execPath}" "${
|
|
123
|
+
`exec env -i PATH="$PATH" HOME="${homeDir}" TERM="$TERM" TERM_PROGRAM="$TERM_PROGRAM" ${productAssignments} ${imeAssignment}` +
|
|
124
|
+
`"${process.execPath}" "${AGENT_BIN}" --name tmux-fixture`,
|
|
80
125
|
'',
|
|
81
126
|
].join('\n'),
|
|
82
127
|
'utf8',
|
|
@@ -95,7 +140,7 @@ function runInTmux(run: ITmuxRun, rows: number, imeCursor: string | undefined):
|
|
|
95
140
|
if (pane.includes('Type a message')) break;
|
|
96
141
|
sleepSync(250);
|
|
97
142
|
}
|
|
98
|
-
expect(pane, '
|
|
143
|
+
expect(pane, 'the CLI never reached the prompt inside tmux').toContain('Type a message');
|
|
99
144
|
sleepSync(400);
|
|
100
145
|
|
|
101
146
|
tmuxCall('send-keys', '-l', COMPOSITION);
|
|
@@ -127,7 +172,7 @@ describe.skipIf(TMUX === undefined)(
|
|
|
127
172
|
let run: ITmuxRun;
|
|
128
173
|
|
|
129
174
|
beforeEach(() => {
|
|
130
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
175
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'agent-tmux-ime-')));
|
|
131
176
|
run = { root, socket: join(root, 'sock') };
|
|
132
177
|
});
|
|
133
178
|
|
|
@@ -154,7 +199,7 @@ describe.skipIf(TMUX === undefined)(
|
|
|
154
199
|
expect(probe.cursorVisible).toBe(false);
|
|
155
200
|
}, 90_000);
|
|
156
201
|
|
|
157
|
-
it('
|
|
202
|
+
it('PRODUCT_IME_CURSOR=0 kill switch: tmux reports the cursor hidden even at 24 rows', () => {
|
|
158
203
|
const probe = runInTmux(run, 24, '0');
|
|
159
204
|
const inputRow = probe.pane.findIndex((line) => line.includes(`> ${COMPOSITION}`));
|
|
160
205
|
expect(inputRow).toBeGreaterThanOrEqual(0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI-062 — PTY regression for real-terminal-cursor positioning (the OS-IME evidence).
|
|
3
3
|
*
|
|
4
|
-
* Runs the BUILT
|
|
4
|
+
* Runs the BUILT CLI binary in a real pseudo-terminal and interprets the raw ANSI stream the
|
|
5
5
|
* way a terminal emulator (and the OS IME) does. Two geometries from the implementation contract
|
|
6
6
|
* (.design/investigations/2026-07-25-cli-062-ime-cursor-design.md):
|
|
7
7
|
*
|
|
@@ -105,7 +105,7 @@ describe('CLI-062 — IME hardware-cursor positioning through a real PTY', () =>
|
|
|
105
105
|
let session: IPtySession | undefined;
|
|
106
106
|
|
|
107
107
|
beforeEach(() => {
|
|
108
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
108
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-ime-')));
|
|
109
109
|
writeTuiProviderSettings(projectDir);
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -132,7 +132,7 @@ describe('CLI-062 — IME hardware-cursor positioning through a real PTY', () =>
|
|
|
132
132
|
* PROVES, per cell: the built binary's observable cursor contract under exactly the environment
|
|
133
133
|
* that terminal exports — positioned on the input row when the capability gate says yes, and no
|
|
134
134
|
* cursor shown at all when it says no (Terminal.app's default-off I5 branch, and the
|
|
135
|
-
* `
|
|
135
|
+
* `PRODUCT_IME_CURSOR=0` kill switch).
|
|
136
136
|
*
|
|
137
137
|
* DOES NOT PROVE, for a `documented` row: anything about that emulator's own rendering, its
|
|
138
138
|
* pre-edit display, or its OS IME. Terminal.app's historical Korean-IME SIGSEGV in particular is
|
|
@@ -145,7 +145,7 @@ describe('CLI-062 terminal matrix — observable cursor contract per terminal',
|
|
|
145
145
|
let session: IPtySession | undefined;
|
|
146
146
|
|
|
147
147
|
beforeEach(() => {
|
|
148
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
148
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-ime-matrix-')));
|
|
149
149
|
writeTuiProviderSettings(projectDir);
|
|
150
150
|
});
|
|
151
151
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Isolated HOME for PTY children (HARNESS-025).
|
|
3
3
|
*
|
|
4
4
|
* A PTY test that forwards the developer's real `HOME` runs its child against whatever happens to
|
|
5
|
-
* be in that home directory — a
|
|
5
|
+
* be in that home directory — a `~/state` settings file, a shell rc, an `$EDITOR` preference.
|
|
6
6
|
* The suite then passes or fails depending on the machine it runs on: green on CI (empty HOME),
|
|
7
7
|
* red on a developer box whose real config contradicts the fixture. That is the conditional flake
|
|
8
8
|
* this module removes.
|
|
@@ -29,11 +29,11 @@ function installExitHook(): void {
|
|
|
29
29
|
/**
|
|
30
30
|
* Create a throwaway HOME directory for a PTY child and return its absolute path.
|
|
31
31
|
*
|
|
32
|
-
* The directory is empty: the child sees no
|
|
32
|
+
* The directory is empty: the child sees no `~/state`, no shell rc, no user preferences. Callers
|
|
33
33
|
* that need seeded content write it themselves after creating the directory.
|
|
34
34
|
*/
|
|
35
35
|
export function createIsolatedHome(): string {
|
|
36
|
-
const dir = mkdtempSync(join(tmpdir(), '
|
|
36
|
+
const dir = mkdtempSync(join(tmpdir(), 'agent-pty-home-'));
|
|
37
37
|
createdHomes.push(dir);
|
|
38
38
|
installExitHook();
|
|
39
39
|
return dir;
|
|
@@ -55,10 +55,43 @@ export function disposeIsolatedHomes(): void {
|
|
|
55
55
|
* visible at the call site. Pass `overrides` for anything else the fixture needs (e.g. `EDITOR`).
|
|
56
56
|
*/
|
|
57
57
|
export function createPtyEnv(overrides: Readonly<NodeJS.ProcessEnv> = {}): NodeJS.ProcessEnv {
|
|
58
|
+
const home = typeof overrides.HOME === 'string' ? overrides.HOME : createIsolatedHome();
|
|
58
59
|
return {
|
|
59
60
|
PATH: process.env['PATH'] ?? '',
|
|
60
|
-
HOME:
|
|
61
|
+
HOME: home,
|
|
61
62
|
TERM: 'xterm-256color',
|
|
63
|
+
...fixtureProductEnvironment(home),
|
|
62
64
|
...overrides,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
67
|
+
|
|
68
|
+
/** Product identity and state roots for the compiled CLI's isolated PTY fixtures. */
|
|
69
|
+
export function fixtureProductEnvironment(home: string): Record<string, string> {
|
|
70
|
+
const id = 'fixture-agent';
|
|
71
|
+
const stateRoot = join(home, 'state');
|
|
72
|
+
return {
|
|
73
|
+
PRODUCT_ID: id,
|
|
74
|
+
PRODUCT_DISPLAY_NAME: 'Fixture Agent',
|
|
75
|
+
PRODUCT_CLI_NAME: id,
|
|
76
|
+
PRODUCT_ENV_PREFIX: 'FIXTURE_AGENT_',
|
|
77
|
+
PRODUCT_PACKAGE_SCOPE: '@fixture-agent',
|
|
78
|
+
PRODUCT_APP_ID: 'org.example.fixture-agent',
|
|
79
|
+
PRODUCT_PROTOCOL_SCHEME: id,
|
|
80
|
+
PRODUCT_TELEMETRY_SERVICE_NAME: `${id}.agent`,
|
|
81
|
+
PRODUCT_DAEMON_NAMESPACE: `${id}.daemon`,
|
|
82
|
+
PRODUCT_DESKTOP_EXECUTABLE: `${id}-runtime`,
|
|
83
|
+
PRODUCT_MCP_CLIENT_NAME: `${id}-client`,
|
|
84
|
+
PRODUCT_MODEL_TOOL_PREFIX: 'fixture_agent_command_',
|
|
85
|
+
PRODUCT_PROMPT_TAG: 'fixture_agent_references',
|
|
86
|
+
PRODUCT_EDITOR_TEMP_PREFIX: `${id}-editor-`,
|
|
87
|
+
PRODUCT_USER_STATE_DIR: stateRoot,
|
|
88
|
+
PRODUCT_PROJECT_STATE_DIR: '.fixture-agent',
|
|
89
|
+
PRODUCT_CACHE_DIR: join(stateRoot, 'cache'),
|
|
90
|
+
PRODUCT_LOG_DIR: join(stateRoot, 'logs'),
|
|
91
|
+
PRODUCT_BROWSER_NAMESPACE: `${id}.browser`,
|
|
92
|
+
PRODUCT_BROWSER_CREDENTIAL_DATABASE: `${id}-credentials`,
|
|
93
|
+
PRODUCT_CREDENTIAL_SERVICE: `org.example.${id}.credentials`,
|
|
94
|
+
PRODUCT_CRYPTO_NAMESPACE: `${id}.crypto`,
|
|
95
|
+
SECURITY_MASTER_KEY_DERIVATION_PATH: '[123,0]',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Real-PTY organization-policy engineering regression scenario (CLI-083).
|
|
3
3
|
*
|
|
4
|
-
* Drives the built
|
|
4
|
+
* Drives the built Fixture Agent CLI with an isolated HOME. Nothing injects a policy object into the
|
|
5
5
|
* command module: the refusal can only come from the shipped `loadOrgPolicy()` startup path. The
|
|
6
6
|
* update check is disabled so this isolated scenario performs no startup network request.
|
|
7
7
|
*/
|
|
@@ -17,7 +17,7 @@ import { spawnTui } from './pty-driver.js';
|
|
|
17
17
|
import type { IPtySession } from './pty-driver.js';
|
|
18
18
|
|
|
19
19
|
function writeOrgPolicyFixture(homeDir: string): void {
|
|
20
|
-
const settingsDir = join(homeDir, '
|
|
20
|
+
const settingsDir = join(homeDir, 'state');
|
|
21
21
|
mkdirSync(settingsDir, { recursive: true });
|
|
22
22
|
writeFileSync(
|
|
23
23
|
join(settingsDir, 'settings.json'),
|
|
@@ -43,7 +43,7 @@ describe('organization policy through the built CLI (CLI-083)', () => {
|
|
|
43
43
|
let session: IPtySession | undefined;
|
|
44
44
|
|
|
45
45
|
beforeEach(() => {
|
|
46
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
46
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-org-policy-')));
|
|
47
47
|
homeDir = join(projectDir, 'home');
|
|
48
48
|
writeOrgPolicyFixture(homeDir);
|
|
49
49
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Real-PTY provider-setup ask suite (CMD-004 TC-09 done-gate).
|
|
3
3
|
*
|
|
4
|
-
* Drives the BUILT
|
|
4
|
+
* Drives the BUILT CLI binary through `/provider add` so the unified ask seam renders end-to-end on
|
|
5
5
|
* a real terminal: the provider command self-asks (via the injected `askHandler` → `TuiInteractionChannel.askUser`)
|
|
6
6
|
* and the TUI renders the request through `PendingActionPrompt`. Proves the masked secret field hides
|
|
7
7
|
* the typed API key on the real CLI — the User Execution evidence the done-gate requires (no human
|
|
@@ -26,7 +26,7 @@ describe('provider setup ask through a real PTY (CMD-004 TC-09)', () => {
|
|
|
26
26
|
let session: IPtySession | undefined;
|
|
27
27
|
|
|
28
28
|
beforeEach(() => {
|
|
29
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
29
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-pty-provider-')));
|
|
30
30
|
writeTuiProviderSettings(projectDir);
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PTY TUI driver (CLI-074 TC-07/08).
|
|
3
3
|
*
|
|
4
|
-
* Built-CLI convenience over the shared `spawnPty` harness (TEST-007): spawns the built
|
|
4
|
+
* Built-CLI convenience over the shared `spawnPty` harness (TEST-007): spawns the built CLI in
|
|
5
5
|
* a real pseudo-terminal so Ink renders exactly as in a user terminal, with per-key paced input
|
|
6
6
|
* (expect(1)-style burst input gets bundled as a bracketed paste — the failure mode this driver
|
|
7
7
|
* exists to avoid). Test-only; lives in a dedicated vitest project (*.ptytest.ts).
|
|
@@ -11,17 +11,18 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
12
|
|
|
13
13
|
import { spawnPty } from './spawn-pty.js';
|
|
14
|
+
import { fixtureProductEnvironment } from './isolated-home.js';
|
|
14
15
|
|
|
15
16
|
import type { IPtyRunSession } from './spawn-pty.js';
|
|
16
17
|
|
|
17
18
|
const REPO_ROOT = resolve(__dirname, '../../../../..');
|
|
18
|
-
const
|
|
19
|
+
const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Signal the PTY child and await its ACTUAL exit before the caller removes the project dir.
|
|
22
23
|
* `dispose()` sends SIGTERM (then SIGKILL after the grace window) but returns synchronously;
|
|
23
24
|
* `expectExit()` resolves once the child has exited and **throws** if it has not within the timeout.
|
|
24
|
-
* Awaiting this closes the INFRA-026 race where a dying child writes
|
|
25
|
+
* Awaiting this closes the INFRA-026 race where a dying child writes under the selected state root between
|
|
25
26
|
* `rmSync`'s readdir and rmdir. A child that refuses to die fails the test loudly — the exit is awaited
|
|
26
27
|
* at the source, never masked by a removal fallback.
|
|
27
28
|
* Extracted so the compose is unit-testable with a fake session.
|
|
@@ -69,7 +70,7 @@ export interface IPtySession {
|
|
|
69
70
|
* Teardown that awaits ACTUAL child exit: signals the PTY child (`kill()`/`dispose()`) then awaits
|
|
70
71
|
* `expectExit`, which resolves once the child has exited and **throws** if it has not within the
|
|
71
72
|
* timeout (default ≥ the SIGTERM→SIGKILL grace). Await this before removing the project dir so the
|
|
72
|
-
* dying child cannot write
|
|
73
|
+
* dying child cannot write under the selected state root between `rmSync`'s readdir and rmdir (INFRA-026).
|
|
73
74
|
*/
|
|
74
75
|
disposeAsync(timeoutMs?: number): Promise<void>;
|
|
75
76
|
}
|
|
@@ -95,7 +96,7 @@ export function writeTuiProviderSettings(projectDir: string): void {
|
|
|
95
96
|
// ARCH-042: a capabilityless built CLI is Restricted and must not read project settings. PTY
|
|
96
97
|
// fixtures therefore configure the host-owned user layer under the same isolated HOME that
|
|
97
98
|
// `spawnTui` derives from the fixture root.
|
|
98
|
-
const settingsDir = join(projectDir, 'home', '
|
|
99
|
+
const settingsDir = join(projectDir, 'home', 'state');
|
|
99
100
|
mkdirSync(settingsDir, { recursive: true });
|
|
100
101
|
writeFileSync(
|
|
101
102
|
join(settingsDir, 'settings.json'),
|
|
@@ -121,12 +122,13 @@ export function spawnTui(options: ISpawnTuiOptions): IPtySession {
|
|
|
121
122
|
|
|
122
123
|
const session: IPtyRunSession = spawnPty({
|
|
123
124
|
command: process.execPath,
|
|
124
|
-
args: [
|
|
125
|
+
args: [AGENT_BIN, ...defaultedArgs],
|
|
125
126
|
cwd: options.projectDir,
|
|
126
127
|
env: {
|
|
127
128
|
PATH: process.env['PATH'] ?? '',
|
|
128
129
|
HOME: options.homeDir,
|
|
129
130
|
TERM: 'xterm-256color',
|
|
131
|
+
...fixtureProductEnvironment(options.homeDir),
|
|
130
132
|
// Never inherit real provider keys into PTY runs.
|
|
131
133
|
...(options.env ?? {}),
|
|
132
134
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* INFRA-018: deterministic conversation through the real binary via `--session-log` (replay).
|
|
3
3
|
*
|
|
4
|
-
* Runs in the PTY vitest project against the BUILT
|
|
4
|
+
* Runs in the PTY vitest project against the BUILT CLI. Boots with
|
|
5
5
|
* `--session-log <fixture>` so the INFRA-017 ReplayProvider answers from a recorded log (no model
|
|
6
6
|
* key, no network), sends a user message, and asserts the recorded assistant response renders and
|
|
7
7
|
* commits to `<Static>` scrollback while the input stays pinned — closing SCREEN-010 TC-02/03
|
|
@@ -30,7 +30,7 @@ describe('replay conversation through the real binary (INFRA-018)', () => {
|
|
|
30
30
|
let session: IPtySession | undefined;
|
|
31
31
|
|
|
32
32
|
beforeEach(() => {
|
|
33
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
33
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-replay-pty-')));
|
|
34
34
|
writeTuiProviderSettings(projectDir); // dummy provider profile so the CLI boots; key never used
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SCREEN-005 User Execution Test Scenarios — prompt footers and key-hint affordance on the BUILT
|
|
3
|
-
*
|
|
3
|
+
* CLI binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
|
|
4
4
|
*
|
|
5
5
|
* Scenario 1: `/` opens the autocomplete → unified footer + `> ` indicator on the selected row.
|
|
6
6
|
* Scenario 2: a replayed model turn drives a CMD-004 ask in each shape (single-select,
|
|
@@ -41,7 +41,7 @@ describe('SCREEN-005 prompt footers through the real binary', () => {
|
|
|
41
41
|
let session: IPtySession | undefined;
|
|
42
42
|
|
|
43
43
|
beforeEach(() => {
|
|
44
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
44
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen005-pty-')));
|
|
45
45
|
writeTuiProviderSettings(projectDir);
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SCREEN-006 User Execution Test Scenarios — color/motion degradation and palette-token colors on
|
|
3
|
-
* the BUILT
|
|
3
|
+
* the BUILT CLI binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
|
|
4
4
|
*
|
|
5
5
|
* Scenario 1 (NO_COLOR legibility): `NO_COLOR=1` through a replayed conversation → the raw pty
|
|
6
6
|
* transcript contains ZERO SGR color sequences (30-38/40-48/90-97/100-107 params, incl. 38;5 /
|
|
@@ -60,7 +60,7 @@ describe('SCREEN-006 color/motion through the real binary', () => {
|
|
|
60
60
|
let session: IPtySession | undefined;
|
|
61
61
|
|
|
62
62
|
beforeEach(() => {
|
|
63
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
63
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen006-pty-')));
|
|
64
64
|
writeTuiProviderSettings(projectDir);
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -126,9 +126,9 @@ describe('SCREEN-006 color/motion through the real binary', () => {
|
|
|
126
126
|
// eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
|
|
127
127
|
expect(raw).toMatch(/\x1b\[32m[^\x1b]*✓ Shell/);
|
|
128
128
|
|
|
129
|
-
// Assistant label renders the accent token (cyan → ESC[36m ... "
|
|
129
|
+
// Assistant label renders the accent token (cyan → ESC[36m ... "Fixture Agent:").
|
|
130
130
|
// eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
|
|
131
|
-
expect(raw).toMatch(/\x1b\[36m(?:\x1b\[[0-9;]*m)*
|
|
131
|
+
expect(raw).toMatch(/\x1b\[36m(?:\x1b\[[0-9;]*m)*Fixture Agent:/);
|
|
132
132
|
|
|
133
133
|
// Markdown diff block carries the resolved theme's SGR pair (added line: light green on a dark
|
|
134
134
|
// green background) — SCREEN-2002 moved the values into the theme, the bytes are unchanged.
|