test: dedupe loader and audit suites

This commit is contained in:
Peter Steinberger
2026-03-28 00:46:25 +00:00
parent b4fe0faf1b
commit 6a039bca30
3 changed files with 265 additions and 263 deletions

View File

@@ -176,8 +176,8 @@ describe("resolveHeartbeatPrompt", () => {
expected: "ping",
},
] as const;
for (const testCase of cases) {
expect(resolveHeartbeatPrompt(testCase.cfg)).toBe(testCase.expected);
for (const { cfg, expected } of cases) {
expect(resolveHeartbeatPrompt(cfg)).toBe(expected);
}
});
});
@@ -346,11 +346,8 @@ describe("resolveHeartbeatDeliveryTarget", () => {
},
},
];
for (const testCase of cases) {
expect(
resolveHeartbeatDeliveryTarget({ cfg: testCase.cfg, entry: testCase.entry }),
testCase.name,
).toEqual(testCase.expected);
for (const { cfg, entry, name, expected } of cases) {
expect(resolveHeartbeatDeliveryTarget({ cfg, entry }), name).toEqual(expected);
}
});
@@ -359,18 +356,18 @@ describe("resolveHeartbeatDeliveryTarget", () => {
{ to: "-100111:topic:42", expectedTo: "-100111", expectedThreadId: 42 },
{ to: "-100111", expectedTo: "-100111", expectedThreadId: undefined },
] as const;
for (const testCase of cases) {
for (const { to, expectedTo, expectedThreadId } of cases) {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: testCase.to },
heartbeat: { target: "telegram", to },
},
},
};
const result = resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry });
expect(result.channel).toBe("telegram");
expect(result.to).toBe(testCase.expectedTo);
expect(result.threadId).toBe(testCase.expectedThreadId);
expect(result.to).toBe(expectedTo);
expect(result.threadId).toBe(expectedThreadId);
}
});
@@ -398,16 +395,16 @@ describe("resolveHeartbeatDeliveryTarget", () => {
},
] as const;
for (const testCase of cases) {
for (const { accountId, expected } of cases) {
const cfg: OpenClawConfig = {
agents: {
defaults: {
heartbeat: { target: "telegram", to: "-100123", accountId: testCase.accountId },
heartbeat: { target: "telegram", to: "-100123", accountId },
},
},
channels: { telegram: { accounts: { work: { botToken: "token" } } } },
};
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual(testCase.expected);
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual(expected);
}
});
@@ -779,8 +776,8 @@ describe("runHeartbeatOnce", () => {
},
]);
for (const testCase of cases) {
const tmpDir = await createCaseDir(testCase.caseDir);
for (const { name, caseDir, peerKind, peerId, message, applyOverride, runOptions } of cases) {
const tmpDir = await createCaseDir(caseDir);
const storePath = path.join(tmpDir, "sessions.json");
const cfg: OpenClawConfig = {
agents: {
@@ -800,10 +797,10 @@ describe("runHeartbeatOnce", () => {
const overrideSessionKey = buildAgentPeerSessionKey({
agentId,
channel: "whatsapp",
peerKind: testCase.peerKind,
peerId: testCase.peerId,
peerKind,
peerId,
});
testCase.applyOverride({ cfg, sessionKey: overrideSessionKey });
applyOverride({ cfg, sessionKey: overrideSessionKey });
await fs.writeFile(
storePath,
@@ -815,16 +812,16 @@ describe("runHeartbeatOnce", () => {
lastTo: "120363401234567890@g.us",
},
[overrideSessionKey]: {
sessionId: `sid-${testCase.peerKind}`,
sessionId: `sid-${peerKind}`,
updatedAt: Date.now() + 10_000,
lastChannel: "whatsapp",
lastTo: testCase.peerId,
lastTo: peerId,
},
}),
);
replySpy.mockClear();
replySpy.mockResolvedValue([{ text: testCase.message }]);
replySpy.mockResolvedValue([{ text: message }]);
const sendWhatsApp = vi
.fn<
(
@@ -837,21 +834,17 @@ describe("runHeartbeatOnce", () => {
await runHeartbeatOnce({
cfg,
...testCase.runOptions({ sessionKey: overrideSessionKey }),
...runOptions({ sessionKey: overrideSessionKey }),
deps: createHeartbeatDeps(sendWhatsApp),
});
expect(sendWhatsApp, testCase.name).toHaveBeenCalledTimes(1);
expect(sendWhatsApp, testCase.name).toHaveBeenCalledWith(
testCase.peerId,
testCase.message,
expect.any(Object),
);
expect(replySpy, testCase.name).toHaveBeenCalledWith(
expect(sendWhatsApp, name).toHaveBeenCalledTimes(1);
expect(sendWhatsApp, name).toHaveBeenCalledWith(peerId, message, expect.any(Object));
expect(replySpy, name).toHaveBeenCalledWith(
expect.objectContaining({
SessionKey: overrideSessionKey,
From: testCase.peerId,
To: testCase.peerId,
From: peerId,
To: peerId,
Provider: "heartbeat",
}),
expect.objectContaining({ isHeartbeat: true, suppressToolErrorWarnings: false }),
@@ -939,8 +932,8 @@ describe("runHeartbeatOnce", () => {
},
]);
for (const testCase of cases) {
const tmpDir = await createCaseDir(testCase.caseDir);
for (const { name, caseDir, replies, expectedTexts } of cases) {
const tmpDir = await createCaseDir(caseDir);
const storePath = path.join(tmpDir, "sessions.json");
const cfg: OpenClawConfig = {
agents: {
@@ -972,7 +965,7 @@ describe("runHeartbeatOnce", () => {
);
replySpy.mockClear();
replySpy.mockResolvedValue(testCase.replies);
replySpy.mockResolvedValue(replies);
const sendWhatsApp = vi
.fn<
(
@@ -988,9 +981,9 @@ describe("runHeartbeatOnce", () => {
deps: createHeartbeatDeps(sendWhatsApp),
});
expect(sendWhatsApp, testCase.name).toHaveBeenCalledTimes(testCase.expectedTexts.length);
for (const [index, text] of testCase.expectedTexts.entries()) {
expect(sendWhatsApp, testCase.name).toHaveBeenNthCalledWith(
expect(sendWhatsApp, name).toHaveBeenCalledTimes(expectedTexts.length);
for (const [index, text] of expectedTexts.entries()) {
expect(sendWhatsApp, name).toHaveBeenNthCalledWith(
index + 1,
"120363401234567890@g.us",
text,
@@ -1241,19 +1234,27 @@ describe("runHeartbeatOnce", () => {
},
];
for (const testCase of cases) {
const { res, replySpy, sendWhatsApp } = await runHeartbeatFileScenario(testCase);
for (const {
name,
expectedStatus,
expectedSkipReason,
expectedReplyCalls,
expectedSendCalls,
expectCronContext,
...scenario
} of cases) {
const { res, replySpy, sendWhatsApp } = await runHeartbeatFileScenario(scenario);
try {
expect(res.status, testCase.name).toBe(testCase.expectedStatus);
expect(res.status, name).toBe(expectedStatus);
if (res.status === "skipped") {
expect(res.reason, testCase.name).toBe(testCase.expectedSkipReason);
expect(res.reason, name).toBe(expectedSkipReason);
}
expect(replySpy, testCase.name).toHaveBeenCalledTimes(testCase.expectedReplyCalls);
expect(sendWhatsApp, testCase.name).toHaveBeenCalledTimes(testCase.expectedSendCalls);
if (testCase.expectCronContext) {
expect(replySpy, name).toHaveBeenCalledTimes(expectedReplyCalls);
expect(sendWhatsApp, name).toHaveBeenCalledTimes(expectedSendCalls);
if (expectCronContext) {
const calledCtx = replySpy.mock.calls[0]?.[0] as { Provider?: string; Body?: string };
expect(calledCtx.Provider, testCase.name).toBe("cron-event");
expect(calledCtx.Body, testCase.name).toContain("scheduled reminder has been triggered");
expect(calledCtx.Provider, name).toBe("cron-event");
expect(calledCtx.Body, name).toContain("scheduled reminder has been triggered");
}
} finally {
replySpy.mockRestore();

View File

@@ -748,70 +748,69 @@ describe("loadOpenClawPlugins", () => {
expect(bundled?.status).toBe("disabled");
});
it("handles bundled telegram plugin enablement and override rules", () => {
setupBundledTelegramPlugin();
const cases = [
{
name: "loads bundled telegram plugin when enabled",
config: {
plugins: {
allow: ["telegram"],
entries: {
telegram: { enabled: true },
},
it.each([
{
name: "loads bundled telegram plugin when enabled",
config: {
plugins: {
allow: ["telegram"],
entries: {
telegram: { enabled: true },
},
} satisfies PluginLoadConfig,
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
expectTelegramLoaded(registry);
},
} satisfies PluginLoadConfig,
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
expectTelegramLoaded(registry);
},
{
name: "loads bundled channel plugins when channels.<id>.enabled=true",
config: {
channels: {
telegram: {
enabled: true,
},
},
plugins: {
},
{
name: "loads bundled channel plugins when channels.<id>.enabled=true",
config: {
channels: {
telegram: {
enabled: true,
},
} satisfies PluginLoadConfig,
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
expectTelegramLoaded(registry);
},
},
{
name: "still respects explicit disable via plugins.entries for bundled channels",
config: {
channels: {
telegram: {
enabled: true,
},
},
plugins: {
entries: {
telegram: { enabled: false },
},
},
} satisfies PluginLoadConfig,
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
const telegram = registry.plugins.find((entry) => entry.id === "telegram");
expect(telegram?.status).toBe("disabled");
expect(telegram?.error).toBe("disabled in config");
plugins: {
enabled: true,
},
} satisfies PluginLoadConfig,
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
expectTelegramLoaded(registry);
},
] as const;
for (const testCase of cases) {
},
{
name: "still respects explicit disable via plugins.entries for bundled channels",
config: {
channels: {
telegram: {
enabled: true,
},
},
plugins: {
entries: {
telegram: { enabled: false },
},
},
} satisfies PluginLoadConfig,
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
const telegram = registry.plugins.find((entry) => entry.id === "telegram");
expect(telegram?.status).toBe("disabled");
expect(telegram?.error).toBe("disabled in config");
},
},
] as const)(
"handles bundled telegram plugin enablement and override rules: $name",
({ config, assert }) => {
setupBundledTelegramPlugin();
const registry = loadOpenClawPlugins({
cache: false,
workspaceDir: cachedBundledTelegramDir,
config: testCase.config,
config,
});
testCase.assert(registry);
}
});
assert(registry);
},
);
it("preserves package.json metadata for bundled memory plugins", () => {
const registry = loadBundledMemoryPluginRegistry({
@@ -830,150 +829,146 @@ describe("loadOpenClawPlugins", () => {
expect(memory?.name).toBe("Memory (Core)");
expect(memory?.version).toBe("1.2.3");
});
it("handles config-path and scoped plugin loads", () => {
const scenarios = [
{
label: "loads plugins from config paths",
run: () => {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
const plugin = writePlugin({
id: "allowed-config-path",
filename: "allowed-config-path.cjs",
body: `module.exports = {
it.each([
{
label: "loads plugins from config paths",
run: () => {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
const plugin = writePlugin({
id: "allowed-config-path",
filename: "allowed-config-path.cjs",
body: `module.exports = {
id: "allowed-config-path",
register(api) {
api.registerGatewayMethod("allowed-config-path.ping", ({ respond }) => respond(true, { ok: true }));
},
};`,
});
});
const registry = loadOpenClawPlugins({
cache: false,
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["allowed-config-path"],
},
const registry = loadOpenClawPlugins({
cache: false,
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["allowed-config-path"],
},
});
},
});
const loaded = registry.plugins.find((entry) => entry.id === "allowed-config-path");
expect(loaded?.status).toBe("loaded");
expect(Object.keys(registry.gatewayHandlers)).toContain("allowed-config-path.ping");
},
const loaded = registry.plugins.find((entry) => entry.id === "allowed-config-path");
expect(loaded?.status).toBe("loaded");
expect(Object.keys(registry.gatewayHandlers)).toContain("allowed-config-path.ping");
},
{
label: "limits imports to the requested plugin ids",
run: () => {
useNoBundledPlugins();
const allowed = writePlugin({
id: "allowed-scoped-only",
filename: "allowed-scoped-only.cjs",
body: `module.exports = { id: "allowed-scoped-only", register() {} };`,
});
const skippedMarker = path.join(makeTempDir(), "skipped-loaded.txt");
const skipped = writePlugin({
id: "skipped-scoped-only",
filename: "skipped-scoped-only.cjs",
body: `require("node:fs").writeFileSync(${JSON.stringify(skippedMarker)}, "loaded", "utf-8");
},
{
label: "limits imports to the requested plugin ids",
run: () => {
useNoBundledPlugins();
const allowed = writePlugin({
id: "allowed-scoped-only",
filename: "allowed-scoped-only.cjs",
body: `module.exports = { id: "allowed-scoped-only", register() {} };`,
});
const skippedMarker = path.join(makeTempDir(), "skipped-loaded.txt");
const skipped = writePlugin({
id: "skipped-scoped-only",
filename: "skipped-scoped-only.cjs",
body: `require("node:fs").writeFileSync(${JSON.stringify(skippedMarker)}, "loaded", "utf-8");
module.exports = { id: "skipped-scoped-only", register() { throw new Error("skipped plugin should not load"); } };`,
});
});
const registry = loadOpenClawPlugins({
cache: false,
config: {
plugins: {
load: { paths: [allowed.file, skipped.file] },
allow: ["allowed-scoped-only", "skipped-scoped-only"],
},
const registry = loadOpenClawPlugins({
cache: false,
config: {
plugins: {
load: { paths: [allowed.file, skipped.file] },
allow: ["allowed-scoped-only", "skipped-scoped-only"],
},
onlyPluginIds: ["allowed-scoped-only"],
});
},
onlyPluginIds: ["allowed-scoped-only"],
});
expect(registry.plugins.map((entry) => entry.id)).toEqual(["allowed-scoped-only"]);
expect(fs.existsSync(skippedMarker)).toBe(false);
},
expect(registry.plugins.map((entry) => entry.id)).toEqual(["allowed-scoped-only"]);
expect(fs.existsSync(skippedMarker)).toBe(false);
},
{
label: "keeps scoped plugin loads in a separate cache entry",
run: () => {
useNoBundledPlugins();
const allowed = writePlugin({
id: "allowed-cache-scope",
filename: "allowed-cache-scope.cjs",
body: `module.exports = { id: "allowed-cache-scope", register() {} };`,
});
const extra = writePlugin({
id: "extra-cache-scope",
filename: "extra-cache-scope.cjs",
body: `module.exports = { id: "extra-cache-scope", register() {} };`,
});
const options = {
config: {
plugins: {
load: { paths: [allowed.file, extra.file] },
allow: ["allowed-cache-scope", "extra-cache-scope"],
},
},
{
label: "keeps scoped plugin loads in a separate cache entry",
run: () => {
useNoBundledPlugins();
const allowed = writePlugin({
id: "allowed-cache-scope",
filename: "allowed-cache-scope.cjs",
body: `module.exports = { id: "allowed-cache-scope", register() {} };`,
});
const extra = writePlugin({
id: "extra-cache-scope",
filename: "extra-cache-scope.cjs",
body: `module.exports = { id: "extra-cache-scope", register() {} };`,
});
const options = {
config: {
plugins: {
load: { paths: [allowed.file, extra.file] },
allow: ["allowed-cache-scope", "extra-cache-scope"],
},
};
},
};
const full = loadOpenClawPlugins(options);
const scoped = loadOpenClawPlugins({
...options,
onlyPluginIds: ["allowed-cache-scope"],
});
const scopedAgain = loadOpenClawPlugins({
...options,
onlyPluginIds: ["allowed-cache-scope"],
});
const full = loadOpenClawPlugins(options);
const scoped = loadOpenClawPlugins({
...options,
onlyPluginIds: ["allowed-cache-scope"],
});
const scopedAgain = loadOpenClawPlugins({
...options,
onlyPluginIds: ["allowed-cache-scope"],
});
expect(full.plugins.map((entry) => entry.id).toSorted()).toEqual([
"allowed-cache-scope",
"extra-cache-scope",
]);
expect(scoped).not.toBe(full);
expect(scoped.plugins.map((entry) => entry.id)).toEqual(["allowed-cache-scope"]);
expect(scopedAgain).toBe(scoped);
},
expect(full.plugins.map((entry) => entry.id).toSorted()).toEqual([
"allowed-cache-scope",
"extra-cache-scope",
]);
expect(scoped).not.toBe(full);
expect(scoped.plugins.map((entry) => entry.id)).toEqual(["allowed-cache-scope"]);
expect(scopedAgain).toBe(scoped);
},
{
label: "can load a scoped registry without replacing the active global registry",
run: () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "allowed-nonactivating-scope",
filename: "allowed-nonactivating-scope.cjs",
body: `module.exports = { id: "allowed-nonactivating-scope", register() {} };`,
});
const previousRegistry = createEmptyPluginRegistry();
setActivePluginRegistry(previousRegistry, "existing-registry");
resetGlobalHookRunner();
},
{
label: "can load a scoped registry without replacing the active global registry",
run: () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "allowed-nonactivating-scope",
filename: "allowed-nonactivating-scope.cjs",
body: `module.exports = { id: "allowed-nonactivating-scope", register() {} };`,
});
const previousRegistry = createEmptyPluginRegistry();
setActivePluginRegistry(previousRegistry, "existing-registry");
resetGlobalHookRunner();
const scoped = loadOpenClawPlugins({
cache: false,
activate: false,
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["allowed-nonactivating-scope"],
},
const scoped = loadOpenClawPlugins({
cache: false,
activate: false,
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["allowed-nonactivating-scope"],
},
onlyPluginIds: ["allowed-nonactivating-scope"],
});
},
onlyPluginIds: ["allowed-nonactivating-scope"],
});
expect(scoped.plugins.map((entry) => entry.id)).toEqual(["allowed-nonactivating-scope"]);
expect(getActivePluginRegistry()).toBe(previousRegistry);
expect(getActivePluginRegistryKey()).toBe("existing-registry");
expect(getGlobalHookRunner()).toBeNull();
},
expect(scoped.plugins.map((entry) => entry.id)).toEqual(["allowed-nonactivating-scope"]);
expect(getActivePluginRegistry()).toBe(previousRegistry);
expect(getActivePluginRegistryKey()).toBe("existing-registry");
expect(getGlobalHookRunner()).toBeNull();
},
] as const;
for (const scenario of scenarios) {
scenario.run();
}
},
] as const)("handles config-path and scoped plugin loads: $label", ({ run }) => {
run();
});
it("only publishes plugin commands to the global registry during activating loads", async () => {
@@ -2674,9 +2669,9 @@ module.exports = {
},
] as const;
for (const scenario of scenarios) {
const registry = scenario.loadRegistry();
scenario.assert(registry);
for (const { loadRegistry, assert } of scenarios) {
const registry = loadRegistry();
assert(registry);
}
});

View File

@@ -553,10 +553,12 @@ description: test skill
},
] as const;
for (const testCase of cases) {
const res = await testCase.run();
testCase.assert(res);
}
await Promise.all(
cases.map(async (testCase) => {
const res = await testCase.run();
testCase.assert(res);
}),
);
});
it("scores dangerous gateway.tools.allow over HTTP by exposure", async () => {
@@ -2230,23 +2232,25 @@ description: test skill
},
] as const;
for (const testCase of cases) {
await withChannelSecurityStateDir(async () => {
const res = await runSecurityAudit({
config: testCase.cfg,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [discordPlugin],
});
await Promise.all(
cases.map(async (testCase) => {
await withChannelSecurityStateDir(async () => {
const res = await runSecurityAudit({
config: testCase.cfg,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [discordPlugin],
});
expect(
res.findings.some(
(finding) => finding.checkId === "channels.discord.commands.native.no_allowlists",
),
testCase.name,
).toBe(testCase.expectFinding);
});
}
expect(
res.findings.some(
(finding) => finding.checkId === "channels.discord.commands.native.no_allowlists",
),
testCase.name,
).toBe(testCase.expectFinding);
});
}),
);
});
it("keeps source-configured channel security findings when resolved inspection is incomplete", async () => {
@@ -2440,26 +2444,28 @@ description: test skill
},
] as const;
for (const testCase of cases) {
await withChannelSecurityStateDir(async () => {
const res = await runSecurityAudit({
config: testCase.resolvedConfig,
sourceConfig: testCase.sourceConfig,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [testCase.plugin(testCase.sourceConfig)],
});
await Promise.all(
cases.map(async (testCase) => {
await withChannelSecurityStateDir(async () => {
const res = await runSecurityAudit({
config: testCase.resolvedConfig,
sourceConfig: testCase.sourceConfig,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [testCase.plugin(testCase.sourceConfig)],
});
expect(res.findings, testCase.name).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: testCase.expectedCheckId,
severity: "warn",
}),
]),
);
});
}
expect(res.findings, testCase.name).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: testCase.expectedCheckId,
severity: "warn",
}),
]),
);
});
}),
);
});
it("adds a read-only resolution warning when channel account resolveAccount throws", async () => {