Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/core/tests/brush-interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
83 changes: 82 additions & 1 deletion packages/core/tests/vim-interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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<void>((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,
);
});
6 changes: 5 additions & 1 deletion packages/core/tests/vim-provides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 1 addition & 16 deletions packages/shell/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down
Loading