mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
perf(test): reduce hot-suite import and setup overhead
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
9
src/browser/pw-ai-state.ts
Normal file
9
src/browser/pw-ai-state.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
let pwAiLoaded = false;
|
||||
|
||||
export function markPwAiLoaded(): void {
|
||||
pwAiLoaded = true;
|
||||
}
|
||||
|
||||
export function isPwAiLoaded(): boolean {
|
||||
return pwAiLoaded;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { markPwAiLoaded } from "./pw-ai-state.js";
|
||||
|
||||
markPwAiLoaded();
|
||||
|
||||
export {
|
||||
type BrowserConsoleMessage,
|
||||
closePageByTargetIdViaPlaywright,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]),
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user