diff --git a/src/channels/allowlists/resolve-utils.test.ts b/src/channels/allowlists/resolve-utils.test.ts index 346cd182787..8702fbd46a7 100644 --- a/src/channels/allowlists/resolve-utils.test.ts +++ b/src/channels/allowlists/resolve-utils.test.ts @@ -1,9 +1,10 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, canonicalizeAllowlistWithResolvedIds, patchAllowlistUsersInConfigEntries, + summarizeMapping, } from "./resolve-utils.js"; describe("buildAllowlistResolutionSummary", () => { @@ -94,3 +95,23 @@ describe("patchAllowlistUsersInConfigEntries", () => { expect((patched.beta as { users: string[] }).users).toEqual(["*"]); }); }); + +describe("summarizeMapping", () => { + it("logs sampled resolved and unresolved entries", () => { + const runtime = { log: vi.fn() }; + + summarizeMapping("discord allowlist", ["a", "b", "c", "d", "e", "f", "g"], ["x", "y"], runtime); + + expect(runtime.log).toHaveBeenCalledWith( + "discord allowlist resolved: a, b, c, d, e, f (+1)\ndiscord allowlist unresolved: x, y", + ); + }); + + it("skips logging when both lists are empty", () => { + const runtime = { log: vi.fn() }; + + summarizeMapping("discord allowlist", [], [], runtime); + + expect(runtime.log).not.toHaveBeenCalled(); + }); +}); diff --git a/src/channels/allowlists/resolve-utils.ts b/src/channels/allowlists/resolve-utils.ts index 5ec898c1a1a..2199eaf4ecf 100644 --- a/src/channels/allowlists/resolve-utils.ts +++ b/src/channels/allowlists/resolve-utils.ts @@ -1,5 +1,6 @@ import { mapAllowFromEntries } from "../../plugin-sdk/channel-config-helpers.js"; import type { RuntimeEnv } from "../../runtime.js"; +import { summarizeStringEntries } from "../../shared/string-sample.js"; export type AllowlistUserResolutionLike = { input: string; @@ -150,15 +151,10 @@ export function summarizeMapping( ): void { const lines: string[] = []; if (mapping.length > 0) { - const sample = mapping.slice(0, 6); - const suffix = mapping.length > sample.length ? ` (+${mapping.length - sample.length})` : ""; - lines.push(`${label} resolved: ${sample.join(", ")}${suffix}`); + lines.push(`${label} resolved: ${summarizeStringEntries({ entries: mapping, limit: 6 })}`); } if (unresolved.length > 0) { - const sample = unresolved.slice(0, 6); - const suffix = - unresolved.length > sample.length ? ` (+${unresolved.length - sample.length})` : ""; - lines.push(`${label} unresolved: ${sample.join(", ")}${suffix}`); + lines.push(`${label} unresolved: ${summarizeStringEntries({ entries: unresolved, limit: 6 })}`); } if (lines.length > 0) { runtime.log?.(lines.join("\n")); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 2a9791d2642..b0825d03345 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -43,6 +43,7 @@ import { createDiscordRetryRunner } from "../../infra/retry-policy.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { getPluginCommandSpecs } from "../../plugins/commands.js"; import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js"; +import { summarizeStringEntries } from "../../shared/string-sample.js"; import { resolveDiscordAccount } from "../accounts.js"; import { fetchDiscordApplicationId } from "../probe.js"; import { normalizeDiscordToken } from "../token.js"; @@ -103,25 +104,6 @@ export type MonitorDiscordOpts = { setStatus?: DiscordMonitorStatusSink; }; -function summarizeAllowList(list?: string[]) { - if (!list || list.length === 0) { - return "any"; - } - const sample = list.slice(0, 4).map((entry) => String(entry)); - const suffix = list.length > sample.length ? ` (+${list.length - sample.length})` : ""; - return `${sample.join(", ")}${suffix}`; -} - -function summarizeGuilds(entries?: Record) { - if (!entries || Object.keys(entries).length === 0) { - return "any"; - } - const keys = Object.keys(entries); - const sample = keys.slice(0, 4); - const suffix = keys.length > sample.length ? ` (+${keys.length - sample.length})` : ""; - return `${sample.join(", ")}${suffix}`; -} - function formatThreadBindingDurationForConfigLabel(durationMs: number): string { const label = formatThreadBindingDurationLabel(durationMs); return label === "disabled" ? "off" : label; @@ -402,8 +384,23 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { allowFrom = allowlistResolved.allowFrom; if (shouldLogVerbose()) { + const allowFromSummary = summarizeStringEntries({ + entries: allowFrom ?? [], + limit: 4, + emptyText: "any", + }); + const groupDmChannelSummary = summarizeStringEntries({ + entries: groupDmChannels ?? [], + limit: 4, + emptyText: "any", + }); + const guildSummary = summarizeStringEntries({ + entries: Object.keys(guildEntries ?? {}), + limit: 4, + emptyText: "any", + }); logVerbose( - `discord: config dm=${dmEnabled ? "on" : "off"} dmPolicy=${dmPolicy} allowFrom=${summarizeAllowList(allowFrom)} groupDm=${groupDmEnabled ? "on" : "off"} groupDmChannels=${summarizeAllowList(groupDmChannels)} groupPolicy=${groupPolicy} guilds=${summarizeGuilds(guildEntries)} historyLimit=${historyLimit} mediaMaxMb=${Math.round(mediaMaxBytes / (1024 * 1024))} native=${nativeEnabled ? "on" : "off"} nativeSkills=${nativeSkillsEnabled ? "on" : "off"} accessGroups=${useAccessGroups ? "on" : "off"} threadBindings=${threadBindingsEnabled ? "on" : "off"} threadIdleTimeout=${formatThreadBindingDurationForConfigLabel(threadBindingIdleTimeoutMs)} threadMaxAge=${formatThreadBindingDurationForConfigLabel(threadBindingMaxAgeMs)}`, + `discord: config dm=${dmEnabled ? "on" : "off"} dmPolicy=${dmPolicy} allowFrom=${allowFromSummary} groupDm=${groupDmEnabled ? "on" : "off"} groupDmChannels=${groupDmChannelSummary} groupPolicy=${groupPolicy} guilds=${guildSummary} historyLimit=${historyLimit} mediaMaxMb=${Math.round(mediaMaxBytes / (1024 * 1024))} native=${nativeEnabled ? "on" : "off"} nativeSkills=${nativeSkillsEnabled ? "on" : "off"} accessGroups=${useAccessGroups ? "on" : "off"} threadBindings=${threadBindingsEnabled ? "on" : "off"} threadIdleTimeout=${formatThreadBindingDurationForConfigLabel(threadBindingIdleTimeoutMs)} threadMaxAge=${formatThreadBindingDurationForConfigLabel(threadBindingMaxAgeMs)}`, ); } diff --git a/src/shared/string-sample.test.ts b/src/shared/string-sample.test.ts new file mode 100644 index 00000000000..4cff7957fe0 --- /dev/null +++ b/src/shared/string-sample.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { summarizeStringEntries } from "./string-sample.js"; + +describe("summarizeStringEntries", () => { + it("returns emptyText for empty lists", () => { + expect(summarizeStringEntries({ entries: [], emptyText: "any" })).toBe("any"); + }); + + it("joins short lists without a suffix", () => { + expect(summarizeStringEntries({ entries: ["a", "b"], limit: 4 })).toBe("a, b"); + }); + + it("adds a remainder suffix when truncating", () => { + expect( + summarizeStringEntries({ + entries: ["a", "b", "c", "d", "e"], + limit: 4, + }), + ).toBe("a, b, c, d (+1)"); + }); +}); diff --git a/src/shared/string-sample.ts b/src/shared/string-sample.ts new file mode 100644 index 00000000000..1529b06b04a --- /dev/null +++ b/src/shared/string-sample.ts @@ -0,0 +1,14 @@ +export function summarizeStringEntries(params: { + entries?: ReadonlyArray | null; + limit?: number; + emptyText?: string; +}): string { + const entries = params.entries ?? []; + if (entries.length === 0) { + return params.emptyText ?? ""; + } + const limit = Math.max(1, Math.floor(params.limit ?? 6)); + const sample = entries.slice(0, limit); + const suffix = entries.length > sample.length ? ` (+${entries.length - sample.length})` : ""; + return `${sample.join(", ")}${suffix}`; +}