perf(test): reduce hot-suite import and setup overhead

This commit is contained in:
Peter Steinberger
2026-02-13 20:26:26 +00:00
parent 1655df7ac0
commit 2086cdfb9b
11 changed files with 312 additions and 472 deletions

View File

@@ -18,198 +18,169 @@ function buildModel(): Model<"openai-responses"> {
};
}
function installFailingFetchCapture() {
const originalFetch = globalThis.fetch;
let lastBody: unknown;
const fetchImpl: typeof fetch = async (_input, init) => {
const rawBody = init?.body;
const bodyText = (() => {
if (!rawBody) {
return "";
}
if (typeof rawBody === "string") {
return rawBody;
}
if (rawBody instanceof Uint8Array) {
return Buffer.from(rawBody).toString("utf8");
}
if (rawBody instanceof ArrayBuffer) {
return Buffer.from(new Uint8Array(rawBody)).toString("utf8");
}
return null;
})();
lastBody = bodyText ? (JSON.parse(bodyText) as unknown) : undefined;
throw new Error("intentional fetch abort (test)");
};
globalThis.fetch = fetchImpl;
return {
getLastBody: () => lastBody as Record<string, unknown> | undefined,
restore: () => {
globalThis.fetch = originalFetch;
},
};
}
describe("openai-responses reasoning replay", () => {
it("replays reasoning for tool-call-only turns (OpenAI requires it)", async () => {
const cap = installFailingFetchCapture();
try {
const model = buildModel();
const model = buildModel();
const controller = new AbortController();
controller.abort();
let payload: Record<string, unknown> | undefined;
const assistantToolOnly: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5.2",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
const assistantToolOnly: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5.2",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
content: [
{
type: "thinking",
thinking: "internal",
thinkingSignature: JSON.stringify({
type: "reasoning",
id: "rs_test",
summary: [],
}),
},
stopReason: "toolUse",
timestamp: Date.now(),
content: [
{
type: "toolCall",
id: "call_123|fc_123",
name: "noop",
arguments: {},
},
],
};
const toolResult: ToolResultMessage = {
role: "toolResult",
toolCallId: "call_123|fc_123",
toolName: "noop",
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: Date.now(),
};
const stream = streamOpenAIResponses(
model,
{
systemPrompt: "system",
messages: [
{
type: "thinking",
thinking: "internal",
thinkingSignature: JSON.stringify({
type: "reasoning",
id: "rs_test",
summary: [],
}),
role: "user",
content: "Call noop.",
timestamp: Date.now(),
},
assistantToolOnly,
toolResult,
{
type: "toolCall",
id: "call_123|fc_123",
name: "noop",
arguments: {},
role: "user",
content: "Now reply with ok.",
timestamp: Date.now(),
},
],
};
const toolResult: ToolResultMessage = {
role: "toolResult",
toolCallId: "call_123|fc_123",
toolName: "noop",
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: Date.now(),
};
const stream = streamOpenAIResponses(
model,
{
systemPrompt: "system",
messages: [
{
role: "user",
content: "Call noop.",
timestamp: Date.now(),
},
assistantToolOnly,
toolResult,
{
role: "user",
content: "Now reply with ok.",
timestamp: Date.now(),
},
],
tools: [
{
name: "noop",
description: "no-op",
parameters: Type.Object({}, { additionalProperties: false }),
},
],
tools: [
{
name: "noop",
description: "no-op",
parameters: Type.Object({}, { additionalProperties: false }),
},
],
},
{
apiKey: "test",
signal: controller.signal,
onPayload: (nextPayload) => {
payload = nextPayload as Record<string, unknown>;
},
{ apiKey: "test" },
);
},
);
await stream.result();
await stream.result();
const body = cap.getLastBody();
const input = Array.isArray(body?.input) ? body?.input : [];
const types = input
.map((item) =>
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
)
.filter((t): t is string => typeof t === "string");
const input = Array.isArray(payload?.input) ? payload?.input : [];
const types = input
.map((item) =>
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
)
.filter((t): t is string => typeof t === "string");
expect(types).toContain("reasoning");
expect(types).toContain("function_call");
expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call"));
} finally {
cap.restore();
}
expect(types).toContain("reasoning");
expect(types).toContain("function_call");
expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call"));
});
it("still replays reasoning when paired with an assistant message", async () => {
const cap = installFailingFetchCapture();
try {
const model = buildModel();
const model = buildModel();
const controller = new AbortController();
controller.abort();
let payload: Record<string, unknown> | undefined;
const assistantWithText: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5.2",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
content: [
{
type: "thinking",
thinking: "internal",
thinkingSignature: JSON.stringify({
type: "reasoning",
id: "rs_test",
summary: [],
}),
},
{ type: "text", text: "hello", textSignature: "msg_test" },
],
};
const stream = streamOpenAIResponses(
model,
const assistantWithText: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5.2",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
content: [
{
systemPrompt: "system",
messages: [
{ role: "user", content: "Hi", timestamp: Date.now() },
assistantWithText,
{ role: "user", content: "Ok", timestamp: Date.now() },
],
type: "thinking",
thinking: "internal",
thinkingSignature: JSON.stringify({
type: "reasoning",
id: "rs_test",
summary: [],
}),
},
{ apiKey: "test" },
);
{ type: "text", text: "hello", textSignature: "msg_test" },
],
};
await stream.result();
const stream = streamOpenAIResponses(
model,
{
systemPrompt: "system",
messages: [
{ role: "user", content: "Hi", timestamp: Date.now() },
assistantWithText,
{ role: "user", content: "Ok", timestamp: Date.now() },
],
},
{
apiKey: "test",
signal: controller.signal,
onPayload: (nextPayload) => {
payload = nextPayload as Record<string, unknown>;
},
},
);
const body = cap.getLastBody();
const input = Array.isArray(body?.input) ? body?.input : [];
const types = input
.map((item) =>
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
)
.filter((t): t is string => typeof t === "string");
await stream.result();
expect(types).toContain("reasoning");
expect(types).toContain("message");
} finally {
cap.restore();
}
const input = Array.isArray(payload?.input) ? payload?.input : [];
const types = input
.map((item) =>
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
)
.filter((t): t is string => typeof t === "string");
expect(types).toContain("reasoning");
expect(types).toContain("message");
});
});

