test: dedupe pairing and channel contract suites

This commit is contained in:
Peter Steinberger
2026-03-28 07:31:31 +00:00
parent e7c1fcba0c
commit f36354e401
8 changed files with 1470 additions and 976 deletions

View File

@@ -19,7 +19,7 @@ const signalSender: SignalSender = {
e164: "+15550001111",
};
const cases: ChannelSmokeCase[] = [
const channelSmokeCases: ChannelSmokeCase[] = [
{
name: "bluebubbles",
storeAllowFrom: ["attacker-user"],
@@ -47,23 +47,41 @@ const cases: ChannelSmokeCase[] = [
},
];
function expandChannelIngressCases(cases: readonly ChannelSmokeCase[]) {
return cases.flatMap((testCase) =>
(["message", "reaction"] as const).map((ingress) => ({
testCase,
ingress,
})),
);
}
describe("security/dm-policy-shared channel smoke", () => {
for (const testCase of cases) {
for (const ingress of ["message", "reaction"] as const) {
it(`[${testCase.name}] blocks group ${ingress} when sender is only in pairing store`, () => {
const access = resolveDmGroupAccessWithLists({
isGroup: true,
dmPolicy: "pairing",
groupPolicy: "allowlist",
allowFrom: ["owner-user"],
groupAllowFrom: ["group-owner"],
storeAllowFrom: testCase.storeAllowFrom,
isSenderAllowed: testCase.isSenderAllowed,
});
expect(access.decision).toBe("block");
expect(access.reasonCode).toBe(DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED);
expect(access.reason).toBe("groupPolicy=allowlist (not allowlisted)");
});
}
function expectBlockedGroupAccess(params: {
storeAllowFrom: string[];
isSenderAllowed: (allowFrom: string[]) => boolean;
}) {
const access = resolveDmGroupAccessWithLists({
isGroup: true,
dmPolicy: "pairing",
groupPolicy: "allowlist",
allowFrom: ["owner-user"],
groupAllowFrom: ["group-owner"],
storeAllowFrom: params.storeAllowFrom,
isSenderAllowed: params.isSenderAllowed,
});
expect(access.decision).toBe("block");
expect(access.reasonCode).toBe(DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED);
expect(access.reason).toBe("groupPolicy=allowlist (not allowlisted)");
}
it.each(expandChannelIngressCases(channelSmokeCases))(
"[$testCase.name] blocks group $ingress when sender is only in pairing store",
({ testCase }) => {
expectBlockedGroupAccess({
storeAllowFrom: testCase.storeAllowFrom,
isSenderAllowed: testCase.isSenderAllowed,
});
},
);
});

View File

@@ -11,6 +11,45 @@ import {
import { installChannelRuntimeGroupPolicyFallbackSuite } from "./suites.js";
describe("channel runtime group policy contract", () => {
type ResolvedGroupPolicy = ReturnType<typeof resolveDiscordRuntimeGroupPolicy>;
function expectResolvedGroupPolicyCase(
resolved: Pick<ResolvedGroupPolicy, "groupPolicy" | "providerMissingFallbackApplied">,
expected: Pick<ResolvedGroupPolicy, "groupPolicy" | "providerMissingFallbackApplied">,
) {
expect(resolved.groupPolicy).toBe(expected.groupPolicy);
expect(resolved.providerMissingFallbackApplied).toBe(expected.providerMissingFallbackApplied);
}
function expectAllowedZaloGroupAccess(params: Parameters<typeof evaluateZaloGroupAccess>[0]) {
expect(evaluateZaloGroupAccess(params)).toMatchObject({
allowed: true,
groupPolicy: "allowlist",
reason: "allowed",
});
}
function expectResolvedDiscordGroupPolicyCase(params: {
providerConfigPresent: Parameters<
typeof resolveDiscordRuntimeGroupPolicy
>[0]["providerConfigPresent"];
groupPolicy: Parameters<typeof resolveDiscordRuntimeGroupPolicy>[0]["groupPolicy"];
expected: Pick<ResolvedGroupPolicy, "groupPolicy" | "providerMissingFallbackApplied">;
}) {
expectResolvedGroupPolicyCase(resolveDiscordRuntimeGroupPolicy(params), params.expected);
}
function expectAllowedZaloGroupAccessCase(
params: Omit<Parameters<typeof evaluateZaloGroupAccess>[0], "groupAllowFrom"> & {
groupAllowFrom: readonly string[];
},
) {
expectAllowedZaloGroupAccess({
...params,
groupAllowFrom: [...params.groupAllowFrom],
});
}
describe("slack", () => {
installChannelRuntimeGroupPolicyFallbackSuite({
resolve: resolveSlackRuntimeGroupPolicy,
@@ -60,13 +99,17 @@ describe("channel runtime group policy contract", () => {
missingDefaultLabel: "ignores explicit global defaults when provider config is missing",
});
it("respects explicit provider policy", () => {
const resolved = resolveDiscordRuntimeGroupPolicy({
it.each([
{
providerConfigPresent: false,
groupPolicy: "disabled",
});
expect(resolved.groupPolicy).toBe("disabled");
expect(resolved.providerMissingFallbackApplied).toBe(false);
expected: {
groupPolicy: "disabled",
providerMissingFallbackApplied: false,
},
},
] as const)("respects explicit provider policy %#", (testCase) => {
expectResolvedDiscordGroupPolicyCase(testCase);
});
});
@@ -79,19 +122,16 @@ describe("channel runtime group policy contract", () => {
missingDefaultLabel: "ignores explicit global defaults when provider config is missing",
});
it("keeps provider-owned group access evaluation", () => {
const decision = evaluateZaloGroupAccess({
it.each([
{
providerConfigPresent: true,
configuredGroupPolicy: "allowlist",
defaultGroupPolicy: "open",
groupAllowFrom: ["zl:12345"],
senderId: "12345",
});
expect(decision).toMatchObject({
allowed: true,
groupPolicy: "allowlist",
reason: "allowed",
});
},
] as const)("keeps provider-owned group access evaluation %#", (testCase) => {
expectAllowedZaloGroupAccessCase(testCase);
});
});
});

View File

@@ -49,227 +49,129 @@ describe("channel plugin registry", () => {
setActivePluginRegistry(emptyRegistry);
});
function expectListedChannelPluginIds(expectedIds: string[]) {
expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(expectedIds);
}
function expectRegistryActivationCase(run: () => void) {
run();
}
afterEach(() => {
setActivePluginRegistry(emptyRegistry);
clearPluginDiscoveryCache();
clearPluginManifestRegistryCache();
});
it("sorts channel plugins by configured order", () => {
const orderedPlugins: Array<[string, number]> = [
["demo-middle", 20],
["demo-first", 10],
["demo-last", 30],
];
const registry = createTestRegistry(
orderedPlugins.map(([id, order]) => ({
pluginId: id,
plugin: createPlugin(id, order),
source: "test",
})),
);
setActivePluginRegistry(registry);
const pluginIds = listChannelPlugins().map((plugin) => plugin.id);
expect(pluginIds).toEqual(["demo-first", "demo-middle", "demo-last"]);
});
it("refreshes cached channel lookups when the same registry instance is re-activated", () => {
const registry = createTestRegistry([
{
pluginId: "demo-alpha",
plugin: createPlugin("demo-alpha"),
source: "test",
it.each([
{
name: "sorts channel plugins by configured order",
run: () => {
const orderedPlugins: Array<[string, number]> = [
["demo-middle", 20],
["demo-first", 10],
["demo-last", 30],
];
const registry = createTestRegistry(
orderedPlugins.map(([id, order]) => ({
pluginId: id,
plugin: createPlugin(id, order),
source: "test",
})),
);
setActivePluginRegistry(registry);
expectListedChannelPluginIds(["demo-first", "demo-middle", "demo-last"]);
},
]);
setActivePluginRegistry(registry, "registry-test");
expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(["demo-alpha"]);
},
{
name: "refreshes cached channel lookups when the same registry instance is re-activated",
run: () => {
const registry = createTestRegistry([
{
pluginId: "demo-alpha",
plugin: createPlugin("demo-alpha"),
source: "test",
},
]);
setActivePluginRegistry(registry, "registry-test");
expectListedChannelPluginIds(["demo-alpha"]);
registry.channels = [
{
pluginId: "demo-beta",
plugin: createPlugin("demo-beta"),
source: "test",
registry.channels = [
{
pluginId: "demo-beta",
plugin: createPlugin("demo-beta"),
source: "test",
},
] as typeof registry.channels;
setActivePluginRegistry(registry, "registry-test");
expectListedChannelPluginIds(["demo-beta"]);
},
] as typeof registry.channels;
setActivePluginRegistry(registry, "registry-test");
expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(["demo-beta"]);
},
] as const)("$name", ({ run }) => {
expectRegistryActivationCase(run);
});
});
describe("channel plugin catalog", () => {
it("includes external catalog entries", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-"));
const catalogPath = path.join(dir, "catalog.json");
fs.writeFileSync(
catalogPath,
JSON.stringify({
entries: [
{
name: "@openclaw/demo-channel",
openclaw: {
channel: {
id: "demo-channel",
label: "Demo Channel",
selectionLabel: "Demo Channel",
docsPath: "/channels/demo-channel",
blurb: "Demo entry",
order: 999,
},
install: {
npmSpec: "@openclaw/demo-channel",
},
},
},
],
}),
);
const ids = listChannelPluginCatalogEntries({ catalogPaths: [catalogPath] }).map(
(entry) => entry.id,
);
expect(ids).toContain("demo-channel");
});
it("preserves plugin ids when they differ from channel ids", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-catalog-state-"));
const pluginDir = path.join(stateDir, "extensions", "demo-channel-plugin");
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: "@vendor/demo-channel-plugin",
openclaw: {
extensions: ["./index.js"],
channel: {
id: "demo-channel",
label: "Demo Channel",
selectionLabel: "Demo Channel",
docsPath: "/channels/demo-channel",
blurb: "Demo channel",
},
install: {
npmSpec: "@vendor/demo-channel-plugin",
},
function createCatalogEntry(params: {
packageName: string;
channelId: string;
label: string;
blurb: string;
order?: number;
}) {
return {
name: params.packageName,
openclaw: {
channel: {
id: params.channelId,
label: params.label,
selectionLabel: params.label,
docsPath: `/channels/${params.channelId}`,
blurb: params.blurb,
...(params.order === undefined ? {} : { order: params.order }),
},
install: {
npmSpec: params.packageName,
},
}),
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify({
id: "@vendor/demo-runtime",
configSchema: {},
}),
);
fs.writeFileSync(path.join(pluginDir, "index.js"), "module.exports = {}", "utf-8");
const entry = listChannelPluginCatalogEntries({
env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
},
}).find((item) => item.id === "demo-channel");
};
}
expect(entry?.pluginId).toBe("@vendor/demo-runtime");
});
it("uses the provided env for external catalog path resolution", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-home-"));
const catalogPath = path.join(home, "catalog.json");
function writeCatalogFile(catalogPath: string, entry: Record<string, unknown>) {
fs.writeFileSync(
catalogPath,
JSON.stringify({
entries: [
{
name: "@openclaw/env-demo-channel",
openclaw: {
channel: {
id: "env-demo-channel",
label: "Env Demo Channel",
selectionLabel: "Env Demo Channel",
docsPath: "/channels/env-demo-channel",
blurb: "Env demo entry",
order: 1000,
},
install: {
npmSpec: "@openclaw/env-demo-channel",
},
},
},
],
entries: [entry],
}),
);
}
const ids = listChannelPluginCatalogEntries({
env: {
...process.env,
OPENCLAW_PLUGIN_CATALOG_PATHS: "~/catalog.json",
OPENCLAW_HOME: home,
HOME: home,
},
}).map((entry) => entry.id);
expect(ids).toContain("env-demo-channel");
});
it("uses the provided env for default catalog paths", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-state-"));
const catalogPath = path.join(stateDir, "plugins", "catalog.json");
fs.mkdirSync(path.dirname(catalogPath), { recursive: true });
fs.writeFileSync(
catalogPath,
JSON.stringify({
entries: [
{
name: "@openclaw/default-env-demo",
openclaw: {
channel: {
id: "default-env-demo",
label: "Default Env Demo",
selectionLabel: "Default Env Demo",
docsPath: "/channels/default-env-demo",
blurb: "Default env demo entry",
},
install: {
npmSpec: "@openclaw/default-env-demo",
},
},
},
],
}),
);
const ids = listChannelPluginCatalogEntries({
env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir,
},
}).map((entry) => entry.id);
expect(ids).toContain("default-env-demo");
});
it("keeps discovered plugins ahead of external catalog overrides", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-state-"));
const pluginDir = path.join(stateDir, "extensions", "demo-channel-plugin");
const catalogPath = path.join(stateDir, "catalog.json");
function writeDiscoveredChannelPlugin(params: {
stateDir: string;
packageName: string;
channelLabel: string;
pluginId: string;
blurb: string;
}) {
const pluginDir = path.join(params.stateDir, "extensions", "demo-channel-plugin");
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: "@vendor/demo-channel-plugin",
name: params.packageName,
openclaw: {
extensions: ["./index.js"],
channel: {
id: "demo-channel",
label: "Demo Channel Runtime",
selectionLabel: "Demo Channel Runtime",
label: params.channelLabel,
selectionLabel: params.channelLabel,
docsPath: "/channels/demo-channel",
blurb: "discovered plugin",
blurb: params.blurb,
},
install: {
npmSpec: "@vendor/demo-channel-plugin",
npmSpec: params.packageName,
},
},
}),
@@ -278,49 +180,201 @@ describe("channel plugin catalog", () => {
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify({
id: "@vendor/demo-channel-runtime",
id: params.pluginId,
configSchema: {},
}),
"utf8",
);
fs.writeFileSync(path.join(pluginDir, "index.js"), "module.exports = {}", "utf8");
fs.writeFileSync(
catalogPath,
JSON.stringify({
entries: [
{
name: "@vendor/demo-channel-catalog",
openclaw: {
channel: {
id: "demo-channel",
label: "Demo Channel Catalog",
selectionLabel: "Demo Channel Catalog",
docsPath: "/channels/demo-channel",
blurb: "external catalog",
},
install: {
npmSpec: "@vendor/demo-channel-catalog",
},
},
},
],
return pluginDir;
}
function expectCatalogIdsContain(params: {
expectedId: string;
catalogPaths?: string[];
env?: NodeJS.ProcessEnv;
}) {
const ids = listChannelPluginCatalogEntries({
...(params.catalogPaths ? { catalogPaths: params.catalogPaths } : {}),
...(params.env ? { env: params.env } : {}),
}).map((entry) => entry.id);
expect(ids).toContain(params.expectedId);
}
function findCatalogEntry(params: {
channelId: string;
catalogPaths?: string[];
env?: NodeJS.ProcessEnv;
}) {
return listChannelPluginCatalogEntries({
...(params.catalogPaths ? { catalogPaths: params.catalogPaths } : {}),
...(params.env ? { env: params.env } : {}),
}).find((entry) => entry.id === params.channelId);
}
function expectCatalogEntryMatch(params: {
channelId: string;
expected: Record<string, unknown>;
catalogPaths?: string[];
env?: NodeJS.ProcessEnv;
}) {
expect(
findCatalogEntry({
channelId: params.channelId,
...(params.catalogPaths ? { catalogPaths: params.catalogPaths } : {}),
...(params.env ? { env: params.env } : {}),
}),
"utf8",
);
).toMatchObject(params.expected);
}
const entry = listChannelPluginCatalogEntries({
catalogPaths: [catalogPath],
env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir,
CLAWDBOT_STATE_DIR: undefined,
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
it.each([
{
name: "includes external catalog entries",
setup: () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-"));
const catalogPath = path.join(dir, "catalog.json");
writeCatalogFile(
catalogPath,
createCatalogEntry({
packageName: "@openclaw/demo-channel",
channelId: "demo-channel",
label: "Demo Channel",
blurb: "Demo entry",
order: 999,
}),
);
return {
channelId: "demo-channel",
catalogPaths: [catalogPath],
expected: { id: "demo-channel" },
};
},
}).find((item) => item.id === "demo-channel");
},
{
name: "preserves plugin ids when they differ from channel ids",
setup: () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-catalog-state-"));
writeDiscoveredChannelPlugin({
stateDir,
packageName: "@vendor/demo-channel-plugin",
channelLabel: "Demo Channel",
pluginId: "@vendor/demo-runtime",
blurb: "Demo channel",
});
return {
channelId: "demo-channel",
env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
},
expected: { pluginId: "@vendor/demo-runtime" },
};
},
},
{
name: "keeps discovered plugins ahead of external catalog overrides",
setup: () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-state-"));
const catalogPath = path.join(stateDir, "catalog.json");
writeDiscoveredChannelPlugin({
stateDir,
packageName: "@vendor/demo-channel-plugin",
channelLabel: "Demo Channel Runtime",
pluginId: "@vendor/demo-channel-runtime",
blurb: "discovered plugin",
});
writeCatalogFile(
catalogPath,
createCatalogEntry({
packageName: "@vendor/demo-channel-catalog",
channelId: "demo-channel",
label: "Demo Channel Catalog",
blurb: "external catalog",
}),
);
return {
channelId: "demo-channel",
catalogPaths: [catalogPath],
env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir,
CLAWDBOT_STATE_DIR: undefined,
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
},
expected: {
install: { npmSpec: "@vendor/demo-channel-plugin" },
meta: { label: "Demo Channel Runtime" },
pluginId: "@vendor/demo-channel-runtime",
},
};
},
},
] as const)("$name", ({ setup }) => {
const setupResult = setup();
const { channelId, expected } = setupResult;
expectCatalogEntryMatch({
channelId,
expected,
...("catalogPaths" in setupResult ? { catalogPaths: setupResult.catalogPaths } : {}),
...("env" in setupResult ? { env: setupResult.env } : {}),
});
});
expect(entry?.install.npmSpec).toBe("@vendor/demo-channel-plugin");
expect(entry?.meta.label).toBe("Demo Channel Runtime");
expect(entry?.pluginId).toBe("@vendor/demo-channel-runtime");
it.each([
{
name: "uses the provided env for external catalog path resolution",
setup: () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-home-"));
const catalogPath = path.join(home, "catalog.json");
writeCatalogFile(
catalogPath,
createCatalogEntry({
packageName: "@openclaw/env-demo-channel",
channelId: "env-demo-channel",
label: "Env Demo Channel",
blurb: "Env demo entry",
order: 1000,
}),
);
return {
env: {
...process.env,
OPENCLAW_PLUGIN_CATALOG_PATHS: "~/catalog.json",
OPENCLAW_HOME: home,
HOME: home,
},
expectedId: "env-demo-channel",
};
},
},
{
name: "uses the provided env for default catalog paths",
setup: () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-state-"));
const catalogPath = path.join(stateDir, "plugins", "catalog.json");
fs.mkdirSync(path.dirname(catalogPath), { recursive: true });
writeCatalogFile(
catalogPath,
createCatalogEntry({
packageName: "@openclaw/default-env-demo",
channelId: "default-env-demo",
label: "Default Env Demo",
blurb: "Default env demo entry",
}),
);
return {
env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir,
},
expectedId: "default-env-demo",
};
},
},
] as const)("$name", ({ setup }) => {
const { env, expectedId } = setup();
expectCatalogIdsContain({ env, expectedId });
});
});
@@ -393,6 +447,42 @@ function makeDemoConfigWritesCfg(accountIdKey: string) {
}
describe("channel plugin loader", () => {
async function expectLoadedPluginCase(params: {
registry: Parameters<typeof setActivePluginRegistry>[0];
expectedPlugin: ChannelPlugin;
}) {
setActivePluginRegistry(params.registry);
expect(await loadChannelPlugin("demo-loader")).toBe(params.expectedPlugin);
}
async function expectLoadedOutboundCase(params: {
registry: Parameters<typeof setActivePluginRegistry>[0];
expectedOutbound: ChannelOutboundAdapter | undefined;
}) {
setActivePluginRegistry(params.registry);
expect(await loadChannelOutboundAdapter("demo-loader")).toBe(params.expectedOutbound);
}
async function expectReloadedLoaderCase(params: {
load: typeof loadChannelPlugin | typeof loadChannelOutboundAdapter;
firstRegistry: Parameters<typeof setActivePluginRegistry>[0];
secondRegistry: Parameters<typeof setActivePluginRegistry>[0];
firstExpected: ChannelPlugin | ChannelOutboundAdapter | undefined;
secondExpected: ChannelPlugin | ChannelOutboundAdapter | undefined;
}) {
setActivePluginRegistry(params.firstRegistry);
expect(await params.load("demo-loader")).toBe(params.firstExpected);
setActivePluginRegistry(params.secondRegistry);
expect(await params.load("demo-loader")).toBe(params.secondExpected);
}
async function expectOutboundAdapterMissingCase(
registry: Parameters<typeof setActivePluginRegistry>[0],
) {
setActivePluginRegistry(registry);
expect(await loadChannelOutboundAdapter("demo-loader")).toBeUndefined();
}
beforeEach(() => {
setActivePluginRegistry(emptyRegistry);
});
@@ -403,61 +493,124 @@ describe("channel plugin loader", () => {
clearPluginManifestRegistryCache();
});
it("loads channel plugins from the active registry", async () => {
setActivePluginRegistry(registryWithDemoLoader);
const plugin = await loadChannelPlugin("demo-loader");
expect(plugin).toBe(demoLoaderPlugin);
});
it("loads outbound adapters from registered plugins", async () => {
setActivePluginRegistry(registryWithDemoLoader);
const outbound = await loadChannelOutboundAdapter("demo-loader");
expect(outbound).toBe(demoOutbound);
});
it("refreshes cached plugin values when registry changes", async () => {
setActivePluginRegistry(registryWithDemoLoader);
expect(await loadChannelPlugin("demo-loader")).toBe(demoLoaderPlugin);
setActivePluginRegistry(registryWithDemoLoaderV2);
expect(await loadChannelPlugin("demo-loader")).toBe(demoLoaderPluginV2);
});
it("refreshes cached outbound values when registry changes", async () => {
setActivePluginRegistry(registryWithDemoLoader);
expect(await loadChannelOutboundAdapter("demo-loader")).toBe(demoOutbound);
setActivePluginRegistry(registryWithDemoLoaderV2);
expect(await loadChannelOutboundAdapter("demo-loader")).toBe(demoOutboundV2);
});
it("returns undefined when plugin has no outbound adapter", async () => {
setActivePluginRegistry(registryWithDemoLoaderNoOutbound);
expect(await loadChannelOutboundAdapter("demo-loader")).toBeUndefined();
it.each([
{
name: "loads channel plugins from the active registry",
kind: "plugin" as const,
registry: registryWithDemoLoader,
expectedPlugin: demoLoaderPlugin,
},
{
name: "loads outbound adapters from registered plugins",
kind: "outbound" as const,
registry: registryWithDemoLoader,
expectedOutbound: demoOutbound,
},
{
name: "refreshes cached plugin values when registry changes",
kind: "reload-plugin" as const,
firstRegistry: registryWithDemoLoader,
secondRegistry: registryWithDemoLoaderV2,
firstExpected: demoLoaderPlugin,
secondExpected: demoLoaderPluginV2,
},
{
name: "refreshes cached outbound values when registry changes",
kind: "reload-outbound" as const,
firstRegistry: registryWithDemoLoader,
secondRegistry: registryWithDemoLoaderV2,
firstExpected: demoOutbound,
secondExpected: demoOutboundV2,
},
{
name: "returns undefined when plugin has no outbound adapter",
kind: "missing-outbound" as const,
registry: registryWithDemoLoaderNoOutbound,
},
] as const)("$name", async (testCase) => {
switch (testCase.kind) {
case "plugin":
await expectLoadedPluginCase({
registry: testCase.registry,
expectedPlugin: testCase.expectedPlugin,
});
return;
case "outbound":
await expectLoadedOutboundCase({
registry: testCase.registry,
expectedOutbound: testCase.expectedOutbound,
});
return;
case "reload-plugin":
await expectReloadedLoaderCase({
load: loadChannelPlugin,
firstRegistry: testCase.firstRegistry,
secondRegistry: testCase.secondRegistry,
firstExpected: testCase.firstExpected,
secondExpected: testCase.secondExpected,
});
return;
case "reload-outbound":
await expectReloadedLoaderCase({
load: loadChannelOutboundAdapter,
firstRegistry: testCase.firstRegistry,
secondRegistry: testCase.secondRegistry,
firstExpected: testCase.firstExpected,
secondExpected: testCase.secondExpected,
});
return;
case "missing-outbound":
await expectOutboundAdapterMissingCase(testCase.registry);
return;
}
});
});
describe("resolveChannelConfigWrites", () => {
it("defaults to allow when unset", () => {
const cfg = {};
expect(resolveChannelConfigWrites({ cfg, channelId: demoOriginChannelId })).toBe(true);
});
it("blocks when channel config disables writes", () => {
const cfg = { channels: { [demoOriginChannelId]: { configWrites: false } } };
expect(resolveChannelConfigWrites({ cfg, channelId: demoOriginChannelId })).toBe(false);
});
it("account override wins over channel default", () => {
const cfg = makeDemoConfigWritesCfg("work");
function expectResolvedChannelConfigWrites(params: {
cfg: Record<string, unknown>;
channelId: string;
accountId?: string;
expected: boolean;
}) {
expect(
resolveChannelConfigWrites({ cfg, channelId: demoOriginChannelId, accountId: "work" }),
).toBe(false);
});
resolveChannelConfigWrites({
cfg: params.cfg,
channelId: params.channelId,
...(params.accountId ? { accountId: params.accountId } : {}),
}),
).toBe(params.expected);
}
it("matches account ids case-insensitively", () => {
const cfg = makeDemoConfigWritesCfg("Work");
expect(
resolveChannelConfigWrites({ cfg, channelId: demoOriginChannelId, accountId: "work" }),
).toBe(false);
it.each([
{
name: "defaults to allow when unset",
cfg: {},
channelId: demoOriginChannelId,
expected: true,
},
{
name: "blocks when channel config disables writes",
cfg: { channels: { [demoOriginChannelId]: { configWrites: false } } },
channelId: demoOriginChannelId,
expected: false,
},
{
name: "account override wins over channel default",
cfg: makeDemoConfigWritesCfg("work"),
channelId: demoOriginChannelId,
accountId: "work",
expected: false,
},
{
name: "matches account ids case-insensitively",
cfg: makeDemoConfigWritesCfg("Work"),
channelId: demoOriginChannelId,
accountId: "work",
expected: false,
},
] as const)("$name", (testCase) => {
expectResolvedChannelConfigWrites(testCase);
});
});
@@ -489,27 +642,56 @@ describe("authorizeConfigWrite", () => {
});
}
it("blocks when a target account disables writes", () => {
expectConfigWriteBlocked({
function expectAuthorizedConfigWriteCase(
input: Parameters<typeof authorizeConfigWrite>[0],
expected: ReturnType<typeof authorizeConfigWrite>,
) {
expect(authorizeConfigWrite(input)).toEqual(expected);
}
function expectResolvedConfigWriteTargetCase(pathSegments: readonly string[], expected: unknown) {
expect(resolveConfigWriteTargetFromPath([...pathSegments])).toEqual(expected);
}
function expectExplicitConfigWriteTargetCase(
input: Parameters<typeof resolveExplicitConfigWriteTarget>[0],
expected: ReturnType<typeof resolveExplicitConfigWriteTarget>,
) {
expect(resolveExplicitConfigWriteTarget(input)).toEqual(expected);
}
function expectFormattedDeniedMessage(
result: Exclude<ReturnType<typeof authorizeConfigWrite>, { allowed: true }>,
) {
expect(
formatConfigWriteDeniedMessage({
result,
}),
).toContain(`channels.${demoTargetChannelId}.accounts.work.configWrites=true`);
}
it.each([
{
name: "blocks when a target account disables writes",
disabledAccountId: "work",
reason: "target-disabled",
blockedScope: "target",
});
});
it("blocks when the origin account disables writes", () => {
expectConfigWriteBlocked({
},
{
name: "blocks when the origin account disables writes",
disabledAccountId: "default",
reason: "origin-disabled",
blockedScope: "origin",
});
},
] as const)("$name", (testCase) => {
expectConfigWriteBlocked(testCase);
});
it("allows bypass for internal operator.admin writes", () => {
const cfg = makeDemoConfigWritesCfg("work");
expect(
authorizeConfigWrite({
cfg,
it.each([
{
name: "allows bypass for internal operator.admin writes",
input: {
cfg: makeDemoConfigWritesCfg("work"),
origin: { channelId: demoOriginChannelId, accountId: "default" },
target: resolveExplicitConfigWriteTarget({
channelId: demoTargetChannelId,
@@ -519,57 +701,71 @@ describe("authorizeConfigWrite", () => {
channel: INTERNAL_MESSAGE_CHANNEL,
gatewayClientScopes: ["operator.admin"],
}),
}),
).toEqual({ allowed: true });
});
it("treats non-channel config paths as global writes", () => {
const cfg = makeDemoConfigWritesCfg("work");
expect(
authorizeConfigWrite({
cfg,
},
expected: { allowed: true },
},
{
name: "treats non-channel config paths as global writes",
input: {
cfg: makeDemoConfigWritesCfg("work"),
origin: { channelId: demoOriginChannelId, accountId: "default" },
target: resolveConfigWriteTargetFromPath(["messages", "ackReaction"]),
}),
).toEqual({ allowed: true });
},
expected: { allowed: true },
},
] as const)("$name", ({ input, expected }) => {
expectAuthorizedConfigWriteCase(input, expected);
});
it("rejects ambiguous channel collection writes", () => {
expect(resolveConfigWriteTargetFromPath(["channels", "demo-channel"])).toEqual({
kind: "ambiguous",
scopes: [{ channelId: "demo-channel" }],
});
expect(resolveConfigWriteTargetFromPath(["channels", "demo-channel", "accounts"])).toEqual({
kind: "ambiguous",
scopes: [{ channelId: "demo-channel" }],
});
it.each([
{
name: "rejects bare channel collection writes",
pathSegments: ["channels", "demo-channel"],
expected: { kind: "ambiguous", scopes: [{ channelId: "demo-channel" }] },
},
{
name: "rejects account collection writes",
pathSegments: ["channels", "demo-channel", "accounts"],
expected: { kind: "ambiguous", scopes: [{ channelId: "demo-channel" }] },
},
] as const)("$name", ({ pathSegments, expected }) => {
expectResolvedConfigWriteTargetCase(pathSegments, expected);
});
it("resolves explicit channel and account targets", () => {
expect(resolveExplicitConfigWriteTarget({ channelId: demoOriginChannelId })).toEqual({
kind: "channel",
scope: { channelId: demoOriginChannelId },
});
expect(
resolveExplicitConfigWriteTarget({ channelId: demoTargetChannelId, accountId: "work" }),
).toEqual({
kind: "account",
scope: { channelId: demoTargetChannelId, accountId: "work" },
});
it.each([
{
name: "resolves explicit channel target",
input: { channelId: demoOriginChannelId },
expected: {
kind: "channel",
scope: { channelId: demoOriginChannelId },
},
},
{
name: "resolves explicit account target",
input: { channelId: demoTargetChannelId, accountId: "work" },
expected: {
kind: "account",
scope: { channelId: demoTargetChannelId, accountId: "work" },
},
},
] as const)("$name", ({ input, expected }) => {
expectExplicitConfigWriteTargetCase(input, expected);
});
it("formats denied messages consistently", () => {
expect(
formatConfigWriteDeniedMessage({
result: {
allowed: false,
reason: "target-disabled",
blockedScope: {
kind: "target",
scope: { channelId: demoTargetChannelId, accountId: "work" },
},
it.each([
{
name: "formats denied messages consistently",
result: {
allowed: false,
reason: "target-disabled",
blockedScope: {
kind: "target",
scope: { channelId: demoTargetChannelId, accountId: "work" },
},
}),
).toContain(`channels.${demoTargetChannelId}.accounts.work.configWrites=true`);
} as const,
},
] as const)("$name", ({ result }) => {
expectFormattedDeniedMessage(result);
});
});

View File

@@ -4,9 +4,18 @@ import { sessionBindingContractChannelIds } from "./manifest.js";
const discordSessionBindingAdapterChannels = ["discord"] as const;
describe("channel contract registry", () => {
it("keeps core session binding coverage aligned with built-in adapters", () => {
function expectSessionBindingCoverage(expectedChannelIds: readonly string[]) {
expect([...sessionBindingContractChannelIds]).toEqual(
expect.arrayContaining([...discordSessionBindingAdapterChannels, "telegram"]),
expect.arrayContaining([...expectedChannelIds]),
);
}
it.each([
{
name: "keeps core session binding coverage aligned with built-in adapters",
expectedChannelIds: [...discordSessionBindingAdapterChannels, "telegram"],
},
] as const)("$name", ({ expectedChannelIds }) => {
expectSessionBindingCoverage(expectedChannelIds);
});
});