mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): detect owned agents via kind:0 OA-owner signal in profile panel
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 <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
co-authored by
Taylor Ho
parent
b2ad3074ba
commit
462d2190a5
@@ -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({
|
||||
<div
|
||||
aria-hidden
|
||||
className={className}
|
||||
data-testid={dataTestid}
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: jdenticon produces safe SVG
|
||||
dangerouslySetInnerHTML={{ __html: svgHtml }}
|
||||
style={{ width: size, height: size, flexShrink: 0 }}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
useProfileQuery,
|
||||
useUnfollowMutation,
|
||||
useUserProfileQuery,
|
||||
useUsersBatchQuery,
|
||||
} from "@/features/profile/hooks";
|
||||
import {
|
||||
ChannelsFocusedView,
|
||||
@@ -156,6 +157,12 @@ export function UserProfilePanel({
|
||||
|
||||
const relayAgentsQuery = useRelayAgentsQuery({ enabled: true });
|
||||
const managedAgentsQuery = useManagedAgentsQuery({ enabled: true });
|
||||
// kind:0-derived agent flag (a verified NIP-OA `auth` tag on the target's
|
||||
// profile) — the same authoritative signal the archive gate's OA-owner
|
||||
// resolution trusts. The relay-agents registry + local managed-agents list
|
||||
// below can both miss an owned agent that was deployed elsewhere, so we OR
|
||||
// this in to keep `isBot` from drifting from the gate.
|
||||
const usersBatchQuery = useUsersBatchQuery([pubkey]);
|
||||
const channelsQuery = useChannelsQuery();
|
||||
const presenceQuery = usePresenceQuery([pubkey]);
|
||||
const userStatusQuery = useUserStatusQuery([pubkey]);
|
||||
@@ -176,7 +183,10 @@ export function UserProfilePanel({
|
||||
const managedAgent = managedAgentsQuery.data?.find(
|
||||
(agent) => 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
|
||||
|
||||
@@ -305,6 +305,7 @@ function ProfileHero({
|
||||
{isBot ? (
|
||||
<BotIdenticon
|
||||
className="shrink-0 rounded"
|
||||
data-testid="profile-bot-indicator"
|
||||
size={20}
|
||||
value={displayName}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user