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, }) => {