mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(ci): repair e2e mocks and tool schemas
This commit is contained in:
@@ -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") {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user