View File

@@ -0,0 +1,9 @@
let pwAiLoaded = false;
export function markPwAiLoaded(): void {
pwAiLoaded = true;
}
export function isPwAiLoaded(): boolean {
return pwAiLoaded;
}

View File

@@ -1,3 +1,7 @@
import { markPwAiLoaded } from "./pw-ai-state.js";
markPwAiLoaded();
export {
type BrowserConsoleMessage,
closePageByTargetIdViaPlaywright,

View File

@@ -7,6 +7,7 @@ import { safeEqualSecret } from "../security/secret-equal.js";
import { resolveBrowserConfig, resolveProfile } from "./config.js";
import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./control-auth.js";
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
import { isPwAiLoaded } from "./pw-ai-state.js";
import { registerBrowserRoutes } from "./routes/index.js";
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
@@ -196,11 +197,13 @@ export async function stopBrowserControlServer(): Promise<void> {
}
state = null;
// Optional: Playwright is not always available (e.g. embedded gateway builds).
try {
const mod = await import("./pw-ai.js");
await mod.closePlaywrightBrowserConnection();
} catch {
// ignore
// Optional: avoid importing heavy Playwright bridge when this process never used it.
if (isPwAiLoaded()) {
try {
const mod = await import("./pw-ai.js");
await mod.closePlaywrightBrowserConnection();
} catch {
// ignore
}
}
}

View File

@@ -21,20 +21,12 @@ vi.mock("../../../discord/send.js", async () => {
};
});
const loadHandleDiscordMessageAction = async () => {
const mod = await import("./discord/handle-action.js");
return mod.handleDiscordMessageAction;
};
const loadDiscordMessageActions = async () => {
const mod = await import("./discord.js");
return mod.discordMessageActions;
};
const { handleDiscordMessageAction } = await import("./discord/handle-action.js");
const { discordMessageActions } = await import("./discord.js");
describe("discord message actions", () => {
it("lists channel and upload actions by default", async () => {
const cfg = { channels: { discord: { token: "d0" } } } as OpenClawConfig;
const discordMessageActions = await loadDiscordMessageActions();
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
expect(actions).toContain("emoji-upload");
@@ -46,7 +38,6 @@ describe("discord message actions", () => {
const cfg = {
channels: { discord: { token: "d0", actions: { channels: false } } },
} as OpenClawConfig;
const discordMessageActions = await loadDiscordMessageActions();
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
expect(actions).not.toContain("channel-create");
@@ -56,7 +47,6 @@ describe("discord message actions", () => {
describe("handleDiscordMessageAction", () => {
it("forwards context accountId for send", async () => {
sendMessageDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "send",
@@ -79,7 +69,6 @@ describe("handleDiscordMessageAction", () => {
it("falls back to params accountId when context missing", async () => {
sendPollDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "poll",
@@ -106,7 +95,6 @@ describe("handleDiscordMessageAction", () => {
it("forwards accountId for thread replies", async () => {
sendMessageDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "thread-reply",
@@ -129,7 +117,6 @@ describe("handleDiscordMessageAction", () => {
it("accepts threadId for thread replies (tool compatibility)", async () => {
sendMessageDiscord.mockClear();
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
await handleDiscordMessageAction({
action: "thread-reply",

View File

@@ -27,14 +27,20 @@ vi.mock("../runtime.js", () => ({
},
}));
const { registerCronCli } = await import("./cron-cli.js");
function buildProgram() {
const program = new Command();
program.exitOverride();
registerCronCli(program);
return program;
}
describe("cron cli", () => {
it("trims model and thinking on cron add", { timeout: 60_000 }, async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
[
@@ -68,10 +74,7 @@ describe("cron cli", () => {
it("defaults isolated cron add to announce delivery", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
[
@@ -98,10 +101,7 @@ describe("cron cli", () => {
it("infers sessionTarget from payload when --session is omitted", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
["cron", "add", "--name", "Main reminder", "--cron", "* * * * *", "--system-event", "hi"],
@@ -129,10 +129,7 @@ describe("cron cli", () => {
it("supports --keep-after-run on cron add", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
[
@@ -159,10 +156,7 @@ describe("cron cli", () => {
it("sends agent id on cron add", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
[
@@ -190,10 +184,7 @@ describe("cron cli", () => {
it("omits empty model and thinking on cron edit", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
["cron", "edit", "job-1", "--message", "hello", "--model", " ", "--thinking", " "],
@@ -212,10 +203,7 @@ describe("cron cli", () => {
it("trims model and thinking on cron edit", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
[
@@ -244,10 +232,7 @@ describe("cron cli", () => {
it("sets and clears agent id on cron edit", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"], {
from: "user",
@@ -269,10 +254,7 @@ describe("cron cli", () => {
it("allows model/thinking updates without --message", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"], {
from: "user",
@@ -291,10 +273,7 @@ describe("cron cli", () => {
it("updates delivery settings without requiring --message", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
["cron", "edit", "job-1", "--deliver", "--channel", "telegram", "--to", "19098680"],
@@ -319,10 +298,7 @@ describe("cron cli", () => {
it("supports --no-deliver on cron edit", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(["cron", "edit", "job-1", "--no-deliver"], { from: "user" });
@@ -338,10 +314,7 @@ describe("cron cli", () => {
it("does not include undefined delivery fields when updating message", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
// Update message without delivery flags - should NOT include undefined delivery fields
await program.parseAsync(["cron", "edit", "job-1", "--message", "Updated message"], {
@@ -376,10 +349,7 @@ describe("cron cli", () => {
it("includes delivery fields when explicitly provided with message", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
// Update message AND delivery - should include both
await program.parseAsync(
@@ -416,10 +386,7 @@ describe("cron cli", () => {
it("includes best-effort delivery when provided with message", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
["cron", "edit", "job-1", "--message", "Updated message", "--best-effort-deliver"],
@@ -442,10 +409,7 @@ describe("cron cli", () => {
it("includes no-best-effort delivery when provided with message", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
const program = buildProgram();
await program.parseAsync(
["cron", "edit", "job-1", "--message", "Updated message", "--no-best-effort-deliver"],

View File

@@ -79,6 +79,17 @@ vi.mock("../runtime.js", () => ({
},
}));
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { readConfigFileSnapshot, writeConfigFile } = await import("../config/config.js");
const { checkUpdateStatus, fetchNpmTagVersion, resolveNpmChannelTag } =
await import("../infra/update-check.js");
const { runCommandWithTimeout } = await import("../process/exec.js");
const { runDaemonRestart } = await import("./daemon-cli.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand, registerUpdateCli, updateStatusCommand, updateWizardCommand } =
await import("./update-cli.js");
describe("update-cli", () => {
const baseSnapshot = {
valid: true,
@@ -100,13 +111,8 @@ describe("update-cli", () => {
});
};
beforeEach(async () => {
beforeEach(() => {
vi.clearAllMocks();
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { readConfigFileSnapshot } = await import("../config/config.js");
const { checkUpdateStatus, fetchNpmTagVersion, resolveNpmChannelTag } =
await import("../infra/update-check.js");
const { runCommandWithTimeout } = await import("../process/exec.js");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(process.cwd());
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
@@ -154,18 +160,12 @@ describe("update-cli", () => {
});
it("exports updateCommand and registerUpdateCli", async () => {
const { updateCommand, registerUpdateCli, updateWizardCommand } =
await import("./update-cli.js");
expect(typeof updateCommand).toBe("function");
expect(typeof registerUpdateCli).toBe("function");
expect(typeof updateWizardCommand).toBe("function");
}, 20_000);
it("updateCommand runs update and outputs result", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "ok",
mode: "git",
@@ -193,9 +193,6 @@ describe("update-cli", () => {
});
it("updateStatusCommand prints table output", async () => {
const { defaultRuntime } = await import("../runtime.js");
const { updateStatusCommand } = await import("./update-cli.js");
await updateStatusCommand({ json: false });
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => call[0]);
@@ -203,9 +200,6 @@ describe("update-cli", () => {
});
it("updateStatusCommand emits JSON", async () => {
const { defaultRuntime } = await import("../runtime.js");
const { updateStatusCommand } = await import("./update-cli.js");
await updateStatusCommand({ json: true });
const last = vi.mocked(defaultRuntime.log).mock.calls.at(-1)?.[0];
@@ -215,9 +209,6 @@ describe("update-cli", () => {
});
it("defaults to dev channel for git installs when unset", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { updateCommand } = await import("./update-cli.js");
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "git",
@@ -240,11 +231,6 @@ describe("update-cli", () => {
"utf-8",
);
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { checkUpdateStatus } = await import("../infra/update-check.js");
const { updateCommand } = await import("./update-cli.js");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
@@ -275,10 +261,6 @@ describe("update-cli", () => {
});
it("uses stored beta channel when configured", async () => {
const { readConfigFileSnapshot } = await import("../config/config.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { updateCommand } = await import("./update-cli.js");
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
...baseSnapshot,
config: { update: { channel: "beta" } },
@@ -305,13 +287,6 @@ describe("update-cli", () => {
"utf-8",
);
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { readConfigFileSnapshot } = await import("../config/config.js");
const { resolveNpmChannelTag } = await import("../infra/update-check.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { updateCommand } = await import("./update-cli.js");
const { checkUpdateStatus } = await import("../infra/update-check.js");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
...baseSnapshot,
@@ -358,10 +333,6 @@ describe("update-cli", () => {
"utf-8",
);
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { updateCommand } = await import("./update-cli.js");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
@@ -380,10 +351,6 @@ describe("update-cli", () => {
});
it("updateCommand outputs JSON when --json is set", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "ok",
mode: "git",
@@ -409,10 +376,6 @@ describe("update-cli", () => {
});
it("updateCommand exits with error on failure", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "error",
mode: "git",
@@ -430,10 +393,6 @@ describe("update-cli", () => {
});
it("updateCommand restarts daemon by default", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { runDaemonRestart } = await import("./daemon-cli.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "ok",
mode: "git",
@@ -450,10 +409,6 @@ describe("update-cli", () => {
});
it("updateCommand skips restart when --no-restart is set", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { runDaemonRestart } = await import("./daemon-cli.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "ok",
mode: "git",
@@ -469,11 +424,6 @@ describe("update-cli", () => {
});
it("updateCommand skips success message when restart does not run", async () => {
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { runDaemonRestart } = await import("./daemon-cli.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "ok",
mode: "git",
@@ -492,9 +442,6 @@ describe("update-cli", () => {
});
it("updateCommand validates timeout option", async () => {
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
@@ -505,10 +452,6 @@ describe("update-cli", () => {
});
it("persists update channel when --channel is set", async () => {
const { writeConfigFile } = await import("../config/config.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { updateCommand } = await import("./update-cli.js");
const mockResult: UpdateRunResult = {
status: "ok",
mode: "git",
@@ -537,13 +480,6 @@ describe("update-cli", () => {
"utf-8",
);
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { resolveNpmChannelTag } = await import("../infra/update-check.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
const { checkUpdateStatus } = await import("../infra/update-check.js");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
@@ -590,13 +526,6 @@ describe("update-cli", () => {
"utf-8",
);
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const { resolveNpmChannelTag } = await import("../infra/update-check.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { defaultRuntime } = await import("../runtime.js");
const { updateCommand } = await import("./update-cli.js");
const { checkUpdateStatus } = await import("../infra/update-check.js");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: tempDir,
@@ -634,9 +563,6 @@ describe("update-cli", () => {
});
it("updateWizardCommand requires a TTY", async () => {
const { defaultRuntime } = await import("../runtime.js");
const { updateWizardCommand } = await import("./update-cli.js");
setTty(false);
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
@@ -656,10 +582,6 @@ describe("update-cli", () => {
setTty(true);
process.env.OPENCLAW_GIT_DIR = tempDir;
const { checkUpdateStatus } = await import("../infra/update-check.js");
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { updateWizardCommand } = await import("./update-cli.js");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/test/path",
installKind: "package",

View File

@@ -22,21 +22,17 @@ vi.mock("../../agents/agent-scope.js", () => ({
listAgentIds: mocks.listAgentIds,
}));
const { resolveSessionKeyForRequest } = await import("./session.js");
describe("resolveSessionKeyForRequest", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.listAgentIds.mockReturnValue(["main"]);
});
async function importFresh() {
return await import("./session.js");
}
const baseCfg: OpenClawConfig = {};
it("returns sessionKey when --to resolves a session key via context", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
mocks.loadSessionStore.mockReturnValue({
"agent:main:main": { sessionId: "sess-1", updatedAt: 0 },
@@ -50,8 +46,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("finds session by sessionId via reverse lookup in primary store", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
mocks.loadSessionStore.mockReturnValue({
"agent:main:main": { sessionId: "target-session-id", updatedAt: 0 },
@@ -65,8 +59,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("finds session by sessionId in non-primary agent store", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
mocks.resolveStorePath.mockImplementation(
(_store: string | undefined, opts?: { agentId?: string }) => {
@@ -94,8 +86,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("returns correct sessionStore when session found in non-primary agent store", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
const mybotStore = {
"agent:mybot:main": { sessionId: "target-session-id", updatedAt: 0 },
};
@@ -123,8 +113,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("returns undefined sessionKey when sessionId not found in any store", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
mocks.resolveStorePath.mockImplementation(
(_store: string | undefined, opts?: { agentId?: string }) => {
@@ -144,8 +132,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("does not search other stores when explicitSessionKey is set", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
mocks.loadSessionStore.mockReturnValue({
@@ -162,8 +148,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("searches other stores when --to derives a key that does not match --session-id", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
mocks.resolveStorePath.mockImplementation(
(_store: string | undefined, opts?: { agentId?: string }) => {
@@ -199,8 +183,6 @@ describe("resolveSessionKeyForRequest", () => {
});
it("skips already-searched primary store when iterating agents", async () => {
const { resolveSessionKeyForRequest } = await importFresh();
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
mocks.resolveStorePath.mockImplementation(
(_store: string | undefined, opts?: { agentId?: string }) => {

View File

@@ -15,10 +15,11 @@ vi.mock("../../config/config.js", () => {
};
});
const { skillsHandlers } = await import("./skills.js");
describe("skills.update", () => {
it("strips embedded CR/LF from apiKey", async () => {
writtenConfig = null;
const { skillsHandlers } = await import("./skills.js");
let ok: boolean | null = null;
let error: unknown = null;

View File

@@ -2,23 +2,22 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterAll, describe, expect, it } from "vitest";
import { resolvePluginTools } from "./tools.js";
type TempPlugin = { dir: string; file: string; id: string };
const tempDirs: string[] = [];
const fixtureRoot = path.join(os.tmpdir(), `openclaw-plugin-tools-${randomUUID()}`);
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
function makeTempDir() {
const dir = path.join(os.tmpdir(), `openclaw-plugin-tools-${randomUUID()}`);
function makeFixtureDir(id: string) {
const dir = path.join(fixtureRoot, id);
fs.mkdirSync(dir, { recursive: true });
tempDirs.push(dir);
return dir;
}
function writePlugin(params: { id: string; body: string }): TempPlugin {
const dir = makeTempDir();
const dir = makeFixtureDir(params.id);
const file = path.join(dir, `${params.id}.js`);
fs.writeFileSync(file, params.body, "utf-8");
fs.writeFileSync(
@@ -36,18 +35,7 @@ function writePlugin(params: { id: string; body: string }): TempPlugin {
return { dir, file, id: params.id };
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
}
});
describe("resolvePluginTools optional tools", () => {
const pluginBody = `
const pluginBody = `
export default { register(api) {
api.registerTool(
{
@@ -63,92 +51,11 @@ export default { register(api) {
} }
`;
it("skips optional tools without explicit allowlist", () => {
const plugin = writePlugin({ id: "optional-demo", body: pluginBody });
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
},
},
workspaceDir: plugin.dir,
},
});
expect(tools).toHaveLength(0);
});
it("allows optional tools by name", () => {
const plugin = writePlugin({ id: "optional-demo", body: pluginBody });
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
},
},
workspaceDir: plugin.dir,
},
toolAllowlist: ["optional_tool"],
});
expect(tools.map((tool) => tool.name)).toContain("optional_tool");
});
it("allows optional tools via plugin groups", () => {
const plugin = writePlugin({ id: "optional-demo", body: pluginBody });
const toolsAll = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
},
},
workspaceDir: plugin.dir,
},
toolAllowlist: ["group:plugins"],
});
expect(toolsAll.map((tool) => tool.name)).toContain("optional_tool");
const toolsPlugin = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
},
},
workspaceDir: plugin.dir,
},
toolAllowlist: ["optional-demo"],
});
expect(toolsPlugin.map((tool) => tool.name)).toContain("optional_tool");
});
it("rejects plugin id collisions with core tool names", () => {
const plugin = writePlugin({ id: "message", body: pluginBody });
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
},
},
workspaceDir: plugin.dir,
},
existingToolNames: new Set(["message"]),
toolAllowlist: ["message"],
});
expect(tools).toHaveLength(0);
});
it("skips conflicting tool names but keeps other tools", () => {
const plugin = writePlugin({
id: "multi",
body: `
const optionalDemoPlugin = writePlugin({ id: "optional-demo", body: pluginBody });
const coreNameCollisionPlugin = writePlugin({ id: "message", body: pluginBody });
const multiToolPlugin = writePlugin({
id: "multi",
body: `
export default { register(api) {
api.registerTool({
name: "message",
@@ -168,17 +75,105 @@ export default { register(api) {
});
} }
`,
});
});
afterAll(() => {
try {
fs.rmSync(fixtureRoot, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
});
describe("resolvePluginTools optional tools", () => {
it("skips optional tools without explicit allowlist", () => {
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [plugin.id],
load: { paths: [optionalDemoPlugin.file] },
allow: [optionalDemoPlugin.id],
},
},
workspaceDir: plugin.dir,
workspaceDir: optionalDemoPlugin.dir,
},
});
expect(tools).toHaveLength(0);
});
it("allows optional tools by name", () => {
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [optionalDemoPlugin.file] },
allow: [optionalDemoPlugin.id],
},
},
workspaceDir: optionalDemoPlugin.dir,
},
toolAllowlist: ["optional_tool"],
});
expect(tools.map((tool) => tool.name)).toContain("optional_tool");
});
it("allows optional tools via plugin groups", () => {
const toolsAll = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [optionalDemoPlugin.file] },
allow: [optionalDemoPlugin.id],
},
},
workspaceDir: optionalDemoPlugin.dir,
},
toolAllowlist: ["group:plugins"],
});
expect(toolsAll.map((tool) => tool.name)).toContain("optional_tool");
const toolsPlugin = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [optionalDemoPlugin.file] },
allow: [optionalDemoPlugin.id],
},
},
workspaceDir: optionalDemoPlugin.dir,
},
toolAllowlist: ["optional-demo"],
});
expect(toolsPlugin.map((tool) => tool.name)).toContain("optional_tool");
});
it("rejects plugin id collisions with core tool names", () => {
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [coreNameCollisionPlugin.file] },
allow: [coreNameCollisionPlugin.id],
},
},
workspaceDir: coreNameCollisionPlugin.dir,
},
existingToolNames: new Set(["message"]),
toolAllowlist: ["message"],
});
expect(tools).toHaveLength(0);
});
it("skips conflicting tool names but keeps other tools", () => {
const tools = resolvePluginTools({
context: {
config: {
plugins: {
load: { paths: [multiToolPlugin.file] },
allow: [multiToolPlugin.id],
},
},
workspaceDir: multiToolPlugin.dir,
},
existingToolNames: new Set(["message"]),
});

View File

@@ -62,7 +62,9 @@ export async function getDeterministicFreePortBlock(params?: {
// Allocate in blocks to avoid derived-port overlaps (e.g. port+3).
const blockSize = Math.max(maxOffset + 1, 8);
for (let attempt = 0; attempt < usable; attempt += 1) {
// Scan in block-size steps. Tests consume neighboring derived ports (+1/+2/...),
// so probing every single offset is wasted work and slows large suites.
for (let attempt = 0; attempt < usable; attempt += blockSize) {
const start = base + ((nextTestPortOffset + attempt) % usable);
// eslint-disable-next-line no-await-in-loop
const ok = (await Promise.all(offsets.map((offset) => isPortFree(start + offset)))).every(