From 4267a1b87d76675fb2212a6ff3740257a756bb3d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Jan 2026 11:19:10 +0100 Subject: [PATCH] test: cover discord config + slug routing --- src/config/config.test.ts | 59 ++++++++++++++ src/config/sessions.test.ts | 13 ++++ src/discord/monitor.test.ts | 149 ++++++++++++++++++++++++++++++++++++ src/discord/monitor.ts | 18 ++--- 4 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 src/discord/monitor.test.ts diff --git a/src/config/config.test.ts b/src/config/config.test.ts index d007254a9..1f6ac93bb 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -206,6 +206,65 @@ describe("config identity defaults", () => { }); }); +import fs from "node:fs/promises"; +describe("config discord", () => { + let previousHome: string | undefined; + + beforeEach(() => { + previousHome = process.env.HOME; + }); + + afterEach(() => { + process.env.HOME = previousHome; + }); + + it("loads discord guild map + dm group settings", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdis"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdis.json"), + JSON.stringify( + { + discord: { + enabled: true, + dm: { + enabled: true, + allowFrom: ["steipete"], + groupEnabled: true, + groupChannels: ["clawd-dm"], + }, + guilds: { + "123": { + slug: "friends-of-clawd", + requireMention: false, + users: ["steipete"], + channels: { + general: { allow: true }, + }, + }, + }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.discord?.enabled).toBe(true); + expect(cfg.discord?.dm?.groupEnabled).toBe(true); + expect(cfg.discord?.dm?.groupChannels).toEqual(["clawd-dm"]); + expect(cfg.discord?.guilds?.["123"]?.slug).toBe("friends-of-clawd"); + expect(cfg.discord?.guilds?.["123"]?.channels?.general?.allow).toBe(true); + }); + }); +}); + describe("Nix integration (U3, U5, U9)", () => { describe("U3: isNixMode env var detection", () => { it("isNixMode is false when CLAWDIS_NIX_MODE is not set", async () => { diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index afec54a51..6422f0b45 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { + buildGroupDisplayName, deriveSessionKey, loadSessionStore, resolveSessionKey, @@ -51,6 +52,18 @@ describe("sessions", () => { ).toBe("discord:group:12345"); }); + it("builds discord display name with guild+channel slugs", () => { + expect( + buildGroupDisplayName({ + surface: "discord", + room: "#general", + space: "friends-of-clawd", + id: "123", + key: "discord:group:123", + }), + ).toBe("discord:friends-of-clawd#general"); + }); + it("collapses direct chats to main by default", () => { expect(resolveSessionKey("per-sender", { From: "+1555" })).toBe("main"); }); diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts new file mode 100644 index 000000000..e3d6dee50 --- /dev/null +++ b/src/discord/monitor.test.ts @@ -0,0 +1,149 @@ +import { + allowListMatches, + normalizeDiscordAllowList, + normalizeDiscordSlug, + resolveDiscordChannelConfig, + resolveDiscordGuildEntry, + resolveGroupDmAllow, + type DiscordGuildEntryResolved, +} from "./monitor.js"; + +const fakeGuild = (id: string, name: string) => + ({ id, name } as unknown as import("discord.js").Guild); + +const makeEntries = ( + entries: Record>, +): Record => { + const out: Record = {}; + for (const [key, value] of Object.entries(entries)) { + out[key] = { + slug: value.slug, + requireMention: value.requireMention, + users: value.users, + channels: value.channels, + }; + } + return out; +}; + +describe("discord allowlist helpers", () => { + it("normalizes slugs", () => { + expect(normalizeDiscordSlug("Friends of Clawd")) + .toBe("friends-of-clawd"); + expect(normalizeDiscordSlug("#General")) + .toBe("general"); + expect(normalizeDiscordSlug("Dev__Chat")) + .toBe("dev-chat"); + }); + + it("matches ids or names", () => { + const allow = normalizeDiscordAllowList( + ["123", "steipete", "Friends of Clawd"], + ["discord:", "user:", "guild:", "channel:"], + ); + expect(allow).not.toBeNull(); + expect(allowListMatches(allow!, { id: "123" })).toBe(true); + expect(allowListMatches(allow!, { name: "steipete" })).toBe(true); + expect(allowListMatches(allow!, { name: "friends-of-clawd" })).toBe(true); + expect(allowListMatches(allow!, { name: "other" })).toBe(false); + }); +}); + +describe("discord guild/channel resolution", () => { + it("resolves guild entry by id", () => { + const guildEntries = makeEntries({ + "123": { slug: "friends-of-clawd" }, + }); + const resolved = resolveDiscordGuildEntry({ + guild: fakeGuild("123", "Friends of Clawd"), + guildEntries, + }); + expect(resolved?.id).toBe("123"); + expect(resolved?.slug).toBe("friends-of-clawd"); + }); + + it("resolves guild entry by slug key", () => { + const guildEntries = makeEntries({ + "friends-of-clawd": { slug: "friends-of-clawd" }, + }); + const resolved = resolveDiscordGuildEntry({ + guild: fakeGuild("123", "Friends of Clawd"), + guildEntries, + }); + expect(resolved?.id).toBe("123"); + expect(resolved?.slug).toBe("friends-of-clawd"); + }); + + it("resolves channel config by slug", () => { + const guildInfo: DiscordGuildEntryResolved = { + channels: { + general: { allow: true }, + help: { allow: true, requireMention: true }, + }, + }; + const channel = resolveDiscordChannelConfig({ + guildInfo, + channelId: "456", + channelName: "General", + channelSlug: "general", + }); + expect(channel?.allowed).toBe(true); + expect(channel?.requireMention).toBeUndefined(); + + const help = resolveDiscordChannelConfig({ + guildInfo, + channelId: "789", + channelName: "Help", + channelSlug: "help", + }); + expect(help?.allowed).toBe(true); + expect(help?.requireMention).toBe(true); + }); + + it("denies channel when config present but no match", () => { + const guildInfo: DiscordGuildEntryResolved = { + channels: { + general: { allow: true }, + }, + }; + const channel = resolveDiscordChannelConfig({ + guildInfo, + channelId: "999", + channelName: "random", + channelSlug: "random", + }); + expect(channel?.allowed).toBe(false); + }); +}); + +describe("discord group DM gating", () => { + it("allows all when no allowlist", () => { + expect( + resolveGroupDmAllow({ + channels: undefined, + channelId: "1", + channelName: "dm", + channelSlug: "dm", + }), + ).toBe(true); + }); + + it("matches group DM allowlist", () => { + expect( + resolveGroupDmAllow({ + channels: ["clawd-dm"], + channelId: "1", + channelName: "Clawd DM", + channelSlug: "clawd-dm", + }), + ).toBe(true); + expect( + resolveGroupDmAllow({ + channels: ["clawd-dm"], + channelId: "1", + channelName: "Other", + channelSlug: "other", + }), + ).toBe(false); + }); +}); diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 0ac5c3e8b..904e2755c 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -42,13 +42,13 @@ type DiscordHistoryEntry = { messageId?: string; }; -type DiscordAllowList = { +export type DiscordAllowList = { allowAll: boolean; ids: Set; names: Set; }; -type DiscordGuildEntryResolved = { +export type DiscordGuildEntryResolved = { id?: string; slug?: string; requireMention?: boolean; @@ -56,7 +56,7 @@ type DiscordGuildEntryResolved = { channels?: Record; }; -type DiscordChannelConfigResolved = { +export type DiscordChannelConfigResolved = { allowed: boolean; requireMention?: boolean; }; @@ -440,7 +440,7 @@ function buildGuildLabel(message: import("discord.js").Message) { return `${message.guild?.name ?? "Guild"} #${channelName} id:${message.channelId}`; } -function normalizeDiscordAllowList( +export function normalizeDiscordAllowList( raw: Array | undefined, prefixes: string[], ): DiscordAllowList | null { @@ -490,7 +490,7 @@ function normalizeDiscordName(value?: string | null) { return value.trim().toLowerCase(); } -function normalizeDiscordSlug(value?: string | null) { +export function normalizeDiscordSlug(value?: string | null) { if (!value) return ""; let text = value.trim().toLowerCase(); if (!text) return ""; @@ -501,7 +501,7 @@ function normalizeDiscordSlug(value?: string | null) { return text; } -function allowListMatches( +export function allowListMatches( allowList: DiscordAllowList, candidates: { id?: string; @@ -523,7 +523,7 @@ function allowListMatches( return false; } -function resolveDiscordGuildEntry(params: { +export function resolveDiscordGuildEntry(params: { guild: import("discord.js").Guild | null; guildEntries: Record | undefined; }): DiscordGuildEntryResolved | null { @@ -570,7 +570,7 @@ function resolveDiscordGuildEntry(params: { return null; } -function resolveDiscordChannelConfig(params: { +export function resolveDiscordChannelConfig(params: { guildInfo: DiscordGuildEntryResolved | null; channelId: string; channelName?: string; @@ -594,7 +594,7 @@ function resolveDiscordChannelConfig(params: { return { allowed: true }; } -function resolveGroupDmAllow(params: { +export function resolveGroupDmAllow(params: { channels: Array | undefined; channelId: string; channelName?: string;