From ed0210a1877468a77453f78e980dbc5ea5c4bd09 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 10:01:41 +0100 Subject: [PATCH] test: streamline slow import-heavy suites --- ...rs-workspace-skills-managed-skills.test.ts | 99 +++++++++++-------- .../doctor/shared/config-flow-steps.test.ts | 35 ++++++- .../shared/legacy-web-search-migrate.test.ts | 69 +++++-------- src/infra/node-pairing.test.ts | 14 +-- src/plugin-sdk/persistent-dedupe.test.ts | 33 ++----- .../web-provider-public-artifacts.test.ts | 48 ++------- 6 files changed, 136 insertions(+), 162 deletions(-) diff --git a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts index d35c1aa546d..8c4acf6828c 100644 --- a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts @@ -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 } } } }, }), diff --git a/src/commands/doctor/shared/config-flow-steps.test.ts b/src/commands/doctor/shared/config-flow-steps.test.ts index 413982109d4..ab1aa49f956 100644 --- a/src/commands/doctor/shared/config-flow-steps.test.ts +++ b/src/commands/doctor/shared/config-flow-steps.test.ts @@ -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: {}, diff --git a/src/commands/doctor/shared/legacy-web-search-migrate.test.ts b/src/commands/doctor/shared/legacy-web-search-migrate.test.ts index e757041ab0c..955a9c08853 100644 --- a/src/commands/doctor/shared/legacy-web-search-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-web-search-migrate.test.ts @@ -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..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.", - ]); - }); }); diff --git a/src/infra/node-pairing.test.ts b/src/infra/node-pairing.test.ts index 14e86538acb..ea4f2ae727b 100644 --- a/src/infra/node-pairing.test.ts +++ b/src/infra/node-pairing.test.ts @@ -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); diff --git a/src/plugin-sdk/persistent-dedupe.test.ts b/src/plugin-sdk/persistent-dedupe.test.ts index 31c740ff6e7..ad75e9ebaab 100644 --- a/src/plugin-sdk/persistent-dedupe.test.ts +++ b/src/plugin-sdk/persistent-dedupe.test.ts @@ -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", diff --git a/src/plugins/web-provider-public-artifacts.test.ts b/src/plugins/web-provider-public-artifacts.test.ts index d095cd8511a..e7dd9dc98b7 100644 --- a/src/plugins/web-provider-public-artifacts.test.ts +++ b/src/plugins/web-provider-public-artifacts.test.ts @@ -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 { @@ -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); - } - }); });