Skip to content

Commit 5c8ff37

Browse files
Gustav-SimonssonTheGreatAxios
authored andcommitted
perf(tests): cut fixed wall-clock waits from the slowest files
1 parent a43e3bb commit 5c8ff37

20 files changed

Lines changed: 617 additions & 516 deletions

‎scripts/ci-timings.json‎

Lines changed: 452 additions & 447 deletions
Large diffs are not rendered by default.

‎scripts/test-parallel.test.ts‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ function processAlive(pid: number): boolean {
1515
}
1616

1717
// These probes are `bun -e` one-liners, so they never import project modules
18-
// and finish in seconds. Stall windows are ~1.5s: under the `--parallel`
19-
// load this runner exists to enable, a delayed output tick can look like a
20-
// stall against a tighter window, so the window keeps several ticks of
21-
// scheduling headroom while a broken never-resetting timer still fires
22-
// mid-run (the tick test's total runtime exceeds the window).
18+
// and finish in seconds. The tick test keeps a ~1.5s stall window: under the
19+
// `--parallel` load this runner exists to enable, a delayed output tick can
20+
// look like a stall against a tighter window, so the window keeps several
21+
// ticks of scheduling headroom while a broken never-resetting timer still
22+
// fires mid-run (the tick test's total runtime exceeds the window). The
23+
// remaining tests drive children that never emit output, so their stall is
24+
// declared deterministically on the watchdog's first tick; they use a short
25+
// window so the suite does not pay the tick test's headroom in wall clock.
2326
const TEST_STALL_MS = 1_500;
27+
const SILENT_CHILD_STALL_MS = 400;
2428

