Files
openclaw/src/channels/channel-config.test.ts

192 lines
5.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { MsgContext } from "../auto-reply/templating.js";
import {
buildChannelKeyCandidates,
normalizeChannelSlug,
resolveChannelEntryMatch,
resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision,
applyChannelMatchMeta,
resolveChannelMatchConfig,
} from "./channel-config.js";
import { validateSenderIdentity } from "./sender-identity.js";
describe("buildChannelKeyCandidates", () => {
it("dedupes and trims keys", () => {
expect(buildChannelKeyCandidates(" a ", "a", "", "b", "b")).toEqual(["a", "b"]);
});
});
describe("normalizeChannelSlug", () => {
it("normalizes names into slugs", () => {
expect(normalizeChannelSlug("My Team")).toBe("my-team");
expect(normalizeChannelSlug("#General Chat")).toBe("general-chat");
expect(normalizeChannelSlug(" Dev__Chat ")).toBe("dev-chat");
});
});
describe("resolveChannelEntryMatch", () => {
it("returns matched entry and wildcard metadata", () => {
const entries = { a: { allow: true }, "*": { allow: false } };
const match = resolveChannelEntryMatch({
entries,
keys: ["missing", "a"],
wildcardKey: "*",
});
expect(match.entry).toBe(entries.a);
expect(match.key).toBe("a");
expect(match.wildcardEntry).toBe(entries["*"]);
expect(match.wildcardKey).toBe("*");
});
});
describe("resolveChannelEntryMatchWithFallback", () => {
it("prefers direct matches over parent and wildcard", () => {
const entries = { a: { allow: true }, parent: { allow: false }, "*": { allow: false } };
const match = resolveChannelEntryMatchWithFallback({
entries,
keys: ["a"],
parentKeys: ["parent"],
wildcardKey: "*",
});
expect(match.entry).toBe(entries.a);
expect(match.matchSource).toBe("direct");
expect(match.matchKey).toBe("a");
});
it("falls back to parent when direct misses", () => {
const entries = { parent: { allow: false }, "*": { allow: true } };
const match = resolveChannelEntryMatchWithFallback({
entries,
keys: ["missing"],
parentKeys: ["parent"],
wildcardKey: "*",
});
expect(match.entry).toBe(entries.parent);
expect(match.matchSource).toBe("parent");
expect(match.matchKey).toBe("parent");
});
it("falls back to wildcard when no direct or parent match", () => {
const entries = { "*": { allow: true } };
const match = resolveChannelEntryMatchWithFallback({
entries,
keys: ["missing"],
parentKeys: ["still-missing"],
wildcardKey: "*",
});
expect(match.entry).toBe(entries["*"]);
expect(match.matchSource).toBe("wildcard");
expect(match.matchKey).toBe("*");
});
it("matches normalized keys when normalizeKey is provided", () => {
const entries = { "My Team": { allow: true } };
const match = resolveChannelEntryMatchWithFallback({
entries,
keys: ["my-team"],
normalizeKey: normalizeChannelSlug,
});
expect(match.entry).toBe(entries["My Team"]);
expect(match.matchSource).toBe("direct");
expect(match.matchKey).toBe("My Team");
});
});
describe("applyChannelMatchMeta", () => {
it("copies match metadata onto resolved configs", () => {
const resolved = applyChannelMatchMeta(
{ allowed: true },
{ matchKey: "general", matchSource: "direct" },
);
expect(resolved.matchKey).toBe("general");
expect(resolved.matchSource).toBe("direct");
});
});
describe("resolveChannelMatchConfig", () => {
it("returns null when no entry is matched", () => {
const resolved = resolveChannelMatchConfig({ matchKey: "x" }, () => ({ allowed: true }));
expect(resolved).toBeNull();
});
it("resolves entry and applies match metadata", () => {
const resolved = resolveChannelMatchConfig(
{ entry: { allow: true }, matchKey: "*", matchSource: "wildcard" },
() => ({ allowed: true }),
);
expect(resolved?.matchKey).toBe("*");
expect(resolved?.matchSource).toBe("wildcard");
});
});
describe("validateSenderIdentity", () => {
it("allows direct messages without sender fields", () => {
const ctx: MsgContext = { ChatType: "direct" };
expect(validateSenderIdentity(ctx)).toEqual([]);
});
it("requires some sender identity for non-direct chats", () => {
const ctx: MsgContext = { ChatType: "group" };
expect(validateSenderIdentity(ctx)).toContain(
"missing sender identity (SenderId/SenderName/SenderUsername/SenderE164)",
);
});
it("validates SenderE164 and SenderUsername shape", () => {
const ctx: MsgContext = {
ChatType: "group",
SenderE164: "123",
SenderUsername: "@ada lovelace",
};
expect(validateSenderIdentity(ctx)).toEqual([
"invalid SenderE164: 123",
'SenderUsername should not include "@": @ada lovelace',
"SenderUsername should not include whitespace: @ada lovelace",
]);
});
});
describe("resolveNestedAllowlistDecision", () => {
it("allows when outer allowlist is disabled", () => {
expect(
resolveNestedAllowlistDecision({
outerConfigured: false,
outerMatched: false,
innerConfigured: false,
innerMatched: false,
}),
).toBe(true);
});
it("blocks when outer allowlist is configured but missing match", () => {
expect(
resolveNestedAllowlistDecision({
outerConfigured: true,
outerMatched: false,
innerConfigured: false,
innerMatched: false,
}),
).toBe(false);
});
it("requires inner match when inner allowlist is configured", () => {
expect(
resolveNestedAllowlistDecision({
outerConfigured: true,
outerMatched: true,
innerConfigured: true,
innerMatched: false,
}),
).toBe(false);
expect(
resolveNestedAllowlistDecision({
outerConfigured: true,
outerMatched: true,
innerConfigured: true,
innerMatched: true,
}),
).toBe(true);
});
});