windows-development-sandbox 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/features/accounts/accounts.def.d.ts +1 -0
- package/dist/app/features/accounts/accounts.def.js +12 -3
- package/dist/app/features/accounts/accounts.run.d.ts +1 -0
- package/dist/app/features/accounts/accounts.run.js +20 -11
- package/dist/app/features/defs.d.ts +1 -0
- package/dist/app/features/settings/settings-file.d.ts +7 -0
- package/dist/app/features/settings/settings-file.js +19 -2
- package/dist/app/features/settings/settings-rows.js +9 -1
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +7 -1
- package/dist/bin/chunk-BQ23GBLC.js +78790 -0
- package/dist/bin/chunk-F7IMME5G.js +10432 -0
- package/dist/bin/chunk-LQHEUN2A.js +7 -0
- package/dist/bin/claude-host.js +10 -4
- package/dist/bin/claude-sbx.js +8 -2
- package/dist/bin/codex-sbx.js +8 -2
- package/dist/bin/gemini-sbx.js +8 -2
- package/dist/bin/sandbox.js +7 -2
- package/dist/bin/sbx-hide.js +8 -2
- package/dist/bin/sbx-show.js +8 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/launch/prepare.js +24 -7
- package/dist/cli/commands/launch/running.js +4 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/commands/storage/offer.js +1 -1
- package/dist/cli/menu/live.d.ts +29 -0
- package/dist/cli/menu/live.js +92 -21
- package/dist/cli/menu/menu.d.ts +2 -1
- package/dist/cli/menu/menu.js +16 -2
- package/dist/cli/menu/pages/list-page.d.ts +5 -0
- package/dist/cli/menu/pages/list-page.js +19 -0
- package/dist/cli/menu/shell.d.ts +2 -0
- package/dist/cli/menu/shell.js +1 -1
- package/dist/cli/services/accounts-service.d.ts +7 -0
- package/dist/cli/services/accounts-service.js +20 -0
- package/dist/cli/tui/app.d.ts +4 -0
- package/dist/cli/tui/app.js +5 -0
- package/dist/cli/tui/terminal.d.ts +6 -0
- package/dist/cli/tui/terminal.js +8 -1
- package/dist/cli/validate.js +8 -4
- package/dist/core/package-info.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +1 -0
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +19 -4
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/storage/contained/state.d.ts +3 -2
- package/dist/host/storage/contained/state.js +5 -2
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +9 -0
- package/dist/runtime/daemon.js +57 -3
- package/dist/runtime/db.js +9 -0
- package/dist/runtime/main.js +195 -12
- package/dist/runtime/netproxy.js +9 -0
- package/dist/runtime/view.js +195 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +10 -1
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/menu-perf.test.js +198 -12
- package/dist/selftest/menu.test.js +4 -0
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/run-dir.d.ts +6 -0
- package/dist/shared/channel/run-dir.js +9 -0
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +236 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Windows Terminal's own pseudo console) and runs the real viewer/attach client (runTerminalClient), which is attached
|
|
3
3
|
// to an inner ptyhost running a stand-in agent. The agent turns SGR mouse tracking on and echoes every byte it reads.
|
|
4
4
|
// Checks: clicks and the wheel reach the agent, Backspace stays Backspace, a split escape sequence arrives whole,
|
|
5
|
-
// 500 printed lines scroll through as lines (no alternate screen, no scrollback clear),
|
|
6
|
-
// a screen clear still gets mouse tracking.
|
|
5
|
+
// 500 printed lines scroll through as lines (no alternate screen, no scrollback clear), a viewer that joins after
|
|
6
|
+
// a screen clear still gets mouse tracking, and drag and motion reports reach the agent only once it asked for them.
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import net from 'node:net';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -14,7 +14,7 @@ import { SYSTEM } from '../shared/system.js';
|
|
|
14
14
|
import { CSC, openViewer, tail, WAIT } from './support/live.js';
|
|
15
15
|
/**
|
|
16
16
|
* The stand-in agent: VT input and SGR mouse tracking on, echoes what it reads ("got <1b>[<0;10;5M"). L prints 500
|
|
17
|
-
* lines, K clears the screen, q quits.
|
|
17
|
+
* lines, K clears the screen, D turns drag reports on (1002), A every motion (1003), q quits.
|
|
18
18
|
*/
|
|
19
19
|
const AGENT_CS = String.raw `
|
|
20
20
|
using System;
|
|
@@ -44,6 +44,8 @@ static class MouseAgent
|
|
|
44
44
|
Say("got " + got + "\r\n");
|
|
45
45
|
if (got == "L") { var lines = new StringBuilder(); for (int i = 1; i <= 500; i++) lines.Append("line " + i + "\r\n"); Say(lines.ToString() + "LINES-DONE\r\n"); }
|
|
46
46
|
if (got == "K") Say("\x1b[2J\x1b[HCLEARED\r\n");
|
|
47
|
+
if (got == "D") Say("\x1b[?1002hDRAG-ON\r\n");
|
|
48
|
+
if (got == "A") Say("\x1b[?1003hMOVE-ON\r\n");
|
|
47
49
|
if (got == "q") break;
|
|
48
50
|
}
|
|
49
51
|
// Exits with mouse tracking still on (like a crash): the client must turn it off.
|
|
@@ -120,6 +122,75 @@ export const liveTerminalSuite = {
|
|
|
120
122
|
stdio: 'ignore',
|
|
121
123
|
});
|
|
122
124
|
}
|
|
125
|
+
t.section('a ptyhost ended from outside: what it recorded is ended, and nothing else');
|
|
126
|
+
{
|
|
127
|
+
// ptyhost records its program's process tree (PTYHOST_TREE_FILE); --end-tree ends it once ptyhost is gone,
|
|
128
|
+
// checking every process by id and start time.
|
|
129
|
+
const record = t.at('tree.json');
|
|
130
|
+
const ping = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE');
|
|
131
|
+
const holder = '"' + SYSTEM.cmd() + '" /c "' + ping + '" -n 120 127.0.0.1 >nul';
|
|
132
|
+
const host = spawn(ptyhost, [String(await freePort()), 'tok', '80', '24', t.at('tree.log'), holder], {
|
|
133
|
+
env: { ...process.env, PTYHOST_BIND: 'loopback', PTYHOST_TREE_FILE: record },
|
|
134
|
+
stdio: 'ignore',
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
});
|
|
137
|
+
const read = (file) => {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const alive = (pid) => {
|
|
146
|
+
try {
|
|
147
|
+
process.kill(pid, 0);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
// The program (cmd) and the PING it started, once ptyhost has followed the tree (every 3 s).
|
|
155
|
+
const recorded = await t.waitFor(() => {
|
|
156
|
+
const r = read(record);
|
|
157
|
+
return r && r.ptyhost.pid === host.pid && r.processes.length >= 2 ? r : null;
|
|
158
|
+
}, WAIT.screen);
|
|
159
|
+
t.check(recorded !== null, 'ptyhost records its program and what it started', JSON.stringify(read(record)));
|
|
160
|
+
if (recorded) {
|
|
161
|
+
const endTree = (file) => {
|
|
162
|
+
const r = spawnSync(ptyhost, ['--end-tree', file], { encoding: 'utf8', windowsHide: true });
|
|
163
|
+
return { status: r.status, stdout: (r.stdout ?? '').trim() };
|
|
164
|
+
};
|
|
165
|
+
const copy = t.at('tree-copy.json');
|
|
166
|
+
fs.writeFileSync(copy, JSON.stringify(recorded));
|
|
167
|
+
const whileRunning = endTree(copy);
|
|
168
|
+
// The same ids with other start times: what a reused id looks like.
|
|
169
|
+
const forged = t.at('tree-forged.json');
|
|
170
|
+
fs.writeFileSync(forged, JSON.stringify({
|
|
171
|
+
ptyhost: { pid: recorded.ptyhost.pid, start: '1' },
|
|
172
|
+
processes: recorded.processes.map((p) => ({ pid: p.pid, start: String(BigInt(p.start) + 1n) })),
|
|
173
|
+
}));
|
|
174
|
+
const reused = endTree(forged);
|
|
175
|
+
t.check(whileRunning.status === 3 &&
|
|
176
|
+
reused.status === 0 &&
|
|
177
|
+
reused.stdout === 'ended 0 process(es)' &&
|
|
178
|
+
recorded.processes.every((p) => alive(p.pid)), 'nothing is ended while the ptyhost that recorded it runs, nor a process whose id was reused (start time)', JSON.stringify({ whileRunning, reused }));
|
|
179
|
+
host.kill();
|
|
180
|
+
await t.waitFor(() => host.exitCode !== null, 10000);
|
|
181
|
+
const left = fs.existsSync(record);
|
|
182
|
+
const ended = endTree(record);
|
|
183
|
+
await t.waitFor(() => recorded.processes.every((p) => !alive(p.pid)), 10000);
|
|
184
|
+
t.check(left && ended.status === 0 && recorded.processes.every((p) => !alive(p.pid)), 'a ptyhost ended from outside leaves its record, and --end-tree ends the processes in it', JSON.stringify({ left, ended, alive: recorded.processes.filter((p) => alive(p.pid)) }));
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (host.exitCode === null)
|
|
188
|
+
host.kill();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* it ended between the check and the kill: nothing left to stop */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
123
194
|
t.section("the agents' job: a last-resort backstop, set once");
|
|
124
195
|
{
|
|
125
196
|
// A job of the test's own (PTYHOST_JOB_NAME), never the agents' real one.
|
|
@@ -289,9 +360,46 @@ export const liveTerminalSuite = {
|
|
|
289
360
|
t.check(head.includes('\x1b[?1000h') && head.includes('\x1b[?1006h'), "a viewer joining after a screen clear gets the agent's mouse tracking before the replay", 'late viewer: ' + JSON.stringify(late.output().slice(0, 300)));
|
|
290
361
|
late.close();
|
|
291
362
|
}
|
|
363
|
+
if (kind) {
|
|
364
|
+
// Raw mouse input (passed through, the client sees the agent's modes): a drag and a move are dropped while
|
|
365
|
+
// the agent tracks clicks only (1000), the drag passes with 1002, every move with 1003.
|
|
366
|
+
const drag = '\x1b[<32;11;5M';
|
|
367
|
+
const move = '\x1b[<35;12;6M';
|
|
368
|
+
const round = async (setup, ready, marker) => {
|
|
369
|
+
if (setup) {
|
|
370
|
+
send(setup);
|
|
371
|
+
await t.waitFor(() => screen().includes(ready), WAIT.screen);
|
|
372
|
+
}
|
|
373
|
+
const from = screen().length;
|
|
374
|
+
send(drag);
|
|
375
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
376
|
+
send(move);
|
|
377
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
378
|
+
send(marker);
|
|
379
|
+
await t.waitFor(() => screen()
|
|
380
|
+
.slice(from)
|
|
381
|
+
.includes('got ' + marker), WAIT.screen);
|
|
382
|
+
return screen().slice(from);
|
|
383
|
+
};
|
|
384
|
+
const clicksOnly = await round('', '', 'y');
|
|
385
|
+
const drags = await round('D', 'DRAG-ON', 'z');
|
|
386
|
+
const moves = await round('A', 'MOVE-ON', 'w');
|
|
387
|
+
// (Two reports read at once are echoed on one line.)
|
|
388
|
+
const has = (text, report) => text.includes('<1b>' + report.slice(1));
|
|
389
|
+
t.check(clicksOnly.includes('got y') &&
|
|
390
|
+
!has(clicksOnly, drag) &&
|
|
391
|
+
!has(clicksOnly, move) &&
|
|
392
|
+
drags.includes('got z') &&
|
|
393
|
+
has(drags, drag) &&
|
|
394
|
+
!has(drags, move) &&
|
|
395
|
+
has(moves, drag) &&
|
|
396
|
+
has(moves, move), 'drag and motion reports reach the agent only once it asked for them (1002 drags, 1003 every move)', JSON.stringify({ clicksOnly: tail(clicksOnly, 200), drags: tail(drags, 200), moves: tail(moves, 200) }));
|
|
397
|
+
}
|
|
292
398
|
send('q');
|
|
293
399
|
const ended = await t.waitFor(() => screen().includes('CLIENT-DONE'), WAIT.exit);
|
|
294
|
-
|
|
400
|
+
const last = screen().slice(screen().lastIndexOf('got q'));
|
|
401
|
+
t.check(Boolean(ended) && (!kind || (last.includes('\x1b[?1000l') && last.includes('\x1b[?1003l'))), 'the client ends with the agent' +
|
|
402
|
+
(kind ? ' and turns mouse tracking (clicks, drags, motion) off in the terminal again' : ''), 'screen: ' + tail(screen()));
|
|
295
403
|
}
|
|
296
404
|
finally {
|
|
297
405
|
for (const v of viewers)
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
// The menu stays quick: no key or draw waits for a child process (tasklist is faked and counted),
|
|
2
|
-
// "Loading…" instead of holding up the keys, a
|
|
3
|
-
//
|
|
1
|
+
// The menu stays quick: no key or draw waits for a child process (tasklist is faked and counted), the first screen does
|
|
2
|
+
// not wait for the process list, a slow tab load shows "Loading…" instead of holding up the keys, a section seen before
|
|
3
|
+
// shows at once, a held key is drawn once, Esc is a key at once, an idle menu writes nothing, and the terminal writes
|
|
4
|
+
// only the rows that changed (the screen after the diffs is the same as a full draw, also after a resize). The process
|
|
5
|
+
// list is fetched only when it may have changed, and the settings file is read once for a whole list.
|
|
6
|
+
import fs from 'node:fs';
|
|
4
7
|
import { EventEmitter } from 'node:events';
|
|
5
8
|
import { PassThrough } from 'node:stream';
|
|
9
|
+
import { ConfigFile } from '../app/features/settings/index.js';
|
|
6
10
|
import { DEFAULTS } from '../core/config/index.js';
|
|
7
11
|
import { sleep } from '../core/util/index.js';
|
|
8
12
|
import { Menu } from '../cli/menu/menu.js';
|
|
9
|
-
import { LiveStatus } from '../cli/menu/live.js';
|
|
13
|
+
import { LiveStatus, listGap } from '../cli/menu/live.js';
|
|
10
14
|
import { ListPage } from '../cli/menu/pages/list-page.js';
|
|
11
15
|
import { App, Text, Terminal } from '../cli/tui/index.js';
|
|
12
|
-
import { supportsSyncUpdate } from '../cli/tui/terminal.js';
|
|
13
|
-
import { isSandboxRunning, useTasklistCache, useTasklistRunners } from '../host/sandbox/index.js';
|
|
16
|
+
import { ESCAPE_TIMEOUT_MS, supportsSyncUpdate } from '../cli/tui/terminal.js';
|
|
17
|
+
import { isSandboxRunning, readTasklist, sandboxPids, useTasklistCache, useTasklistRunners, } from '../host/sandbox/index.js';
|
|
18
|
+
import { staleMountNote } from '../host/storage/index.js';
|
|
14
19
|
const TASKLIST = [
|
|
15
20
|
'"System Idle Process","0","Services","0","8 K"',
|
|
16
21
|
'"WindowsSandboxClient.exe","4242","Console","1","9,000 K"',
|
|
@@ -124,6 +129,26 @@ export const menuPerfSuite = {
|
|
|
124
129
|
app.close();
|
|
125
130
|
t.check(result === 'bye' && handled === 31 && renders < 10, 'keys that are already waiting are handled first and drawn once, not once each', 'handled ' + handled + ', drawn ' + renders);
|
|
126
131
|
}
|
|
132
|
+
t.section('Esc is a key at once');
|
|
133
|
+
{
|
|
134
|
+
const tty = new FakeTty();
|
|
135
|
+
const term = tty.terminal();
|
|
136
|
+
term.start();
|
|
137
|
+
const times = [];
|
|
138
|
+
for (let i = 0; i < 3; i++) {
|
|
139
|
+
const at = Date.now();
|
|
140
|
+
tty.type('\x1b');
|
|
141
|
+
const key = await term.key();
|
|
142
|
+
times.push(Date.now() - at);
|
|
143
|
+
if (key.name !== 'escape')
|
|
144
|
+
times.push(9999);
|
|
145
|
+
}
|
|
146
|
+
tty.type('\x1b[B');
|
|
147
|
+
const arrow = await term.key();
|
|
148
|
+
term.stop();
|
|
149
|
+
t.check(Math.max(...times) < 250 && ESCAPE_TIMEOUT_MS <= 50, 'Esc arrives in ' + times.join(', ') + ' ms (Node waits 500 ms for the rest of a sequence by default)', times.join(', '));
|
|
150
|
+
t.check(arrow.name === 'down', 'an arrow key is still one key, not Esc and letters');
|
|
151
|
+
}
|
|
127
152
|
t.section('a slow load does not hold up the keys');
|
|
128
153
|
{
|
|
129
154
|
const tty = new FakeTty();
|
|
@@ -131,9 +156,10 @@ export const menuPerfSuite = {
|
|
|
131
156
|
class Slow extends ListPage {
|
|
132
157
|
label = 'Slow';
|
|
133
158
|
rows = [];
|
|
159
|
+
next = ['alpha', 'beta'];
|
|
134
160
|
async load() {
|
|
135
161
|
await sleep(500);
|
|
136
|
-
this.rows =
|
|
162
|
+
this.rows = this.next;
|
|
137
163
|
}
|
|
138
164
|
options() {
|
|
139
165
|
return { path: [], items: this.rows.map((r) => ({ value: r, label: r })) };
|
|
@@ -150,6 +176,14 @@ export const menuPerfSuite = {
|
|
|
150
176
|
t.check(waited < 400 && page.loading && text().includes('Loading'), 'the tab opens at once and says Loading… while the data comes', 'waited ' + waited + ' ms');
|
|
151
177
|
const arrived = await t.waitFor(() => (text().includes('beta') ? true : null), 5000, 20);
|
|
152
178
|
t.check(arrived === true && !page.loading, 'the list is built when the data arrives');
|
|
179
|
+
// Back on the section later: what it had shows at once, the fresh data replaces it when it is there.
|
|
180
|
+
page.next = ['alpha', 'gamma'];
|
|
181
|
+
const again = Date.now();
|
|
182
|
+
await page.show();
|
|
183
|
+
const shownIn = Date.now() - again;
|
|
184
|
+
t.check(shownIn < 100 && text().includes('beta'), 'a section seen before shows its last data at once (in ' + shownIn + ' ms), not Loading…', text());
|
|
185
|
+
const fresh = await t.waitFor(() => (text().includes('gamma') ? true : null), 5000, 20);
|
|
186
|
+
t.check(fresh === true && !text().includes('beta'), 'and its fresh data when it has loaded');
|
|
153
187
|
}
|
|
154
188
|
t.section('no child process on a key or a draw');
|
|
155
189
|
const saved = { hostOnly: process.env.AGENT_SANDBOX_HOST_ONLY };
|
|
@@ -177,24 +211,34 @@ export const menuPerfSuite = {
|
|
|
177
211
|
return TASKLIST;
|
|
178
212
|
},
|
|
179
213
|
refreshMs: 100,
|
|
214
|
+
// The sandbox's process is gone at every look: the list is fetched again on every tick.
|
|
215
|
+
processAlive: () => false,
|
|
180
216
|
});
|
|
217
|
+
const startedAt = Date.now();
|
|
181
218
|
const done = menu.start();
|
|
182
|
-
const shown = await t.waitFor(() => tty.writes.length > 0, 20000,
|
|
219
|
+
const shown = await t.waitFor(() => tty.writes.length > 0, 20000, 5);
|
|
220
|
+
const firstPaint = Date.now() - startedAt;
|
|
183
221
|
t.check(shown === true, 'the menu draws its first screen');
|
|
222
|
+
t.check(firstPaint < 1000, 'the first screen does not wait for a 1.5 second process list (drawn after ' + firstPaint + ' ms)');
|
|
184
223
|
const slowest = { ms: 0, key: '' };
|
|
224
|
+
const took = [];
|
|
185
225
|
const press = async (name, bytes) => {
|
|
186
226
|
const before = tty.writes.length;
|
|
187
227
|
const at = Date.now();
|
|
188
228
|
tty.type(bytes);
|
|
189
229
|
// Not every key changes the screen: give the ones that do a chance, and measure those.
|
|
190
|
-
await t.waitFor(() => tty.writes.length > before, 400,
|
|
191
|
-
const
|
|
192
|
-
if (tty.writes.length > before
|
|
193
|
-
|
|
230
|
+
await t.waitFor(() => tty.writes.length > before, 400, 2);
|
|
231
|
+
const ms = Date.now() - at;
|
|
232
|
+
if (tty.writes.length > before)
|
|
233
|
+
took.push(ms);
|
|
234
|
+
if (tty.writes.length > before && ms > slowest.ms) {
|
|
235
|
+
slowest.ms = ms;
|
|
194
236
|
slowest.key = name;
|
|
195
237
|
}
|
|
196
238
|
await sleep(15);
|
|
197
239
|
};
|
|
240
|
+
// The pages have their data (the first process list came after 1.5 s).
|
|
241
|
+
await sleep(1700);
|
|
198
242
|
const syncBefore = calls.sync;
|
|
199
243
|
for (const section of ['2', '3', '4', '5', '6', '7', '1'])
|
|
200
244
|
await press('section ' + section, section);
|
|
@@ -209,6 +253,21 @@ export const menuPerfSuite = {
|
|
|
209
253
|
' ' +
|
|
210
254
|
slowest.ms +
|
|
211
255
|
' ms)');
|
|
256
|
+
const median = [...took].sort((a, b) => a - b)[Math.floor(took.length / 2)] ?? 9999;
|
|
257
|
+
t.check(median < 100, 'a key is usually drawn at once (median ' + median + ' ms over ' + took.length + ' keys)');
|
|
258
|
+
// Left alone, the menu writes nothing: the process list keeps coming (every tick here), but nothing on the
|
|
259
|
+
// screen changes, so nothing is drawn.
|
|
260
|
+
await sleep(300);
|
|
261
|
+
const idleFrom = tty.writes.length;
|
|
262
|
+
const fetchesFrom = calls.async;
|
|
263
|
+
await sleep(3000);
|
|
264
|
+
const idleWrites = tty.writes.slice(idleFrom);
|
|
265
|
+
const idleBytes = idleWrites.join('').length;
|
|
266
|
+
t.check(idleWrites.length <= 1 && idleBytes < 4000 && calls.async > fetchesFrom, 'an idle menu writes nothing for 3 s (' +
|
|
267
|
+
idleWrites.length +
|
|
268
|
+
' writes, ' +
|
|
269
|
+
idleBytes +
|
|
270
|
+
' bytes) while the process list is looked at in the background', Text.strip(idleWrites.join('')).slice(0, 300));
|
|
212
271
|
tty.type(KEYS.ctrlC);
|
|
213
272
|
const code = await Promise.race([done, sleep(5000).then(() => -1)]);
|
|
214
273
|
t.check(code === 130, 'Ctrl+C leaves the menu');
|
|
@@ -244,5 +303,132 @@ export const menuPerfSuite = {
|
|
|
244
303
|
await sleep(80);
|
|
245
304
|
t.check(live.line() === 'status b' && changes === 2, 'a stopped status looks at nothing');
|
|
246
305
|
}
|
|
306
|
+
t.section('the process list is fetched only when it may have changed');
|
|
307
|
+
{
|
|
308
|
+
t.equal([0, 1, 2, 3, 4, 10, -1].map((n) => listGap(n, 2500, 30000)), [2500, 5000, 10000, 20000, 30000, 30000, 2500], 'while nothing changes, the gap between two lists doubles up to the longest');
|
|
309
|
+
t.equal(sandboxPids(TASKLIST + '\r\n"WindowsSandbox.exe","77","Console","1","5 K"'), [4242, 77], 'the ids of the sandbox programs are read from the list');
|
|
310
|
+
// A sandbox runs: its programs are checked by id, and the list is fetched again only when one has ended.
|
|
311
|
+
let fetches = 0;
|
|
312
|
+
let derived = 0;
|
|
313
|
+
let alive = true;
|
|
314
|
+
const running = new LiveStatus({
|
|
315
|
+
fetch: () => {
|
|
316
|
+
fetches++;
|
|
317
|
+
return Promise.resolve(TASKLIST);
|
|
318
|
+
},
|
|
319
|
+
derive: () => String(++derived),
|
|
320
|
+
onChange: () => undefined,
|
|
321
|
+
intervalMs: 20,
|
|
322
|
+
alive: () => alive,
|
|
323
|
+
needsList: () => true,
|
|
324
|
+
});
|
|
325
|
+
await running.start();
|
|
326
|
+
await sleep(400);
|
|
327
|
+
t.check(fetches === 1 && derived > 5, 'while the sandbox runs, its programs are checked by id: no new list (' +
|
|
328
|
+
fetches +
|
|
329
|
+
' fetched), the line is still derived (' +
|
|
330
|
+
derived +
|
|
331
|
+
' times)');
|
|
332
|
+
alive = false;
|
|
333
|
+
const refetched = await t.waitFor(() => fetches > 1, 3000, 10);
|
|
334
|
+
running.stop();
|
|
335
|
+
t.check(refetched === true, 'when one of them has ended, the list is fetched again');
|
|
336
|
+
// None runs: fewer and fewer lists, one at once after a key press or when the data folder changes.
|
|
337
|
+
fetches = 0;
|
|
338
|
+
let signal = 'a';
|
|
339
|
+
const idle = new LiveStatus({
|
|
340
|
+
fetch: () => {
|
|
341
|
+
fetches++;
|
|
342
|
+
return Promise.resolve('"System Idle Process","0","Services","0","8 K"');
|
|
343
|
+
},
|
|
344
|
+
derive: () => 'none',
|
|
345
|
+
onChange: () => undefined,
|
|
346
|
+
intervalMs: 20,
|
|
347
|
+
idleMaxMs: 100000,
|
|
348
|
+
needsList: () => true,
|
|
349
|
+
signal: () => signal,
|
|
350
|
+
});
|
|
351
|
+
await idle.start();
|
|
352
|
+
await sleep(1000);
|
|
353
|
+
const quiet = fetches;
|
|
354
|
+
t.check(quiet >= 2 && quiet <= 8, 'with nothing running, a second of 20 ms ticks fetched the list ' + quiet + ' times (not 50)');
|
|
355
|
+
signal = 'b';
|
|
356
|
+
const onSignal = await t.waitFor(() => fetches > quiet, 1000, 5);
|
|
357
|
+
t.check(onSignal === true, 'a change in the data folder fetches the list at once');
|
|
358
|
+
await sleep(100);
|
|
359
|
+
const beforeKey = fetches;
|
|
360
|
+
idle.touch();
|
|
361
|
+
const onKey = await t.waitFor(() => fetches > beforeKey, 1000, 5);
|
|
362
|
+
idle.stop();
|
|
363
|
+
t.check(onKey === true, 'so does a key press (someone is looking)');
|
|
364
|
+
// Inside a sandbox the list is never needed: nothing runs tasklist, not even a question asked meanwhile.
|
|
365
|
+
fetches = 0;
|
|
366
|
+
let syncRuns = 0;
|
|
367
|
+
useTasklistRunners({
|
|
368
|
+
sync: () => {
|
|
369
|
+
syncRuns++;
|
|
370
|
+
return '';
|
|
371
|
+
},
|
|
372
|
+
async: () => Promise.resolve(''),
|
|
373
|
+
});
|
|
374
|
+
try {
|
|
375
|
+
const inside = new LiveStatus({
|
|
376
|
+
fetch: () => {
|
|
377
|
+
fetches++;
|
|
378
|
+
return Promise.resolve('');
|
|
379
|
+
},
|
|
380
|
+
derive: () => 'inside',
|
|
381
|
+
onChange: () => undefined,
|
|
382
|
+
intervalMs: 20,
|
|
383
|
+
needsList: () => false,
|
|
384
|
+
});
|
|
385
|
+
await inside.start();
|
|
386
|
+
await sleep(200);
|
|
387
|
+
const list = readTasklist();
|
|
388
|
+
inside.stop();
|
|
389
|
+
t.check(fetches === 0 && syncRuns === 0 && list === '', 'when the list is not needed, it is never fetched and nothing asks tasklist meanwhile');
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
useTasklistRunners();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
t.section('nothing before the menu waits for tasklist');
|
|
396
|
+
{
|
|
397
|
+
let asked = 0;
|
|
398
|
+
const note = staleMountNote(t.dir('no-mounts'), 'main', () => {
|
|
399
|
+
asked++;
|
|
400
|
+
return false;
|
|
401
|
+
});
|
|
402
|
+
t.check(note === null && asked === 0, 'with no image mounted, "is a sandbox running?" is not even asked');
|
|
403
|
+
}
|
|
404
|
+
t.section('the settings file is read once for a whole list');
|
|
405
|
+
{
|
|
406
|
+
const file = t.write(t.at('settings', 'config.json'), JSON.stringify({ defaultHarness: 'codex', workspaceFilter: { exclude: ['a', 'b'] } }));
|
|
407
|
+
const config = new ConfigFile('user', file);
|
|
408
|
+
const reads = { count: 0 };
|
|
409
|
+
const original = fs.readFileSync;
|
|
410
|
+
const counting = ((...args) => {
|
|
411
|
+
if (String(args[0]) === file)
|
|
412
|
+
reads.count++;
|
|
413
|
+
return original(...args);
|
|
414
|
+
});
|
|
415
|
+
fs.readFileSync = counting;
|
|
416
|
+
let first;
|
|
417
|
+
try {
|
|
418
|
+
first = Array.from({ length: 200 }, () => config.valueInFile(['defaultHarness']));
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
fs.readFileSync = original;
|
|
422
|
+
}
|
|
423
|
+
t.check(reads.count === 1 && first.every((v) => v === 'codex'), '200 values in one run read the file once (' + reads.count + ' reads)');
|
|
424
|
+
const exclude = ['workspaceFilter', 'exclude'];
|
|
425
|
+
config.valueInFile(exclude).push('c');
|
|
426
|
+
t.equal(config.valueInFile(exclude), ['a', 'b'], 'a value handed out is a copy (changing it changes nothing)');
|
|
427
|
+
fs.writeFileSync(file, JSON.stringify({ defaultHarness: 'gemini' }));
|
|
428
|
+
await Promise.resolve();
|
|
429
|
+
t.equal(config.valueInFile(['defaultHarness']), 'gemini', 'the next run reads the file again (a change outside)');
|
|
430
|
+
const result = config.set(['defaultHarness'], 'opencode');
|
|
431
|
+
t.equal([result, config.valueInFile(['defaultHarness'])], ['saved', 'opencode'], 'a save is seen at once');
|
|
432
|
+
}
|
|
247
433
|
},
|
|
248
434
|
};
|
|
@@ -725,6 +725,10 @@ export const menuSuite = {
|
|
|
725
725
|
['main', false, false],
|
|
726
726
|
['second', true, true],
|
|
727
727
|
], 'rename keeps the default and active marks');
|
|
728
|
+
const ensured = [service.ensure('codex', 'main'), service.activeName('codex'), fs.readFileSync(codexAuth, 'utf8')];
|
|
729
|
+
t.equal(ensured, ['main', 'main', '{"token":"A"}'], 'a launch with --account switches the sandbox to that login');
|
|
730
|
+
t.equal([service.ensure('codex', null), service.activeName('codex'), fs.readFileSync(codexAuth, 'utf8')], ['second', 'second', '{"token":"B2"}'], 'a launch without --account switches the sandbox back to the default login');
|
|
731
|
+
t.equal([service.ensure('claude', null), service.ensure('nobody', 'x')], [null, null], 'an agent with no saved login, or one that keeps no login: nothing to switch');
|
|
728
732
|
await t.throws(() => service.save('x', false, 'nobody'), 'Unknown agent', 'an unknown agent: refused');
|
|
729
733
|
await t.throws(() => service.save('bad name', false, 'codex'), 'Invalid account name', 'bad names are refused');
|
|
730
734
|
t.section('every screen');
|