mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 02:51:37 +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", () => {
|
describe("openai-responses reasoning replay", () => {
|
||||||
it("replays reasoning for tool-call-only turns (OpenAI requires it)", async () => {
|
it("replays reasoning for tool-call-only turns (OpenAI requires it)", async () => {
|
||||||
const cap = installFailingFetchCapture();
|
const model = buildModel();
|
||||||
try {
|
const controller = new AbortController();
|
||||||
const model = buildModel();
|
controller.abort();
|
||||||
|
let payload: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
const assistantToolOnly: AssistantMessage = {
|
const assistantToolOnly: AssistantMessage = {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
api: "openai-responses",
|
api: "openai-responses",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-5.2",
|
model: "gpt-5.2",
|
||||||
usage: {
|
usage: {
|
||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
totalTokens: 0,
|
totalTokens: 0,
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 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(),
|
type: "toolCall",
|
||||||
content: [
|
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",
|
role: "user",
|
||||||
thinking: "internal",
|
content: "Call noop.",
|
||||||
thinkingSignature: JSON.stringify({
|
timestamp: Date.now(),
|
||||||
type: "reasoning",
|
|
||||||
id: "rs_test",
|
|
||||||
summary: [],
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
|
assistantToolOnly,
|
||||||
|
toolResult,
|
||||||
{
|
{
|
||||||
type: "toolCall",
|
role: "user",
|
||||||
id: "call_123|fc_123",
|
content: "Now reply with ok.",
|
||||||
name: "noop",
|
timestamp: Date.now(),
|
||||||
arguments: {},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
tools: [
|
||||||
|
{
|
||||||
const toolResult: ToolResultMessage = {
|
name: "noop",
|
||||||
role: "toolResult",
|
description: "no-op",
|
||||||
toolCallId: "call_123|fc_123",
|
parameters: Type.Object({}, { additionalProperties: false }),
|
||||||
toolName: "noop",
|
},
|
||||||
content: [{ type: "text", text: "ok" }],
|
],
|
||||||
isError: false,
|
},
|
||||||
timestamp: Date.now(),
|
{
|
||||||
};
|
apiKey: "test",
|
||||||
|
signal: controller.signal,
|
||||||
const stream = streamOpenAIResponses(
|
onPayload: (nextPayload) => {
|
||||||
model,
|
payload = nextPayload as Record<string, unknown>;
|
||||||
{
|
|
||||||
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 }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{ apiKey: "test" },
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await stream.result();
|
await stream.result();
|
||||||
|
|
||||||
const body = cap.getLastBody();
|
const input = Array.isArray(payload?.input) ? payload?.input : [];
|
||||||
const input = Array.isArray(body?.input) ? body?.input : [];
|
const types = input
|
||||||
const types = input
|
.map((item) =>
|
||||||
.map((item) =>
|
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
|
||||||
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
|
)
|
||||||
)
|
.filter((t): t is string => typeof t === "string");
|
||||||
.filter((t): t is string => typeof t === "string");
|
|
||||||
|
|
||||||
expect(types).toContain("reasoning");
|
expect(types).toContain("reasoning");
|
||||||
expect(types).toContain("function_call");
|
expect(types).toContain("function_call");
|
||||||
expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call"));
|
expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call"));
|
||||||
} finally {
|
|
||||||
cap.restore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("still replays reasoning when paired with an assistant message", async () => {
|
it("still replays reasoning when paired with an assistant message", async () => {
|
||||||
const cap = installFailingFetchCapture();
|
const model = buildModel();
|
||||||
try {
|
const controller = new AbortController();
|
||||||
const model = buildModel();
|
controller.abort();
|
||||||
|
let payload: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
const assistantWithText: AssistantMessage = {
|
const assistantWithText: AssistantMessage = {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
api: "openai-responses",
|
api: "openai-responses",
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-5.2",
|
model: "gpt-5.2",
|
||||||
usage: {
|
usage: {
|
||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
totalTokens: 0,
|
totalTokens: 0,
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
},
|
},
|
||||||
stopReason: "stop",
|
stopReason: "stop",
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
content: [
|
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,
|
|
||||||
{
|
{
|
||||||
systemPrompt: "system",
|
type: "thinking",
|
||||||
messages: [
|
thinking: "internal",
|
||||||
{ role: "user", content: "Hi", timestamp: Date.now() },
|
thinkingSignature: JSON.stringify({
|
||||||
assistantWithText,
|
type: "reasoning",
|
||||||
{ role: "user", content: "Ok", timestamp: Date.now() },
|
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();
|
await stream.result();
|
||||||
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");
|
|
||||||
|
|
||||||
expect(types).toContain("reasoning");
|
const input = Array.isArray(payload?.input) ? payload?.input : [];
|
||||||
expect(types).toContain("message");
|
const types = input
|
||||||
} finally {
|
.map((item) =>
|
||||||
cap.restore();
|
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 {
|
export {
|
||||||
type BrowserConsoleMessage,
|
type BrowserConsoleMessage,
|
||||||
closePageByTargetIdViaPlaywright,
|
closePageByTargetIdViaPlaywright,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { safeEqualSecret } from "../security/secret-equal.js";
|
|||||||
import { resolveBrowserConfig, resolveProfile } from "./config.js";
|
import { resolveBrowserConfig, resolveProfile } from "./config.js";
|
||||||
import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./control-auth.js";
|
import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./control-auth.js";
|
||||||
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
|
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
|
||||||
|
import { isPwAiLoaded } from "./pw-ai-state.js";
|
||||||
import { registerBrowserRoutes } from "./routes/index.js";
|
import { registerBrowserRoutes } from "./routes/index.js";
|
||||||
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
|
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
|
||||||
|
|
||||||
@@ -196,11 +197,13 @@ export async function stopBrowserControlServer(): Promise<void> {
|
|||||||
}
|
}
|
||||||
state = null;
|
state = null;
|
||||||
|
|
||||||
// Optional: Playwright is not always available (e.g. embedded gateway builds).
|
// Optional: avoid importing heavy Playwright bridge when this process never used it.
|
||||||
try {
|
if (isPwAiLoaded()) {
|
||||||
const mod = await import("./pw-ai.js");
|
try {
|
||||||
await mod.closePlaywrightBrowserConnection();
|
const mod = await import("./pw-ai.js");
|
||||||
} catch {
|
await mod.closePlaywrightBrowserConnection();
|
||||||
// ignore
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,20 +21,12 @@ vi.mock("../../../discord/send.js", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadHandleDiscordMessageAction = async () => {
|
const { handleDiscordMessageAction } = await import("./discord/handle-action.js");
|
||||||
const mod = await import("./discord/handle-action.js");
|
const { discordMessageActions } = await import("./discord.js");
|
||||||
return mod.handleDiscordMessageAction;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadDiscordMessageActions = async () => {
|
|
||||||
const mod = await import("./discord.js");
|
|
||||||
return mod.discordMessageActions;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("discord message actions", () => {
|
describe("discord message actions", () => {
|
||||||
it("lists channel and upload actions by default", async () => {
|
it("lists channel and upload actions by default", async () => {
|
||||||
const cfg = { channels: { discord: { token: "d0" } } } as OpenClawConfig;
|
const cfg = { channels: { discord: { token: "d0" } } } as OpenClawConfig;
|
||||||
const discordMessageActions = await loadDiscordMessageActions();
|
|
||||||
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
||||||
|
|
||||||
expect(actions).toContain("emoji-upload");
|
expect(actions).toContain("emoji-upload");
|
||||||
@@ -46,7 +38,6 @@ describe("discord message actions", () => {
|
|||||||
const cfg = {
|
const cfg = {
|
||||||
channels: { discord: { token: "d0", actions: { channels: false } } },
|
channels: { discord: { token: "d0", actions: { channels: false } } },
|
||||||
} as OpenClawConfig;
|
} as OpenClawConfig;
|
||||||
const discordMessageActions = await loadDiscordMessageActions();
|
|
||||||
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
const actions = discordMessageActions.listActions?.({ cfg }) ?? [];
|
||||||
|
|
||||||
expect(actions).not.toContain("channel-create");
|
expect(actions).not.toContain("channel-create");
|
||||||
@@ -56,7 +47,6 @@ describe("discord message actions", () => {
|
|||||||
describe("handleDiscordMessageAction", () => {
|
describe("handleDiscordMessageAction", () => {
|
||||||
it("forwards context accountId for send", async () => {
|
it("forwards context accountId for send", async () => {
|
||||||
sendMessageDiscord.mockClear();
|
sendMessageDiscord.mockClear();
|
||||||
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
|
|
||||||
|
|
||||||
await handleDiscordMessageAction({
|
await handleDiscordMessageAction({
|
||||||
action: "send",
|
action: "send",
|
||||||
@@ -79,7 +69,6 @@ describe("handleDiscordMessageAction", () => {
|
|||||||
|
|
||||||
it("falls back to params accountId when context missing", async () => {
|
it("falls back to params accountId when context missing", async () => {
|
||||||
sendPollDiscord.mockClear();
|
sendPollDiscord.mockClear();
|
||||||
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
|
|
||||||
|
|
||||||
await handleDiscordMessageAction({
|
await handleDiscordMessageAction({
|
||||||
action: "poll",
|
action: "poll",
|
||||||
@@ -106,7 +95,6 @@ describe("handleDiscordMessageAction", () => {
|
|||||||
|
|
||||||
it("forwards accountId for thread replies", async () => {
|
it("forwards accountId for thread replies", async () => {
|
||||||
sendMessageDiscord.mockClear();
|
sendMessageDiscord.mockClear();
|
||||||
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
|
|
||||||
|
|
||||||
await handleDiscordMessageAction({
|
await handleDiscordMessageAction({
|
||||||
action: "thread-reply",
|
action: "thread-reply",
|
||||||
@@ -129,7 +117,6 @@ describe("handleDiscordMessageAction", () => {
|
|||||||
|
|
||||||
it("accepts threadId for thread replies (tool compatibility)", async () => {
|
it("accepts threadId for thread replies (tool compatibility)", async () => {
|
||||||
sendMessageDiscord.mockClear();
|
sendMessageDiscord.mockClear();
|
||||||
const handleDiscordMessageAction = await loadHandleDiscordMessageAction();
|
|
||||||
|
|
||||||
await handleDiscordMessageAction({
|
await handleDiscordMessageAction({
|
||||||
action: "thread-reply",
|
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", () => {
|
describe("cron cli", () => {
|
||||||
it("trims model and thinking on cron add", { timeout: 60_000 }, async () => {
|
it("trims model and thinking on cron add", { timeout: 60_000 }, async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
[
|
[
|
||||||
@@ -68,10 +74,7 @@ describe("cron cli", () => {
|
|||||||
it("defaults isolated cron add to announce delivery", async () => {
|
it("defaults isolated cron add to announce delivery", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
[
|
[
|
||||||
@@ -98,10 +101,7 @@ describe("cron cli", () => {
|
|||||||
it("infers sessionTarget from payload when --session is omitted", async () => {
|
it("infers sessionTarget from payload when --session is omitted", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
["cron", "add", "--name", "Main reminder", "--cron", "* * * * *", "--system-event", "hi"],
|
["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 () => {
|
it("supports --keep-after-run on cron add", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
[
|
[
|
||||||
@@ -159,10 +156,7 @@ describe("cron cli", () => {
|
|||||||
it("sends agent id on cron add", async () => {
|
it("sends agent id on cron add", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
[
|
[
|
||||||
@@ -190,10 +184,7 @@ describe("cron cli", () => {
|
|||||||
it("omits empty model and thinking on cron edit", async () => {
|
it("omits empty model and thinking on cron edit", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
["cron", "edit", "job-1", "--message", "hello", "--model", " ", "--thinking", " "],
|
["cron", "edit", "job-1", "--message", "hello", "--model", " ", "--thinking", " "],
|
||||||
@@ -212,10 +203,7 @@ describe("cron cli", () => {
|
|||||||
it("trims model and thinking on cron edit", async () => {
|
it("trims model and thinking on cron edit", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
[
|
[
|
||||||
@@ -244,10 +232,7 @@ describe("cron cli", () => {
|
|||||||
it("sets and clears agent id on cron edit", async () => {
|
it("sets and clears agent id on cron edit", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"], {
|
await program.parseAsync(["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"], {
|
||||||
from: "user",
|
from: "user",
|
||||||
@@ -269,10 +254,7 @@ describe("cron cli", () => {
|
|||||||
it("allows model/thinking updates without --message", async () => {
|
it("allows model/thinking updates without --message", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"], {
|
await program.parseAsync(["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"], {
|
||||||
from: "user",
|
from: "user",
|
||||||
@@ -291,10 +273,7 @@ describe("cron cli", () => {
|
|||||||
it("updates delivery settings without requiring --message", async () => {
|
it("updates delivery settings without requiring --message", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
["cron", "edit", "job-1", "--deliver", "--channel", "telegram", "--to", "19098680"],
|
["cron", "edit", "job-1", "--deliver", "--channel", "telegram", "--to", "19098680"],
|
||||||
@@ -319,10 +298,7 @@ describe("cron cli", () => {
|
|||||||
it("supports --no-deliver on cron edit", async () => {
|
it("supports --no-deliver on cron edit", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(["cron", "edit", "job-1", "--no-deliver"], { from: "user" });
|
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 () => {
|
it("does not include undefined delivery fields when updating message", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
// Update message without delivery flags - should NOT include undefined delivery fields
|
// Update message without delivery flags - should NOT include undefined delivery fields
|
||||||
await program.parseAsync(["cron", "edit", "job-1", "--message", "Updated message"], {
|
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 () => {
|
it("includes delivery fields when explicitly provided with message", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
// Update message AND delivery - should include both
|
// Update message AND delivery - should include both
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
@@ -416,10 +386,7 @@ describe("cron cli", () => {
|
|||||||
it("includes best-effort delivery when provided with message", async () => {
|
it("includes best-effort delivery when provided with message", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
["cron", "edit", "job-1", "--message", "Updated message", "--best-effort-deliver"],
|
["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 () => {
|
it("includes no-best-effort delivery when provided with message", async () => {
|
||||||
callGatewayFromCli.mockClear();
|
callGatewayFromCli.mockClear();
|
||||||
|
|
||||||
const { registerCronCli } = await import("./cron-cli.js");
|
const program = buildProgram();
|
||||||
const program = new Command();
|
|
||||||
program.exitOverride();
|
|
||||||
registerCronCli(program);
|
|
||||||
|
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
["cron", "edit", "job-1", "--message", "Updated message", "--no-best-effort-deliver"],
|
["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", () => {
|
describe("update-cli", () => {
|
||||||
const baseSnapshot = {
|
const baseSnapshot = {
|
||||||
valid: true,
|
valid: true,
|
||||||
@@ -100,13 +111,8 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
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(resolveOpenClawPackageRoot).mockResolvedValue(process.cwd());
|
||||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
|
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
|
||||||
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
||||||
@@ -154,18 +160,12 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("exports updateCommand and registerUpdateCli", async () => {
|
it("exports updateCommand and registerUpdateCli", async () => {
|
||||||
const { updateCommand, registerUpdateCli, updateWizardCommand } =
|
|
||||||
await import("./update-cli.js");
|
|
||||||
expect(typeof updateCommand).toBe("function");
|
expect(typeof updateCommand).toBe("function");
|
||||||
expect(typeof registerUpdateCli).toBe("function");
|
expect(typeof registerUpdateCli).toBe("function");
|
||||||
expect(typeof updateWizardCommand).toBe("function");
|
expect(typeof updateWizardCommand).toBe("function");
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|
||||||
it("updateCommand runs update and outputs result", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -193,9 +193,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateStatusCommand prints table output", async () => {
|
it("updateStatusCommand prints table output", async () => {
|
||||||
const { defaultRuntime } = await import("../runtime.js");
|
|
||||||
const { updateStatusCommand } = await import("./update-cli.js");
|
|
||||||
|
|
||||||
await updateStatusCommand({ json: false });
|
await updateStatusCommand({ json: false });
|
||||||
|
|
||||||
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => call[0]);
|
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => call[0]);
|
||||||
@@ -203,9 +200,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateStatusCommand emits JSON", async () => {
|
it("updateStatusCommand emits JSON", async () => {
|
||||||
const { defaultRuntime } = await import("../runtime.js");
|
|
||||||
const { updateStatusCommand } = await import("./update-cli.js");
|
|
||||||
|
|
||||||
await updateStatusCommand({ json: true });
|
await updateStatusCommand({ json: true });
|
||||||
|
|
||||||
const last = vi.mocked(defaultRuntime.log).mock.calls.at(-1)?.[0];
|
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 () => {
|
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({
|
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -240,11 +231,6 @@ describe("update-cli", () => {
|
|||||||
"utf-8",
|
"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(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
@@ -275,10 +261,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses stored beta channel when configured", async () => {
|
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({
|
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||||
...baseSnapshot,
|
...baseSnapshot,
|
||||||
config: { update: { channel: "beta" } },
|
config: { update: { channel: "beta" } },
|
||||||
@@ -305,13 +287,6 @@ describe("update-cli", () => {
|
|||||||
"utf-8",
|
"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(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||||
...baseSnapshot,
|
...baseSnapshot,
|
||||||
@@ -358,10 +333,6 @@ describe("update-cli", () => {
|
|||||||
"utf-8",
|
"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(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
@@ -380,10 +351,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand outputs JSON when --json is set", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -409,10 +376,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand exits with error on failure", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "error",
|
status: "error",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -430,10 +393,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand restarts daemon by default", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -450,10 +409,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand skips restart when --no-restart is set", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -469,11 +424,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand skips success message when restart does not run", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -492,9 +442,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateCommand validates timeout option", async () => {
|
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.error).mockClear();
|
||||||
vi.mocked(defaultRuntime.exit).mockClear();
|
vi.mocked(defaultRuntime.exit).mockClear();
|
||||||
|
|
||||||
@@ -505,10 +452,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("persists update channel when --channel is set", async () => {
|
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 = {
|
const mockResult: UpdateRunResult = {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
mode: "git",
|
mode: "git",
|
||||||
@@ -537,13 +480,6 @@ describe("update-cli", () => {
|
|||||||
"utf-8",
|
"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(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
@@ -590,13 +526,6 @@ describe("update-cli", () => {
|
|||||||
"utf-8",
|
"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(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: tempDir,
|
root: tempDir,
|
||||||
@@ -634,9 +563,6 @@ describe("update-cli", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateWizardCommand requires a TTY", async () => {
|
it("updateWizardCommand requires a TTY", async () => {
|
||||||
const { defaultRuntime } = await import("../runtime.js");
|
|
||||||
const { updateWizardCommand } = await import("./update-cli.js");
|
|
||||||
|
|
||||||
setTty(false);
|
setTty(false);
|
||||||
vi.mocked(defaultRuntime.error).mockClear();
|
vi.mocked(defaultRuntime.error).mockClear();
|
||||||
vi.mocked(defaultRuntime.exit).mockClear();
|
vi.mocked(defaultRuntime.exit).mockClear();
|
||||||
@@ -656,10 +582,6 @@ describe("update-cli", () => {
|
|||||||
setTty(true);
|
setTty(true);
|
||||||
process.env.OPENCLAW_GIT_DIR = tempDir;
|
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({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
root: "/test/path",
|
root: "/test/path",
|
||||||
installKind: "package",
|
installKind: "package",
|
||||||
|
|||||||
@@ -22,21 +22,17 @@ vi.mock("../../agents/agent-scope.js", () => ({
|
|||||||
listAgentIds: mocks.listAgentIds,
|
listAgentIds: mocks.listAgentIds,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { resolveSessionKeyForRequest } = await import("./session.js");
|
||||||
|
|
||||||
describe("resolveSessionKeyForRequest", () => {
|
describe("resolveSessionKeyForRequest", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.listAgentIds.mockReturnValue(["main"]);
|
mocks.listAgentIds.mockReturnValue(["main"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function importFresh() {
|
|
||||||
return await import("./session.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseCfg: OpenClawConfig = {};
|
const baseCfg: OpenClawConfig = {};
|
||||||
|
|
||||||
it("returns sessionKey when --to resolves a session key via context", async () => {
|
it("returns sessionKey when --to resolves a session key via context", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
|
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
|
||||||
mocks.loadSessionStore.mockReturnValue({
|
mocks.loadSessionStore.mockReturnValue({
|
||||||
"agent:main:main": { sessionId: "sess-1", updatedAt: 0 },
|
"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 () => {
|
it("finds session by sessionId via reverse lookup in primary store", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
|
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
|
||||||
mocks.loadSessionStore.mockReturnValue({
|
mocks.loadSessionStore.mockReturnValue({
|
||||||
"agent:main:main": { sessionId: "target-session-id", updatedAt: 0 },
|
"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 () => {
|
it("finds session by sessionId in non-primary agent store", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
||||||
mocks.resolveStorePath.mockImplementation(
|
mocks.resolveStorePath.mockImplementation(
|
||||||
(_store: string | undefined, opts?: { agentId?: string }) => {
|
(_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 () => {
|
it("returns correct sessionStore when session found in non-primary agent store", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
const mybotStore = {
|
const mybotStore = {
|
||||||
"agent:mybot:main": { sessionId: "target-session-id", updatedAt: 0 },
|
"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 () => {
|
it("returns undefined sessionKey when sessionId not found in any store", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
||||||
mocks.resolveStorePath.mockImplementation(
|
mocks.resolveStorePath.mockImplementation(
|
||||||
(_store: string | undefined, opts?: { agentId?: string }) => {
|
(_store: string | undefined, opts?: { agentId?: string }) => {
|
||||||
@@ -144,8 +132,6 @@ describe("resolveSessionKeyForRequest", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not search other stores when explicitSessionKey is set", async () => {
|
it("does not search other stores when explicitSessionKey is set", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
||||||
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
|
mocks.resolveStorePath.mockReturnValue("/tmp/main-store.json");
|
||||||
mocks.loadSessionStore.mockReturnValue({
|
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 () => {
|
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.listAgentIds.mockReturnValue(["main", "mybot"]);
|
||||||
mocks.resolveStorePath.mockImplementation(
|
mocks.resolveStorePath.mockImplementation(
|
||||||
(_store: string | undefined, opts?: { agentId?: string }) => {
|
(_store: string | undefined, opts?: { agentId?: string }) => {
|
||||||
@@ -199,8 +183,6 @@ describe("resolveSessionKeyForRequest", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("skips already-searched primary store when iterating agents", async () => {
|
it("skips already-searched primary store when iterating agents", async () => {
|
||||||
const { resolveSessionKeyForRequest } = await importFresh();
|
|
||||||
|
|
||||||
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
mocks.listAgentIds.mockReturnValue(["main", "mybot"]);
|
||||||
mocks.resolveStorePath.mockImplementation(
|
mocks.resolveStorePath.mockImplementation(
|
||||||
(_store: string | undefined, opts?: { agentId?: string }) => {
|
(_store: string | undefined, opts?: { agentId?: string }) => {
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ vi.mock("../../config/config.js", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { skillsHandlers } = await import("./skills.js");
|
||||||
|
|
||||||
describe("skills.update", () => {
|
describe("skills.update", () => {
|
||||||
it("strips embedded CR/LF from apiKey", async () => {
|
it("strips embedded CR/LF from apiKey", async () => {
|
||||||
writtenConfig = null;
|
writtenConfig = null;
|
||||||
const { skillsHandlers } = await import("./skills.js");
|
|
||||||
|
|
||||||
let ok: boolean | null = null;
|
let ok: boolean | null = null;
|
||||||
let error: unknown = null;
|
let error: unknown = null;
|
||||||
|
|||||||
@@ -2,23 +2,22 @@ import { randomUUID } from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
import { resolvePluginTools } from "./tools.js";
|
import { resolvePluginTools } from "./tools.js";
|
||||||
|
|
||||||
type TempPlugin = { dir: string; file: string; id: string };
|
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: {} };
|
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
|
||||||
|
|
||||||
function makeTempDir() {
|
function makeFixtureDir(id: string) {
|
||||||
const dir = path.join(os.tmpdir(), `openclaw-plugin-tools-${randomUUID()}`);
|
const dir = path.join(fixtureRoot, id);
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
tempDirs.push(dir);
|
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
function writePlugin(params: { id: string; body: string }): TempPlugin {
|
function writePlugin(params: { id: string; body: string }): TempPlugin {
|
||||||
const dir = makeTempDir();
|
const dir = makeFixtureDir(params.id);
|
||||||
const file = path.join(dir, `${params.id}.js`);
|
const file = path.join(dir, `${params.id}.js`);
|
||||||
fs.writeFileSync(file, params.body, "utf-8");
|
fs.writeFileSync(file, params.body, "utf-8");
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -36,18 +35,7 @@ function writePlugin(params: { id: string; body: string }): TempPlugin {
|
|||||||
return { dir, file, id: params.id };
|
return { dir, file, id: params.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
const pluginBody = `
|
||||||
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 = `
|
|
||||||
export default { register(api) {
|
export default { register(api) {
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
{
|
{
|
||||||
@@ -63,92 +51,11 @@ export default { register(api) {
|
|||||||
} }
|
} }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
it("skips optional tools without explicit allowlist", () => {
|
const optionalDemoPlugin = writePlugin({ id: "optional-demo", body: pluginBody });
|
||||||
const plugin = writePlugin({ id: "optional-demo", body: pluginBody });
|
const coreNameCollisionPlugin = writePlugin({ id: "message", body: pluginBody });
|
||||||
const tools = resolvePluginTools({
|
const multiToolPlugin = writePlugin({
|
||||||
context: {
|
id: "multi",
|
||||||
config: {
|
body: `
|
||||||
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: `
|
|
||||||
export default { register(api) {
|
export default { register(api) {
|
||||||
api.registerTool({
|
api.registerTool({
|
||||||
name: "message",
|
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({
|
const tools = resolvePluginTools({
|
||||||
context: {
|
context: {
|
||||||
config: {
|
config: {
|
||||||
plugins: {
|
plugins: {
|
||||||
load: { paths: [plugin.file] },
|
load: { paths: [optionalDemoPlugin.file] },
|
||||||
allow: [plugin.id],
|
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"]),
|
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).
|
// Allocate in blocks to avoid derived-port overlaps (e.g. port+3).
|
||||||
const blockSize = Math.max(maxOffset + 1, 8);
|
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);
|
const start = base + ((nextTestPortOffset + attempt) % usable);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const ok = (await Promise.all(offsets.map((offset) => isPortFree(start + offset)))).every(
|
const ok = (await Promise.all(offsets.map((offset) => isPortFree(start + offset)))).every(
|
||||||
|
|||||||
Reference in New Issue
Block a user