Download src/agents/tools/agents-wait-tool.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 23.8 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/tools/agents-wait-tool.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/tools/agents-wait-tool.test.ts
-
curl -L -o agents-wait-tool.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/tools/agents-wait-tool.test.ts
23.8 kB
| import { expectDefined } from "@openclaw/normalization-core"; | |
| import ts from "typescript"; | |
| import { beforeEach, describe, expect, it, onTestFinished, vi } from "vitest"; | |
| import { applyCodeModeCatalog } from "../code-mode.js"; | |
| import { | |
| createCodeModeHarness, | |
| resetCodeModeTestState, | |
| runUntilCompleted, | |
| } from "../code-mode.test-support.js"; | |
| import type { SubagentRunRecord } from "../subagents/registry/subagent-registry.types.js"; | |
| const records = new Map<string, SubagentRunRecord>(); | |
| const registryEvents = vi.hoisted(() => ({ listeners: new Set<() => void>() })); | |
| vi.mock("../subagents/registry/subagent-registry.js", () => ({ | |
| getSubagentRunsByRunIds: (runIds: readonly string[]) => ({ | |
| entries: new Map( | |
| runIds.flatMap((runId) => { | |
| const entry = | |
| records.get(runId) ?? | |
| [...records.values()].find((candidate) => candidate.swarmRunId === runId); | |
| return entry ? [[runId, entry] as const] : []; | |
| }), | |
| ), | |
| }), | |
| })); | |
| vi.mock("../subagents/registry/subagent-registry-state.js", () => ({ | |
| onSubagentRegistryPersisted: (listener: () => void) => { | |
| registryEvents.listeners.add(listener); | |
| return () => registryEvents.listeners.delete(listener); | |
| }, | |
| })); | |
| import { isToolResultError } from "../tool-result-error.js"; | |
| import { createAgentsWaitTool, waitForCollectorCompletion } from "./agents-wait-tool.js"; | |
| function createMainSessionWaitTool() { | |
| return createAgentsWaitTool({ | |
| agentSessionKey: "agent:main:main", | |
| agentId: "main", | |
| config: { tools: { swarm: true } }, | |
| }); | |
| } | |
| function collectorRun( | |
| runId: string, | |
| requesterSessionKey: string, | |
| completion?: SubagentRunRecord["collectorCompletion"], | |
| ): SubagentRunRecord { | |
| return { | |
| runId, | |
| childSessionKey: `agent:worker:subagent:${runId}`, | |
| controllerSessionKey: requesterSessionKey, | |
| requesterSessionKey, | |
| requesterDisplayKey: requesterSessionKey, | |
| task: runId, | |
| cleanup: "keep", | |
| createdAt: Date.now(), | |
| execution: { status: completion ? "terminal" : "running" }, | |
| collect: true, | |
| swarmRequesterSessionKey: requesterSessionKey, | |
| groupId: "group", | |
| completion: { required: false, resultText: completion ? `result-${runId}` : undefined }, | |
| collectorCompletion: completion, | |
| }; | |
| } | |
| describe("agents_wait", () => { | |
| beforeEach(() => { | |
| records.clear(); | |
| registryEvents.listeners.clear(); | |
| }); | |
| it("composes real collector outputs through discovery, describe, and generated declarations", async () => { | |
| onTestFinished(resetCodeModeTestState); | |
| const entry = collectorRun("ready", "agent:main:main", { | |
| status: "done", | |
| structured: { answer: 42 }, | |
| }); | |
| records.set(entry.runId, entry); | |
| const h = createCodeModeHarness(); | |
| const tool = createMainSessionWaitTool(); | |
| applyCodeModeCatalog({ ...h.ctx, tools: [...h.tools, tool] }); | |
| const execTool = expectDefined(h.tools[0], "Code Mode exec"); | |
| const waitTool = expectDefined(h.tools[1], "Code Mode wait"); | |
| expect(execTool.description).toContain("completed: Array<"); | |
| const listed = await runUntilCompleted({ | |
| execTool, | |
| waitTool, | |
| code: 'return await API.list("tools");', | |
| }); | |
| expect(listed).toMatchObject({ | |
| status: "completed", | |
| value: { files: [{ path: "tools/agents_wait.d.ts" }] }, | |
| telemetry: { describeCount: 0, callCount: 0 }, | |
| }); | |
| const result = await runUntilCompleted({ | |
| execTool, | |
| waitTool, | |
| code: 'const [wait] = await catalog.search("agents_wait"); const description = await wait.describe(); const file = await API.read("tools/agents_wait.d.ts"); const result = await wait({ids:["ready"],timeoutSeconds:0}); return {description, file, ids:result.completed.map(item=>item.runId), structured:result.completed[0].structured};', | |
| }); | |
| expect(result).toMatchObject({ | |
| status: "completed", | |
| telemetry: { describeCount: 2, callCount: 1 }, | |
| value: { | |
| ids: ["ready"], | |
| structured: { answer: 42 }, | |
| description: { outputSchema: tool.outputSchema }, | |
| }, | |
| }); | |
| const { file } = result.value as { file: { content: string } }; | |
| // Consume the intact guest declaration, with opaque collector-specific output. | |
| expect(file.content).not.toContain("truncated: true"); | |
| const fileName = "/collector-consumer.ts"; | |
| const source = ts.createSourceFile( | |
| fileName, | |
| file.content + | |
| "\n" + | |
| [ | |
| "async function consume() {", | |
| 'const result = await agents_wait({ids:["ready"]});', | |
| "const ids: string[] = result.completed.map(item => item.runId);", | |
| "// @ts-expect-error No invented builds field.", | |
| "result.builds.map(item => item.id);", | |
| "// @ts-expect-error Collector structured output is unknown without its own schema.", | |
| "result.completed[0].structured.answer;", | |
| "return ids;", | |
| "}", | |
| "// @ts-expect-error Required ids stay required.", | |
| "agents_wait({});", | |
| ].join("\n"), | |
| ts.ScriptTarget.ESNext, | |
| true, | |
| ); | |
| const options = { noEmit: true, strict: true, types: [], target: ts.ScriptTarget.ESNext }; | |
| const host = ts.createCompilerHost(options); | |
| const original = host.getSourceFile.bind(host); | |
| host.getSourceFile = (name, ...args) => (name === fileName ? source : original(name, ...args)); | |
| const program = ts.createProgram([fileName], options, host); | |
| expect( | |
| ts | |
| .getPreEmitDiagnostics(program) | |
| .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")), | |
| ).toEqual([]); | |
| }); | |
| it("settles a parked collector bridge from a registry write event", async () => { | |
| const entry = collectorRun("event-driven", "agent:main:main"); | |
| records.set(entry.runId, entry); | |
| const completion = waitForCollectorCompletion({ | |
| runId: entry.runId, | |
| currentSessionKeys: new Set(["agent:main:main"]), | |
| }); | |
| entry.completion = { required: false, resultText: "event result" }; | |
| entry.collectorCompletion = { status: "done" }; | |
| for (const listener of registryEvents.listeners) { | |
| listener(); | |
| } | |
| await expect(completion).resolves.toMatchObject({ | |
| runId: "event-driven", | |
| status: "done", | |
| result: "event result", | |
| }); | |
| expect(registryEvents.listeners.size).toBe(0); | |
| }); | |
| it("returns retained visible output when a successful collector ends with NO_REPLY", async () => { | |
| const entry = collectorRun("retained", "agent:main:main", { status: "done" }); | |
| entry.execution = { status: "terminal", outcome: { status: "ok" } }; | |
| entry.completion = { | |
| required: false, | |
| resultText: "NO_REPLY", | |
| fallbackResultText: "retained collector result", | |
| }; | |
| records.set(entry.runId, entry); | |
| await expect( | |
| waitForCollectorCompletion({ | |
| runId: entry.runId, | |
| currentSessionKeys: new Set(["agent:main:main"]), | |
| }), | |
| ).resolves.toMatchObject({ result: "retained collector result" }); | |
| }); | |
| it("rejects when abort wins the listener-registration race", async () => { | |
| const entry = collectorRun("abort-race", "agent:main:main"); | |
| records.set(entry.runId, entry); | |
| const controller = new AbortController(); | |
| const originalAddEventListener = controller.signal.addEventListener.bind(controller.signal); | |
| vi.spyOn(controller.signal, "addEventListener").mockImplementation((...args) => { | |
| controller.abort(); | |
| originalAddEventListener(...args); | |
| }); | |
| await expect( | |
| waitForCollectorCompletion({ | |
| runId: entry.runId, | |
| currentSessionKeys: new Set(["agent:main:main"]), | |
| signal: controller.signal, | |
| }), | |
| ).rejects.toThrow("agents.run wait aborted"); | |
| expect(registryEvents.listeners.size).toBe(0); | |
| }); | |
| it("returns the first completed child and leaves siblings pending", async () => { | |
| records.set("one", collectorRun("one", "agent:main:main")); | |
| records.set("two", collectorRun("two", "agent:main:main")); | |
| const tool = createMainSessionWaitTool(); | |
| setTimeout(() => { | |
| const entry = records.get("two"); | |
| if (!entry) { | |
| return; | |
| } | |
| entry.completion = { required: false, resultText: "result-two" }; | |
| entry.collectorCompletion = { | |
| status: "done", | |
| structured: { winner: 2 }, | |
| }; | |
| for (const listener of registryEvents.listeners) { | |
| listener(); | |
| } | |
| }, 5); | |
| const result = await tool.execute("call", { ids: ["one", "two"], timeoutSeconds: 1 }); | |
| expect(result.details).toEqual({ | |
| completed: [ | |
| { | |
| runId: "two", | |
| status: "done", | |
| result: "result-two", | |
| structured: { winner: 2 }, | |
| sessionKey: "agent:worker:subagent:two", | |
| }, | |
| ], | |
| pending: ["one"], | |
| }); | |
| }); | |
| it("parks without reading until a registry mutation wakes it", async () => { | |
| vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "performance"] }); | |
| const entry = collectorRun("local-wake", "agent:main:main"); | |
| records.set(entry.runId, entry); | |
| const controller = new AbortController(); | |
| const tool = createMainSessionWaitTool(); | |
| const reads = vi.spyOn(records, "get"); | |
| let result: unknown; | |
| const waiting = tool | |
| .execute("call", { ids: [entry.runId], timeoutSeconds: 1 }, controller.signal) | |
| .then((value) => { | |
| result = value.details; | |
| }); | |
| try { | |
| const initialReads = reads.mock.calls.length; | |
| await vi.advanceTimersByTimeAsync(750); | |
| expect(reads).toHaveBeenCalledTimes(initialReads); | |
| entry.collectorCompletion = { status: "done" }; | |
| for (const listener of registryEvents.listeners) { | |
| listener(); | |
| } | |
| await vi.advanceTimersByTimeAsync(0); | |
| expect(result).toMatchObject({ completed: [{ runId: entry.runId }], pending: [] }); | |
| expect(registryEvents.listeners.size).toBe(0); | |
| expect(vi.getTimerCount()).toBe(0); | |
| } finally { | |
| reads.mockRestore(); | |
| controller.abort(); | |
| await waiting.catch(() => {}); | |
| vi.useRealTimers(); | |
| } | |
| }); | |
| it("projects an authorized collector failure without failing a mixed batch", async () => { | |
| const failed = collectorRun("failed", "agent:main:main", { | |
| status: "failed", | |
| structured: { partial: true }, | |
| }); | |
| failed.execution = { | |
| status: "terminal", | |
| outcome: { status: "error", error: "provider failed after tool output" }, | |
| }; | |
| failed.completion = { required: false, resultText: null, capturedAt: 10 }; | |
| records.set(failed.runId, failed); | |
| records.set("pending", collectorRun("pending", "agent:main:main")); | |
| const tool = createMainSessionWaitTool(); | |
| const result = await tool.execute("call", { | |
| ids: [failed.runId, "pending"], | |
| timeoutSeconds: 0, | |
| }); | |
| expect(result.details).toEqual({ | |
| completed: [ | |
| { | |
| runId: failed.runId, | |
| status: "failed", | |
| result: "", | |
| structured: { partial: true }, | |
| error: "provider failed after tool output", | |
| sessionKey: failed.childSessionKey, | |
| }, | |
| ], | |
| pending: ["pending"], | |
| }); | |
| expect(isToolResultError(result)).toBe(false); | |
| }); | |
| it.each([-60_000, 60_000])( | |
| "keeps its elapsed deadline after a %d ms clock step", | |
| async (step) => { | |
| vi.useFakeTimers({ toFake: ["Date", "performance", "setTimeout", "clearTimeout"] }); | |
| vi.setSystemTime(100_000); | |
| const controller = new AbortController(); | |
| records.set("clock-step", collectorRun("clock-step", "agent:main:main")); | |
| const tool = createMainSessionWaitTool(); | |
| let result: unknown; | |
| const waiting = tool | |
| .execute("clock", { ids: ["clock-step"], timeoutSeconds: 0.1 }, controller.signal) | |
| .then((value) => { | |
| result = value.details; | |
| }); | |
| try { | |
| await vi.advanceTimersByTimeAsync(25); | |
| vi.setSystemTime(Date.now() + step); | |
| await vi.advanceTimersByTimeAsync(74); | |
| expect(result).toBeUndefined(); | |
| await vi.advanceTimersByTimeAsync(1); | |
| expect(result).toEqual({ completed: [], pending: ["clock-step"] }); | |
| } finally { | |
| controller.abort(); | |
| await waiting.catch(() => {}); | |
| vi.useRealTimers(); | |
| } | |
| }, | |
| ); | |
| it("orders completions by durable capture time with input-order ties", async () => { | |
| const later = collectorRun("later", "agent:main:main", { status: "done" }); | |
| later.completion = { required: false, resultText: "later", capturedAt: 10 }; | |
| const earlier = collectorRun("earlier", "agent:main:main", { status: "done" }); | |
| earlier.completion = { required: false, resultText: "earlier", capturedAt: 5 }; | |
| records.set(later.runId, later); | |
| records.set(earlier.runId, earlier); | |
| const tool = createMainSessionWaitTool(); | |
| const result = await tool.execute("call", { | |
| ids: ["later", "earlier"], | |
| timeoutSeconds: 0, | |
| }); | |
| expect(result.details).toMatchObject({ | |
| completed: [{ runId: "earlier" }, { runId: "later" }], | |
| pending: [], | |
| }); | |
| const tied = collectorRun("tied", "agent:main:main", { status: "done" }); | |
| tied.completion = { required: false, resultText: "tied", capturedAt: 5 }; | |
| records.set(tied.runId, tied); | |
| records.set("foreign", collectorRun("foreign", "agent:other:main", { status: "done" })); | |
| records.set("pending-one", collectorRun("pending-one", "agent:main:main")); | |
| records.set("pending-two", collectorRun("pending-two", "agent:main:main")); | |
| const mixed = await tool.execute("mixed", { | |
| ids: ["later", "missing", "tied", "pending-two", "foreign", "earlier", "pending-one"], | |
| timeoutSeconds: 0, | |
| }); | |
| expect(mixed.details).toMatchObject({ | |
| completed: [{ runId: "tied" }, { runId: "earlier" }, { runId: "later" }], | |
| pending: ["pending-two", "pending-one"], | |
| errors: [ | |
| { runId: "missing", error: "not_found" }, | |
| { runId: "foreign", error: "not_owner" }, | |
| ], | |
| }); | |
| expect(isToolResultError(mixed)).toBe(false); | |
| }); | |
| it("is idempotent and returns per-id ownership and unknown errors", async () => { | |
| const done = collectorRun("done", "agent:worker:subagent:owner", { status: "done" }); | |
| done.swarmWaitOwnerSessionKeys = ["agent:worker:subagent:owner", "agent:main:main"]; | |
| records.set("done", done); | |
| records.set("owner", collectorRun("owner", "agent:main:main")); | |
| records.set("foreign", collectorRun("foreign", "agent:other:main", { status: "failed" })); | |
| const tool = createAgentsWaitTool({ | |
| agentSessionKey: "agent:main:main", | |
| agentId: "main", | |
| config: { tools: { swarm: { enabled: true, waitTimeoutSecondsMax: 1 } } }, | |
| }); | |
| const first = await tool.execute("call", { | |
| ids: ["done", "foreign", "missing"], | |
| timeoutSeconds: 5, | |
| }); | |
| const second = await tool.execute("call", { | |
| ids: ["done", "foreign", "missing"], | |
| timeoutSeconds: 5, | |
| }); | |
| expect(second.details).toEqual(first.details); | |
| expect(first.details).toMatchObject({ | |
| completed: [{ runId: "done", status: "done" }], | |
| pending: [], | |
| errors: [ | |
| { runId: "foreign", error: "not_owner" }, | |
| { runId: "missing", error: "not_found" }, | |
| ], | |
| }); | |
| expect(isToolResultError(first)).toBe(false); | |
| }); | |
| it("authorizes a snapshotted ancestor after the ordinary spawner row is archived", async () => { | |
| const ownerSessionKey = "agent:worker:subagent:ordinary-owner"; | |
| records.set("ordinary-owner", { | |
| runId: "ordinary-owner", | |
| childSessionKey: ownerSessionKey, | |
| controllerSessionKey: "agent:main:main", | |
| requesterSessionKey: "agent:main:main", | |
| requesterDisplayKey: "main", | |
| task: "spawn collector", | |
| cleanup: "delete", | |
| createdAt: Date.now(), | |
| execution: { status: "running" }, | |
| }); | |
| const completed = collectorRun("nested", ownerSessionKey, { status: "done" }); | |
| completed.swarmWaitOwnerSessionKeys = [ownerSessionKey, "agent:main:main"]; | |
| records.set(completed.runId, completed); | |
| records.delete("ordinary-owner"); | |
| const tool = createMainSessionWaitTool(); | |
| const result = await tool.execute("call", { ids: ["nested"], timeoutSeconds: 0 }); | |
| expect(result.details).toMatchObject({ | |
| completed: [{ runId: "nested", status: "done" }], | |
| pending: [], | |
| }); | |
| }); | |
| it("keeps the public collector id after gateway run replacement", async () => { | |
| const remapped = collectorRun("gateway-run", "agent:main:main", { status: "done" }); | |
| remapped.swarmRunId = "collector-run"; | |
| records.set(remapped.runId, remapped); | |
| const tool = createMainSessionWaitTool(); | |
| const result = await tool.execute("call", { ids: ["collector-run"], timeoutSeconds: 0 }); | |
| expect(result.details).toMatchObject({ | |
| completed: [{ runId: "collector-run", status: "done" }], | |
| pending: [], | |
| }); | |
| }); | |
| it("re-resolves a collector replaced while waiting", async () => { | |
| const pending = collectorRun("old-gateway-run", "agent:main:main"); | |
| pending.swarmRunId = "collector-run"; | |
| records.set(pending.runId, pending); | |
| const tool = createMainSessionWaitTool(); | |
| setTimeout(() => { | |
| records.delete(pending.runId); | |
| const completed = collectorRun("new-gateway-run", "agent:main:main", { status: "done" }); | |
| completed.swarmRunId = "collector-run"; | |
| records.set(completed.runId, completed); | |
| for (const listener of registryEvents.listeners) { | |
| listener(); | |
| } | |
| }, 5); | |
| const result = await tool.execute("call", { ids: ["collector-run"], timeoutSeconds: 1 }); | |
| expect(result.details).toMatchObject({ | |
| completed: [{ runId: "collector-run", status: "done" }], | |
| pending: [], | |
| }); | |
| }); | |
| it.each(["tool", "bridge"] as const)( | |
| "rejects a foreign replacement while the %s is parked", | |
| async (boundary) => { | |
| vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "performance"] }); | |
| const pending = collectorRun("old-gateway-run", "agent:main:main"); | |
| pending.swarmRunId = "collector-run"; | |
| records.set(pending.runId, pending); | |
| const controller = new AbortController(); | |
| const waiting = | |
| boundary === "tool" | |
| ? createMainSessionWaitTool() | |
| .execute("wait", { ids: ["collector-run"], timeoutSeconds: 1 }, controller.signal) | |
| .then((result) => result.details) | |
| : waitForCollectorCompletion({ | |
| runId: "collector-run", | |
| currentSessionKeys: new Set(["agent:main:main"]), | |
| currentAgentId: "main", | |
| signal: controller.signal, | |
| }); | |
| const observed = waiting.then( | |
| (value) => ({ value }), | |
| (error: unknown) => ({ error }), | |
| ); | |
| try { | |
| expect(registryEvents.listeners.size).toBe(1); | |
| records.delete(pending.runId); | |
| const foreign = collectorRun("new-gateway-run", "agent:other:main", { status: "done" }); | |
| foreign.swarmRunId = "collector-run"; | |
| foreign.completion = { required: false, resultText: "foreign result" }; | |
| records.set(foreign.runId, foreign); | |
| for (const listener of registryEvents.listeners) { | |
| listener(); | |
| } | |
| expect(await observed).toEqual( | |
| boundary === "tool" | |
| ? { | |
| value: { | |
| completed: [], | |
| pending: [], | |
| errors: [{ runId: "collector-run", error: "not_owner" }], | |
| success: false, | |
| }, | |
| } | |
| : { | |
| error: expect.objectContaining({ message: "agents.run not_owner: collector-run" }), | |
| }, | |
| ); | |
| expect(registryEvents.listeners.size).toBe(0); | |
| expect(vi.getTimerCount()).toBe(0); | |
| } finally { | |
| controller.abort(); | |
| await observed; | |
| vi.useRealTimers(); | |
| } | |
| }, | |
| ); | |
| it("does not treat a routed completion owner as the spawning session", async () => { | |
| const routed = collectorRun("routed", "agent:main:main", { status: "done" }); | |
| routed.controllerSessionKey = "agent:worker:route-a"; | |
| routed.swarmRequesterSessionKey = "agent:worker:route-a"; | |
| records.set(routed.runId, routed); | |
| const routedOwner = createAgentsWaitTool({ | |
| agentSessionKey: "agent:worker:route-a", | |
| agentId: "worker", | |
| config: { tools: { swarm: true } }, | |
| }); | |
| const completionOwner = createMainSessionWaitTool(); | |
| const allowed = await routedOwner.execute("owner", { ids: ["routed"], timeoutSeconds: 0 }); | |
| const denied = await completionOwner.execute("proxy", { | |
| ids: ["routed"], | |
| timeoutSeconds: 0, | |
| }); | |
| expect(allowed.details).toMatchObject({ completed: [{ runId: "routed" }] }); | |
| expect(denied.details).toEqual({ | |
| completed: [], | |
| pending: [], | |
| errors: [{ runId: "routed", error: "not_owner" }], | |
| success: false, | |
| }); | |
| expect(isToolResultError(denied)).toBe(true); | |
| }); | |
| it("rejects a foreign collector with the same bare requester key", async () => { | |
| const foreign = collectorRun("foreign-global", "global", { status: "done" }); | |
| foreign.requesterAgentId = "ops"; | |
| records.set(foreign.runId, foreign); | |
| const tool = createAgentsWaitTool({ | |
| agentSessionKey: "global", | |
| agentId: "research", | |
| config: { | |
| agents: { ownership: "explicit", entries: { research: {}, ops: {} } }, | |
| tools: { swarm: true }, | |
| }, | |
| }); | |
| const result = await tool.execute("wait", { | |
| ids: [foreign.runId], | |
| timeoutSeconds: 0, | |
| }); | |
| expect(result.details).toMatchObject({ | |
| errors: [{ runId: foreign.runId, error: "not_owner" }], | |
| success: false, | |
| }); | |
| }); | |
| it("marks entirely missing collector batches as failures without losing per-id errors", async () => { | |
| const tool = createMainSessionWaitTool(); | |
| const result = await tool.execute("call", { ids: ["missing"], timeoutSeconds: 0 }); | |
| expect(result.details).toEqual({ | |
| completed: [], | |
| pending: [], | |
| errors: [{ runId: "missing", error: "not_found" }], | |
| success: false, | |
| }); | |
| expect(isToolResultError(result)).toBe(true); | |
| }); | |
| it("rejects collector batches containing only blank run ids", async () => { | |
| const tool = createMainSessionWaitTool(); | |
| await expect(tool.execute("call", { ids: [" ", "\t"], timeoutSeconds: 0 })).rejects.toThrow( | |
| "at least one non-empty run id", | |
| ); | |
| }); | |
| it.each(["before", "during", "registration"] as const)( | |
| "rejects when the wait is aborted %s collector waiting", | |
| async (abortTiming) => { | |
| records.set("pending", collectorRun("pending", "agent:main:main")); | |
| const tool = createMainSessionWaitTool(); | |
| const controller = new AbortController(); | |
| if (abortTiming === "before") { | |
| controller.abort(); | |
| } | |
| if (abortTiming === "registration") { | |
| const addEventListener = controller.signal.addEventListener.bind(controller.signal); | |
| vi.spyOn(controller.signal, "addEventListener").mockImplementation((...args) => { | |
| controller.abort(); | |
| addEventListener(...args); | |
| }); | |
| } | |
| const result = tool.execute( | |
| "call", | |
| { ids: ["pending"], timeoutSeconds: 1 }, | |
| controller.signal, | |
| ); | |
| if (abortTiming === "during") { | |
| controller.abort(); | |
| } | |
| await expect(result).rejects.toMatchObject({ | |
| name: "AbortError", | |
| message: "agents_wait aborted.", | |
| }); | |
| }, | |
| ); | |
| it("rejects oversized wait batches before waiting", async () => { | |
| const tool = createMainSessionWaitTool(); | |
| await expect( | |
| tool.execute("call", { | |
| ids: Array.from({ length: 1_001 }, (_, index) => `run-${index}`), | |
| }), | |
| ).rejects.toThrow("at most 1000 ids"); | |
| }); | |
| }); | |