From a043c01baacd388911b052888c1f5d922a3ef9fd Mon Sep 17 00:00:00 2001 From: pax-k Date: Mon, 27 Jul 2026 17:43:50 +0300 Subject: [PATCH] fix(desktop): allow authorized agent mentions Signed-off-by: pax-k --- .../lib/agentAutocompleteEligibility.test.mjs | 34 +++++++++ .../lib/agentAutocompleteEligibility.ts | 8 +++ .../src/features/messages/lib/useMentions.ts | 5 +- desktop/tests/e2e/mentions.spec.ts | 71 ++++++++++++++++++- 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 4e02b7bd681..0f4d30b4beb 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -5,6 +5,7 @@ import { coalesceAgentAutocompleteCandidates, getMentionableAgentPubkeys, getSharedChannelIds, + isAgentMentionable, isAgentIdentityInManagedList, relayAgentIsSharedWithUser, shouldHideAgentFromMentions, @@ -162,6 +163,39 @@ test("isAgentIdentityInManagedList: keeps people and only current managed agent ); }); +test("isAgentMentionable: keeps people, managed agents, and invocable relay agents", () => { + const mentionableAgentPubkeys = new Set([PUB_A, PUB_B]); + + assert.equal( + isAgentMentionable( + { isAgent: false, pubkey: PUB_C }, + mentionableAgentPubkeys, + ), + true, + ); + assert.equal( + isAgentMentionable( + { isAgent: true, pubkey: PUB_A.toUpperCase() }, + mentionableAgentPubkeys, + ), + true, + ); + assert.equal( + isAgentMentionable( + { isAgent: true, pubkey: PUB_B.toUpperCase() }, + mentionableAgentPubkeys, + ), + true, + ); + assert.equal( + isAgentMentionable( + { isAgent: true, pubkey: PUB_C }, + mentionableAgentPubkeys, + ), + false, + ); +}); + test("shouldHideAgentFromMentions: never hides non-agents", () => { assert.equal( shouldHideAgentFromMentions({ diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts index e4afe7fea4a..f71bae69413 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -64,6 +64,14 @@ export function isAgentIdentityInManagedList( ); } +export function isAgentMentionable( + candidate: { isAgent?: boolean; pubkey: string }, + mentionableAgentPubkeys: ReadonlySet, +) { + const pubkey = normalizePubkey(candidate.pubkey); + return candidate.isAgent !== true || mentionableAgentPubkeys.has(pubkey); +} + export function shouldHideAgentFromMentions({ isAgent, isMember, diff --git a/desktop/src/features/messages/lib/useMentions.ts b/desktop/src/features/messages/lib/useMentions.ts index 0c73b753390..4ea5678a44b 100644 --- a/desktop/src/features/messages/lib/useMentions.ts +++ b/desktop/src/features/messages/lib/useMentions.ts @@ -16,7 +16,7 @@ import { coalesceAutocompleteCandidatesByKey, getMentionableAgentPubkeys, getSharedChannelIds, - isAgentIdentityInManagedList, + isAgentMentionable, shouldHideAgentFromMentions, } from "@/features/agents/lib/agentAutocompleteEligibility"; import { @@ -246,7 +246,7 @@ export function useMentions( if (isArchivedDiscovery(pubkey)) { return; } - if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) { + if (!isAgentMentionable(candidate, mentionableAgentPubkeys)) { return; } if ( @@ -420,7 +420,6 @@ export function useMentions( managedAgentNamesByPubkey, managedAgentPersonaIds, managedAgentPersonaIdsByPubkey, - managedAgentPubkeys, managedAgentsQuery.data, memberPubkeys, members, diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index 694b5abef50..0e08cb17471 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -826,7 +826,7 @@ test("managed relay agents are visible in channel mentions regardless of relay p await expect(dropdown.getByText("agent")).toBeVisible(); }); -test("relay-only agents stay hidden from channel mentions even when allowlisted", async ({ +test("allowlisted relay-only agents can be mentioned and receive a p tag", async ({ page, }) => { await installMockBridge(page, { @@ -836,17 +836,84 @@ test("relay-only agents stay hidden from channel mentions even when allowlisted" name: "quinn", respondTo: "allowlist", respondToAllowlist: [MOCK_VIEWER_PUBKEY], + channelNames: ["general"], }, ], }); await page.goto("/"); + + const channelId = await page + .getByTestId("channel-general") + .getAttribute("data-channel-id"); + if (!channelId) { + throw new Error("General channel id missing."); + } + await page.evaluate( + async ({ agentPubkey, generalChannelId }) => { + const bridge = window as Window & { + __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( + command: string, + payload?: Record, + ) => Promise; + __BUZZ_E2E_QUERY_CLIENT__?: { + invalidateQueries: () => Promise; + }; + }; + const invoke = bridge.__BUZZ_E2E_INVOKE_MOCK_COMMAND__; + if (!invoke) { + throw new Error("Mock bridge is not installed."); + } + await invoke("add_channel_members", { + channelId: generalChannelId, + pubkeys: [agentPubkey], + role: "bot", + }); + await bridge.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries(); + }, + { + agentPubkey: ALLOWLIST_RELAY_AGENT_PUBKEY, + generalChannelId: channelId, + }, + ); + await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); + const signedEventCount = await page.evaluate( + () => window.__BUZZ_E2E_SIGNED_EVENTS__?.length ?? 0, + ); const input = page.getByTestId("message-input"); await input.fill("@quinn"); - await expect(autocomplete(page)).toHaveCount(0); + const dropdown = autocomplete(page); + await expect(dropdown.getByText("quinn")).toBeVisible(); + await input.press("Enter"); + await page.keyboard.type(" can you help?"); + await page.getByTestId("send-message").click(); + + await expect + .poll(() => + page.evaluate( + ({ agentPubkey, baselineCount }) => { + const event = window.__BUZZ_E2E_SIGNED_EVENTS__ + ?.slice(baselineCount) + .find( + (candidate) => + candidate.kind === 9 && + candidate.tags.some( + ([tagName, pubkey]) => + tagName === "p" && pubkey === agentPubkey, + ), + ); + return event?.tags ?? []; + }, + { + agentPubkey: ALLOWLIST_RELAY_AGENT_PUBKEY, + baselineCount: signedEventCount, + }, + ), + ) + .toContainEqual(["p", ALLOWLIST_RELAY_AGENT_PUBKEY]); }); test("mentioning an in-channel stopped managed agent starts it before sending", async ({