test: narrow hotspot boundaries

This commit is contained in:
Peter Steinberger
2026-04-17 01:09:28 +01:00
parent 0dc4c4076c
commit 59b98334f6
5 changed files with 174 additions and 160 deletions

View File

@@ -4,9 +4,8 @@ const resolveAgentWorkspaceDir = vi.hoisted(() =>
vi.fn((_cfg?: unknown, _agentId?: unknown) => "/tmp/openclaw-workspace"),
);
const resolveDefaultAgentId = vi.hoisted(() => vi.fn((_cfg?: unknown) => "default"));
const listChannelPluginCatalogEntries = vi.hoisted(() => vi.fn((_opts?: unknown): unknown[] => []));
const getChannelPluginCatalogEntry = vi.hoisted(() =>
vi.fn((_id?: unknown, _opts?: unknown) => undefined),
const listTrustedChannelPluginCatalogEntries = vi.hoisted(() =>
vi.fn((_params?: unknown): unknown[] => []),
);
const getChannelSetupPlugin = vi.hoisted(() => vi.fn((_channel?: unknown) => undefined));
const listChannelSetupPlugins = vi.hoisted(() => vi.fn((): unknown[] => []));
@@ -30,12 +29,6 @@ vi.mock("../agents/agent-scope.js", () => ({
resolveDefaultAgentId: (cfg?: unknown) => resolveDefaultAgentId(cfg),
}));
vi.mock("../channels/plugins/catalog.js", () => ({
listChannelPluginCatalogEntries: (opts?: unknown) => listChannelPluginCatalogEntries(opts),
getChannelPluginCatalogEntry: (id?: unknown, opts?: unknown) =>
getChannelPluginCatalogEntry(id, opts),
}));
vi.mock("../channels/plugins/setup-registry.js", () => ({
getChannelSetupPlugin: (channel?: unknown) => getChannelSetupPlugin(channel),
listChannelSetupPlugins: () => listChannelSetupPlugins(),
@@ -63,6 +56,11 @@ vi.mock("../commands/channel-setup/registry.js", () => ({
resolveChannelSetupWizardAdapterForPlugin: () => undefined,
}));
vi.mock("../commands/channel-setup/trusted-catalog.js", () => ({
listTrustedChannelPluginCatalogEntries: (params?: unknown) =>
listTrustedChannelPluginCatalogEntries(params),
}));
vi.mock("../config/channel-configured.js", () => ({
isChannelConfigured: (cfg?: unknown, channel?: unknown) => isChannelConfigured(cfg, channel),
}));
@@ -90,10 +88,11 @@ describe("setupChannels workspace shadow exclusion", () => {
vi.clearAllMocks();
resolveAgentWorkspaceDir.mockReturnValue("/tmp/openclaw-workspace");
resolveDefaultAgentId.mockReturnValue("default");
listChannelPluginCatalogEntries.mockReturnValue([
listTrustedChannelPluginCatalogEntries.mockReturnValue([
{
id: "telegram",
pluginId: "@openclaw/telegram-plugin",
origin: "bundled",
},
]);
getChannelSetupPlugin.mockReturnValue(undefined);
@@ -112,13 +111,7 @@ describe("setupChannels workspace shadow exclusion", () => {
isChannelConfigured.mockReturnValue(true);
});
it("preloads configured external plugins from the bundled fallback for untrusted shadows", async () => {
listChannelPluginCatalogEntries.mockImplementation((opts?: unknown) =>
(opts as { excludeWorkspace?: boolean } | undefined)?.excludeWorkspace
? [{ id: "telegram", pluginId: "@openclaw/telegram-plugin", origin: "bundled" }]
: [{ id: "telegram", pluginId: "evil-telegram-shadow", origin: "workspace" }],
);
it("preloads configured external plugins from the trusted catalog boundary", async () => {
await setupChannels(
{} as never,
{} as never,
@@ -128,10 +121,12 @@ describe("setupChannels workspace shadow exclusion", () => {
} as never,
);
const fallbackCall = listChannelPluginCatalogEntries.mock.calls.find(
([opts]) => (opts as { excludeWorkspace?: boolean } | undefined)?.excludeWorkspace === true,
expect(listTrustedChannelPluginCatalogEntries).toHaveBeenCalledWith(
expect.objectContaining({
cfg: {},
workspaceDir: "/tmp/openclaw-workspace",
}),
);
expect(fallbackCall).toBeTruthy();
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
expect.objectContaining({
channel: "telegram",
@@ -142,7 +137,7 @@ describe("setupChannels workspace shadow exclusion", () => {
});
it("keeps trusted workspace overrides eligible during preload", async () => {
listChannelPluginCatalogEntries.mockReturnValue([
listTrustedChannelPluginCatalogEntries.mockReturnValue([
{ id: "telegram", pluginId: "trusted-telegram-shadow", origin: "workspace" },
]);

View File

@@ -12,14 +12,13 @@ import {
} from "./embeddings-gemini.js";
import {
createGeminiBatchFetchMock,
createGeminiFetchMock,
createJsonResponseFetchMock,
installFetchMock,
mockResolvedProviderKey,
parseFetchBody,
readFirstFetchRequest,
type JsonFetchMock,
} from "./embeddings-provider.test-support.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../../agents/model-auth.js", async () => {
const { createModelAuthMockModule } = await import("../../test-utils/model-auth-mock.js");
@@ -42,7 +41,6 @@ async function createProviderWithFetch(
options: Partial<Parameters<typeof createGeminiEmbeddingProvider>[0]> & { model: string },
) {
installFetchMock(fetchMock as unknown as typeof globalThis.fetch);
mockPublicPinnedHostname();
mockResolvedProviderKey(authModule.resolveApiKeyForProvider);
const { provider } = await createGeminiEmbeddingProvider({
config: {} as never,
@@ -169,55 +167,26 @@ describe("gemini embedding provider", () => {
expect(parseFetchBody(legacyFetch, 0)).not.toHaveProperty("outputDimensionality");
expect(parseFetchBody(legacyFetch, 1)).not.toHaveProperty("outputDimensionality");
const v2QueryFetch = createGeminiFetchMock([3, 4, Number.NaN]);
const v2QueryProvider = await createProviderWithFetch(v2QueryFetch, {
model: "gemini-embedding-2-preview",
const v2Fetch = createJsonResponseFetchMock((input) => {
const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
return url.endsWith(":batchEmbedContents")
? {
embeddings: Array.from({ length: 2 }, () => ({
values: [0, Number.POSITIVE_INFINITY, 5],
})),
}
: { embedding: { values: [3, 4, Number.NaN] } };
});
await expect(v2QueryProvider.embedQuery(" ")).resolves.toEqual([]);
await expect(v2QueryProvider.embedBatch([])).resolves.toEqual([]);
await expect(v2QueryProvider.embedQuery("test query")).resolves.toEqual([0.6, 0.8, 0]);
const v2BatchFetch = createGeminiBatchFetchMock(2, [0, Number.POSITIVE_INFINITY, 5]);
const v2BatchProvider = await createProviderWithFetch(v2BatchFetch, {
model: "gemini-embedding-2-preview",
});
const batch = await v2BatchProvider.embedBatch(["text1", "text2"]);
expect(batch).toEqual([
[0, 0, 1],
[0, 0, 1],
]);
expect(parseFetchBody(v2QueryFetch)).toMatchObject({
outputDimensionality: 3072,
taskType: "RETRIEVAL_QUERY",
content: { parts: [{ text: "test query" }] },
});
expect(parseFetchBody(v2BatchFetch).requests).toEqual([
{
model: "models/gemini-embedding-2-preview",
content: { parts: [{ text: "text1" }] },
taskType: "RETRIEVAL_DOCUMENT",
outputDimensionality: 3072,
},
{
model: "models/gemini-embedding-2-preview",
content: { parts: [{ text: "text2" }] },
taskType: "RETRIEVAL_DOCUMENT",
outputDimensionality: 3072,
},
]);
});
it("supports custom dimensions, task type, multimodal inputs, and endpoint URL", async () => {
const fetchMock = createGeminiBatchFetchMock(2);
const provider = await createProviderWithFetch(fetchMock, {
const v2Provider = await createProviderWithFetch(v2Fetch, {
model: "gemini-embedding-2-preview",
outputDimensionality: 768,
taskType: "SEMANTIC_SIMILARITY",
});
await expect(v2Provider.embedQuery(" ")).resolves.toEqual([]);
await expect(v2Provider.embedBatch([])).resolves.toEqual([]);
await expect(v2Provider.embedQuery("test query")).resolves.toEqual([0.6, 0.8, 0]);
await provider.embedQuery("test");
const structuredBatch = await provider.embedBatchInputs?.([
const structuredBatch = await v2Provider.embedBatchInputs?.([
{
text: "Image file: diagram.png",
parts: [
@@ -234,19 +203,20 @@ describe("gemini embedding provider", () => {
},
]);
expect(structuredBatch).toEqual([
[0.2672612419124244, 0.5345224838248488, 0.8017837257372732],
[0.2672612419124244, 0.5345224838248488, 0.8017837257372732],
[0, 0, 1],
[0, 0, 1],
]);
const { url } = readFirstFetchRequest(fetchMock);
const { url } = readFirstFetchRequest(v2Fetch);
expect(url).toBe(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-2-preview:embedContent",
);
expect(parseFetchBody(fetchMock, 0)).toMatchObject({
expect(parseFetchBody(v2Fetch, 0)).toMatchObject({
outputDimensionality: 768,
taskType: "SEMANTIC_SIMILARITY",
content: { parts: [{ text: "test query" }] },
});
expect(parseFetchBody(fetchMock, 1).requests).toEqual([
expect(parseFetchBody(v2Fetch, 1).requests).toEqual([
{
model: "models/gemini-embedding-2-preview",
content: {

View File

@@ -7,6 +7,48 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vites
import { resolveOAuthDir } from "../config/paths.js";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
import { withEnvAsync } from "../test-utils/env.js";
vi.mock("../channels/plugins/pairing.js", () => ({
getPairingAdapter: () => null,
}));
vi.mock("../infra/file-lock.js", () => ({
withFileLock: async (_path: string, _options: unknown, fn: () => unknown) => await fn(),
}));
vi.mock("../plugin-sdk/json-store.js", async () => {
const fs = await import("node:fs/promises");
const path = await import("node:path");
return {
readJsonFileWithFallback: async <T>(filePath: string, fallback: T) => {
let raw: string;
try {
raw = await fs.readFile(filePath, "utf8");
} catch (err) {
if ((err as { code?: string }).code === "ENOENT") {
return { value: fallback, exists: false };
}
return { value: fallback, exists: false };
}
try {
const parsed = JSON.parse(raw) as T;
return {
value: parsed ?? fallback,
exists: true,
};
} catch {
return { value: fallback, exists: true };
}
},
writeJsonFileAtomically: async (filePath: string, value: unknown) => {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
},
};
});
import * as jsonStore from "../plugin-sdk/json-store.js";
import {
addChannelAllowFromStoreEntry,
clearPairingAllowFromReadCacheForTest,
@@ -552,7 +594,7 @@ describe("pairing store", () => {
await withTempStateDir(async (stateDir) => {
for (const variant of [
{
createReadSpy: () => vi.spyOn(fs, "readFile"),
createReadSpy: () => vi.spyOn(jsonStore, "readJsonFileWithFallback"),
readAllowFrom: () => readChannelAllowFromStore("telegram", process.env, "yy"),
},
{

View File

@@ -2,7 +2,6 @@
import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
import { i18n } from "../../i18n/index.ts";
import { getSafeLocalStorage } from "../../local-storage.ts";
import { renderChatSessionSelect } from "../app-render.helpers.ts";
import type { AppViewState } from "../app-view-state.ts";
@@ -18,7 +17,6 @@ import type { GatewayBrowserClient } from "../gateway.ts";
import type { ModelCatalogEntry } from "../types.ts";
import type { SessionsListResult } from "../types.ts";
import { renderChat, type ChatProps } from "./chat.ts";
import { renderOverview, type OverviewProps } from "./overview.ts";
function createSessions(): SessionsListResult {
return {
@@ -197,60 +195,6 @@ function createProps(overrides: Partial<ChatProps> = {}): ChatProps {
};
}
function createOverviewProps(overrides: Partial<OverviewProps> = {}): OverviewProps {
return {
warnQueryToken: false,
connected: false,
hello: null,
settings: {
gatewayUrl: "",
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
locale: "en",
},
password: "",
lastError: null,
lastErrorCode: null,
presenceCount: 0,
sessionsCount: null,
cronEnabled: null,
cronNext: null,
lastChannelsRefresh: null,
modelAuthStatus: null,
usageResult: null,
sessionsResult: null,
skillsReport: null,
cronJobs: [],
cronStatus: null,
attentionItems: [],
eventLog: [],
overviewLogLines: [],
showGatewayToken: false,
showGatewayPassword: false,
onSettingsChange: () => undefined,
onPasswordChange: () => undefined,
onSessionKeyChange: () => undefined,
onToggleGatewayTokenVisibility: () => undefined,
onToggleGatewayPasswordVisibility: () => undefined,
onConnect: () => undefined,
onRefresh: () => undefined,
onNavigate: () => undefined,
onRefreshLogs: () => undefined,
...overrides,
};
}
describe("chat view", () => {
it("renders BTW side results outside transcript history", () => {
const container = document.createElement("div");
@@ -543,37 +487,6 @@ describe("chat view", () => {
expect(groupedLogo?.getAttribute("src")).toBe("/openclaw/favicon.svg");
});
it("keeps the persisted overview locale selected before i18n hydration finishes", async () => {
const container = document.createElement("div");
const props = createOverviewProps({
settings: {
...createOverviewProps().settings,
locale: "zh-CN",
},
});
getSafeLocalStorage()?.clear();
await i18n.setLocale("en");
render(renderOverview(props), container);
await Promise.resolve();
let select = container.querySelector<HTMLSelectElement>("select");
expect(i18n.getLocale()).toBe("en");
expect(select?.value).toBe("zh-CN");
expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (Simplified Chinese)");
await i18n.setLocale("zh-CN");
render(renderOverview(props), container);
await Promise.resolve();
select = container.querySelector<HTMLSelectElement>("select");
expect(select?.value).toBe("zh-CN");
expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (简体中文)");
await i18n.setLocale("en");
});
it("renders compacting indicator as a badge", () => {
const container = document.createElement("div");
render(

View File

@@ -0,0 +1,94 @@
/* @vitest-environment jsdom */
import { render } from "lit";
import { describe, expect, it } from "vitest";
import { i18n } from "../../i18n/index.ts";
import { getSafeLocalStorage } from "../../local-storage.ts";
import { renderOverview, type OverviewProps } from "./overview.ts";
function createOverviewProps(overrides: Partial<OverviewProps> = {}): OverviewProps {
return {
warnQueryToken: false,
connected: false,
hello: null,
settings: {
gatewayUrl: "",
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
locale: "en",
},
password: "",
lastError: null,
lastErrorCode: null,
presenceCount: 0,
sessionsCount: null,
cronEnabled: null,
cronNext: null,
lastChannelsRefresh: null,
modelAuthStatus: null,
usageResult: null,
sessionsResult: null,
skillsReport: null,
cronJobs: [],
cronStatus: null,
attentionItems: [],
eventLog: [],
overviewLogLines: [],
showGatewayToken: false,
showGatewayPassword: false,
onSettingsChange: () => undefined,
onPasswordChange: () => undefined,
onSessionKeyChange: () => undefined,
onToggleGatewayTokenVisibility: () => undefined,
onToggleGatewayPasswordVisibility: () => undefined,
onConnect: () => undefined,
onRefresh: () => undefined,
onNavigate: () => undefined,
onRefreshLogs: () => undefined,
...overrides,
};
}
describe("overview view rendering", () => {
it("keeps the persisted overview locale selected before i18n hydration finishes", async () => {
const container = document.createElement("div");
const props = createOverviewProps({
settings: {
...createOverviewProps().settings,
locale: "zh-CN",
},
});
getSafeLocalStorage()?.clear();
await i18n.setLocale("en");
render(renderOverview(props), container);
await Promise.resolve();
let select = container.querySelector<HTMLSelectElement>("select");
expect(i18n.getLocale()).toBe("en");
expect(select?.value).toBe("zh-CN");
expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (Simplified Chinese)");
await i18n.setLocale("zh-CN");
render(renderOverview(props), container);
await Promise.resolve();
select = container.querySelector<HTMLSelectElement>("select");
expect(select?.value).toBe("zh-CN");
expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (简体中文)");
await i18n.setLocale("en");
});
});