fix(routing): exclude peer-specific bindings from guild-wide matching (#15274)

* fix(routing): exclude peer-specific bindings from guild-wide matching (#14752)

* fix(routing): enforce binding scope AND semantics + regressions

* fix(routing): document strict binding-scope behavior (#15274) (thanks @lailoo)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
大猫子
2026-02-14 10:05:09 +08:00
committed by GitHub
parent 1b95220a99
commit dbe026214f
6 changed files with 326 additions and 38 deletions

View File

@@ -169,6 +169,126 @@ describe("resolveAgentRoute", () => {
expect(route.matchedBy).toBe("binding.guild");
});
test("peer+guild binding does not act as guild-wide fallback when peer mismatches (#14752)", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "olga",
match: {
channel: "discord",
peer: { kind: "channel", id: "CHANNEL_A" },
guildId: "GUILD_1",
},
},
{
agentId: "main",
match: {
channel: "discord",
guildId: "GUILD_1",
},
},
],
};
const route = resolveAgentRoute({
cfg,
channel: "discord",
peer: { kind: "channel", id: "CHANNEL_B" },
guildId: "GUILD_1",
});
expect(route.agentId).toBe("main");
expect(route.matchedBy).toBe("binding.guild");
});
test("peer+guild binding requires guild match even when peer matches", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "wrongguild",
match: {
channel: "discord",
peer: { kind: "channel", id: "c1" },
guildId: "g1",
},
},
{
agentId: "rightguild",
match: {
channel: "discord",
guildId: "g2",
},
},
],
};
const route = resolveAgentRoute({
cfg,
channel: "discord",
peer: { kind: "channel", id: "c1" },
guildId: "g2",
});
expect(route.agentId).toBe("rightguild");
expect(route.matchedBy).toBe("binding.guild");
});
test("peer+team binding does not act as team-wide fallback when peer mismatches", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "roomonly",
match: {
channel: "slack",
peer: { kind: "channel", id: "C_A" },
teamId: "T1",
},
},
{
agentId: "teamwide",
match: {
channel: "slack",
teamId: "T1",
},
},
],
};
const route = resolveAgentRoute({
cfg,
channel: "slack",
teamId: "T1",
peer: { kind: "channel", id: "C_B" },
});
expect(route.agentId).toBe("teamwide");
expect(route.matchedBy).toBe("binding.team");
});
test("peer+team binding requires team match even when peer matches", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "wrongteam",
match: {
channel: "slack",
peer: { kind: "channel", id: "C1" },
teamId: "T1",
},
},
{
agentId: "rightteam",
match: {
channel: "slack",
teamId: "T2",
},
},
],
};
const route = resolveAgentRoute({
cfg,
channel: "slack",
teamId: "T2",
peer: { kind: "channel", id: "C1" },
});
expect(route.agentId).toBe("rightteam");
expect(route.matchedBy).toBe("binding.team");
});
test("missing accountId in binding matches default account only", () => {
const cfg: OpenClawConfig = {
bindings: [{ agentId: "defaultAcct", match: { channel: "whatsapp" } }],
@@ -592,4 +712,37 @@ describe("role-based agent routing", () => {
expect(route.agentId).toBe("main");
expect(route.matchedBy).toBe("default");
});
test("peer+guild+roles binding does not act as guild+roles fallback when peer mismatches", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "peer-roles",
match: {
channel: "discord",
peer: { kind: "channel", id: "c-target" },
guildId: "g1",
roles: ["r1"],
},
},
{
agentId: "guild-roles",
match: {
channel: "discord",
guildId: "g1",
roles: ["r1"],
},
},
],
};
const route = resolveAgentRoute({
cfg,
channel: "discord",
guildId: "g1",
memberRoleIds: ["r1"],
peer: { kind: "channel", id: "c-other" },
});
expect(route.agentId).toBe("guild-roles");
expect(route.matchedBy).toBe("binding.guild+roles");
});
});