From 462d2190a51dd2458add86ed275514b0882ea075 Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Sun, 14 Jun 2026 13:23:31 -0700 Subject: [PATCH] fix(desktop): detect owned agents via kind:0 OA-owner signal in profile panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owned agents rendered as humans in the profile panel's archive flow: the Archive button + confirm modal showed the human variant even for an agent the viewer owns (repro: tho's agent Edna). Root cause: two gates disagreed. The archive button's canArchive gate resolves correctly via OA-ownership, but the human-vs-agent framing used a separate signal — isBot = Boolean(relayAgent || managedAgent) — that checks the relay-agents registry + the local managed-agents list. An owned agent deployed elsewhere can miss BOTH lists, so isBot was false and the panel rendered the human framing while the button still showed. Fix: OR in the kind:0-derived agent flag (isAgent on the users-batch summary, which the backend sets from profile_has_valid_oa_owner — a verified NIP-OA auth tag on the target's kind:0). That's the same authoritative signal the archive gate's resolveOaOwner trusts, so isBot can no longer drift from the gate. Client-only change, no relay/registry change. - UserProfilePanel: query useUsersBatchQuery([pubkey]) and OR its isAgent into isBot (keyed by lowercased pubkey, matching the house pattern). - BotIdenticon: forward an optional data-testid to its wrapper. - UserProfilePanelSections: tag the profile bot indicator with data-testid=profile-bot-indicator for the regression test. - profile.spec.ts: regression test — an owned agent seeded with the kind:0 agent flag but absent from relay/managed lists now renders agent framing. Pre-existing, unrelated: profile.spec.ts 'updates the relay-backed profile from settings' fails on clean origin/main too (avatar-url assertion) — not touched by this change. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../src/features/messages/ui/BotIdenticon.tsx | 3 ++ .../features/profile/ui/UserProfilePanel.tsx | 12 ++++- .../profile/ui/UserProfilePanelSections.tsx | 1 + desktop/tests/e2e/profile.spec.ts | 54 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/desktop/src/features/messages/ui/BotIdenticon.tsx b/desktop/src/features/messages/ui/BotIdenticon.tsx index 90309cccb..743110f65 100644 --- a/desktop/src/features/messages/ui/BotIdenticon.tsx +++ b/desktop/src/features/messages/ui/BotIdenticon.tsx @@ -7,6 +7,7 @@ type BotIdenticonProps = { /** Size in pixels (default 20) */ size?: number; className?: string; + "data-testid"?: string; }; /** @@ -17,6 +18,7 @@ export const BotIdenticon = React.memo(function BotIdenticon({ value, size = 20, className, + "data-testid": dataTestid, }: BotIdenticonProps) { const svgHtml = React.useMemo(() => toSvg(value, size), [value, size]); @@ -24,6 +26,7 @@ export const BotIdenticon = React.memo(function BotIdenticon({
agent.pubkey.toLowerCase() === pubkeyLower, ); - const isBot = Boolean(relayAgent || managedAgent); + const isAgentByOaOwner = Boolean( + usersBatchQuery.data?.profiles[pubkeyLower]?.isAgent, + ); + const isBot = Boolean(relayAgent || managedAgent) || isAgentByOaOwner; const isOwner = useIsManagedAgent(isBot ? pubkey : null); // Populate the active-turns store for this agent so useActiveAgentTurns works diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index 6d30e4f0d..6520120c7 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -305,6 +305,7 @@ function ProfileHero({ {isBot ? ( diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index ae1db3ab2..292cfb2d3 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -615,6 +615,60 @@ test("renders agent memories seeded through the Playwright mock bridge", async ( await expect(page.getByTestId("agent-memory-list")).toContainText("orphan"); }); +test("owned agent absent from relay/managed lists still renders agent framing", async ({ + page, +}) => { + // Regression: bot-detection used to rely solely on the relay-agents registry + // + the local managed-agents list. An owned agent deployed elsewhere can miss + // BOTH lists, so the panel rendered it as a human (wrong archive framing). + // The fix ORs in the kind:0 NIP-OA agent flag (same signal the archive gate + // trusts), surfaced via the users-batch summary's `isAgent`. + const ednaPubkey = + "16aaadcf39011edbd887e4abefe5837170621db277e234f3f6c220d38ba75ecf"; + await installMockBridge(page, { + // Seeded as an agent (kind:0 NIP-OA owner) but NOT as a managed agent and + // NOT in the relay-agents registry — exactly the bug scenario. + searchProfiles: [ + { pubkey: ednaPubkey, displayName: "Edna", isAgent: true }, + ], + }); + await page.goto("/"); + + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + await waitForMockLiveSubscription(page, "general"); + + await page.evaluate( + ({ pubkey }) => { + const emit = ( + window as Window & { + __BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: { + channelName: string; + content: string; + pubkey: string; + }) => unknown; + } + ).__BUZZ_E2E_EMIT_MOCK_MESSAGE__; + if (!emit) { + throw new Error("Mock message emitter is unavailable."); + } + emit({ channelName: "general", content: "Edna check-in", pubkey }); + }, + { pubkey: ednaPubkey }, + ); + + const messageRow = page + .getByTestId("message-row") + .filter({ hasText: "Edna check-in" }); + await expect(messageRow).toBeVisible(); + await messageRow.locator("button").first().click(); + + await expect(page.getByTestId("user-profile-panel")).toBeVisible(); + // The bot indicator only renders when isBot resolves true — the assertion + // that the OA-owner signal now drives agent framing. + await expect(page.getByTestId("profile-bot-indicator")).toBeVisible(); +}); + test("renders settings in the app shell with a back button", async ({ page, }) => {