import { beforeAll, describe, expect, it, vi } from "vitest" ;
const createChannelPairingController = vi.hoisted(() => vi.fn());
const evaluateGroupRouteAccessForPolicy = vi.hoisted(() => vi.fn());
const isDangerousNameMatchingEnabled = vi.hoisted(() => vi.fn());
const resolveAllowlistProviderRuntimeGroupPolicy = vi.hoisted(() => vi.fn());
const resolveDefaultGroupPolicy = vi.hoisted(() => vi.fn());
const resolveDmGroupAccessWithLists = vi.hoisted(() => vi.fn());
const resolveInboundMentionDecision = vi.hoisted(() => vi.fn());
const resolveSenderScopedGroupPolicy = vi.hoisted(() => vi.fn());
const warnMissingProviderGroupPolicyFallbackOnce = vi.hoisted(() => vi.fn());
const sendGoogleChatMessage = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/channel-inbound" , () => ({
resolveInboundMentionDecision,
}));
vi.mock("../runtime-api.js" , () => ({
GROUP_POLICY_BLOCKED_LABEL: { space: "space" },
createChannelPairingController,
evaluateGroupRouteAccessForPolicy,
isDangerousNameMatchingEnabled,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
resolveDmGroupAccessWithLists,
resolveSenderScopedGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
}));
vi.mock("./api.js" , () => ({
sendGoogleChatMessage,
}));
function createCore() {
return {
channel: {
commands: {
shouldComputeCommandAuthorized: vi.fn(() => false ),
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false ),
shouldHandleTextCommands: vi.fn(() => false ),
isControlCommandMessage: vi.fn(() => false ),
},
text: {
hasControlCommand: vi.fn(() => false ),
},
},
};
}
function primeCommonDefaults() {
isDangerousNameMatchingEnabled.mockReturnValue(false );
resolveDefaultGroupPolicy.mockReturnValue("allowlist" );
resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
groupPolicy: "allowlist" ,
providerMissingFallbackApplied: false ,
});
resolveSenderScopedGroupPolicy.mockImplementation(({ groupPolicy }) => groupPolicy);
evaluateGroupRouteAccessForPolicy.mockReturnValue({
allowed: true ,
});
warnMissingProviderGroupPolicyFallbackOnce.mockReturnValue(undefined);
}
const baseAccessConfig = {
channels: { googlechat: {} },
commands: { useAccessGroups: true },
} as const ;
const defaultSender = {
senderId: "users/alice" ,
senderName: "Alice" ,
senderEmail: "alice@example.com" ,
} as const ;
let applyGoogleChatInboundAccessPolicy: typeof import ("./monitor-access.js" ).applyGoogleChatInboundAccessPolicy;
function allowInboundGroupTraffic(options?: {
effectiveGroupAllowFrom?: string[];
effectiveWasMentioned?: boolean ;
}) {
createChannelPairingController.mockReturnValue({
readAllowFromStore: vi.fn(async () => []),
issueChallenge: vi.fn(),
});
resolveDmGroupAccessWithLists.mockReturnValue({
decision: "allow" ,
effectiveAllowFrom: [],
effectiveGroupAllowFrom: options?.effectiveGroupAllowFrom ?? ["users/alice" ],
});
resolveInboundMentionDecision.mockReturnValue({
shouldSkip: false ,
effectiveWasMentioned: options?.effectiveWasMentioned ?? true ,
});
}
async function applyInboundAccessPolicy(
overrides: Partial<Parameters<typeof applyGoogleChatInboundAccessPolicy>[0 ]>,
) {
return applyGoogleChatInboundAccessPolicy({
account: {
accountId: "default" ,
config: {},
} as never,
config: baseAccessConfig as never,
core: createCore() as never,
space: { name: "spaces/AAA" , displayName: "Team Room" } as never,
message: { annotations: [] } as never,
isGroup: true ,
rawBody: "hello team" ,
logVerbose: vi.fn(),
...defaultSender,
...overrides,
} as never);
}
describe("googlechat inbound access policy" , () => {
beforeAll(async () => {
({ applyGoogleChatInboundAccessPolicy } = await import ("./monitor-access.js" ));
});
it("issues a pairing challenge for unauthorized DMs in pairing mode" , async () => {
primeCommonDefaults();
const issueChallenge = vi.fn(async ({ onCreated, sendPairingReply }) => {
onCreated?.();
await sendPairingReply("pairing text" );
});
createChannelPairingController.mockReturnValue({
readAllowFromStore: vi.fn(async () => []),
issueChallenge,
});
resolveDmGroupAccessWithLists.mockReturnValue({
decision: "pairing" ,
reason: "pairing_required" ,
effectiveAllowFrom: [],
effectiveGroupAllowFrom: [],
});
sendGoogleChatMessage.mockResolvedValue({ ok: true });
const statusSink = vi.fn();
const logVerbose = vi.fn();
await expect(
applyGoogleChatInboundAccessPolicy({
account: {
accountId: "default" ,
config: {
dm: { policy: "pairing" },
},
} as never,
config: {
channels: { googlechat: {} },
} as never,
core: createCore() as never,
space: { name: "spaces/AAA" , displayName: "DM" } as never,
message: { annotations: [] } as never,
isGroup: false ,
senderId: "users/abc" ,
senderName: "Alice" ,
senderEmail: "alice@example.com" ,
rawBody: "hello" ,
statusSink,
logVerbose,
}),
).resolves.toEqual({ ok: false });
expect(issueChallenge).toHaveBeenCalledTimes(1 );
expect(sendGoogleChatMessage).toHaveBeenCalledWith({
account: expect.anything(),
space: "spaces/AAA" ,
text: "pairing text" ,
});
expect(statusSink).toHaveBeenCalledWith(
expect.objectContaining({
lastOutboundAt: expect.any(Number),
}),
);
});
it("allows group traffic when sender and mention gates pass" , async () => {
primeCommonDefaults();
allowInboundGroupTraffic();
const core = createCore();
core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true );
core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(true );
await expect(
applyInboundAccessPolicy({
account: {
accountId: "default" ,
config: {
botUser: "users/app-bot" ,
groups: {
"spaces/AAA" : {
users: ["users/alice" ],
requireMention: true ,
systemPrompt: " group prompt " ,
},
},
},
} as never,
core: core as never,
message: {
annotations: [
{
type: "USER_MENTION" ,
userMention: { user: { name: "users/app-bot" } },
},
],
} as never,
}),
).resolves.toEqual({
ok: true ,
commandAuthorized: true ,
effectiveWasMentioned: true ,
groupSystemPrompt: "group prompt" ,
});
});
it("preserves allowlist group policy when a routed space has no sender allowlist" , async () => {
primeCommonDefaults();
allowInboundGroupTraffic({
effectiveGroupAllowFrom: [],
effectiveWasMentioned: false ,
});
resolveSenderScopedGroupPolicy.mockReturnValue("open" );
resolveSenderScopedGroupPolicy.mockClear();
resolveDmGroupAccessWithLists.mockClear();
await expect(
applyInboundAccessPolicy({
account: {
accountId: "default" ,
config: {
groups: {
"spaces/AAA" : {
enabled: true ,
},
},
},
} as never,
}),
).resolves.toEqual({
ok: true ,
commandAuthorized: undefined,
effectiveWasMentioned: false ,
groupSystemPrompt: undefined,
});
expect(resolveSenderScopedGroupPolicy).not.toHaveBeenCalled();
expect(resolveDmGroupAccessWithLists).toHaveBeenCalledWith(
expect.objectContaining({
groupPolicy: "allowlist" ,
groupAllowFrom: [],
}),
);
});
it("drops unauthorized group control commands" , async () => {
primeCommonDefaults();
allowInboundGroupTraffic({
effectiveGroupAllowFrom: [],
effectiveWasMentioned: false ,
});
const core = createCore();
core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true );
core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(false );
core.channel.commands.isControlCommandMessage.mockReturnValue(true );
const logVerbose = vi.fn();
await expect(
applyInboundAccessPolicy({
core: core as never,
rawBody: "/admin" ,
logVerbose,
}),
).resolves.toEqual({ ok: false });
expect(logVerbose).toHaveBeenCalledWith("googlechat: drop control command from users/alice" );
});
it("does not match group policy by mutable space displayName when the stable id differs" , async () => {
primeCommonDefaults();
allowInboundGroupTraffic();
const logVerbose = vi.fn();
await expect(
applyInboundAccessPolicy({
account: {
accountId: "default" ,
config: {
groups: {
"Finance Ops" : {
users: ["users/alice" ],
requireMention: true ,
systemPrompt: "finance-only prompt" ,
},
},
},
} as never,
core: createCore() as never,
space: { name: "spaces/BBB" , displayName: "Finance Ops" } as never,
message: {
annotations: [
{
type: "USER_MENTION" ,
userMention: { user: { name: "users/app" } },
},
],
} as never,
rawBody: "show quarter close status" ,
logVerbose,
}),
).resolves.toEqual({ ok: false });
expect(logVerbose).toHaveBeenCalledWith(
"Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: Finance Ops" ,
);
expect(logVerbose).toHaveBeenCalledWith(
"drop group message (deprecated mutable group key matched, space=spaces/BBB)" ,
);
});
it("fails closed instead of falling back to wildcard when a deprecated room key matches" , async () => {
primeCommonDefaults();
resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
groupPolicy: "open" ,
providerMissingFallbackApplied: false ,
});
allowInboundGroupTraffic();
const logVerbose = vi.fn();
await expect(
applyInboundAccessPolicy({
account: {
accountId: "default" ,
config: {
groupPolicy: "open" ,
groups: {
"*" : {
users: ["users/alice" ],
},
"Finance Ops" : {
enabled: false ,
users: ["users/bob" ],
},
},
},
} as never,
core: createCore() as never,
space: { name: "spaces/BBB" , displayName: "Finance Ops" } as never,
rawBody: "show quarter close status" ,
logVerbose,
}),
).resolves.toEqual({ ok: false });
expect(logVerbose).toHaveBeenCalledWith(
"drop group message (deprecated mutable group key matched, space=spaces/BBB)" ,
);
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-07)
¤
*© Formatika GbR, Deutschland