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