test: clear channel setup plugin install broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 11:37:29 +01:00
parent 3ff0da7993
commit 25e780732e

View File

@@ -316,6 +316,56 @@ function expectPluginLoadedFromLocalPath(
expect(result.cfg.plugins?.load?.paths).toContain(expectedPath);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (!isRecord(value)) {
throw new Error(`expected ${label} to be an object`);
}
return value;
}
function requireArray(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) {
throw new Error(`expected ${label} to be an array`);
}
return value;
}
function expectRecordFields(value: unknown, label: string, expected: Record<string, unknown>) {
const record = requireRecord(value, label);
for (const [key, expectedValue] of Object.entries(expected)) {
expect(record[key]).toEqual(expectedValue);
}
}
type MockWithCalls = { mock: { calls: unknown[][] } };
function requireMockCallArg(mock: MockWithCalls, callIndex: number, argIndex = 0) {
return requireRecord(mock.mock.calls[callIndex]?.[argIndex], "mock call argument");
}
function requireSelectOptions(select: MockWithCalls) {
return requireArray(requireMockCallArg(select, 0).options, "select options");
}
function requireOptionByValue(options: unknown[], value: string) {
const option = options.find(
(candidate) => requireRecord(candidate, "select option").value === value,
);
return requireRecord(option, `select option ${value}`);
}
function expectLoadOpenClawPluginFields(expected: Record<string, unknown>, callIndex = 0) {
expectRecordFields(
requireMockCallArg(vi.mocked(loadOpenClawPlugins), callIndex),
"loadOpenClawPlugins args",
expected,
);
}
describe("ensureChannelSetupPluginInstalled", () => {
it("installs from npm and enables the plugin", async () => {
const runtime = makeRuntime();
@@ -341,19 +391,15 @@ describe("ensureChannelSetupPluginInstalled", () => {
expect(result.installed).toBe(true);
expect(result.cfg.plugins?.entries?.["bundled-chat"]?.enabled).toBe(true);
expect(result.cfg.plugins?.allow).toContain("bundled-chat");
expect(result.cfg.plugins?.installs).toEqual({
"bundled-chat": expect.objectContaining({
source: "npm",
spec: bundledChatNpmSpec,
installPath: "/tmp/bundled-chat",
}),
expectRecordFields(result.cfg.plugins?.installs?.["bundled-chat"], "plugin install record", {
source: "npm",
spec: bundledChatNpmSpec,
installPath: "/tmp/bundled-chat",
});
expectRecordFields(requireMockCallArg(installPluginFromNpmSpec, 0), "npm install args", {
expectedIntegrity: bundledChatIntegrity,
spec: bundledChatNpmSpec,
});
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
expectedIntegrity: bundledChatIntegrity,
spec: bundledChatNpmSpec,
}),
);
});
it("installs npm channel plugins into the active profile extensions dir", async () => {
@@ -378,12 +424,10 @@ describe("ensureChannelSetupPluginInstalled", () => {
runtime,
});
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
extensionsDir: path.join(profileStateDir, "extensions"),
spec: bundledChatNpmSpec,
}),
);
expectRecordFields(requireMockCallArg(installPluginFromNpmSpec, 0), "npm install args", {
extensionsDir: path.join(profileStateDir, "extensions"),
spec: bundledChatNpmSpec,
});
});
it("uses local path when selected", async () => {
@@ -476,12 +520,10 @@ describe("ensureChannelSetupPluginInstalled", () => {
});
expect(select).not.toHaveBeenCalled();
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@openclaw/wecom@beta",
expectedPluginId: "wecom-openclaw-plugin",
}),
);
expectRecordFields(requireMockCallArg(installPluginFromNpmSpec, 0), "npm install args", {
spec: "@openclaw/wecom@beta",
expectedPluginId: "wecom-openclaw-plugin",
});
expect(result.cfg.plugins?.installs?.["wecom-openclaw-plugin"]?.spec).toBe("@openclaw/wecom");
});
@@ -499,16 +541,15 @@ describe("ensureChannelSetupPluginInstalled", () => {
runtime,
});
expect(select).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: "local",
options: expect.arrayContaining([
expect.objectContaining({
value: "local",
hint: bundledPluginRootAt("/opt/openclaw", "bundled-chat"),
}),
]),
}),
const selectArgs = requireMockCallArg(select, 0);
expect(selectArgs.initialValue).toBe("local");
expectRecordFields(
requireOptionByValue(requireSelectOptions(select), "local"),
"local option",
{
value: "local",
hint: bundledPluginRootAt("/opt/openclaw", "bundled-chat"),
},
);
});
@@ -560,20 +601,17 @@ describe("ensureChannelSetupPluginInstalled", () => {
runtime,
});
expect(select).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: "npm",
options: [
expect.objectContaining({
value: "npm",
label: `Download from npm (${bundledChatForkNpmSpec})`,
}),
expect.objectContaining({
value: "skip",
}),
],
}),
);
const selectArgs = requireMockCallArg(select, 0);
expect(selectArgs.initialValue).toBe("npm");
const options = requireSelectOptions(select);
expect(options).toHaveLength(2);
expectRecordFields(options[0], "npm option", {
value: "npm",
label: `Download from npm (${bundledChatForkNpmSpec})`,
});
expectRecordFields(options[1], "skip option", {
value: "skip",
});
});
it("offers ClawHub as the first-class install source for channel catalog entries", async () => {
@@ -604,20 +642,17 @@ describe("ensureChannelSetupPluginInstalled", () => {
runtime,
});
expect(select).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: "clawhub",
options: [
expect.objectContaining({
value: "clawhub",
label: "Download from ClawHub (clawhub:openclaw/clawhub-chat@2026.5.2)",
}),
expect.objectContaining({
value: "skip",
}),
],
}),
);
const selectArgs = requireMockCallArg(select, 0);
expect(selectArgs.initialValue).toBe("clawhub");
const options = requireSelectOptions(select);
expect(options).toHaveLength(2);
expectRecordFields(options[0], "clawhub option", {
value: "clawhub",
label: "Download from ClawHub (clawhub:openclaw/clawhub-chat@2026.5.2)",
});
expectRecordFields(options[1], "skip option", {
value: "skip",
});
});
it("falls back to local path after npm install failure", async () => {
@@ -698,16 +733,14 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
includeSetupOnlyChannelPlugins: true,
}),
);
expectLoadOpenClawPluginFields({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
includeSetupOnlyChannelPlugins: true,
});
});
it("loads the setup plugin registry from the auto-enabled config snapshot", () => {
@@ -740,13 +773,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
config: cfg,
env: process.env,
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: autoEnabledConfig,
activationSourceConfig: cfg,
autoEnabledReasons: {},
}),
);
expectLoadOpenClawPluginFields({
config: autoEnabledConfig,
activationSourceConfig: cfg,
autoEnabledReasons: {},
});
});
it("scopes channel reloads when setup starts from an empty registry", () => {
@@ -761,17 +792,15 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["@vendor/external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
}),
);
expectLoadOpenClawPluginFields({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["@vendor/external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
});
expect(getChannelPluginCatalogEntry).toHaveBeenCalledWith("external-chat", {
workspaceDir: "/tmp/openclaw-workspace",
});
@@ -799,11 +828,9 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: [],
}),
);
expectLoadOpenClawPluginFields({
onlyPluginIds: [],
});
});
it("scopes channel reloads when the global registry is populated but the pinned channel registry is empty", () => {
@@ -834,13 +861,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
releasePinnedPluginChannelRegistry(pinnedChannelRegistry);
}
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
activationSourceConfig: cfg,
autoEnabledReasons: {},
onlyPluginIds: ["@vendor/external-chat-plugin"],
}),
);
expectLoadOpenClawPluginFields({
activationSourceConfig: cfg,
autoEnabledReasons: {},
onlyPluginIds: ["@vendor/external-chat-plugin"],
});
});
it("can load a channel-scoped snapshot without activating the global registry", () => {
@@ -855,18 +880,16 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["@vendor/external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
activate: false,
}),
);
expectLoadOpenClawPluginFields({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["@vendor/external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
activate: false,
});
expect(getChannelPluginCatalogEntry).toHaveBeenCalledWith("external-chat", {
workspaceDir: "/tmp/openclaw-workspace",
});
@@ -886,11 +909,9 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: ["@vendor/external-chat-plugin"],
}),
);
expectLoadOpenClawPluginFields({
onlyPluginIds: ["@vendor/external-chat-plugin"],
});
expect(getChannelPluginCatalogEntry).toHaveBeenNthCalledWith(1, "external-chat", {
workspaceDir: "/tmp/openclaw-workspace",
});
@@ -920,11 +941,9 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: ["trusted-external-chat-shadow"],
}),
);
expectLoadOpenClawPluginFields({
onlyPluginIds: ["trusted-external-chat-shadow"],
});
expect(getChannelPluginCatalogEntry).toHaveBeenCalledTimes(1);
});
@@ -939,11 +958,9 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: [],
}),
);
expectLoadOpenClawPluginFields({
onlyPluginIds: [],
});
});
it("scopes snapshots by a unique discovered manifest match when catalog mapping is missing", () => {
@@ -966,18 +983,16 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["custom-external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
activate: false,
}),
);
expectLoadOpenClawPluginFields({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["custom-external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
activate: false,
});
});
it("scopes snapshots by activation-declared channel ownership when direct channel lists are empty", () => {
@@ -992,23 +1007,21 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: ["custom-external-chat-plugin"],
}),
);
expect(loadPluginManifestRegistry).toHaveBeenCalledWith(
expect.objectContaining({
config: cfg,
workspaceDir: "/tmp/openclaw-workspace",
candidates: expect.arrayContaining([
expect.objectContaining({
idHint: "custom-external-chat-plugin",
origin: "bundled",
}),
]),
}),
);
expectLoadOpenClawPluginFields({
onlyPluginIds: ["custom-external-chat-plugin"],
});
const manifestCall = loadPluginManifestRegistry.mock.calls
.map((call) => requireRecord(call[0], "manifest registry args"))
.find((args) =>
requireArray(args.candidates, "manifest candidates").some((candidate) => {
const record = requireRecord(candidate, "manifest candidate");
return record.idHint === "custom-external-chat-plugin" && record.origin === "bundled";
}),
);
expectRecordFields(manifestCall, "manifest registry args", {
config: cfg,
workspaceDir: "/tmp/openclaw-workspace",
});
});
it("uses live manifest discovery for activation-declared setup scoping", () => {
@@ -1155,17 +1168,15 @@ describe("ensureChannelSetupPluginInstalled", () => {
workspaceDir: "/tmp/openclaw-workspace",
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["@vendor/external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
activate: false,
}),
);
expectLoadOpenClawPluginFields({
config: cfg,
activationSourceConfig: cfg,
autoEnabledReasons: {},
workspaceDir: "/tmp/openclaw-workspace",
cache: false,
onlyPluginIds: ["@vendor/external-chat-plugin"],
includeSetupOnlyChannelPlugins: true,
activate: false,
});
});
});