test(core): guard security audit boundaries

This commit is contained in:
Peter Steinberger
2026-04-20 16:39:45 +01:00
parent a73bbe4bdd
commit f304af6b74
3 changed files with 97 additions and 12 deletions

View File

@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";
function stubChannelPlugin(): ChannelPlugin {
return {
id: "discord",
meta: {
id: "discord",
label: "Discord",
selectionLabel: "Discord",
docsPath: "/docs/testing",
blurb: "test stub",
},
capabilities: {
chatTypes: ["direct", "group"],
},
config: {
listAccountIds: () => [],
defaultAccountId: () => "toString",
inspectAccount: () => ({
accountId: "toString",
enabled: true,
configured: true,
config: { dangerouslyAllowNameMatching: true },
}),
resolveAccount: () => ({
accountId: "toString",
enabled: true,
config: { dangerouslyAllowNameMatching: true },
}),
isEnabled: () => true,
isConfigured: () => true,
},
security: {},
};
}
describe("security audit channel account metadata", () => {
it("does not treat prototype properties as explicit account config paths", async () => {
const cfg: OpenClawConfig = {
channels: {
discord: {
enabled: true,
token: "t",
dangerouslyAllowNameMatching: true,
accounts: {},
},
},
};
const findings = await collectChannelSecurityFindings({
cfg,
plugins: [stubChannelPlugin()],
});
const dangerousMatchingFinding = findings.find(
(entry) => entry.checkId === "channels.discord.allowFrom.dangerous_name_matching_enabled",
);
expect(dangerousMatchingFinding).toBeDefined();
expect(dangerousMatchingFinding?.title).not.toContain("(account: toString)");
});
});

View File

@@ -1,17 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import { collectDiscordSecurityAuditFindings } from "../../test/helpers/channels/security-audit-contract.js";
import { describe, expect, it } from "vitest";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";
const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({
readChannelAllowFromStoreMock: vi.fn(async () => [] as string[]),
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
readChannelAllowFromStore: readChannelAllowFromStoreMock,
}));
function stubDiscordPlugin(params: {
resolveAccount: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
inspectAccount?: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
@@ -34,7 +25,24 @@ function stubDiscordPlugin(params: {
nativeSkillsAutoEnabled: true,
},
security: {
collectAuditFindings: collectDiscordSecurityAuditFindings,
collectAuditFindings: ({ account }) => {
const config = (account as { config?: { guilds?: unknown } }).config ?? {};
const guilds =
config.guilds && typeof config.guilds === "object" && !Array.isArray(config.guilds)
? config.guilds
: {};
if (Object.keys(guilds).length === 0) {
return [];
}
return [
{
checkId: "channels.discord.commands.native.no_allowlists",
severity: "warn" as const,
title: "Discord slash commands have no allowlists",
detail: "test stub",
},
];
},
},
config: {
listAccountIds: () => ["default"],
@@ -96,7 +104,6 @@ describe("security audit channel source-config fallback discord", () => {
},
};
readChannelAllowFromStoreMock.mockResolvedValue([]);
const findings = await collectChannelSecurityFindings({
cfg: resolvedConfig,
sourceConfig,

View File

@@ -171,4 +171,18 @@ describe("non-extension test boundaries", () => {
expect(offenders).toEqual([]);
});
it("keeps bundled channel security collector coverage under extension tests", () => {
const files = [...walk(path.join(repoRoot, "src")), ...walk(path.join(repoRoot, "test"))]
.filter((file) => !file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))
.filter((file) => !file.startsWith("test/helpers/"))
.filter((file) => file !== "test/extension-test-boundary.test.ts");
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return source.includes("test/helpers/channels/security-audit-contract.js");
});
expect(offenders).toEqual([]);
});
});