diff --git a/packages/core/tests/brush-interactive.test.ts b/packages/core/tests/brush-interactive.test.ts index 23c04e9468..d184015a1e 100644 --- a/packages/core/tests/brush-interactive.test.ts +++ b/packages/core/tests/brush-interactive.test.ts @@ -126,10 +126,6 @@ describe.skipIf(REGISTRY_SH === undefined)("brush interactive PTY repaint", () = PS1: "AOS$ ", COLUMNS: "80", LINES: "14", - // The runner's input polling accrues active CPU while the shell - // idles between steps; keep the watchdog out of the test's way - // (mirrors the interactive-shell CLI). - AGENTOS_V8_CPU_TIME_LIMIT_MS: "600000", }, // A real PTY merges stdout+stderr; brush paints its prompt on stderr. onStderr: (d: Uint8Array) => term?.write(d), diff --git a/packages/core/tests/vim-interactive.test.ts b/packages/core/tests/vim-interactive.test.ts index bd0e9706d2..27e4085732 100644 --- a/packages/core/tests/vim-interactive.test.ts +++ b/packages/core/tests/vim-interactive.test.ts @@ -19,9 +19,13 @@ const { Terminal } = pkg; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, "../../.."); -const VIM_COMMAND_DIR = resolve(REPO_ROOT, ".local-cmds"); +// Local-fixture locations, overridable so the suite is not tied to one +// machine's layout (CI skips via the fixture gate below). +const VIM_COMMAND_DIR = + process.env.AGENTOS_VIM_FIXTURE_DIR ?? resolve(REPO_ROOT, ".local-cmds"); const VIM_BINARY = resolve(VIM_COMMAND_DIR, "vim"); const SNAP_DIR = + process.env.AGENTOS_VIM_SNAPSHOT_DIR ?? "/home/nathan/progress/agent-os/2026-06-28-just-shell-fix/vim-snapshots"; const VIM_ARGS = [ @@ -251,4 +255,81 @@ describe.skipIf(!existsSync(VIM_BINARY))("interactive vim over VM PTY", () => { }, TEST_TIMEOUT_MS, ); + + // Regression guard for the idle full-screen raw-mode case: the guest must + // survive a long idle stretch on the DEFAULT CPU watchdog (no + // AGENTOS_V8_CPU_TIME_LIMIT_MS override) and still consume a keystroke + // afterwards. Guards both the event-driven kernel wait servicing (idle must + // accrue ~zero active CPU) and the poll/read wake path (the keystroke must + // land promptly, not after a polling slice). + it( + "idles 60s+ in raw mode on the default CPU watchdog, then consumes a keystroke", + async () => { + assertVimAvailable(); + + const { AgentOs } = await import("../src/index.js"); + vm = await AgentOs.create({ + permissions: allowAll, + software: [materializeVimPackage()], + }); + await vm.mkdir("/work", { recursive: true }); + + const term = new Terminal({ cols: 80, rows: 24, allowProposedApi: true }); + let writes = Promise.resolve(); + const { shellId } = vm.openShell({ + command: "vim", + args: VIM_ARGS, + cols: 80, + rows: 24, + cwd: "/work", + env: { TERM: "xterm" }, + }); + const offData = vm.onShellData(shellId, (data) => { + const bytes = Buffer.from(data); + writes = writes.then( + () => new Promise((resolve) => term.write(bytes, resolve)), + ); + }); + + const waitForScreen = async ( + predicate: (current: string) => boolean, + label: string, + timeoutMs = 30_000, + ) => { + const deadline = Date.now() + timeoutMs; + let current = screen(term); + while (Date.now() < deadline) { + await sleep(250); + await writes; + current = screen(term); + if (predicate(current)) { + return current; + } + } + throw new Error(`timed out waiting for ${label}\n\n${current}`); + }; + + await waitForScreen((s) => s.includes("~"), "vim startup screen"); + + // Idle well past a minute. With polling-based input waits this idle + // stretch used to burn the 30s default CPU budget and the watchdog + // killed the isolate. + await sleep(65_000); + + await vm.writeShell(shellId, "iidle-survivor"); + await waitForScreen( + (s) => s.includes("idle-survivor"), + "keystrokes consumed after long idle", + 10_000, + ); + + await vm.writeShell(shellId, "\u001b:q!\r"); + await sleep(1_000); + + offData(); + void __disposeAllSharedSidecarsForTesting().catch(() => {}); + vm = undefined; + }, + TEST_TIMEOUT_MS, + ); }); diff --git a/packages/core/tests/vim-provides.test.ts b/packages/core/tests/vim-provides.test.ts index dd87a3cafa..6c1f452d6d 100644 --- a/packages/core/tests/vim-provides.test.ts +++ b/packages/core/tests/vim-provides.test.ts @@ -20,9 +20,13 @@ const { Terminal } = pkg; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, "../../.."); -const VIM_COMMAND_DIR = resolve(REPO_ROOT, ".local-cmds"); +// Local-fixture locations, overridable so the suite is not tied to one +// machine's layout (CI skips via the fixture gate below). +const VIM_COMMAND_DIR = + process.env.AGENTOS_VIM_FIXTURE_DIR ?? resolve(REPO_ROOT, ".local-cmds"); const VIM_BINARY = resolve(VIM_COMMAND_DIR, "vim"); const SNAP_DIR = + process.env.AGENTOS_VIM_SNAPSHOT_DIR ?? "/home/nathan/progress/agent-os/2026-06-30-package-provisioned-files-env/vim-provides-snapshots"; // Mirror packages/shell/src/main.ts: VIMRUNTIME pointed straight at a runtime diff --git a/packages/shell/src/main.ts b/packages/shell/src/main.ts index 572782426b..2f5872b646 100644 --- a/packages/shell/src/main.ts +++ b/packages/shell/src/main.ts @@ -62,7 +62,6 @@ const fallbackCommandDirs = [ "../secure-exec/registry/native/target/wasm32-wasip1/release/commands", ), ]; -const INTERACTIVE_SHELL_WASM_FUEL_MS = 24 * 60 * 60 * 1000; const BRUSH_SHELL_COMMANDS = new Set(["bash", "sh"]); const SHELL_OPTIONS_WITH_VALUES = new Set([ "--command", @@ -722,28 +721,14 @@ async function runTerminalAttempt( const cli = parseCli(process.argv.slice(2)); const env = buildEnv(cli); -if (cli.tty && env.AGENTOS_V8_CPU_TIME_LIMIT_MS === undefined) { - // Interactive sessions idle for hours; the wasm runner's input polling - // accrues active-CPU against the default 30s watchdog and kills the guest - // (vim/reedline die mid-session). Match the 24h interactive fuel budget. - env.AGENTOS_V8_CPU_TIME_LIMIT_MS = String(INTERACTIVE_SHELL_WASM_FUEL_MS); -} const mounts = buildMounts(cli); -const limits = cli.tty - ? { - resources: { - maxWasmFuel: INTERACTIVE_SHELL_WASM_FUEL_MS, - }, - } - : undefined; const vm: ShellVmHandle = cli.actor - ? await createActorShellVm({ software, mounts, limits }) + ? await createActorShellVm({ software, mounts }) : await AgentOs.create({ mounts, permissions: allowAll, software, - limits, }); let exitCode = 1;