fix(coding-agent): restored ask tool timeout

Ensured ask tool timeouts abort stalled UI selectors and return the recommended option instead of hanging. Added regression coverage for selectors that never settle.

Fixes #4995
This commit is contained in:
roboomp
2026-07-09 23:14:17 +00:00
parent e8d0a93db6
commit 4f4f852ad6
3 changed files with 128 additions and 12 deletions
@@ -0,0 +1,91 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
import type { AgentToolContext, AgentToolResult } from "@oh-my-pi/pi-agent-core";
import { getThemeByName, setThemeInstance } from "../src/modes/theme/theme";
import type { ToolSession } from "../src/tools";
import { AskTool, type AskToolDetails } from "../src/tools/ask";
type AskExecutionResult = AgentToolResult<AskToolDetails>;
async function drainMicrotasks(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
}
function createAskTool(): AskTool {
return new AskTool({
hasUI: true,
settings: {
get(key: string): unknown {
if (key === "ask.timeout") return 0.01;
if (key === "ask.notify") return "off";
if (key === "speech.enabled") return false;
return undefined;
},
},
getPlanModeState: () => ({ enabled: false }),
} as unknown as ToolSession);
}
describe("AskTool timeout", () => {
beforeAll(async () => {
const loaded = await getThemeByName("dark");
if (!loaded) throw new Error("theme unavailable");
setThemeInstance(loaded);
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("auto-selects the recommended option when the selector does not settle", async () => {
vi.useFakeTimers();
const select = vi.fn(() => new Promise<string | undefined>(() => {}));
const abort = vi.fn();
const context = {
hasUI: true,
ui: {
select,
editor: vi.fn(),
},
abort,
} as unknown as AgentToolContext;
let result: AskExecutionResult | undefined;
let rejection: unknown;
void createAskTool()
.execute(
"ask-timeout",
{
questions: [
{
id: "db",
question: "Which database?",
options: [{ label: "SQLite" }, { label: "Postgres" }],
recommended: 1,
},
],
},
undefined,
undefined,
context,
)
.then(
value => {
result = value;
},
error => {
rejection = error;
},
);
await drainMicrotasks();
vi.advanceTimersByTime(10);
await drainMicrotasks();
expect(rejection).toBeUndefined();
expect(result?.details?.selectedOptions).toEqual(["Postgres"]);
expect(result?.details?.timedOut).toBe(true);
expect(abort).not.toHaveBeenCalled();
});
});