mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
test: streamline slow import-heavy suites
This commit is contained in:
@@ -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 } } } },
|
||||
}),
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user