Files
oh-my-pi/packages/coding-agent/test/eval/bridge-timeout.test.ts
T
2026-07-27 16:43:53 +02:00

171 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import {
EVAL_TIMEOUT_PAUSE_OP,
EVAL_TIMEOUT_RESUME_OP,
isEvalTimeoutControlEvent,
withBridgeTimeoutPause,
} from "../../src/eval/bridge-timeout";
import { executeWithKernelBase, type GenericKernel } from "../../src/eval/executor-base";
import type { JsStatusEvent } from "../../src/eval/js/shared/types";
import type { KernelDisplayOutput } from "../../src/eval/py/display";
describe("withBridgeTimeoutPause", () => {
it("emits one pause before the operation and one resume after it settles", async () => {
const events: JsStatusEvent[] = [];
const value = await withBridgeTimeoutPause(
event => events.push(event),
async () => {
await Bun.sleep(80);
return "done";
},
{ deferExternalAbort: true },
);
expect(value).toBe("done");
expect(events.map(event => event.op)).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
expect(events.every(event => event.deferExternalAbort === true)).toBe(true);
const settledCount = events.length;
await Bun.sleep(40);
expect(events.length).toBe(settledCount);
});
it("resumes timeout accounting even when the operation throws", async () => {
const events: JsStatusEvent[] = [];
await expect(
withBridgeTimeoutPause(
event => events.push(event),
async () => {
await Bun.sleep(20);
throw new Error("boom");
},
),
).rejects.toThrow("boom");
expect(events.map(event => event.op)).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
});
it("runs the operation without emitting when no status sink is wired", async () => {
let ran = 0;
const value = await withBridgeTimeoutPause(undefined, async () => {
ran++;
await Bun.sleep(20);
return 42;
});
expect(value).toBe(42);
expect(ran).toBe(1);
});
it("identifies timeout-control events as non-renderable status", () => {
expect(isEvalTimeoutControlEvent({ op: EVAL_TIMEOUT_PAUSE_OP })).toBe(true);
expect(isEvalTimeoutControlEvent({ op: EVAL_TIMEOUT_RESUME_OP })).toBe(true);
expect(isEvalTimeoutControlEvent({ op: "agent", id: "subagent-1" })).toBe(false);
});
});
class TestCancelledError extends Error {
readonly timedOut: boolean;
constructor(timedOut: boolean) {
super(timedOut ? "timed out" : "cancelled");
this.name = "TestCancelledError";
this.timedOut = timedOut;
}
}
it("defers external aborts until an in-flight agent bridge call resumes", async () => {
const abortController = new AbortController();
const entered = Promise.withResolvers<void>();
const triggerAbort = Promise.withResolvers<void>();
const observed = Promise.withResolvers<boolean>();
const release = Promise.withResolvers<void>();
const kernel: GenericKernel<Record<string, string | null>> = {
async execute(_code, options) {
entered.resolve();
await triggerAbort.promise;
options.onDisplay({
type: "status",
event: { op: EVAL_TIMEOUT_PAUSE_OP, deferExternalAbort: true },
} satisfies KernelDisplayOutput);
abortController.abort(new Error("external interrupt"));
observed.resolve(options.signal?.aborted ?? false);
await release.promise;
options.onDisplay({
type: "status",
event: { op: EVAL_TIMEOUT_RESUME_OP, deferExternalAbort: true },
} satisfies KernelDisplayOutput);
return { status: "ok", cancelled: false, timedOut: false };
},
};
const resultPromise = executeWithKernelBase({
kernel,
code: "agent('slow')",
options: { signal: abortController.signal },
runIdPrefix: "test",
errorLogLabel: "test",
cancelledErrorClass: TestCancelledError,
buildKernelEnvPatch: () => ({}),
formatKernelTimeoutAnnotation: () => "kernel timed out",
formatTimeoutAnnotation: () => "timed out",
});
await entered.promise;
triggerAbort.resolve();
expect(await observed.promise).toBe(false);
release.resolve();
const result = await resultPromise;
expect(result.cancelled).toBe(true);
expect(result.exitCode).toBeUndefined();
});
it("does not defer external aborts for a completion bridge call", async () => {
const abortController = new AbortController();
const entered = Promise.withResolvers<void>();
const triggerAbort = Promise.withResolvers<void>();
const observed = Promise.withResolvers<boolean>();
const release = Promise.withResolvers<void>();
const kernel: GenericKernel<Record<string, string | null>> = {
async execute(_code, options) {
entered.resolve();
await triggerAbort.promise;
options.onDisplay({
type: "status",
event: { op: EVAL_TIMEOUT_PAUSE_OP },
} satisfies KernelDisplayOutput);
abortController.abort(new Error("external interrupt"));
observed.resolve(options.signal?.aborted ?? false);
await release.promise;
options.onDisplay({
type: "status",
event: { op: EVAL_TIMEOUT_RESUME_OP },
} satisfies KernelDisplayOutput);
return { status: "ok", cancelled: false, timedOut: false };
},
};
const resultPromise = executeWithKernelBase({
kernel,
code: "completion('slow')",
options: { signal: abortController.signal },
runIdPrefix: "test",
errorLogLabel: "test",
cancelledErrorClass: TestCancelledError,
buildKernelEnvPatch: () => ({}),
formatKernelTimeoutAnnotation: () => "kernel timed out",
formatTimeoutAnnotation: () => "timed out",
});
await entered.promise;
triggerAbort.resolve();
expect(await observed.promise).toBe(true);
release.resolve();
const result = await resultPromise;
expect(result.cancelled).toBe(true);
expect(result.exitCode).toBeUndefined();
});