test: dedupe plugin utility config suites

This commit is contained in:
Peter Steinberger
2026-03-28 04:01:55 +00:00
parent c5b1582d48
commit 708ff9145e
10 changed files with 116 additions and 61 deletions

View File

@@ -37,6 +37,22 @@ function expectGeneratedPathResolution(tempRoot: string, expectedRelativePath: s
).toBe(path.join(tempRoot, expectedRelativePath));
}
function expectArtifactPresence(
artifacts: readonly string[] | undefined,
params: { contains?: readonly string[]; excludes?: readonly string[] },
) {
if (params.contains) {
for (const artifact of params.contains) {
expect(artifacts).toContain(artifact);
}
}
if (params.excludes) {
for (const artifact of params.excludes) {
expect(artifacts).not.toContain(artifact);
}
}
}
async function writeGeneratedMetadataModule(params: {
repoRoot: string;
outputPath?: string;
@@ -64,11 +80,13 @@ describe("bundled plugin metadata", () => {
const discord = BUNDLED_PLUGIN_METADATA.find((entry) => entry.dirName === "discord");
expect(discord?.source).toEqual({ source: "./index.ts", built: "index.js" });
expect(discord?.setupSource).toEqual({ source: "./setup-entry.ts", built: "setup-entry.js" });
expect(discord?.publicSurfaceArtifacts).toContain("api.js");
expect(discord?.publicSurfaceArtifacts).toContain("runtime-api.js");
expect(discord?.publicSurfaceArtifacts).toContain("session-key-api.js");
expect(discord?.publicSurfaceArtifacts).not.toContain("test-api.js");
expect(discord?.runtimeSidecarArtifacts).toContain("runtime-api.js");
expectArtifactPresence(discord?.publicSurfaceArtifacts, {
contains: ["api.js", "runtime-api.js", "session-key-api.js"],
excludes: ["test-api.js"],
});
expectArtifactPresence(discord?.runtimeSidecarArtifacts, {
contains: ["runtime-api.js"],
});
expect(discord?.manifest.id).toBe("discord");
expect(discord?.manifest.channelConfigs?.discord).toEqual(
expect.objectContaining({

View File

@@ -72,6 +72,10 @@ function expectStartupPluginIds(config: OpenClawConfig, expected: readonly strin
).toEqual(expected);
}
function expectManifestRegistryFixture() {
expect(loadPluginManifestRegistry).toHaveBeenCalled();
}
describe("resolveGatewayStartupPluginIds", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockReturnValue(["demo-channel"]);
@@ -119,5 +123,6 @@ describe("resolveGatewayStartupPluginIds", () => {
],
] as const)("%s", (_name, config, expected) => {
expectStartupPluginIds(config, expected);
expectManifestRegistryFixture();
});
});

View File

@@ -46,6 +46,14 @@ function createCliRegistry() {
};
}
function expectPluginLoaderConfig(config: OpenClawConfig) {
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config,
}),
);
}
describe("registerPluginCliCommands", () => {
beforeEach(() => {
mocks.memoryRegister.mockClear();
@@ -101,11 +109,7 @@ describe("registerPluginCliCommands", () => {
config: rawConfig,
env: process.env,
});
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: autoEnabledConfig,
}),
);
expectPluginLoaderConfig(autoEnabledConfig);
expect(mocks.memoryRegister).toHaveBeenCalledWith(
expect.objectContaining({
config: autoEnabledConfig,

View File

@@ -20,6 +20,15 @@ function expectResolvedEnableState(
expect(resolveEnableState(...params)).toEqual(expected);
}
function expectMemoryPluginState(
config: Parameters<typeof normalizePluginsConfig>[0],
expected: ReturnType<typeof resolveEnableState>,
) {
expect(resolveEnableState("memory-core", "bundled", normalizePluginsConfig(config))).toEqual(
expected,
);
}
describe("normalizePluginsConfig", () => {
it.each([
[{}, "memory-core"],
@@ -177,22 +186,18 @@ describe("resolveEnableState", () => {
);
it("keeps the selected memory slot plugin enabled even when omitted from plugins.allow", () => {
const state = resolveEnableState(
"memory-core",
"bundled",
normalizePluginsConfig({
expectMemoryPluginState(
{
allow: ["telegram"],
slots: { memory: "memory-core" },
}),
},
{ enabled: true },
);
expect(state).toEqual({ enabled: true });
});
it("keeps explicit disable authoritative for the selected memory slot plugin", () => {
const state = resolveEnableState(
"memory-core",
"bundled",
normalizePluginsConfig({
expectMemoryPluginState(
{
allow: ["telegram"],
slots: { memory: "memory-core" },
entries: {
@@ -200,9 +205,9 @@ describe("resolveEnableState", () => {
enabled: false,
},
},
}),
},
{ enabled: false, reason: "disabled in config" },
);
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
});
it.each([

View File

@@ -15,6 +15,13 @@ function expectEnableResult(
params.assert(result);
}
function expectEnabledAllowlist(
result: ReturnType<typeof enablePluginInConfig>,
expected: string[],
) {
expect(result.config.plugins?.allow).toEqual(expected);
}
describe("enablePluginInConfig", () => {
it.each([
{
@@ -36,7 +43,7 @@ describe("enablePluginInConfig", () => {
pluginId: "google",
expectedEnabled: true,
assert: (result: ReturnType<typeof enablePluginInConfig>) => {
expect(result.config.plugins?.allow).toEqual(["memory-core", "google"]);
expectEnabledAllowlist(result, ["memory-core", "google"]);
},
},
{
@@ -73,7 +80,7 @@ describe("enablePluginInConfig", () => {
expectedEnabled: true,
assert: (result: ReturnType<typeof enablePluginInConfig>) => {
expect(result.config.channels?.telegram?.enabled).toBe(true);
expect(result.config.plugins?.allow).toEqual(["memory-core", "telegram"]);
expectEnabledAllowlist(result, ["memory-core", "telegram"]);
},
},
{

View File

@@ -9,6 +9,20 @@ function expectRecordedInstall(pluginId: string, next: ReturnType<typeof recordP
expect(typeof next.plugins?.installs?.[pluginId]?.installedAt).toBe("string");
}
function createExpectedResolutionFields(
overrides: Partial<ReturnType<typeof buildNpmResolutionInstallFields>>,
) {
return {
resolvedName: undefined,
resolvedVersion: undefined,
resolvedSpec: undefined,
integrity: undefined,
shasum: undefined,
resolvedAt: undefined,
...overrides,
};
}
describe("buildNpmResolutionInstallFields", () => {
it.each([
{
@@ -21,26 +35,19 @@ describe("buildNpmResolutionInstallFields", () => {
shasum: "deadbeef",
resolvedAt: "2026-02-22T00:00:00.000Z",
},
expected: {
expected: createExpectedResolutionFields({
resolvedName: "@openclaw/demo",
resolvedVersion: "1.2.3",
resolvedSpec: "@openclaw/demo@1.2.3",
integrity: "sha512-abc",
shasum: "deadbeef",
resolvedAt: "2026-02-22T00:00:00.000Z",
},
}),
},
{
name: "returns undefined fields when resolution is missing",
input: undefined,
expected: {
resolvedName: undefined,
resolvedVersion: undefined,
resolvedSpec: undefined,
integrity: undefined,
shasum: undefined,
resolvedAt: undefined,
},
expected: createExpectedResolutionFields({}),
},
] as const)("$name", ({ input, expected }) => {
expect(buildNpmResolutionInstallFields(input)).toEqual(expected);

View File

@@ -18,6 +18,22 @@ function createLazyModuleLifecycle() {
};
}
async function expectLifecycleStarted(params: {
overrideEnvVar?: string;
loadDefaultModule?: () => Promise<Record<string, unknown>>;
loadOverrideModule?: (spec: string) => Promise<Record<string, unknown>>;
startExportNames: string[];
stopExportNames?: string[];
}) {
return startLazyPluginServiceModule({
...(params.overrideEnvVar ? { overrideEnvVar: params.overrideEnvVar } : {}),
loadDefaultModule: params.loadDefaultModule ?? (async () => createLazyModuleLifecycle().module),
...(params.loadOverrideModule ? { loadOverrideModule: params.loadOverrideModule } : {}),
startExportNames: params.startExportNames,
...(params.stopExportNames ? { stopExportNames: params.stopExportNames } : {}),
});
}
describe("startLazyPluginServiceModule", () => {
afterEach(() => {
delete process.env.OPENCLAW_LAZY_SERVICE_SKIP;
@@ -27,7 +43,7 @@ describe("startLazyPluginServiceModule", () => {
it("starts the default module and returns its stop hook", async () => {
const lifecycle = createLazyModuleLifecycle();
const handle = await startLazyPluginServiceModule({
const handle = await expectLifecycleStarted({
loadDefaultModule: async () => lifecycle.module,
startExportNames: ["startDefault"],
stopExportNames: ["stopDefault"],
@@ -58,7 +74,7 @@ describe("startLazyPluginServiceModule", () => {
const start = createAsyncHookMock();
const loadOverrideModule = vi.fn(async () => ({ startOverride: start }));
await startLazyPluginServiceModule({
await expectLifecycleStarted({
overrideEnvVar: "OPENCLAW_LAZY_SERVICE_OVERRIDE",
loadDefaultModule: async () => ({ startDefault: createAsyncHookMock() }),
loadOverrideModule,

View File

@@ -39,6 +39,10 @@ function expectServiceContext(
expect(ctx.config).toBe(config);
expect(ctx.workspaceDir).toBe("/tmp/workspace");
expect(ctx.stateDir).toBe(STATE_DIR);
expectServiceLogger(ctx);
}
function expectServiceLogger(ctx: OpenClawPluginServiceContext) {
expect(ctx.logger).toBeDefined();
expect(typeof ctx.logger.info).toBe("function");
expect(typeof ctx.logger.warn).toBe("function");

View File

@@ -63,6 +63,11 @@ describe("applyExclusiveSlotSelection", () => {
}
}
function expectUnchangedSelection(result: ReturnType<typeof applyExclusiveSlotSelection>) {
expect(result.changed).toBe(false);
expect(result.warnings).toHaveLength(0);
}
it("selects the slot and disables other entries for the same kind", () => {
const config = createMemoryConfig({
slots: { memory: "memory-core" },
@@ -94,8 +99,7 @@ describe("applyExclusiveSlotSelection", () => {
registry: { plugins: [{ id: "memory", kind: "memory" }] },
});
expect(result.changed).toBe(false);
expect(result.warnings).toHaveLength(0);
expectUnchangedSelection(result);
expect(result.config).toBe(config);
});
@@ -140,8 +144,7 @@ describe("applyExclusiveSlotSelection", () => {
selectedId: "custom",
});
expect(result.changed).toBe(false);
expect(result.warnings).toHaveLength(0);
expectUnchangedSelection(result);
expect(result.config).toBe(config);
});
});

View File

@@ -3,24 +3,11 @@ import { describe, expect, it } from "vitest";
import { withPathResolutionEnv } from "../test-utils/env.js";
import { formatPluginSourceForTable, resolvePluginSourceRoots } from "./source-display.js";
function createPluginSourceRoots() {
const stockRoot = path.resolve(
path.sep,
"opt",
"homebrew",
"lib",
"node_modules",
"openclaw",
"extensions",
);
const globalRoot = path.resolve(path.sep, "Users", "x", ".openclaw", "extensions");
const workspaceRoot = path.resolve(path.sep, "Users", "x", "ws", ".openclaw", "extensions");
return {
stock: stockRoot,
global: globalRoot,
workspace: workspaceRoot,
};
}
const PLUGIN_SOURCE_ROOTS = {
stock: path.resolve(path.sep, "opt", "homebrew", "lib", "node_modules", "openclaw", "extensions"),
global: path.resolve(path.sep, "Users", "x", ".openclaw", "extensions"),
workspace: path.resolve(path.sep, "Users", "x", "ws", ".openclaw", "extensions"),
};
function expectFormattedSource(params: {
origin: "bundled" | "workspace" | "global";
@@ -30,13 +17,12 @@ function expectFormattedSource(params: {
expectedValue: string;
expectedRootKey: "stock" | "workspace" | "global";
}) {
const roots = createPluginSourceRoots();
const out = formatPluginSourceForTable(
{
origin: params.origin,
source: path.join(roots[params.sourceKey], params.dirName, params.fileName),
source: path.join(PLUGIN_SOURCE_ROOTS[params.sourceKey], params.dirName, params.fileName),
},
roots,
PLUGIN_SOURCE_ROOTS,
);
expect(out.value).toBe(params.expectedValue);
expect(out.rootKey).toBe(params.expectedRootKey);