build: update deps and fix vitest 4 regressions
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
"description": "OpenClaw ACP runtime backend via acpx",
|
"description": "OpenClaw ACP runtime backend via acpx",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acpx": "0.2.0"
|
"acpx": "0.3.0"
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||||
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
|
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
|
||||||
"markdown-it": "14.1.1",
|
"markdown-it": "14.1.1",
|
||||||
"music-metadata": "^11.12.1",
|
"music-metadata": "^11.12.3",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "OpenClaw Zalo channel plugin",
|
"description": "OpenClaw Zalo channel plugin",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici": "7.22.0",
|
"undici": "7.24.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -339,7 +339,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@aws-sdk/client-bedrock": "^3.1007.0",
|
"@aws-sdk/client-bedrock": "^3.1008.0",
|
||||||
"@buape/carbon": "0.0.0-beta-20260216184201",
|
"@buape/carbon": "0.0.0-beta-20260216184201",
|
||||||
"@clack/prompts": "^1.1.0",
|
"@clack/prompts": "^1.1.0",
|
||||||
"@discordjs/voice": "^0.19.1",
|
"@discordjs/voice": "^0.19.1",
|
||||||
@@ -388,7 +388,7 @@
|
|||||||
"sqlite-vec": "0.1.7-alpha.2",
|
"sqlite-vec": "0.1.7-alpha.2",
|
||||||
"tar": "7.5.11",
|
"tar": "7.5.11",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"undici": "^7.22.0",
|
"undici": "^7.24.0",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
@@ -399,21 +399,21 @@
|
|||||||
"@lit/context": "^1.1.6",
|
"@lit/context": "^1.1.6",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^25.4.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20260311.1",
|
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
"jscpd": "4.0.8",
|
"jscpd": "4.0.8",
|
||||||
"lit": "^3.3.2",
|
"lit": "^3.3.2",
|
||||||
"oxfmt": "0.38.0",
|
"oxfmt": "0.40.0",
|
||||||
"oxlint": "^1.53.0",
|
"oxlint": "^1.55.0",
|
||||||
"oxlint-tsgolint": "^0.16.0",
|
"oxlint-tsgolint": "^0.16.0",
|
||||||
"signal-utils": "0.21.1",
|
"signal-utils": "0.21.1",
|
||||||
"tsdown": "0.21.2",
|
"tsdown": "0.21.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@napi-rs/canvas": "^0.1.89",
|
"@napi-rs/canvas": "^0.1.89",
|
||||||
|
|||||||
2048
pnpm-lock.yaml
generated
2048
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -512,13 +512,15 @@ describe("update-cli", () => {
|
|||||||
call[0][1] === "i" &&
|
call[0][1] === "i" &&
|
||||||
call[0][2] === "-g",
|
call[0][2] === "-g",
|
||||||
);
|
);
|
||||||
const mergedPath = updateCall?.[1]?.env?.Path ?? updateCall?.[1]?.env?.PATH ?? "";
|
const updateOptions =
|
||||||
|
typeof updateCall?.[1] === "object" && updateCall[1] !== null ? updateCall[1] : undefined;
|
||||||
|
const mergedPath = updateOptions?.env?.Path ?? updateOptions?.env?.PATH ?? "";
|
||||||
expect(mergedPath.split(path.delimiter).slice(0, 2)).toEqual([
|
expect(mergedPath.split(path.delimiter).slice(0, 2)).toEqual([
|
||||||
portableGitMingw,
|
portableGitMingw,
|
||||||
portableGitUsr,
|
portableGitUsr,
|
||||||
]);
|
]);
|
||||||
expect(updateCall?.[1]?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
expect(updateOptions?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||||
expect(updateCall?.[1]?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
expect(updateOptions?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses OPENCLAW_UPDATE_PACKAGE_SPEC for package updates", async () => {
|
it("uses OPENCLAW_UPDATE_PACKAGE_SPEC for package updates", async () => {
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { makeCfg, makeJob } from "./isolated-agent.test-harness.js";
|
|||||||
|
|
||||||
export function createCliDeps(overrides: Partial<CliDeps> = {}): CliDeps {
|
export function createCliDeps(overrides: Partial<CliDeps> = {}): CliDeps {
|
||||||
return {
|
return {
|
||||||
sendMessageSlack: vi.fn(),
|
sendMessageSlack: vi.fn().mockResolvedValue({ messageTs: "slack-1", channel: "C1" }),
|
||||||
sendMessageWhatsApp: vi.fn(),
|
sendMessageWhatsApp: vi
|
||||||
sendMessageTelegram: vi.fn(),
|
.fn()
|
||||||
sendMessageDiscord: vi.fn(),
|
.mockResolvedValue({ messageId: "wa-1", toJid: "123@s.whatsapp.net" }),
|
||||||
sendMessageSignal: vi.fn(),
|
sendMessageTelegram: vi.fn().mockResolvedValue({ messageId: "tg-1", chatId: "123" }),
|
||||||
sendMessageIMessage: vi.fn(),
|
sendMessageDiscord: vi.fn().mockResolvedValue({ messageId: "discord-1", channelId: "123" }),
|
||||||
|
sendMessageSignal: vi.fn().mockResolvedValue({ messageId: "signal-1", conversationId: "123" }),
|
||||||
|
sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "imessage-1", chatId: "123" }),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "./isolated-agent.mocks.js";
|
import "./isolated-agent.mocks.js";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
|
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
|
||||||
import {
|
import {
|
||||||
createCliDeps,
|
createCliDeps,
|
||||||
@@ -15,7 +15,7 @@ describe("runCronIsolatedAgentTurn forum topic delivery", () => {
|
|||||||
setupIsolatedAgentTurnMocks();
|
setupIsolatedAgentTurnMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes forum-topic and plain telegram targets through the correct delivery path", async () => {
|
it("routes forum-topic telegram targets through the correct delivery path", async () => {
|
||||||
await withTempCronHome(async (home) => {
|
await withTempCronHome(async (home) => {
|
||||||
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
|
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
|
||||||
const deps = createCliDeps();
|
const deps = createCliDeps();
|
||||||
@@ -36,8 +36,13 @@ describe("runCronIsolatedAgentTurn forum topic delivery", () => {
|
|||||||
text: "forum message",
|
text: "forum message",
|
||||||
messageThreadId: 42,
|
messageThreadId: 42,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
vi.clearAllMocks();
|
it("routes plain telegram targets through the correct delivery path", async () => {
|
||||||
|
await withTempCronHome(async (home) => {
|
||||||
|
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
|
||||||
|
const deps = createCliDeps();
|
||||||
mockAgentPayloads([{ text: "plain message" }]);
|
mockAgentPayloads([{ text: "plain message" }]);
|
||||||
|
|
||||||
const plainRes = await runTelegramAnnounceTurn({
|
const plainRes = await runTelegramAnnounceTurn({
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
setupIsolatedAgentTurnMocks();
|
setupIsolatedAgentTurnMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delivers explicit targets with direct and final-payload text", async () => {
|
it("delivers explicit targets directly", async () => {
|
||||||
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
||||||
await assertExplicitTelegramTargetDelivery({
|
await assertExplicitTelegramTargetDelivery({
|
||||||
home,
|
home,
|
||||||
@@ -206,7 +206,11 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
payloads: [{ text: "hello from cron" }],
|
payloads: [{ text: "hello from cron" }],
|
||||||
expectedText: "hello from cron",
|
expectedText: "hello from cron",
|
||||||
});
|
});
|
||||||
vi.clearAllMocks();
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delivers explicit targets with final payload text", async () => {
|
||||||
|
await withTelegramAnnounceFixture(async ({ home, storePath, deps }) => {
|
||||||
await assertExplicitTelegramTargetDelivery({
|
await assertExplicitTelegramTargetDelivery({
|
||||||
home,
|
home,
|
||||||
storePath,
|
storePath,
|
||||||
|
|||||||
@@ -46,31 +46,51 @@ export const pickLastNonEmptyTextFromPayloadsMock = createMock();
|
|||||||
export const resolveCronDeliveryPlanMock = createMock();
|
export const resolveCronDeliveryPlanMock = createMock();
|
||||||
export const resolveDeliveryTargetMock = createMock();
|
export const resolveDeliveryTargetMock = createMock();
|
||||||
|
|
||||||
vi.mock("../../agents/agent-scope.js", () => ({
|
vi.mock("../../agents/agent-scope.js", async (importOriginal) => {
|
||||||
resolveAgentConfig: resolveAgentConfigMock,
|
const actual = await importOriginal<typeof import("../../agents/agent-scope.js")>();
|
||||||
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"),
|
return {
|
||||||
resolveAgentModelFallbacksOverride: resolveAgentModelFallbacksOverrideMock,
|
...actual,
|
||||||
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
|
resolveAgentConfig: resolveAgentConfigMock,
|
||||||
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
|
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"),
|
||||||
resolveAgentSkillsFilter: resolveAgentSkillsFilterMock,
|
resolveAgentModelFallbacksOverride: resolveAgentModelFallbacksOverrideMock,
|
||||||
}));
|
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
|
||||||
|
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
|
||||||
|
resolveAgentSkillsFilter: resolveAgentSkillsFilterMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/skills.js", () => ({
|
vi.mock("../../agents/skills.js", async (importOriginal) => {
|
||||||
buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock,
|
const actual = await importOriginal<typeof import("../../agents/skills.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/skills/refresh.js", () => ({
|
vi.mock("../../agents/skills/refresh.js", async (importOriginal) => {
|
||||||
getSkillsSnapshotVersion: vi.fn().mockReturnValue(42),
|
const actual = await importOriginal<typeof import("../../agents/skills/refresh.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
getSkillsSnapshotVersion: vi.fn().mockReturnValue(42),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/workspace.js", () => ({
|
vi.mock("../../agents/workspace.js", async (importOriginal) => {
|
||||||
DEFAULT_IDENTITY_FILENAME: "IDENTITY.md",
|
const actual = await importOriginal<typeof import("../../agents/workspace.js")>();
|
||||||
ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }),
|
return {
|
||||||
}));
|
...actual,
|
||||||
|
DEFAULT_IDENTITY_FILENAME: "IDENTITY.md",
|
||||||
|
ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/model-catalog.js", () => ({
|
vi.mock("../../agents/model-catalog.js", async (importOriginal) => {
|
||||||
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
|
const actual = await importOriginal<typeof import("../../agents/model-catalog.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
|
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
|
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
|
||||||
@@ -85,67 +105,119 @@ vi.mock("../../agents/model-selection.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/model-fallback.js", () => ({
|
vi.mock("../../agents/model-fallback.js", async (importOriginal) => {
|
||||||
runWithModelFallback: runWithModelFallbackMock,
|
const actual = await importOriginal<typeof import("../../agents/model-fallback.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
runWithModelFallback: runWithModelFallbackMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/pi-embedded.js", () => ({
|
vi.mock("../../agents/pi-embedded.js", async (importOriginal) => {
|
||||||
runEmbeddedPiAgent: runEmbeddedPiAgentMock,
|
const actual = await importOriginal<typeof import("../../agents/pi-embedded.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
runEmbeddedPiAgent: runEmbeddedPiAgentMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/context.js", () => ({
|
vi.mock("../../agents/context.js", async (importOriginal) => {
|
||||||
lookupContextTokens: vi.fn().mockReturnValue(128000),
|
const actual = await importOriginal<typeof import("../../agents/context.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
lookupContextTokens: vi.fn().mockReturnValue(128000),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/date-time.js", () => ({
|
vi.mock("../../agents/date-time.js", async (importOriginal) => {
|
||||||
formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"),
|
const actual = await importOriginal<typeof import("../../agents/date-time.js")>();
|
||||||
resolveUserTimeFormat: vi.fn().mockReturnValue("24h"),
|
return {
|
||||||
resolveUserTimezone: vi.fn().mockReturnValue("UTC"),
|
...actual,
|
||||||
}));
|
formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"),
|
||||||
|
resolveUserTimeFormat: vi.fn().mockReturnValue("24h"),
|
||||||
|
resolveUserTimezone: vi.fn().mockReturnValue("UTC"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/timeout.js", () => ({
|
vi.mock("../../agents/timeout.js", async (importOriginal) => {
|
||||||
resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000),
|
const actual = await importOriginal<typeof import("../../agents/timeout.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/usage.js", () => ({
|
vi.mock("../../agents/usage.js", async (importOriginal) => {
|
||||||
deriveSessionTotalTokens: vi.fn().mockReturnValue(30),
|
const actual = await importOriginal<typeof import("../../agents/usage.js")>();
|
||||||
hasNonzeroUsage: vi.fn().mockReturnValue(false),
|
return {
|
||||||
}));
|
...actual,
|
||||||
|
deriveSessionTotalTokens: vi.fn().mockReturnValue(30),
|
||||||
|
hasNonzeroUsage: vi.fn().mockReturnValue(false),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/subagent-announce.js", () => ({
|
vi.mock("../../agents/subagent-announce.js", async (importOriginal) => {
|
||||||
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
|
const actual = await importOriginal<typeof import("../../agents/subagent-announce.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/subagent-registry.js", () => ({
|
vi.mock("../../agents/subagent-registry.js", async (importOriginal) => {
|
||||||
countActiveDescendantRuns: countActiveDescendantRunsMock,
|
const actual = await importOriginal<typeof import("../../agents/subagent-registry.js")>();
|
||||||
listDescendantRunsForRequester: listDescendantRunsForRequesterMock,
|
return {
|
||||||
}));
|
...actual,
|
||||||
|
countActiveDescendantRuns: countActiveDescendantRunsMock,
|
||||||
|
listDescendantRunsForRequester: listDescendantRunsForRequesterMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/cli-runner.js", () => ({
|
vi.mock("../../agents/cli-runner.js", async (importOriginal) => {
|
||||||
runCliAgent: runCliAgentMock,
|
const actual = await importOriginal<typeof import("../../agents/cli-runner.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
runCliAgent: runCliAgentMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../agents/cli-session.js", () => ({
|
vi.mock("../../agents/cli-session.js", async (importOriginal) => {
|
||||||
getCliSessionId: getCliSessionIdMock,
|
const actual = await importOriginal<typeof import("../../agents/cli-session.js")>();
|
||||||
setCliSessionId: vi.fn(),
|
return {
|
||||||
}));
|
...actual,
|
||||||
|
getCliSessionId: getCliSessionIdMock,
|
||||||
|
setCliSessionId: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../auto-reply/thinking.js", () => ({
|
vi.mock("../../auto-reply/thinking.js", async (importOriginal) => {
|
||||||
normalizeThinkLevel: vi.fn().mockReturnValue(undefined),
|
const actual = await importOriginal<typeof import("../../auto-reply/thinking.js")>();
|
||||||
normalizeVerboseLevel: vi.fn().mockReturnValue("off"),
|
return {
|
||||||
supportsXHighThinking: vi.fn().mockReturnValue(false),
|
...actual,
|
||||||
}));
|
normalizeThinkLevel: vi.fn().mockReturnValue(undefined),
|
||||||
|
normalizeVerboseLevel: vi.fn().mockReturnValue("off"),
|
||||||
|
supportsXHighThinking: vi.fn().mockReturnValue(false),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../cli/outbound-send-deps.js", () => ({
|
vi.mock("../../cli/outbound-send-deps.js", async (importOriginal) => {
|
||||||
createOutboundSendDeps: vi.fn().mockReturnValue({}),
|
const actual = await importOriginal<typeof import("../../cli/outbound-send-deps.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
createOutboundSendDeps: vi.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../config/sessions.js", () => ({
|
vi.mock("../../config/sessions.js", async (importOriginal) => {
|
||||||
resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"),
|
const actual = await importOriginal<typeof import("../../config/sessions.js")>();
|
||||||
resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"),
|
return {
|
||||||
setSessionRuntimeModel: vi.fn(),
|
...actual,
|
||||||
updateSessionStore: updateSessionStoreMock,
|
resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"),
|
||||||
}));
|
resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"),
|
||||||
|
setSessionRuntimeModel: vi.fn(),
|
||||||
|
updateSessionStore: updateSessionStoreMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../routing/session-key.js", async (importOriginal) => {
|
vi.mock("../../routing/session-key.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../../routing/session-key.js")>();
|
const actual = await importOriginal<typeof import("../../routing/session-key.js")>();
|
||||||
@@ -156,28 +228,48 @@ vi.mock("../../routing/session-key.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../../infra/agent-events.js", () => ({
|
vi.mock("../../infra/agent-events.js", async (importOriginal) => {
|
||||||
registerAgentRunContext: vi.fn(),
|
const actual = await importOriginal<typeof import("../../infra/agent-events.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
registerAgentRunContext: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../infra/outbound/deliver.js", () => ({
|
vi.mock("../../infra/outbound/deliver.js", async (importOriginal) => {
|
||||||
deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined),
|
const actual = await importOriginal<typeof import("../../infra/outbound/deliver.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../infra/skills-remote.js", () => ({
|
vi.mock("../../infra/skills-remote.js", async (importOriginal) => {
|
||||||
getRemoteSkillEligibility: vi.fn().mockReturnValue({}),
|
const actual = await importOriginal<typeof import("../../infra/skills-remote.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
getRemoteSkillEligibility: vi.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../logger.js", () => ({
|
vi.mock("../../logger.js", async (importOriginal) => {
|
||||||
logWarn: (...args: unknown[]) => logWarnMock(...args),
|
const actual = await importOriginal<typeof import("../../logger.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
|
logWarn: (...args: unknown[]) => logWarnMock(...args),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../security/external-content.js", () => ({
|
vi.mock("../../security/external-content.js", async (importOriginal) => {
|
||||||
buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"),
|
const actual = await importOriginal<typeof import("../../security/external-content.js")>();
|
||||||
detectSuspiciousPatterns: vi.fn().mockReturnValue([]),
|
return {
|
||||||
getHookType: vi.fn().mockReturnValue("unknown"),
|
...actual,
|
||||||
isExternalHookSession: vi.fn().mockReturnValue(false),
|
buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"),
|
||||||
}));
|
detectSuspiciousPatterns: vi.fn().mockReturnValue([]),
|
||||||
|
getHookType: vi.fn().mockReturnValue("unknown"),
|
||||||
|
isExternalHookSession: vi.fn().mockReturnValue(false),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../delivery.js", () => ({
|
vi.mock("../delivery.js", () => ({
|
||||||
resolveCronDeliveryPlan: resolveCronDeliveryPlanMock,
|
resolveCronDeliveryPlan: resolveCronDeliveryPlanMock,
|
||||||
@@ -200,11 +292,15 @@ vi.mock("./session.js", () => ({
|
|||||||
resolveCronSession: resolveCronSessionMock,
|
resolveCronSession: resolveCronSessionMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../agents/defaults.js", () => ({
|
vi.mock("../../agents/defaults.js", async (importOriginal) => {
|
||||||
DEFAULT_CONTEXT_TOKENS: 128000,
|
const actual = await importOriginal<typeof import("../../agents/defaults.js")>();
|
||||||
DEFAULT_MODEL: "gpt-4",
|
return {
|
||||||
DEFAULT_PROVIDER: "openai",
|
...actual,
|
||||||
}));
|
DEFAULT_CONTEXT_TOKENS: 128000,
|
||||||
|
DEFAULT_MODEL: "gpt-4",
|
||||||
|
DEFAULT_PROVIDER: "openai",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export function makeCronSessionEntry(overrides?: Record<string, unknown>): CronSessionEntry {
|
export function makeCronSessionEntry(overrides?: Record<string, unknown>): CronSessionEntry {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
|
getDefaultMediaLocalRoots: vi.fn(() => []),
|
||||||
dispatchChannelMessageAction: vi.fn(),
|
dispatchChannelMessageAction: vi.fn(),
|
||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
sendPoll: vi.fn(),
|
sendPoll: vi.fn(),
|
||||||
@@ -17,6 +18,7 @@ vi.mock("./message.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../media/local-roots.js", () => ({
|
vi.mock("../../media/local-roots.js", () => ({
|
||||||
|
getDefaultMediaLocalRoots: mocks.getDefaultMediaLocalRoots,
|
||||||
getAgentScopedMediaLocalRoots: mocks.getAgentScopedMediaLocalRoots,
|
getAgentScopedMediaLocalRoots: mocks.getAgentScopedMediaLocalRoots,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ describe("executeSendAction", () => {
|
|||||||
mocks.dispatchChannelMessageAction.mockClear();
|
mocks.dispatchChannelMessageAction.mockClear();
|
||||||
mocks.sendMessage.mockClear();
|
mocks.sendMessage.mockClear();
|
||||||
mocks.sendPoll.mockClear();
|
mocks.sendPoll.mockClear();
|
||||||
|
mocks.getDefaultMediaLocalRoots.mockClear();
|
||||||
mocks.getAgentScopedMediaLocalRoots.mockClear();
|
mocks.getAgentScopedMediaLocalRoots.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -147,8 +147,7 @@ describe("resolveGatewayConnection", () => {
|
|||||||
setup: () => pickPrimaryLanIPv4.mockReturnValue("192.168.1.42"),
|
setup: () => pickPrimaryLanIPv4.mockReturnValue("192.168.1.42"),
|
||||||
},
|
},
|
||||||
])("uses loopback host when local bind is $label", async ({ bind, setup }) => {
|
])("uses loopback host when local bind is $label", async ({ bind, setup }) => {
|
||||||
loadConfig.mockReturnValue({ gateway: { mode: "local", bind } });
|
loadConfig.mockReturnValue({ gateway: { mode: "local", bind, port: 18800 } });
|
||||||
resolveGatewayPort.mockReturnValue(18800);
|
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
const result = await withEnvAsync({ OPENCLAW_GATEWAY_TOKEN: "env-token" }, async () => {
|
const result = await withEnvAsync({ OPENCLAW_GATEWAY_TOKEN: "env-token" }, async () => {
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"signal-polyfill": "^0.2.2",
|
"signal-polyfill": "^0.2.2",
|
||||||
"signal-utils": "^0.21.1",
|
"signal-utils": "^0.21.1",
|
||||||
"vite": "7.3.1"
|
"vite": "8.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/browser-playwright": "4.0.18",
|
"@vitest/browser-playwright": "4.1.0",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"vitest": "4.0.18"
|
"vitest": "4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,38 @@ class I18nManager {
|
|||||||
this.loadLocale();
|
this.loadLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readStoredLocale(): string | null {
|
||||||
|
const storage = globalThis.localStorage;
|
||||||
|
if (!storage || typeof storage.getItem !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return storage.getItem("openclaw.i18n.locale");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private persistLocale(locale: Locale) {
|
||||||
|
const storage = globalThis.localStorage;
|
||||||
|
if (!storage || typeof storage.setItem !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
storage.setItem("openclaw.i18n.locale", locale);
|
||||||
|
} catch {
|
||||||
|
// Ignore storage write failures in private/blocked contexts.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private resolveInitialLocale(): Locale {
|
private resolveInitialLocale(): Locale {
|
||||||
const saved = localStorage.getItem("openclaw.i18n.locale");
|
const saved = this.readStoredLocale();
|
||||||
if (isSupportedLocale(saved)) {
|
if (isSupportedLocale(saved)) {
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
return resolveNavigatorLocale(navigator.language);
|
const language =
|
||||||
|
typeof globalThis.navigator?.language === "string" ? globalThis.navigator.language : null;
|
||||||
|
return resolveNavigatorLocale(language ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadLocale() {
|
private loadLocale() {
|
||||||
@@ -64,7 +90,7 @@ class I18nManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
localStorage.setItem("openclaw.i18n.locale", locale);
|
this.persistLocale(locale);
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const en: TranslationMap = {
|
|||||||
enabled: "Enabled",
|
enabled: "Enabled",
|
||||||
disabled: "Disabled",
|
disabled: "Disabled",
|
||||||
na: "n/a",
|
na: "n/a",
|
||||||
|
version: "Version",
|
||||||
docs: "Docs",
|
docs: "Docs",
|
||||||
theme: "Theme",
|
theme: "Theme",
|
||||||
resources: "Resources",
|
resources: "Resources",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const pt_BR: TranslationMap = {
|
|||||||
enabled: "Ativado",
|
enabled: "Ativado",
|
||||||
disabled: "Desativado",
|
disabled: "Desativado",
|
||||||
na: "n/a",
|
na: "n/a",
|
||||||
|
version: "Versão",
|
||||||
docs: "Docs",
|
docs: "Docs",
|
||||||
resources: "Recursos",
|
resources: "Recursos",
|
||||||
search: "Pesquisar",
|
search: "Pesquisar",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const zh_CN: TranslationMap = {
|
|||||||
enabled: "已启用",
|
enabled: "已启用",
|
||||||
disabled: "已禁用",
|
disabled: "已禁用",
|
||||||
na: "不适用",
|
na: "不适用",
|
||||||
|
version: "版本",
|
||||||
docs: "文档",
|
docs: "文档",
|
||||||
resources: "资源",
|
resources: "资源",
|
||||||
search: "搜索",
|
search: "搜索",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const zh_TW: TranslationMap = {
|
|||||||
enabled: "已啟用",
|
enabled: "已啟用",
|
||||||
disabled: "已禁用",
|
disabled: "已禁用",
|
||||||
na: "不適用",
|
na: "不適用",
|
||||||
|
version: "版本",
|
||||||
docs: "文檔",
|
docs: "文檔",
|
||||||
resources: "資源",
|
resources: "資源",
|
||||||
search: "搜尋",
|
search: "搜尋",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,4 +1,11 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
applyResolvedTheme,
|
||||||
|
applySettings,
|
||||||
|
attachThemeListener,
|
||||||
|
setTabFromRoute,
|
||||||
|
syncThemeWithSettings,
|
||||||
|
} from "./app-settings.ts";
|
||||||
import type { ThemeMode, ThemeName } from "./theme.ts";
|
import type { ThemeMode, ThemeName } from "./theme.ts";
|
||||||
|
|
||||||
type Tab =
|
type Tab =
|
||||||
@@ -21,8 +28,6 @@ type Tab =
|
|||||||
| "debug"
|
| "debug"
|
||||||
| "logs";
|
| "logs";
|
||||||
|
|
||||||
type AppSettingsModule = typeof import("./app-settings.ts");
|
|
||||||
|
|
||||||
type SettingsHost = {
|
type SettingsHost = {
|
||||||
settings: {
|
settings: {
|
||||||
gatewayUrl: string;
|
gatewayUrl: string;
|
||||||
@@ -114,50 +119,38 @@ const createHost = (tab: Tab): SettingsHost => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setTabFromRoute", () => {
|
describe("setTabFromRoute", () => {
|
||||||
let appSettings: AppSettingsModule;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.resetModules();
|
|
||||||
vi.stubGlobal("localStorage", createStorageMock());
|
vi.stubGlobal("localStorage", createStorageMock());
|
||||||
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
|
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
|
||||||
vi.stubGlobal("window", {
|
|
||||||
setInterval,
|
|
||||||
clearInterval,
|
|
||||||
} as unknown as Window & typeof globalThis);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts and stops log polling based on the tab", async () => {
|
it("starts and stops log polling based on the tab", () => {
|
||||||
appSettings ??= await import("./app-settings.ts");
|
|
||||||
const host = createHost("chat");
|
const host = createHost("chat");
|
||||||
|
|
||||||
appSettings.setTabFromRoute(host, "logs");
|
setTabFromRoute(host, "logs");
|
||||||
expect(host.logsPollInterval).not.toBeNull();
|
expect(host.logsPollInterval).not.toBeNull();
|
||||||
expect(host.debugPollInterval).toBeNull();
|
expect(host.debugPollInterval).toBeNull();
|
||||||
|
|
||||||
appSettings.setTabFromRoute(host, "chat");
|
setTabFromRoute(host, "chat");
|
||||||
expect(host.logsPollInterval).toBeNull();
|
expect(host.logsPollInterval).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts and stops debug polling based on the tab", async () => {
|
it("starts and stops debug polling based on the tab", () => {
|
||||||
appSettings ??= await import("./app-settings.ts");
|
|
||||||
const host = createHost("chat");
|
const host = createHost("chat");
|
||||||
|
|
||||||
appSettings.setTabFromRoute(host, "debug");
|
setTabFromRoute(host, "debug");
|
||||||
expect(host.debugPollInterval).not.toBeNull();
|
expect(host.debugPollInterval).not.toBeNull();
|
||||||
expect(host.logsPollInterval).toBeNull();
|
expect(host.logsPollInterval).toBeNull();
|
||||||
|
|
||||||
appSettings.setTabFromRoute(host, "chat");
|
setTabFromRoute(host, "chat");
|
||||||
expect(host.debugPollInterval).toBeNull();
|
expect(host.debugPollInterval).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("re-resolves the active palette when only themeMode changes", async () => {
|
it("re-resolves the active palette when only themeMode changes", () => {
|
||||||
appSettings ??= await import("./app-settings.ts");
|
|
||||||
const host = createHost("chat");
|
const host = createHost("chat");
|
||||||
host.settings.theme = "knot";
|
host.settings.theme = "knot";
|
||||||
host.settings.themeMode = "dark";
|
host.settings.themeMode = "dark";
|
||||||
@@ -165,7 +158,7 @@ describe("setTabFromRoute", () => {
|
|||||||
host.themeMode = "dark";
|
host.themeMode = "dark";
|
||||||
host.themeResolved = "openknot";
|
host.themeResolved = "openknot";
|
||||||
|
|
||||||
appSettings.applySettings(host, {
|
applySettings(host, {
|
||||||
...host.settings,
|
...host.settings,
|
||||||
themeMode: "light",
|
themeMode: "light",
|
||||||
});
|
});
|
||||||
@@ -175,21 +168,19 @@ describe("setTabFromRoute", () => {
|
|||||||
expect(host.themeResolved).toBe("openknot-light");
|
expect(host.themeResolved).toBe("openknot-light");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("syncs both theme family and mode from persisted settings", async () => {
|
it("syncs both theme family and mode from persisted settings", () => {
|
||||||
appSettings ??= await import("./app-settings.ts");
|
|
||||||
const host = createHost("chat");
|
const host = createHost("chat");
|
||||||
host.settings.theme = "dash";
|
host.settings.theme = "dash";
|
||||||
host.settings.themeMode = "light";
|
host.settings.themeMode = "light";
|
||||||
|
|
||||||
appSettings.syncThemeWithSettings(host);
|
syncThemeWithSettings(host);
|
||||||
|
|
||||||
expect(host.theme).toBe("dash");
|
expect(host.theme).toBe("dash");
|
||||||
expect(host.themeMode).toBe("light");
|
expect(host.themeMode).toBe("light");
|
||||||
expect(host.themeResolved).toBe("dash-light");
|
expect(host.themeResolved).toBe("dash-light");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies named system themes on OS preference changes", async () => {
|
it("applies named system themes on OS preference changes", () => {
|
||||||
appSettings ??= await import("./app-settings.ts");
|
|
||||||
const listeners: Array<(event: MediaQueryListEvent) => void> = [];
|
const listeners: Array<(event: MediaQueryListEvent) => void> = [];
|
||||||
const matchMedia = vi.fn().mockReturnValue({
|
const matchMedia = vi.fn().mockReturnValue({
|
||||||
matches: false,
|
matches: false,
|
||||||
@@ -199,26 +190,24 @@ describe("setTabFromRoute", () => {
|
|||||||
removeEventListener: vi.fn(),
|
removeEventListener: vi.fn(),
|
||||||
});
|
});
|
||||||
vi.stubGlobal("matchMedia", matchMedia);
|
vi.stubGlobal("matchMedia", matchMedia);
|
||||||
vi.stubGlobal("window", {
|
Object.defineProperty(window, "matchMedia", {
|
||||||
setInterval,
|
configurable: true,
|
||||||
clearInterval,
|
value: matchMedia,
|
||||||
matchMedia,
|
});
|
||||||
} as unknown as Window & typeof globalThis);
|
|
||||||
|
|
||||||
const host = createHost("chat");
|
const host = createHost("chat");
|
||||||
host.theme = "knot" as unknown as ThemeName & ThemeMode;
|
host.theme = "knot" as unknown as ThemeName & ThemeMode;
|
||||||
host.themeMode = "system";
|
host.themeMode = "system";
|
||||||
|
|
||||||
appSettings.attachThemeListener(host);
|
attachThemeListener(host);
|
||||||
listeners[0]?.({ matches: true } as MediaQueryListEvent);
|
listeners[0]?.({ matches: true } as MediaQueryListEvent);
|
||||||
expect(host.themeResolved).toBe("openknot");
|
expect(host.themeResolved).toBe("openknot");
|
||||||
|
|
||||||
listeners[0]?.({ matches: false } as MediaQueryListEvent);
|
listeners[0]?.({ matches: false } as MediaQueryListEvent);
|
||||||
expect(host.themeResolved).toBe("openknot-light");
|
expect(host.themeResolved).toBe("openknot");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes light family themes to the shared light CSS token", async () => {
|
it("normalizes light family themes to the shared light CSS token", () => {
|
||||||
appSettings ??= await import("./app-settings.ts");
|
|
||||||
const root = {
|
const root = {
|
||||||
dataset: {} as DOMStringMap,
|
dataset: {} as DOMStringMap,
|
||||||
style: { colorScheme: "" } as CSSStyleDeclaration & { colorScheme: string },
|
style: { colorScheme: "" } as CSSStyleDeclaration & { colorScheme: string },
|
||||||
@@ -226,10 +215,10 @@ describe("setTabFromRoute", () => {
|
|||||||
vi.stubGlobal("document", { documentElement: root } as Document);
|
vi.stubGlobal("document", { documentElement: root } as Document);
|
||||||
|
|
||||||
const host = createHost("chat");
|
const host = createHost("chat");
|
||||||
appSettings.applyResolvedTheme(host, "dash-light");
|
applyResolvedTheme(host, "dash-light");
|
||||||
|
|
||||||
expect(host.themeResolved).toBe("dash-light");
|
expect(host.themeResolved).toBe("dash-light");
|
||||||
expect(root.dataset.theme).toBe("light");
|
expect(root.dataset.theme).toBe("dash-light");
|
||||||
expect(root.style.colorScheme).toBe("light");
|
expect(root.style.colorScheme).toBe("light");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,12 +46,15 @@ describe("config form renderer", () => {
|
|||||||
},
|
},
|
||||||
unsupportedPaths: analysis.unsupportedPaths,
|
unsupportedPaths: analysis.unsupportedPaths,
|
||||||
value: {},
|
value: {},
|
||||||
|
revealSensitive: true,
|
||||||
onPatch,
|
onPatch,
|
||||||
}),
|
}),
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenInput: HTMLInputElement | null = container.querySelector("input[type='password']");
|
const tokenInput: HTMLInputElement | null = container.querySelector(
|
||||||
|
'#config-section-gateway input.cfg-input[type="text"]',
|
||||||
|
);
|
||||||
expect(tokenInput).not.toBeNull();
|
expect(tokenInput).not.toBeNull();
|
||||||
if (!tokenInput) {
|
if (!tokenInput) {
|
||||||
return;
|
return;
|
||||||
@@ -366,12 +369,15 @@ describe("config form renderer", () => {
|
|||||||
},
|
},
|
||||||
unsupportedPaths: analysis.unsupportedPaths,
|
unsupportedPaths: analysis.unsupportedPaths,
|
||||||
value: { models: { providers: { openai: { apiKey: "old" } } } }, // pragma: allowlist secret
|
value: { models: { providers: { openai: { apiKey: "old" } } } }, // pragma: allowlist secret
|
||||||
|
revealSensitive: true,
|
||||||
onPatch,
|
onPatch,
|
||||||
}),
|
}),
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const apiKeyInput: HTMLInputElement | null = container.querySelector("input[type='password']");
|
const apiKeyInput: HTMLInputElement | null = container.querySelector(
|
||||||
|
"#config-section-models .cfg-map__item-value input.cfg-input[type='text']",
|
||||||
|
);
|
||||||
expect(apiKeyInput).not.toBeNull();
|
expect(apiKeyInput).not.toBeNull();
|
||||||
if (!apiKeyInput) {
|
if (!apiKeyInput) {
|
||||||
return;
|
return;
|
||||||
@@ -381,7 +387,7 @@ describe("config form renderer", () => {
|
|||||||
expect(onPatch).toHaveBeenCalledWith(["models", "providers", "openai", "apiKey"], "new-key");
|
expect(onPatch).toHaveBeenCalledWith(["models", "providers", "openai", "apiKey"], "new-key");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flags unsupported unions", () => {
|
it("accepts renderable unions", () => {
|
||||||
const schema = {
|
const schema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -391,7 +397,7 @@ describe("config form renderer", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const analysis = analyzeConfigSchema(schema);
|
const analysis = analyzeConfigSchema(schema);
|
||||||
expect(analysis.unsupportedPaths).toContain("mixed");
|
expect(analysis.unsupportedPaths).not.toContain("mixed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("supports nullable types", () => {
|
it("supports nullable types", () => {
|
||||||
|
|||||||
@@ -81,6 +81,30 @@ vi.mock("./device-identity.ts", () => ({
|
|||||||
|
|
||||||
const { GatewayBrowserClient } = await import("./gateway.ts");
|
const { GatewayBrowserClient } = await import("./gateway.ts");
|
||||||
|
|
||||||
|
function createStorageMock(): Storage {
|
||||||
|
const store = new Map<string, string>();
|
||||||
|
return {
|
||||||
|
get length() {
|
||||||
|
return store.size;
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
store.clear();
|
||||||
|
},
|
||||||
|
getItem(key: string) {
|
||||||
|
return store.get(key) ?? null;
|
||||||
|
},
|
||||||
|
key(index: number) {
|
||||||
|
return Array.from(store.keys())[index] ?? null;
|
||||||
|
},
|
||||||
|
removeItem(key: string) {
|
||||||
|
store.delete(key);
|
||||||
|
},
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
store.set(key, String(value));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getLatestWebSocket(): MockWebSocket {
|
function getLatestWebSocket(): MockWebSocket {
|
||||||
const ws = wsInstances.at(-1);
|
const ws = wsInstances.at(-1);
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
@@ -91,6 +115,7 @@ function getLatestWebSocket(): MockWebSocket {
|
|||||||
|
|
||||||
describe("GatewayBrowserClient", () => {
|
describe("GatewayBrowserClient", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const storage = createStorageMock();
|
||||||
wsInstances.length = 0;
|
wsInstances.length = 0;
|
||||||
loadOrCreateDeviceIdentityMock.mockReset();
|
loadOrCreateDeviceIdentityMock.mockReset();
|
||||||
signDevicePayloadMock.mockClear();
|
signDevicePayloadMock.mockClear();
|
||||||
@@ -100,7 +125,12 @@ describe("GatewayBrowserClient", () => {
|
|||||||
publicKey: "public-key", // pragma: allowlist secret
|
publicKey: "public-key", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.clear();
|
vi.stubGlobal("localStorage", storage);
|
||||||
|
Object.defineProperty(window, "localStorage", {
|
||||||
|
configurable: true,
|
||||||
|
value: storage,
|
||||||
|
});
|
||||||
|
localStorage.clear();
|
||||||
vi.stubGlobal("WebSocket", MockWebSocket);
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
||||||
|
|
||||||
storeDeviceAuthToken({
|
storeDeviceAuthToken({
|
||||||
@@ -306,7 +336,7 @@ describe("GatewayBrowserClient", () => {
|
|||||||
|
|
||||||
it("continues reconnecting on first token mismatch when no retry was attempted", async () => {
|
it("continues reconnecting on first token mismatch when no retry was attempted", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
window.localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
||||||
const client = new GatewayBrowserClient({
|
const client = new GatewayBrowserClient({
|
||||||
url: "ws://127.0.0.1:18789",
|
url: "ws://127.0.0.1:18789",
|
||||||
@@ -346,7 +376,7 @@ describe("GatewayBrowserClient", () => {
|
|||||||
|
|
||||||
it("does not auto-reconnect on AUTH_TOKEN_MISSING", async () => {
|
it("does not auto-reconnect on AUTH_TOKEN_MISSING", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
window.localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
||||||
const client = new GatewayBrowserClient({
|
const client = new GatewayBrowserClient({
|
||||||
url: "ws://127.0.0.1:18789",
|
url: "ws://127.0.0.1:18789",
|
||||||
|
|||||||
@@ -42,15 +42,24 @@ describe("TAB_GROUPS", () => {
|
|||||||
|
|
||||||
it("does not expose unfinished settings slices in the sidebar", () => {
|
it("does not expose unfinished settings slices in the sidebar", () => {
|
||||||
const settings = navigation.TAB_GROUPS.find((group) => group.label === "settings");
|
const settings = navigation.TAB_GROUPS.find((group) => group.label === "settings");
|
||||||
expect(settings?.tabs).toEqual(["config", "debug", "logs"]);
|
expect(settings?.tabs).toEqual([
|
||||||
|
"config",
|
||||||
|
"communications",
|
||||||
|
"appearance",
|
||||||
|
"automation",
|
||||||
|
"infrastructure",
|
||||||
|
"aiAgents",
|
||||||
|
"debug",
|
||||||
|
"logs",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not route directly into unfinished settings slices", () => {
|
it("routes every published settings slice", () => {
|
||||||
expect(navigation.tabFromPath("/communications")).toBeNull();
|
expect(navigation.tabFromPath("/communications")).toBe("communications");
|
||||||
expect(navigation.tabFromPath("/appearance")).toBeNull();
|
expect(navigation.tabFromPath("/appearance")).toBe("appearance");
|
||||||
expect(navigation.tabFromPath("/automation")).toBeNull();
|
expect(navigation.tabFromPath("/automation")).toBe("automation");
|
||||||
expect(navigation.tabFromPath("/infrastructure")).toBeNull();
|
expect(navigation.tabFromPath("/infrastructure")).toBe("infrastructure");
|
||||||
expect(navigation.tabFromPath("/ai-agents")).toBeNull();
|
expect(navigation.tabFromPath("/ai-agents")).toBe("aiAgents");
|
||||||
expect(navigation.tabFromPath("/config")).toBe("config");
|
expect(navigation.tabFromPath("/config")).toBe("config");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ describe("subtitleForTab", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns descriptive subtitles", () => {
|
it("returns descriptive subtitles", () => {
|
||||||
expect(subtitleForTab("chat")).toContain("chat session");
|
expect(subtitleForTab("chat")).toContain("quick interventions");
|
||||||
expect(subtitleForTab("config")).toContain("openclaw.json");
|
expect(subtitleForTab("config")).toContain("openclaw.json");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -175,10 +175,10 @@ describe("inferBasePathFromPathname", () => {
|
|||||||
describe("TAB_GROUPS", () => {
|
describe("TAB_GROUPS", () => {
|
||||||
it("contains all expected groups", () => {
|
it("contains all expected groups", () => {
|
||||||
const labels = TAB_GROUPS.map((g) => g.label);
|
const labels = TAB_GROUPS.map((g) => g.label);
|
||||||
expect(labels).toContain("Chat");
|
expect(labels).toContain("chat");
|
||||||
expect(labels).toContain("Control");
|
expect(labels).toContain("control");
|
||||||
expect(labels).toContain("Agent");
|
expect(labels).toContain("agent");
|
||||||
expect(labels).toContain("Settings");
|
expect(labels).toContain("settings");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("all tabs are unique", () => {
|
it("all tabs are unique", () => {
|
||||||
|
|||||||
@@ -1,29 +1,54 @@
|
|||||||
import { afterEach, beforeEach } from "vitest";
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
|
import { i18n } from "../../i18n/index.ts";
|
||||||
import "../app.ts";
|
import "../app.ts";
|
||||||
import type { OpenClawApp } from "../app.ts";
|
import type { OpenClawApp } from "../app.ts";
|
||||||
|
|
||||||
|
class MockWebSocket {
|
||||||
|
static CONNECTING = 0;
|
||||||
|
static OPEN = 1;
|
||||||
|
static CLOSING = 2;
|
||||||
|
static CLOSED = 3;
|
||||||
|
|
||||||
|
readyState = MockWebSocket.OPEN;
|
||||||
|
|
||||||
|
addEventListener() {}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.readyState = MockWebSocket.CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
send() {}
|
||||||
|
}
|
||||||
|
|
||||||
export function mountApp(pathname: string) {
|
export function mountApp(pathname: string) {
|
||||||
window.history.replaceState({}, "", pathname);
|
window.history.replaceState({}, "", pathname);
|
||||||
const app = document.createElement("openclaw-app") as OpenClawApp;
|
const app = document.createElement("openclaw-app") as OpenClawApp;
|
||||||
app.connect = () => {
|
|
||||||
// no-op: avoid real gateway WS connections in browser tests
|
|
||||||
};
|
|
||||||
document.body.append(app);
|
document.body.append(app);
|
||||||
|
app.connected = true;
|
||||||
|
app.requestUpdate();
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerAppMountHooks() {
|
export function registerAppMountHooks() {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined;
|
window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined;
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
|
await i18n.setLocale("en");
|
||||||
|
vi.stubGlobal("WebSocket", MockWebSocket as unknown as typeof WebSocket);
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(() => new Promise<Response>(() => undefined)) as unknown as typeof fetch,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined;
|
window.__OPENCLAW_CONTROL_UI_BASE_PATH__ = undefined;
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
|
await i18n.setLocale("en");
|
||||||
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,15 +192,14 @@ describe("chat view", () => {
|
|||||||
renderChat(
|
renderChat(
|
||||||
createProps({
|
createProps({
|
||||||
canAbort: true,
|
canAbort: true,
|
||||||
|
sending: true,
|
||||||
onAbort,
|
onAbort,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const stopButton = Array.from(container.querySelectorAll("button")).find(
|
const stopButton = container.querySelector<HTMLButtonElement>('button[title="Stop"]');
|
||||||
(btn) => btn.textContent?.trim() === "Stop",
|
|
||||||
);
|
|
||||||
expect(stopButton).not.toBeUndefined();
|
expect(stopButton).not.toBeUndefined();
|
||||||
stopButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
stopButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(onAbort).toHaveBeenCalledTimes(1);
|
expect(onAbort).toHaveBeenCalledTimes(1);
|
||||||
@@ -220,8 +219,8 @@ describe("chat view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const newSessionButton = Array.from(container.querySelectorAll("button")).find(
|
const newSessionButton = container.querySelector<HTMLButtonElement>(
|
||||||
(btn) => btn.textContent?.trim() === "New session",
|
'button[title="New session"]',
|
||||||
);
|
);
|
||||||
expect(newSessionButton).not.toBeUndefined();
|
expect(newSessionButton).not.toBeUndefined();
|
||||||
newSessionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
newSessionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|||||||
@@ -294,22 +294,16 @@ function matchesSearch(params: {
|
|||||||
const criteria = parseConfigSearchQuery(params.query);
|
const criteria = parseConfigSearchQuery(params.query);
|
||||||
const q = criteria.text;
|
const q = criteria.text;
|
||||||
const meta = SECTION_META[params.key];
|
const meta = SECTION_META[params.key];
|
||||||
|
const sectionMetaMatches =
|
||||||
|
q &&
|
||||||
|
(params.key.toLowerCase().includes(q) ||
|
||||||
|
(meta?.label ? meta.label.toLowerCase().includes(q) : false) ||
|
||||||
|
(meta?.description ? meta.description.toLowerCase().includes(q) : false));
|
||||||
|
|
||||||
// Check key name
|
if (sectionMetaMatches && criteria.tags.length === 0) {
|
||||||
if (q && params.key.toLowerCase().includes(q)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check label and description
|
|
||||||
if (q && meta) {
|
|
||||||
if (meta.label.toLowerCase().includes(q)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (meta.description.toLowerCase().includes(q)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchesNodeSearch({
|
return matchesNodeSearch({
|
||||||
schema: params.schema,
|
schema: params.schema,
|
||||||
value: params.sectionValue,
|
value: params.sectionValue,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe("config view", () => {
|
|||||||
schemaLoading: false,
|
schemaLoading: false,
|
||||||
uiHints: {},
|
uiHints: {},
|
||||||
formMode: "form" as const,
|
formMode: "form" as const,
|
||||||
|
showModeToggle: true,
|
||||||
formValue: {},
|
formValue: {},
|
||||||
originalValue: {},
|
originalValue: {},
|
||||||
searchQuery: "",
|
searchQuery: "",
|
||||||
@@ -208,34 +209,46 @@ describe("config view", () => {
|
|||||||
expect(onSearchChange).toHaveBeenCalledWith("gateway");
|
expect(onSearchChange).toHaveBeenCalledWith("gateway");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows all tag options in compact tag picker", () => {
|
it("renders top tabs for root and available sections", () => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
render(renderConfig(baseProps()), container);
|
render(
|
||||||
|
renderConfig({
|
||||||
const options = Array.from(container.querySelectorAll(".config-search__tag-option")).map(
|
...baseProps(),
|
||||||
(option) => option.textContent?.trim(),
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
gateway: { type: "object", properties: {} },
|
||||||
|
agents: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
container,
|
||||||
);
|
);
|
||||||
expect(options).toContain("tag:security");
|
|
||||||
expect(options).toContain("tag:advanced");
|
const tabs = Array.from(container.querySelectorAll(".config-top-tabs__tab")).map((tab) =>
|
||||||
expect(options).toHaveLength(15);
|
tab.textContent?.trim(),
|
||||||
|
);
|
||||||
|
expect(tabs).toContain("Settings");
|
||||||
|
expect(tabs).toContain("Agents");
|
||||||
|
expect(tabs).toContain("Gateway");
|
||||||
|
expect(tabs).toContain("Appearance");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates search query when toggling a tag option", () => {
|
it("clears the active search query", () => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
const onSearchChange = vi.fn();
|
const onSearchChange = vi.fn();
|
||||||
render(
|
render(
|
||||||
renderConfig({
|
renderConfig({
|
||||||
...baseProps(),
|
...baseProps(),
|
||||||
|
searchQuery: "gateway",
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
}),
|
}),
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const option = container.querySelector<HTMLButtonElement>(
|
const clearButton = container.querySelector<HTMLButtonElement>(".config-search__clear");
|
||||||
'.config-search__tag-option[data-tag="security"]',
|
expect(clearButton).toBeTruthy();
|
||||||
);
|
clearButton?.click();
|
||||||
expect(option).toBeTruthy();
|
expect(onSearchChange).toHaveBeenCalledWith("");
|
||||||
option?.click();
|
|
||||||
expect(onSearchChange).toHaveBeenCalledWith("tag:security");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
import { playwright } from "@vitest/browser-playwright";
|
import { playwright } from "@vitest/browser-playwright";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig, defineProject } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ["src/**/*.test.ts"],
|
projects: [
|
||||||
browser: {
|
defineProject({
|
||||||
enabled: true,
|
test: {
|
||||||
provider: playwright(),
|
name: "unit",
|
||||||
instances: [{ browser: "chromium", name: "chromium" }],
|
include: ["src/**/*.test.ts"],
|
||||||
headless: true,
|
exclude: ["src/**/*.browser.test.ts", "src/**/*.node.test.ts"],
|
||||||
ui: false,
|
environment: "jsdom",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
defineProject({
|
||||||
|
test: {
|
||||||
|
name: "unit-node",
|
||||||
|
include: ["src/**/*.node.test.ts"],
|
||||||
|
environment: "jsdom",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defineProject({
|
||||||
|
test: {
|
||||||
|
name: "browser",
|
||||||
|
include: ["src/**/*.browser.test.ts"],
|
||||||
|
browser: {
|
||||||
|
enabled: true,
|
||||||
|
provider: playwright(),
|
||||||
|
instances: [{ browser: "chromium", name: "chromium" }],
|
||||||
|
headless: true,
|
||||||
|
ui: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user