openclaw / src /agents /cli-runner /execute-plugin.ask-user-timeout.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e249c6d verified
Raw
History Blame Contribute Delete
3.78 kB
import { afterEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import {
closePluginTestAdmissions,
createExecution,
runPlugin,
SUCCESS_RESULT,
} from "./execute-plugin.test-support.js";
async function startBlockedRun(options: {
timeoutMs: number;
getDeadline: () => number | undefined;
onChange?: (listener: () => void) => () => void;
}) {
const { context } = await createExecution({ timeoutMs: options.timeoutMs });
const started = createDeferred();
const run = runPlugin(
context,
async function* ({ abortSignal }) {
if (!abortSignal) {
throw new Error("Host execution did not expose its abort signal.");
}
started.resolve();
await new Promise<never>((_, reject) => {
abortSignal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
});
yield SUCCESS_RESULT;
},
{
noOutputTimeoutMs: 100,
activeToolCount: () => 1,
getActiveLoopbackAskUserDeadline: options.getDeadline,
onActiveLoopbackAskUserDeadlineChange: options.onChange ?? (() => () => {}),
},
);
await started.promise;
return { run };
}
afterEach(() => {
closePluginTestAdmissions();
vi.useRealTimers();
});
describe("plugin-owned CLI ask_user timeout", () => {
it.each([
["uses the exact question deadline", 4_000_000, 3_610_000, "no-output-timeout"],
["keeps the earlier overall deadline authoritative", 150, 150, "overall-timeout"],
] as const)("%s", async (_name, timeoutMs, advanceMs, reason) => {
vi.useFakeTimers();
const deadline = Date.now() + 3_610_000;
const { run } = await startBlockedRun({ timeoutMs, getDeadline: () => deadline });
let completed = false;
void run.then(() => {
completed = true;
});
if (reason === "no-output-timeout") {
await vi.advanceTimersByTimeAsync(900_000);
expect(completed).toBe(false);
await vi.advanceTimersByTimeAsync(advanceMs - 900_000);
} else {
await vi.advanceTimersByTimeAsync(advanceMs);
}
await expect(run).resolves.toMatchObject({
reason,
timedOut: true,
noOutputTimedOut: reason === "no-output-timeout",
});
});
it("does not let a short question shrink the blocked-tool watchdog floor", async () => {
vi.useFakeTimers();
const deadline = Date.now() + 40_000;
const { run } = await startBlockedRun({
timeoutMs: 2_000_000,
getDeadline: () => deadline,
});
let completed = false;
void run.then(() => {
completed = true;
});
await vi.advanceTimersByTimeAsync(899_999);
expect(completed).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(run).resolves.toMatchObject({
reason: "no-output-timeout",
noOutputTimedOut: true,
});
});
it("restores the remaining ordinary watchdog when ask_user clears early", async () => {
vi.useFakeTimers();
let deadline: number | undefined = Date.now() + 3_610_000;
const listeners = new Set<() => void>();
const { run } = await startBlockedRun({
timeoutMs: 2_000_000,
getDeadline: () => deadline,
onChange: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
});
await vi.advanceTimersByTimeAsync(200);
deadline = undefined;
listeners.forEach((listener) => listener());
await vi.advanceTimersByTimeAsync(899_799);
let completed = false;
void run.then(() => {
completed = true;
});
expect(completed).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await expect(run).resolves.toMatchObject({
reason: "no-output-timeout",
noOutputTimedOut: true,
});
});
});