diff --git a/src/security/audit-channel-account-metadata.test.ts b/src/security/audit-channel-account-metadata.test.ts new file mode 100644 index 00000000000..0841058c649 --- /dev/null +++ b/src/security/audit-channel-account-metadata.test.ts @@ -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)"); + }); +}); diff --git a/src/security/audit-channel-source-config-discord.test.ts b/src/security/audit-channel-source-config-discord.test.ts index f4076e26470..6028b62dd04 100644 --- a/src/security/audit-channel-source-config-discord.test.ts +++ b/src/security/audit-channel-source-config-discord.test.ts @@ -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, diff --git a/test/extension-test-boundary.test.ts b/test/extension-test-boundary.test.ts index 69ce50079da..a779f4c1a8c 100644 --- a/test/extension-test-boundary.test.ts +++ b/test/extension-test-boundary.test.ts @@ -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([]); + }); });