test: trim agents shard waits

This commit is contained in:
Peter Steinberger
2026-04-10 21:06:51 +01:00
parent eab6fcedaa
commit 733137615f
4 changed files with 76 additions and 78 deletions

View File

@@ -2,21 +2,16 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
clearActiveMcpLoopbackRuntime,
setActiveMcpLoopbackRuntime,
} from "../gateway/mcp-http.loopback-runtime.js";
import { onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js";
import {
makeBootstrapWarn as realMakeBootstrapWarn,
resolveBootstrapContextForRun as realResolveBootstrapContextForRun,
} from "./bootstrap-files.js";
import { runClaudeCliAgent } from "./cli-runner.js";
import { buildRunClaudeCliAgentParams } from "./cli-runner.js";
import {
createManagedRun,
mockSuccessfulCliRun,
restoreCliRunnerPrepareTestDeps,
setupCliRunnerTestRegistry,
supervisorSpawnMock,
} from "./cli-runner.test-support.js";
import { buildCliEnvAuthLog, executePreparedCliRun } from "./cli-runner/execute.js";
@@ -103,19 +98,6 @@ function buildPreparedCliRunContext(params: {
};
}
function createClaudeSuccessRun(sessionId: string) {
return createManagedRun({
reason: "exit",
exitCode: 0,
exitSignal: null,
durationMs: 50,
stdout: JSON.stringify({ message: "ok", session_id: sessionId }),
stderr: "",
timedOut: false,
noOutputTimedOut: false,
});
}
describe("runCliAgent spawn path", () => {
it("does not inject hardcoded 'Tools are disabled' text into CLI arguments", async () => {
supervisorSpawnMock.mockResolvedValueOnce(
@@ -386,11 +368,8 @@ describe("runCliAgent spawn path", () => {
}
});
it("ignores legacy claudeSessionId on the compat wrapper", async () => {
setupCliRunnerTestRegistry();
supervisorSpawnMock.mockResolvedValueOnce(createClaudeSuccessRun("sid-wrapper"));
await runClaudeCliAgent({
it("ignores legacy claudeSessionId on the compat wrapper", () => {
const params = buildRunClaudeCliAgentParams({
sessionId: "openclaw-session",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
@@ -401,38 +380,26 @@ describe("runCliAgent spawn path", () => {
claudeSessionId: "c9d7b831-1c31-4d22-80b9-1e50ca207d4b",
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as { argv?: string[]; input?: string };
expect(input.argv).not.toContain("--resume");
expect(input.argv).not.toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
expect(input.argv).toContain("--session-id");
expect(input.input).toContain("hi");
expect(params.provider).toBe("claude-cli");
expect(params.prompt).toBe("hi");
expect(params).not.toHaveProperty("cliSessionId");
expect(JSON.stringify(params)).not.toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
});
it("forwards senderIsOwner through the compat wrapper into bundle MCP env", async () => {
setupCliRunnerTestRegistry();
setActiveMcpLoopbackRuntime({ port: 23119, token: "loopback-token-123" });
try {
supervisorSpawnMock.mockResolvedValueOnce(createClaudeSuccessRun("sid-owner"));
it("forwards senderIsOwner through the compat wrapper", () => {
const params = buildRunClaudeCliAgentParams({
sessionId: "openclaw-session",
sessionKey: "agent:main:matrix:room:123",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
prompt: "hi",
model: "opus",
timeoutMs: 1_000,
runId: "run-claude-owner-wrapper",
senderIsOwner: false,
});
await runClaudeCliAgent({
sessionId: "openclaw-session",
sessionKey: "agent:main:matrix:room:123",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
prompt: "hi",
model: "opus",
timeoutMs: 1_000,
runId: "run-claude-owner-wrapper",
senderIsOwner: false,
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
env?: Record<string, string | undefined>;
};
expect(input.env?.OPENCLAW_MCP_SENDER_IS_OWNER).toBe("false");
} finally {
clearActiveMcpLoopbackRuntime("loopback-token-123");
}
expect(params.senderIsOwner).toBe(false);
});
it("runs CLI through supervisor and returns payload", async () => {

View File

@@ -97,10 +97,8 @@ export type RunClaudeCliAgentParams = Omit<RunCliAgentParams, "provider" | "cliS
claudeSessionId?: string;
};
export async function runClaudeCliAgent(
params: RunClaudeCliAgentParams,
): Promise<EmbeddedPiRunResult> {
return runCliAgent({
export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): RunCliAgentParams {
return {
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: params.agentId,
@@ -120,5 +118,11 @@ export async function runClaudeCliAgent(
// an incompatible Claude session on the generic runner path.
images: params.images,
senderIsOwner: params.senderIsOwner,
});
};
}
export async function runClaudeCliAgent(
params: RunClaudeCliAgentParams,
): Promise<EmbeddedPiRunResult> {
return runCliAgent(buildRunClaudeCliAgentParams(params));
}

View File

@@ -16,6 +16,7 @@ let configOverride: Record<string, unknown> = {
};
let addSubagentRunForTests: typeof import("./subagent-registry.js").addSubagentRunForTests;
let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests;
let subagentRegistryTesting: typeof import("./subagent-registry.js").__testing;
let createSessionsSpawnTool: typeof import("./tools/sessions-spawn-tool.js").createSessionsSpawnTool;
vi.mock("../config/config.js", async () => {
@@ -62,14 +63,26 @@ function seedDepthTwoAncestryStore(params?: { sessionIds?: boolean }) {
}
beforeAll(async () => {
({ addSubagentRunForTests, resetSubagentRegistryForTests } =
await import("./subagent-registry.js"));
({
__testing: subagentRegistryTesting,
addSubagentRunForTests,
resetSubagentRegistryForTests,
} = await import("./subagent-registry.js"));
({ createSessionsSpawnTool } = await import("./tools/sessions-spawn-tool.js"));
});
describe("sessions_spawn depth + child limits", () => {
beforeEach(() => {
resetSubagentRegistryForTests();
subagentRegistryTesting.setDepsForTest({
captureSubagentCompletionReply: () => Promise.resolve(undefined),
cleanupBrowserSessionsForLifecycleEnd: () => Promise.resolve(),
ensureRuntimePluginsLoaded: () => {},
onAgentEvent: () => () => {},
persistSubagentRunsToDisk: () => {},
resolveAgentTimeoutMs: () => 1,
runSubagentAnnounceFlow: () => Promise.resolve(true),
});
resetSubagentRegistryForTests({ persist: false });
callGatewayMock.mockClear();
storeTemplatePath = path.join(
os.tmpdir(),

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import {
DEFAULT_LLM_IDLE_TIMEOUT_MS,
@@ -88,6 +88,10 @@ describe("resolveLlmIdleTimeoutMs", () => {
});
describe("streamWithIdleTimeout", () => {
afterEach(() => {
vi.useRealTimers();
});
// Helper to create a mock async iterable
function createMockAsyncIterable<T>(chunks: T[]): AsyncIterable<T> {
return {
@@ -130,6 +134,7 @@ describe("streamWithIdleTimeout", () => {
});
it("throws on idle timeout", async () => {
vi.useFakeTimers();
// Create a stream that never yields
const slowStream: AsyncIterable<unknown> = {
[Symbol.asyncIterator]() {
@@ -152,7 +157,9 @@ describe("streamWithIdleTimeout", () => {
const stream = wrapped(model, context, options) as AsyncIterable<unknown>;
const iterator = stream[Symbol.asyncIterator]();
await expect(iterator.next()).rejects.toThrow(/LLM idle timeout/);
const next = expect(iterator.next()).rejects.toThrow(/LLM idle timeout/);
await vi.advanceTimersByTimeAsync(50);
await next;
});
it("resets timer on each chunk", async () => {
@@ -177,6 +184,7 @@ describe("streamWithIdleTimeout", () => {
});
it("handles stream with delays between chunks", async () => {
vi.useFakeTimers();
// Create a stream with small delays
const delayedStream: AsyncIterable<{ text: string }> = {
[Symbol.asyncIterator]() {
@@ -203,14 +211,22 @@ describe("streamWithIdleTimeout", () => {
const stream = wrapped(model, context, options) as AsyncIterable<{ text: string }>;
const results: { text: string }[] = [];
for await (const chunk of stream) {
results.push(chunk);
const collect = (async () => {
for await (const chunk of stream) {
results.push(chunk);
}
})();
for (let i = 0; i < 3; i++) {
await vi.advanceTimersByTimeAsync(10);
}
await collect;
expect(results).toHaveLength(3);
});
it("calls timeout hook on idle timeout", async () => {
vi.useFakeTimers();
// Create a stream that never yields
const slowStream: AsyncIterable<unknown> = {
[Symbol.asyncIterator]() {
@@ -234,18 +250,16 @@ describe("streamWithIdleTimeout", () => {
const stream = wrapped(model, context, options) as AsyncIterable<unknown>;
const iterator = stream[Symbol.asyncIterator]();
try {
await iterator.next();
// Should not reach here
expect.fail("Expected timeout error");
} catch (error) {
// Verify the error message is preserved
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toMatch(/LLM idle timeout/);
expect(onIdleTimeout).toHaveBeenCalledTimes(1);
const [timeoutError] = onIdleTimeout.mock.calls[0] ?? [];
expect(timeoutError).toBeInstanceOf(Error);
expect((timeoutError as Error).message).toMatch(/LLM idle timeout/);
}
const next = iterator.next().catch((error: unknown) => error);
await vi.advanceTimersByTimeAsync(50);
const error = await next;
// Verify the error message is preserved
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toMatch(/LLM idle timeout/);
expect(onIdleTimeout).toHaveBeenCalledTimes(1);
const [timeoutError] = onIdleTimeout.mock.calls[0] ?? [];
expect(timeoutError).toBeInstanceOf(Error);
expect((timeoutError as Error).message).toMatch(/LLM idle timeout/);
});
});