Agents: preserve role-based routing in bind key matching

This commit is contained in:
Gustavo Madeira Santana
2026-02-25 18:23:23 -05:00
parent 9fc8f8068d
commit c6a3fbe1aa
3 changed files with 135 additions and 0 deletions

View File

@@ -95,4 +95,47 @@ describe("agents bind/unbind commands", () => {
expect(runtime.error).toHaveBeenCalledWith("Bindings are owned by another agent:");
expect(runtime.exit).toHaveBeenCalledWith(1);
});
it("keeps role-based bindings when removing channel-level discord binding", async () => {
readConfigFileSnapshotMock.mockResolvedValue({
...baseConfigSnapshot,
config: {
bindings: [
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
roles: ["111", "222"],
},
},
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
},
},
],
},
});
await agentsUnbindCommand({ bind: ["discord:guild-a"] }, runtime);
expect(writeConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
bindings: [
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
roles: ["111", "222"],
},
},
],
}),
);
expect(runtime.exit).not.toHaveBeenCalled();
});
});

View File

@@ -8,6 +8,16 @@ import type { ChannelChoice } from "./onboard-types.js";
function bindingMatchKey(match: AgentBinding["match"]) {
const accountId = match.accountId?.trim() || DEFAULT_ACCOUNT_ID;
const roles = Array.isArray(match.roles)
? Array.from(
new Set(
match.roles
.map((role) => role.trim())
.filter(Boolean)
.toSorted(),
),
)
: [];
return [
match.channel,
accountId,
@@ -15,6 +25,7 @@ function bindingMatchKey(match: AgentBinding["match"]) {
match.peer?.id ?? "",
match.guildId ?? "",
match.teamId ?? "",
roles.join(","),
].join("|");
}

View File

@@ -8,6 +8,7 @@ import {
applyAgentConfig,
buildAgentSummaries,
pruneAgentConfig,
removeAgentBindings,
} from "./agents.js";
describe("agents helpers", () => {
@@ -111,6 +112,86 @@ describe("agents helpers", () => {
expect(result.config.bindings).toHaveLength(2);
});
it("applyAgentBindings treats role-based bindings as distinct routes", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
guildId: "123",
roles: ["111", "222"],
},
},
],
};
const result = applyAgentBindings(cfg, [
{
agentId: "work",
match: {
channel: "discord",
accountId: "guild-a",
guildId: "123",
},
},
]);
expect(result.added).toHaveLength(1);
expect(result.conflicts).toHaveLength(0);
expect(result.config.bindings).toHaveLength(2);
});
it("removeAgentBindings does not remove role-based bindings when removing channel-level routes", () => {
const cfg: OpenClawConfig = {
bindings: [
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
guildId: "123",
roles: ["111", "222"],
},
},
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
guildId: "123",
},
},
],
};
const result = removeAgentBindings(cfg, [
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
guildId: "123",
},
},
]);
expect(result.removed).toHaveLength(1);
expect(result.conflicts).toHaveLength(0);
expect(result.config.bindings).toEqual([
{
agentId: "main",
match: {
channel: "discord",
accountId: "guild-a",
guildId: "123",
roles: ["111", "222"],
},
},
]);
});
it("pruneAgentConfig removes agent, bindings, and allowlist entries", () => {
const cfg: OpenClawConfig = {
agents: {