fix(ci): repair e2e mocks and tool schemas

This commit is contained in:
Peter Steinberger
2026-02-15 22:45:44 +00:00
parent 0e2d8b8a1e
commit bbcbabab74
9 changed files with 65 additions and 43 deletions

View File

@@ -65,8 +65,9 @@ const processSchema = Type.Object({
offset: Type.Optional(Type.Number({ description: "Log offset" })),
limit: Type.Optional(Type.Number({ description: "Log length" })),
timeout: Type.Optional(
Type.Union([Type.Number(), Type.String()], {
Type.Number({
description: "For poll: wait up to this many milliseconds before returning",
minimum: 0,
}),
),
});
@@ -138,7 +139,7 @@ export function createProcessTool(
eof?: boolean;
offset?: number;
limit?: number;
timeout?: number | string;
timeout?: unknown;
};
if (params.action === "list") {

View File

@@ -783,7 +783,7 @@ describe("sessions tools", () => {
text?: string;
};
expect(details.status).toBe("ok");
expect(details.text).toContain("tokens 1k (in 12 / out 1k)");
expect(details.text).toMatch(/tokens 1(\.0)?k \(in 12 \/ out 1(\.0)?k\)/);
expect(details.text).toContain("prompt/cache 197k");
expect(details.text).not.toContain("1.0k io");
} finally {

View File

@@ -79,7 +79,7 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
it("sessions_spawn allows cross-agent spawning when configured", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
setConfigOverride({
setSessionsSpawnConfigOverride({
session: {
mainKey: "main",
scope: "per-sender",
@@ -133,7 +133,7 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
it("sessions_spawn allows any agent when allowlist is *", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
setConfigOverride({
setSessionsSpawnConfigOverride({
session: {
mainKey: "main",
scope: "per-sender",
@@ -187,7 +187,7 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => {
it("sessions_spawn normalizes allowlisted agent ids", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
setConfigOverride({
setSessionsSpawnConfigOverride({
session: {
mainKey: "main",
scope: "per-sender",

View File

@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { emitAgentEvent } from "../infra/agent-events.js";
import "./test-helpers/fast-core-tools.js";
import { sleep } from "../utils.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import {
getCallGatewayMock,
@@ -112,6 +113,16 @@ function setupSessionsSpawnGatewayMock(opts: {
};
}
const waitFor = async (predicate: () => boolean, timeoutMs = 2000) => {
const start = Date.now();
while (!predicate()) {
if (Date.now() - start > timeoutMs) {
throw new Error(`timed out waiting for condition (timeoutMs=${timeoutMs})`);
}
await sleep(10);
}
};
describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
beforeEach(() => {
resetSessionsSpawnConfigOverride();
@@ -120,16 +131,14 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
it("sessions_spawn runs cleanup flow after subagent completion", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
let patchParams: { key?: string; label?: string } = {};
const patchCalls: Array<{ key?: string; label?: string }> = [];
const ctx = setupSessionsSpawnGatewayMock({
includeSessionsList: true,
includeChatHistory: true,
onSessionsPatch: (params) => {
const rec = params as { key?: string; label?: string } | undefined;
if (typeof rec?.label === "string" && rec.label.trim()) {
patchParams = { key: rec.key, label: rec.label };
}
patchCalls.push({ key: rec?.key, label: rec?.label });
},
});
@@ -165,18 +174,16 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
},
});
vi.useFakeTimers();
try {
await vi.advanceTimersByTimeAsync(500);
} finally {
vi.useRealTimers();
}
await waitFor(() => ctx.waitCalls.some((call) => call.runId === child.runId));
await waitFor(() => patchCalls.some((call) => call.label === "my-task"));
await waitFor(() => ctx.calls.filter((c) => c.method === "agent").length >= 2);
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
expect(childWait?.timeoutMs).toBe(1000);
// Cleanup should patch the label
expect(patchParams.key).toBe(child.sessionKey);
expect(patchParams.label).toBe("my-task");
const labelPatch = patchCalls.find((call) => call.label === "my-task");
expect(labelPatch?.key).toBe(child.sessionKey);
expect(labelPatch?.label).toBe("my-task");
// Two agent calls: subagent spawn + main agent trigger
const agentCalls = ctx.calls.filter((c) => c.method === "agent");
@@ -325,14 +332,14 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
runId: "run-1",
});
vi.useFakeTimers();
try {
await vi.advanceTimersByTimeAsync(500);
} finally {
vi.useRealTimers();
}
const child = ctx.getChild();
if (!child.runId) {
throw new Error("missing child runId");
}
await waitFor(() => ctx.waitCalls.some((call) => call.runId === child.runId));
await waitFor(() => ctx.calls.filter((call) => call.method === "agent").length >= 2);
await waitFor(() => Boolean(deletedKey));
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
expect(childWait?.timeoutMs).toBe(1000);
expect(child.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
@@ -415,12 +422,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
runId: "run-1",
});
vi.useFakeTimers();
try {
await vi.advanceTimersByTimeAsync(500);
} finally {
vi.useRealTimers();
}
await waitFor(() => calls.filter((call) => call.method === "agent").length >= 2);
const mainAgentCall = calls
.filter((call) => call.method === "agent")

View File

@@ -289,8 +289,8 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => {
const request = opts as { method?: string; params?: unknown };
calls.push(request);
if (request.method === "sessions.patch") {
const params = request.params as { model?: unknown } | undefined;
if (typeof params?.model === "string" && params.model.trim()) {
const model = (request.params as { model?: unknown } | undefined)?.model;
if (model === "bad-model") {
throw new Error("invalid model: bad-model");
}
return { ok: true };

View File

@@ -28,6 +28,7 @@ const SessionsSpawnToolSchema = Type.Object({
model: Type.Optional(Type.String()),
thinking: Type.Optional(Type.String()),
runTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
// Back-compat: older callers used timeoutSeconds for this tool.
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
cleanup: optionalStringEnum(["delete", "keep"] as const),
});
@@ -98,14 +99,16 @@ export function createSessionsSpawnTool(opts?: {
});
// Default to 0 (no timeout) when omitted. Sub-agent runs are long-lived
// by default and should not inherit the main agent 600s timeout.
const legacyTimeoutSeconds =
typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds)
? Math.max(0, Math.floor(params.timeoutSeconds))
: undefined;
const timeoutSecondsCandidate =
typeof params.runTimeoutSeconds === "number"
? params.runTimeoutSeconds
: typeof params.timeoutSeconds === "number"
? params.timeoutSeconds
: undefined;
const runTimeoutSeconds =
typeof params.runTimeoutSeconds === "number" && Number.isFinite(params.runTimeoutSeconds)
? Math.max(0, Math.floor(params.runTimeoutSeconds))
: (legacyTimeoutSeconds ?? 0);
typeof timeoutSecondsCandidate === "number" && Number.isFinite(timeoutSecondsCandidate)
? Math.max(0, Math.floor(timeoutSecondsCandidate))
: 0;
let modelWarning: string | undefined;
let modelApplied = false;

View File

@@ -127,7 +127,10 @@ describe("group intro prompts", () => {
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toContain('"channel": "discord"');
expect(extraSystemPrompt).toContain(
`You are replying inside a Discord group chat. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
`You are in the Discord group chat "Release Squad". Participants: Alice, Bob.`,
);
expect(extraSystemPrompt).toContain(
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
@@ -158,8 +161,12 @@ describe("group intro prompts", () => {
const extraSystemPrompt =
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toContain('"channel": "whatsapp"');
expect(extraSystemPrompt).toContain(`You are in the WhatsApp group chat "Ops".`);
expect(extraSystemPrompt).toContain(
`You are replying inside a WhatsApp group chat. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`,
`WhatsApp IDs: SenderId is the participant JID (group participant id).`,
);
expect(extraSystemPrompt).toContain(
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
@@ -190,8 +197,9 @@ describe("group intro prompts", () => {
const extraSystemPrompt =
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toContain('"channel": "telegram"');
expect(extraSystemPrompt).toContain(`You are in the Telegram group chat "Dev Chat".`);
expect(extraSystemPrompt).toContain(
`You are replying inside a Telegram group chat. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
`Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});

View File

@@ -43,6 +43,13 @@ export function installBaseProgramMocks() {
],
configureCommand,
configureCommandWithSections,
configureCommandFromSectionsArg: (sections: unknown, runtime: unknown) => {
const resolved = Array.isArray(sections) ? sections : [];
if (resolved.length > 0) {
return configureCommandWithSections(resolved, runtime);
}
return configureCommand({}, runtime);
},
}));
vi.mock("../commands/setup.js", () => ({ setupCommand }));
vi.mock("../commands/onboard.js", () => ({ onboardCommand }));

View File

@@ -249,6 +249,7 @@ vi.mock("../infra/update-check.js", () => ({
},
registry: { latestVersion: "0.0.0" },
}),
formatGitInstallLabel: vi.fn(() => "main · @ deadbeef"),
compareSemverStrings: vi.fn(() => 0),
}));
vi.mock("../config/config.js", async (importOriginal) => {