2529
describe("runWithWatchdog", () => {
2630
test("passes through a successful run without retrying", async () => {
@@ -58,7 +62,7 @@ describe("runWithWatchdog", () => {
5862
command: process.execPath,
5963
args: [
6064
"-e",
61-
"for (let i = 0; i < 10; i++) { console.log('tick', i); await new Promise(r => setTimeout(r, 250)); }",
65+
"for (let i = 0; i < 10; i++) { console.log('tick', i); await new Promise(r => setTimeout(r, 200)); }",
6266
],
6367
stallMs: TEST_STALL_MS,
6468
});
@@ -83,7 +87,7 @@ describe("runWithWatchdog", () => {
8387
const result = await runWithWatchdog({
8488
command: process.execPath,
8589
args: ["-e", code],
86-
stallMs: TEST_STALL_MS,
90+
stallMs: SILENT_CHILD_STALL_MS,
8791
onStall: (attempt, max) => stalls.push([attempt, max]),
8892
});
8993
expect(result.exitCode).toBe(0);
@@ -100,9 +104,9 @@ describe("runWithWatchdog", () => {
100104
command: process.execPath,
101105
args: [
102106
"-e",
103-
'process.on("SIGTERM", () => {}); setTimeout(() => process.exit(3), 4_000);',
107+
'process.on("SIGTERM", () => {}); setTimeout(() => process.exit(3), 1_500);',
104108
],
105-
stallMs: TEST_STALL_MS,
109+
stallMs: SILENT_CHILD_STALL_MS,
106110
onStall: () => {
107111
throw new Error("must not retry a run that exited on its own");
108112
},
@@ -120,7 +124,7 @@ describe("runWithWatchdog", () => {
120124
const result = await runWithWatchdog({
121125
command: process.execPath,
122126
args: ["-e", "setTimeout(() => {}, 30_000)"],
123-
stallMs: TEST_STALL_MS,
127+
stallMs: SILENT_CHILD_STALL_MS,
124128
attempts: 2,
125129
onStall: (attempt, max) => stalls.push([attempt, max]),
126130
});
@@ -144,7 +148,7 @@ describe("runWithWatchdog", () => {
144148
const result = await runWithWatchdog({
145149
command: process.execPath,
146150
args: ["-e", code],
147-
stallMs: TEST_STALL_MS,
151+
stallMs: SILENT_CHILD_STALL_MS,
148152
attempts: 1,
149153
});
150154
expect(result.exitCode).toBe(1);

‎src/agent/background-shell-tool.test.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import { defined } from "../../tests/helpers/defined.js";
22
import { describe, expect, test } from "bun:test";
33
import { spawnSync } from "node:child_process";
44
import { randomUUID } from "node:crypto";
5+
6+
/** Poll until no process carries `token`; fail instead of asserting on a pid. */
7+
async function waitUntilGone(token: string): Promise<void> {
8+
const started = Date.now();
9+
while (Date.now() - started < 5_000) {
10+
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
11+
if ((probe.stdout?.trim() ?? "").length === 0) return;
12+
await new Promise((r) => setTimeout(r, 50));
13+
}
14+
throw new Error(`tagged child still alive after 5s: ${token}`);
15+
}
516
import { createPermissionGate } from "../permission/gate.js";
617
import { shellCollectDefinition } from "./background-shell-tool.js";
718
import { createAgentToolset } from "./tools.js";
@@ -120,10 +131,7 @@ describe("background shell through the agent toolset", () => {
120131
expect(JSON.parse(String(cancelled.content))).toMatchObject({
121132
status: "cancelling",
122133
});
123-
await new Promise((r) => setTimeout(r, 300));
124-
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
125-
expect(probe.stdout?.trim() ?? "").toBe("");
126-
expect(probe.status).not.toBe(0);
134+
await waitUntilGone(token);
127135
} finally {
128136
await toolset.dispose();
129137
}
@@ -221,10 +229,7 @@ describe("background shell through the agent toolset", () => {
221229
);
222230
expect(started.isError).not.toBe(true);
223231
await toolset.dispose();
224-
await new Promise((r) => setTimeout(r, 300));
225-
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
226-
expect(probe.stdout?.trim() ?? "").toBe("");
227-
expect(probe.status).not.toBe(0);
232+
await waitUntilGone(token);
228233
});
229234
});
230235

‎src/auth/store.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,13 @@ describe("createAuthStore", () => {
164164
test("gives queued same-process writes their own lock window", async () => {
165165
const home = await mkdtemp(join(tmpdir(), "oauth-store-queue-"));
166166
try {
167+
// A short lock window keeps the queued write's handoff fast without
168+
// changing what is asserted; production keeps the 1s default.
167169
const store = createAuthStore<TestTokens>({
168170
filename: "test-auth.json",
169171
settingsDirName: TEST_SETTINGS_DIR,
170172
isTokens: isTestTokens,
173+
lockTimeoutMs: 100,
171174
});
172175
await store.saveProfile(
173176
{
@@ -294,10 +297,12 @@ describe("createAuthStore", () => {
294297
test("fails closed with manual recovery guidance when an orphan lock exists", async () => {
295298
const home = await mkdtemp(join(tmpdir(), "oauth-store-orphan-"));
296299
try {
300+
// A short lock window keeps the timeout fast; production keeps 1s.
297301
const store = createAuthStore<TestTokens>({
298302
filename: "test-auth.json",
299303
settingsDirName: TEST_SETTINGS_DIR,
300304
isTokens: isTestTokens,
305+
lockTimeoutMs: 100,
301306
});
302307
const lockPath = `${store.authPath(home)}.lock`;
303308
await mkdir(join(home, TEST_SETTINGS_DIR), { recursive: true });

‎src/auth/store.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export interface AuthStoreOptions<TTokens extends BaseTokens> {
4040
filename: string;
4141
settingsDirName: string;
4242
isTokens: (value: unknown) => value is TTokens;
43+
/**
44+
* Override the credential-lock wait. Tests pass a short window so they do
45+
* not pay the production 1s in wall clock; production never sets it and
46+
* keeps the default.
47+
*/
48+
lockTimeoutMs?: number;
4349
}
4450

4551
interface AuthFile<TTokens extends BaseTokens> {
@@ -137,7 +143,7 @@ export function createAuthStore<TTokens extends BaseTokens>(
137143
const path = authPath(home);
138144
const lockPath = `${path}.lock`;
139145
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
140-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
146+
const deadline = Date.now() + (options.lockTimeoutMs ?? LOCK_TIMEOUT_MS);
141147
let lock;
142148

143149
while (true) {

‎src/plugins/shell-guard-plugin.test.ts‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ import {
2727

2828
const neverAbort = () => new AbortController().signal;
2929

30+
/** Poll until no process carries `token`; fail instead of asserting on a pid. */
31+
async function waitUntilGone(token: string): Promise<void> {
32+
const started = Date.now();
33+
while (Date.now() - started < 5_000) {
34+
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
35+
if ((probe.stdout?.trim() ?? "").length === 0) return;
36+
await new Promise((r) => setTimeout(r, 50));
37+
}
38+
throw new Error(`tagged child still alive after 5s: ${token}`);
39+
}
40+
3041
function toolContentTrimmed(result: ToolResult): string {
3142
const content = result.content;
3243
return (typeof content === "string" ? content : String(content)).trim();
@@ -170,10 +181,7 @@ describe("runGuardedShell", () => {
170181
const token = `ic_guard_orphan_${randomUUID()}`;
171182
const cmd = `bash -c 'IC_GUARD_TAG=${token} sleep 600 & IC_GUARD_TAG=${token} exec sleep 600'`;
172183
await runGuardedShell({ command: cmd, timeout: 250 }, neverAbort());
173-
await new Promise((r) => setTimeout(r, 300));
174-
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
175-
expect(probe.stdout?.trim() ?? "").toBe("");
176-
expect(probe.status).not.toBe(0);
184+
await waitUntilGone(token);
177185
});
178186

179187
test("abort kills grandchildren in a shell pipeline", async () => {
@@ -187,10 +195,7 @@ describe("runGuardedShell", () => {
187195
);
188196
setTimeout(() => controller.abort(), 80);
189197
await expect(promise).rejects.toThrow(/aborted/);
190-
await new Promise((r) => setTimeout(r, 300));
191-
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
192-
expect(probe.stdout?.trim() ?? "").toBe("");
193-
expect(probe.status).not.toBe(0);
198+
await waitUntilGone(token);
194199
});
195200

196201
test("abort kills the process group", async () => {
@@ -1095,10 +1100,7 @@ describe("shellGuardPlugin", () => {
10951100
).not.toBe("");
10961101
expect(plugin.dispose).toBeDefined();
10971102
await defined(plugin.dispose)();
1098-
await new Promise((r) => setTimeout(r, 300));
1099-
const after = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
1100-
expect(after.stdout?.trim() ?? "").toBe("");
1101-
expect(after.status).not.toBe(0);
1103+
await waitUntilGone(token);
11021104
await defined(plugin.dispose)();
11031105
await running;
11041106
} finally {
@@ -1218,8 +1220,8 @@ describe("shellGuardPlugin", () => {
12181220
signalCode: null,
12191221
kill: () => true,
12201222
}) as ChildProcess;
1221-
await expect(reapLiveChildren(new Set([child]))).rejects.toThrow(
1222-
/still live after 2000ms reap/,
1223+
await expect(reapLiveChildren(new Set([child]), 60)).rejects.toThrow(
1224+
/still live after 60ms reap/,
12231225
);
12241226
}, 10_000);
12251227
});

‎src/plugins/shell-guard-plugin.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,16 +279,19 @@ function childStillLive(child: ChildProcess): boolean {
279279
// Abort SIGKILLs the process group immediately (runGuardedShell onAbort). This
280280
// window is only a backstop for children still tracked at dispose. Leftovers
281281
// after it must fail teardown; do not stretch the process-exit 2s deadline.
282+
// Tests pass a short window so they do not pay the production 2s in wall
283+
// clock; production never passes it and keeps the default.
282284
export async function reapLiveChildren(
283285
liveChildren: Set<ChildProcess>,
286+
reapWindowMs: number = SHELL_GUARD_DISPOSE_REAP_MS,
284287
): Promise<void> {
285288
const remaining = [...liveChildren];
286289
for (const child of remaining) killProcessTree(child);
287290
if (remaining.length === 0) return;
288291
const closed = Promise.all(remaining.map(waitChildClose));
289292
let timer: ReturnType<typeof setTimeout> | undefined;
290293
const timedOut = new Promise<"timeout">((resolve) => {
291-
timer = setTimeout(() => resolve("timeout"), SHELL_GUARD_DISPOSE_REAP_MS);
294+
timer = setTimeout(() => resolve("timeout"), reapWindowMs);
292295
});
293296
try {
294297
const winner = await Promise.race([
@@ -299,7 +302,7 @@ export async function reapLiveChildren(
299302
const stillLive = remaining.filter(childStillLive);
300303
if (stillLive.length > 0) {
301304
throw new Error(
302-
`${stillLive.length} shell child process${stillLive.length === 1 ? "" : "es"} still live after ${SHELL_GUARD_DISPOSE_REAP_MS}ms reap`,
305+
`${stillLive.length} shell child process${stillLive.length === 1 ? "" : "es"} still live after ${reapWindowMs}ms reap`,
303306
);
304307
}
305308
} finally {

‎src/session/hooks.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe("lifecycle hook payload delivery", () => {
103103
process.on("unhandledRejection", onUnhandled);
104104
try {
105105
await manager.dispatchPostRun(summary);
106-
await new Promise((resolve) => setTimeout(resolve, 200));
106+
await new Promise((resolve) => setTimeout(resolve, 100));
107107
} finally {
108108
process.off("unhandledRejection", onUnhandled);
109109
}

‎src/shell/background-shell.test.ts‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,22 @@ import {
1010

1111
const tmpCwd = process.cwd();
1212

13+
/** Poll until no process carries `token`; fail instead of asserting on a pid. */
14+
async function waitUntilGone(token: string): Promise<void> {
15+
const started = Date.now();
16+
while (Date.now() - started < 5_000) {
17+
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
18+
if ((probe.stdout?.trim() ?? "").length === 0) return;
19+
await new Promise((r) => setTimeout(r, 50));
20+
}
21+
throw new Error(`tagged child still alive after 5s: ${token}`);
22+
}
23+
1324
describe("background shell registry", () => {
1425
test("start returns a handle immediately while the process runs", async () => {
1526
const registry = createBackgroundShellRegistry();
1627
const started = registry.start({
17-
command: "sleep 1; echo done",
28+
command: "sleep 0.5; echo done",
1829
cwd: tmpCwd,
1930
});
2031
if ("error" in started) throw new Error(started.error);
@@ -36,7 +47,10 @@ describe("background shell registry", () => {
3647
const started = registry.start({ command: "echo hi", cwd: tmpCwd });
3748
if ("error" in started) throw new Error(started.error);
3849
await registry.collect(started.id, 5_000);
39-
await new Promise((r) => setTimeout(r, 50));
50+
const deadline = Date.now() + 5_000;
51+
while (exits.length === 0 && Date.now() < deadline) {
52+
await new Promise((r) => setTimeout(r, 20));
53+
}
4054
expect(exits).toHaveLength(1);
4155
});
4256

@@ -68,10 +82,7 @@ describe("background shell registry", () => {
6882
expect(registry.cancel(started.id)).toBe(true);
6983
const exited = await registry.collect(started.id, 5_000);
7084
expect(exited.state).toBe("completed");
71-
await new Promise((r) => setTimeout(r, 300));
72-
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
73-
expect(probe.stdout?.trim() ?? "").toBe("");
74-
expect(probe.status).not.toBe(0);
85+
await waitUntilGone(token);
7586
});
7687

7788
test("cancel on an unknown id returns false", () => {
@@ -115,10 +126,7 @@ describe("background shell registry", () => {
115126
});
116127
if ("error" in started) throw new Error(started.error);
117128
registry.disposeAll("session closed");
118-
await new Promise((r) => setTimeout(r, 300));
119-
const probe = spawnSync("pgrep", ["-f", token], { encoding: "utf8" });
120-
expect(probe.stdout?.trim() ?? "").toBe("");
121-
expect(probe.status).not.toBe(0);
129+
await waitUntilGone(token);
122130
const after = await registry.collect(started.id, 0);
123131
expect(after.state).toBe("not-found");
124132
});

‎src/subagent/agent-fleet.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ import { join } from "node:path";
3434
import { tool } from "@intx/agent";
3535
import type { AgentTool } from "@intx/agent";
3636
import { type } from "arktype";
37-
import type { ToolDefinition, ToolResult } from "@intx/types/runtime";
37+
import type {
38+
RetryPolicy,
39+
ToolDefinition,
40+
ToolResult,
41+
} from "@intx/types/runtime";
3842
import type { ReactorEmittedEvent } from "@intx/inference";
3943
import { getLogger } from "@intx/log";
4044

@@ -643,6 +647,13 @@ export type AgentFleetDeps = SubAgentSandboxDeps & {
643647
telemetry?: Telemetry;
644648
/** Tests inject a private queue. Production omits and uses the process singleton. */
645649
admission?: AdmissionQueue;
650+
/**
651+
* Retry-timing overrides forwarded to each spawned run. Tests inject fast
652+
* values so retry suites do not pay the production backoff; production
653+
* omits them and keeps the defaults.
654+
*/
655+
outerRetryDelayMs?: number;
656+
retryPolicy?: RetryPolicy;
646657
};
647658

648659
function resolveDep<T>(value: T | (() => T)): T {
@@ -1334,6 +1345,12 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool {
13341345
: {}),
13351346
signal: childCtl.signal,
13361347
admission,
1348+
...(deps.outerRetryDelayMs !== undefined
1349+
? { outerRetryDelayMs: deps.outerRetryDelayMs }
1350+
: {}),
1351+
...(deps.retryPolicy !== undefined
1352+
? { retryPolicy: deps.retryPolicy }
1353+
: {}),
13371354
onEvent,
13381355
onRunSettled: (summary) => {
13391356
settlement = summary;

0 commit comments

Comments
 (0)