test: streamline slow import-heavy suites

This commit is contained in:
Peter Steinberger
2026-04-25 10:01:41 +01:00
parent f7d276b842
commit ed0210a187
6 changed files with 136 additions and 162 deletions

View File

@@ -3,6 +3,8 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { withEnv } from "../test-utils/env.js";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { writeSkill } from "./skills.e2e-test-helpers.js";
import { createSyntheticSourceInfo } from "./skills/skill-contract.js";
import type { OpenClawSkillMetadata, SkillEntry } from "./skills/types.js";
import { buildWorkspaceSkillsPrompt } from "./skills/workspace.js";
const fixtureSuite = createFixtureSuite("openclaw-skills-prompt-suite-");
@@ -15,6 +17,27 @@ afterAll(async () => {
await fixtureSuite.cleanup();
});
function createSkillEntry(params: {
name: string;
description?: string;
metadata?: OpenClawSkillMetadata;
}): SkillEntry {
const filePath = `/skills/${params.name}/SKILL.md`;
return {
skill: {
name: params.name,
description: params.description ?? params.name,
filePath,
source: "project",
baseDir: path.dirname(filePath),
sourceInfo: createSyntheticSourceInfo(filePath, { source: "project" }),
disableModelInvocation: false,
},
frontmatter: {},
metadata: params.metadata,
};
}
describe("buildWorkspaceSkillsPrompt", () => {
it("prefers workspace skills over managed skills", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
@@ -57,42 +80,38 @@ describe("buildWorkspaceSkillsPrompt", () => {
});
it("gates by bins, config, and always", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const skillsDir = path.join(workspaceDir, "skills");
await writeSkill({
dir: path.join(skillsDir, "bin-skill"),
name: "bin-skill",
description: "Needs a bin",
metadata: '{"openclaw":{"requires":{"bins":["fakebin"]}}}',
});
await writeSkill({
dir: path.join(skillsDir, "anybin-skill"),
name: "anybin-skill",
description: "Needs any bin",
metadata: '{"openclaw":{"requires":{"anyBins":["missingbin","fakebin"]}}}',
});
await writeSkill({
dir: path.join(skillsDir, "config-skill"),
name: "config-skill",
description: "Needs config",
metadata: '{"openclaw":{"requires":{"config":["browser.enabled"]}}}',
});
await writeSkill({
dir: path.join(skillsDir, "always-skill"),
name: "always-skill",
description: "Always on",
metadata: '{"openclaw":{"always":true,"requires":{"env":["MISSING"]}}}',
});
await writeSkill({
dir: path.join(skillsDir, "env-skill"),
name: "env-skill",
description: "Needs env",
metadata: '{"openclaw":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}',
});
const entries = [
createSkillEntry({
name: "bin-skill",
description: "Needs a bin",
metadata: { requires: { bins: ["fakebin"] } },
}),
createSkillEntry({
name: "anybin-skill",
description: "Needs any bin",
metadata: { requires: { anyBins: ["missingbin", "fakebin"] } },
}),
createSkillEntry({
name: "config-skill",
description: "Needs config",
metadata: { requires: { config: ["browser.enabled"] } },
}),
createSkillEntry({
name: "always-skill",
description: "Always on",
metadata: { always: true, requires: { env: ["MISSING"] } },
}),
createSkillEntry({
name: "env-skill",
description: "Needs env",
metadata: { requires: { env: ["ENV_KEY"] }, primaryEnv: "ENV_KEY" },
}),
];
const managedSkillsDir = path.join(workspaceDir, ".managed");
const defaultPrompt = withEnv({ HOME: workspaceDir, PATH: "" }, () =>
buildWorkspaceSkillsPrompt(workspaceDir, {
entries,
managedSkillsDir,
eligibility: {
remote: {
@@ -112,6 +131,7 @@ describe("buildWorkspaceSkillsPrompt", () => {
const gatedPrompt = withEnv({ HOME: workspaceDir, PATH: "" }, () =>
buildWorkspaceSkillsPrompt(workspaceDir, {
entries,
managedSkillsDir,
config: {
browser: { enabled: false },
@@ -135,16 +155,15 @@ describe("buildWorkspaceSkillsPrompt", () => {
});
it("uses skillKey for config lookups", async () => {
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const skillDir = path.join(workspaceDir, "skills", "alias-skill");
await writeSkill({
dir: skillDir,
name: "alias-skill",
description: "Uses skillKey",
metadata: '{"openclaw":{"skillKey":"alias"}}',
});
const prompt = withEnv({ HOME: workspaceDir, PATH: "" }, () =>
buildWorkspaceSkillsPrompt(workspaceDir, {
entries: [
createSkillEntry({
name: "alias-skill",
description: "Uses skillKey",
metadata: { skillKey: "alias" },
}),
],
managedSkillsDir: path.join(workspaceDir, ".managed"),
config: { skills: { entries: { alias: { enabled: false } } } },
}),

View File

@@ -1,6 +1,20 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import type { DoctorConfigPreflightResult } from "../../doctor-config-preflight.js";
const { migrateLegacyConfigMock, stripUnknownConfigKeysMock } = vi.hoisted(() => ({
migrateLegacyConfigMock: vi.fn(),
stripUnknownConfigKeysMock: vi.fn(),
}));
vi.mock("./legacy-config-migrate.js", () => ({
migrateLegacyConfig: migrateLegacyConfigMock,
}));
vi.mock("../../doctor-config-analysis.js", () => ({
stripUnknownConfigKeys: stripUnknownConfigKeysMock,
}));
import { applyLegacyCompatibilityStep, applyUnknownConfigKeyStep } from "./config-flow-steps.js";
function createLegacyStepResult(
@@ -21,7 +35,21 @@ function createLegacyStepResult(
}
describe("doctor config flow steps", () => {
beforeEach(() => {
migrateLegacyConfigMock.mockReset();
migrateLegacyConfigMock.mockImplementation((config: OpenClawConfig) => ({
config,
changes: [],
}));
stripUnknownConfigKeysMock.mockReset();
});
it("collects legacy compatibility issue lines and preview fix hints", () => {
migrateLegacyConfigMock.mockReturnValueOnce({
config: {},
changes: ["Moved heartbeat → agents.defaults.heartbeat."],
});
const result = createLegacyStepResult({
exists: true,
parsed: { heartbeat: { enabled: true } },
@@ -74,6 +102,11 @@ describe("doctor config flow steps", () => {
});
it("removes unknown keys and adds preview hint", () => {
stripUnknownConfigKeysMock.mockReturnValueOnce({
config: {},
removed: ["bogus"],
});
const result = applyUnknownConfigKeyStep({
state: {
cfg: {},

View File

@@ -1,7 +1,30 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import { findLegacyConfigIssues } from "../../../config/legacy.js";
import { migrateLegacyConfig } from "./legacy-config-migrate.js";
vi.mock("../../../plugins/manifest-registry.js", () => ({
loadPluginManifestRegistry: () => ({
plugins: [
{
id: "brave",
origin: "bundled",
contracts: { webSearchProviders: ["brave"] },
},
{
id: "xai",
origin: "bundled",
contracts: { webSearchProviders: ["grok"] },
},
{
id: "moonshot",
origin: "bundled",
contracts: { webSearchProviders: ["kimi"] },
},
],
}),
resolveManifestContractOwnerPluginId: ({ value }: { value: string }) =>
({ brave: "brave", grok: "xai", kimi: "moonshot" })[value as "brave" | "grok" | "kimi"],
}));
import {
listLegacyWebSearchConfigPaths,
migrateLegacyWebSearchConfig,
@@ -89,44 +112,4 @@ describe("legacy web search config", () => {
"tools.web.search.kimi.model",
]);
});
it("participates in shared legacy detection and migration", () => {
const rawConfig = {
tools: {
web: {
search: {
provider: "brave",
brave: {
apiKey: "brave-key",
},
},
},
},
} satisfies OpenClawConfig;
expect(findLegacyConfigIssues(rawConfig)).toEqual([
{
path: "tools.web.search",
message:
'tools.web.search provider-owned config moved to plugins.entries.<plugin>.config.webSearch. Run "openclaw doctor --fix".',
},
]);
const migrated = migrateLegacyConfig(rawConfig);
expect(migrated.config).not.toBeNull();
expect(migrated.config?.tools?.web?.search).toEqual({
provider: "brave",
});
expect(migrated.config?.plugins?.entries?.brave).toEqual({
enabled: true,
config: {
webSearch: {
apiKey: "brave-key",
},
},
});
expect(migrated.changes).toEqual([
"Moved tools.web.search.brave → plugins.entries.brave.config.webSearch.",
]);
});
});

View File

@@ -107,17 +107,12 @@ describe("node pairing tokens", () => {
});
});
test("generates base64url node tokens with 256-bit entropy output length", async () => {
test("generates base64url node tokens and rejects mismatches", async () => {
await withNodePairingDir(async (baseDir) => {
const token = await setupPairedNode(baseDir);
expect(token).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(Buffer.from(token, "base64url")).toHaveLength(32);
});
});
test("verifies token and rejects mismatches", async () => {
await withNodePairingDir(async (baseDir) => {
const token = await setupPairedNode(baseDir);
await expect(verifyNodeToken("node-1", token, baseDir)).resolves.toEqual({
ok: true,
node: expect.objectContaining({ nodeId: "node-1" }),
@@ -125,12 +120,7 @@ describe("node pairing tokens", () => {
await expect(verifyNodeToken("node-1", "x".repeat(token.length), baseDir)).resolves.toEqual({
ok: false,
});
});
});
test("treats multibyte same-length token input as mismatch without throwing", async () => {
await withNodePairingDir(async (baseDir) => {
const token = await setupPairedNode(baseDir);
const multibyteToken = "é".repeat(token.length);
expect(Buffer.from(multibyteToken).length).not.toBe(Buffer.from(token).length);

View File

@@ -15,14 +15,20 @@ function createDedupe(root: string, overrides?: { ttlMs?: number }) {
}
describe("createPersistentDedupe", () => {
it("deduplicates keys and persists across instances", async () => {
it("deduplicates keys, persists across instances, warms up, and checks recent keys", async () => {
const root = await createTempDir("openclaw-dedupe-");
const first = createDedupe(root);
expect(await first.checkAndRecord("m1", { namespace: "a" })).toBe(true);
expect(await first.checkAndRecord("m1", { namespace: "a" })).toBe(false);
expect(await first.checkAndRecord("m2", { namespace: "a" })).toBe(true);
const second = createDedupe(root);
expect(await second.hasRecent("m1", { namespace: "a" })).toBe(true);
expect(await second.hasRecent("missing", { namespace: "a" })).toBe(false);
expect(await second.warmup("a")).toBe(2);
expect(await second.checkAndRecord("m1", { namespace: "a" })).toBe(false);
expect(await second.checkAndRecord("m2", { namespace: "a" })).toBe(false);
expect(await second.checkAndRecord("m3", { namespace: "a" })).toBe(true);
expect(await second.checkAndRecord("m1", { namespace: "b" })).toBe(true);
});
@@ -50,31 +56,6 @@ describe("createPersistentDedupe", () => {
expect(await dedupe.checkAndRecord("memory-only", { namespace: "x" })).toBe(false);
});
it("warmup loads persisted entries into memory", async () => {
const root = await createTempDir("openclaw-dedupe-");
const writer = createDedupe(root);
expect(await writer.checkAndRecord("msg-1", { namespace: "acct" })).toBe(true);
expect(await writer.checkAndRecord("msg-2", { namespace: "acct" })).toBe(true);
const reader = createDedupe(root);
const loaded = await reader.warmup("acct");
expect(loaded).toBe(2);
expect(await reader.checkAndRecord("msg-1", { namespace: "acct" })).toBe(false);
expect(await reader.checkAndRecord("msg-2", { namespace: "acct" })).toBe(false);
expect(await reader.checkAndRecord("msg-3", { namespace: "acct" })).toBe(true);
});
it("checks for recent keys without mutating the store", async () => {
const root = await createTempDir("openclaw-dedupe-");
const writer = createDedupe(root);
expect(await writer.checkAndRecord("peek-me", { namespace: "acct" })).toBe(true);
const reader = createDedupe(root);
expect(await reader.hasRecent("peek-me", { namespace: "acct" })).toBe(true);
expect(await reader.hasRecent("missing", { namespace: "acct" })).toBe(false);
expect(await reader.checkAndRecord("peek-me", { namespace: "acct" })).toBe(false);
});
it.each([
{
name: "returns 0 when no disk file exists",

View File

@@ -3,7 +3,6 @@ import { loadPluginManifestRegistry } from "./manifest-registry.js";
import {
hasBundledWebFetchProviderPublicArtifact,
hasBundledWebSearchProviderPublicArtifact,
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts,
} from "./web-provider-public-artifacts.explicit.js";
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -30,6 +29,8 @@ function supportsSecretRefWebSearchApiKey(
}
const registry = loadPluginManifestRegistry();
const webSearchPluginIds = bundledPluginIdsWithContract("webSearchProviders");
const webFetchPluginIds = bundledPluginIdsWithContract("webFetchProviders");
function bundledPluginIdsWithContract(
contract: "webSearchProviders" | "webFetchProviders",
@@ -42,40 +43,16 @@ function bundledPluginIdsWithContract(
.toSorted((left, right) => left.localeCompare(right));
}
function ownerPluginIdForContractValue(
contract: "webSearchProviders" | "webFetchProviders",
value: string,
): string | undefined {
const normalized = value.toLowerCase();
return registry.plugins.find(
(plugin) =>
plugin.origin === "bundled" &&
plugin.contracts?.[contract]?.some((candidate) => candidate.toLowerCase() === normalized),
)?.id;
}
describe("web provider public artifacts", () => {
it("has a public artifact for every bundled web search provider declared in manifests", () => {
const pluginIds = bundledPluginIdsWithContract("webSearchProviders");
expect(pluginIds).not.toHaveLength(0);
for (const pluginId of pluginIds) {
it("has public artifacts for every bundled web provider declared in manifests", () => {
expect(webSearchPluginIds).not.toHaveLength(0);
for (const pluginId of webSearchPluginIds) {
expect(hasBundledWebSearchProviderPublicArtifact(pluginId)).toBe(true);
}
});
it("keeps public web search artifacts mapped to their manifest owner plugin", () => {
const pluginIds = bundledPluginIdsWithContract("webSearchProviders");
const providers = resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({
onlyPluginIds: pluginIds,
});
expect(providers).not.toBeNull();
for (const provider of providers ?? []) {
expect(ownerPluginIdForContractValue("webSearchProviders", provider.id)).toBe(
provider.pluginId,
);
expect(webFetchPluginIds).not.toHaveLength(0);
for (const pluginId of webFetchPluginIds) {
expect(hasBundledWebFetchProviderPublicArtifact(pluginId)).toBe(true);
}
});
@@ -104,13 +81,4 @@ describe("web provider public artifacts", () => {
.toSorted((left, right) => left.localeCompare(right));
expect(actualPluginIds).toEqual(expectedPluginIds);
});
it("has a public artifact for every bundled web fetch provider declared in manifests", () => {
const pluginIds = bundledPluginIdsWithContract("webFetchProviders");
expect(pluginIds).not.toHaveLength(0);
for (const pluginId of pluginIds) {
expect(hasBundledWebFetchProviderPublicArtifact(pluginId)).toBe(true);
}
});
});