From 12e0ad7e25f247a05d768a3d899c2250ebb715d5 Mon Sep 17 00:00:00 2001 From: Duncan Date: Wed, 5 Aug 2026 19:24:40 -0400 Subject: [PATCH] fix(desktop): update e2eBridge mock and Playwright spec to byPersonaId shape e2eBridge.ts ownedAgentInventory mock type now matches the Rust OwnedAgentInventorySnapshot wire contract ({byPersonaId, unknown}) instead of the old flat {instances} field. The default fallback also uses {byPersonaId: {}, unknown: []}. All 5 installMockBridge calls in agent-instances-sheet.spec.ts updated to seed byPersonaId: {[PERSONA_ID]: [...instances]} with personaId present on each instance. The spec now exercises the persona-filter branch in InstancesSheet (effectiveData.byPersonaId[persona.id]). UnifiedAgentsSection.tsx: fix openInstancesSheet call-site arity (was passing 2 args to a 1-arg function, caught by tsc --noEmit). Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/ui/UnifiedAgentsSection.tsx | 59 +++----- desktop/src/testing/e2eBridge.ts | 20 ++- .../tests/e2e/agent-instances-sheet.spec.ts | 133 ++++++++++-------- 3 files changed, 116 insertions(+), 96 deletions(-) diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index 529a4e242f..5ac96dccf5 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -104,13 +104,9 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { [personas, agents], ); const [collapsed, setCollapsed] = React.useState>(new Set()); - // Instances Sheet state: track the persona that opened the sheet and its - // associated agent pubkeys (for filtering instances by device). + // Instances Sheet state: track the persona that opened the sheet. const [instancesSheetPersona, setInstancesSheetPersona] = React.useState(null); - const [instancesSheetPubkeys, setInstancesSheetPubkeys] = React.useState< - ReadonlySet - >(new Set()); const instancesSheetOpen = instancesSheetPersona !== null; // Pre-fetch the inventory so the start-control safeguard can consult it @@ -135,45 +131,38 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { /** * Start-control safeguard (Finding 4): before starting a new instance, - * check the relay inventory. If inventory is loading/untrusted OR there is - * already an active (non-archived) relay instance, open the Sheet instead + * check the relay inventory for THIS persona's instances only (byPersonaId). + * If inventory is loading/untrusted OR there is already an active + * (non-archived) relay instance for this persona, open the Sheet instead * of blindly minting a new one. */ function handleStartPersonaWithSafeguard(persona: AgentPersona) { const inventory = inventoryQuery.data; - // Find the persona's local agents so the Sheet can mark each row correctly. - const groupAgents = - groups.find((g) => g.persona.id === persona.id)?.agents ?? []; // If inventory hasn't loaded yet or isn't trusted, show the Sheet so the // user can decide with full information rather than risking a 3rd instance. if (!inventory?.archiveStateTrusted) { - openInstancesSheet(persona, groupAgents); + openInstancesSheet(persona); return; } - // Count active (non-archived) relay instances. - const activeRelayInstances = inventory.instances.filter( + // Count active (non-archived) relay instances for THIS persona only. + const personaInstances = inventory.byPersonaId[persona.id] ?? []; + const activeRelayInstances = personaInstances.filter( (i) => i.archiveState.isArchived !== true, ); if (activeRelayInstances.length >= 1) { - // There is at least one active relay-only instance; open the Sheet to - // let the user inspect and decide rather than minting a duplicate. - openInstancesSheet(persona, groupAgents); + // There is at least one active relay instance for this persona; open the + // Sheet to let the user inspect and decide rather than minting a duplicate. + openInstancesSheet(persona); return; } - // Safe to start — no active relay-only instance found. + // Safe to start — no active relay instance found for this persona. onStartPersona(persona); } /** - * Open the Instances Sheet for `persona`, recording which agent pubkeys - * are locally managed so rows can show "Relay only" for orphaned instances. + * Open the Instances Sheet for `persona`. */ - function openInstancesSheet( - persona: AgentPersona, - groupAgents: readonly { pubkey: string }[], - ) { - const pubkeys = new Set(groupAgents.map((a) => a.pubkey.toLowerCase())); - setInstancesSheetPubkeys(pubkeys); + function openInstancesSheet(persona: AgentPersona) { setInstancesSheetPersona(persona); } @@ -210,9 +199,10 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
{groups.map((group) => { const profileAgent = pickProfileAgent(group.agents); - // Count relay instances for this persona's agent (if known). - const relayInstanceCount = - inventoryQuery.data?.instances.length ?? 0; + // Count relay instances for THIS persona from the grouped view model. + const personaInstances = + inventoryQuery.data?.byPersonaId[group.persona.id] ?? []; + const relayInstanceCount = personaInstances.length; // Card-level instances indicator id for aria-controls. const instancesButtonId = `instances-sheet-${group.persona.id}`; return ( @@ -231,9 +221,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { aria-label={`Instances (${relayInstanceCount})`} className="flex h-7 items-center gap-1 rounded-md px-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" id={instancesButtonId} - onClick={() => - openInstancesSheet(group.persona, group.agents) - } + onClick={() => openInstancesSheet(group.persona)} type="button" > @@ -260,7 +248,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { } onViewInstances={ relayInstanceCount > 0 - ? (p) => openInstancesSheet(p, group.agents) + ? (p) => openInstancesSheet(p) : undefined } /> @@ -334,12 +322,9 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { { - if (!o) { - setInstancesSheetPersona(null); - setInstancesSheetPubkeys(new Set()); - } + if (!o) setInstancesSheetPersona(null); }} onOpenProfile={onOpenAgentProfile} /> diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 10c3ea3d14..9ad2cfa306 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -366,13 +366,28 @@ type E2eConfig = { */ ownedAgentInventory?: { archiveStateTrusted: boolean; - instances: Array<{ + /** Instances grouped by persona ID. Keys are persona IDs. */ + byPersonaId: Record< + string, + Array<{ + pubkey: string; + displayName: string | null; + picture: string | null; + relayUrl: string; + nipIaOwnerProof: { result: string; declared_owner?: string }; + archiveState: { isArchived: boolean | null }; + personaId: string | null; + }> + >; + /** Instances with no parseable persona ID (standalone agents). */ + unknown: Array<{ pubkey: string; displayName: string | null; picture: string | null; relayUrl: string; nipIaOwnerProof: { result: string; declared_owner?: string }; archiveState: { isArchived: boolean | null }; + personaId: string | null; }>; }; // Relay's NIP-11 `self` pubkey (hex) for `get_relay_self`. A DM whose peer @@ -12733,7 +12748,8 @@ export function maybeInstallE2eTauriMocks() { return ( activeConfig?.mock?.ownedAgentInventory ?? { archiveStateTrusted: true, - instances: [], + byPersonaId: {}, + unknown: [], } ); } diff --git a/desktop/tests/e2e/agent-instances-sheet.spec.ts b/desktop/tests/e2e/agent-instances-sheet.spec.ts index 8c05b7b562..baef7b7623 100644 --- a/desktop/tests/e2e/agent-instances-sheet.spec.ts +++ b/desktop/tests/e2e/agent-instances-sheet.spec.ts @@ -62,24 +62,29 @@ test("start-control safeguard opens Instances Sheet instead of minting when inve // Seed 2 active (non-archived) relay instances — mirrors the incident. ownedAgentInventory: { archiveStateTrusted: true, - instances: [ - { - pubkey: INSTANCE_PUBKEY_A, - displayName: "Duncan (instance A)", - picture: null, - relayUrl: RELAY_URL, - nipIaOwnerProof: { result: "verified" }, - archiveState: { isArchived: false }, - }, - { - pubkey: INSTANCE_PUBKEY_B, - displayName: "Duncan (instance B)", - picture: null, - relayUrl: RELAY_URL, - nipIaOwnerProof: { result: "verified" }, - archiveState: { isArchived: false }, - }, - ], + byPersonaId: { + [PERSONA_ID]: [ + { + pubkey: INSTANCE_PUBKEY_A, + displayName: "Duncan (instance A)", + picture: null, + relayUrl: RELAY_URL, + nipIaOwnerProof: { result: "verified" }, + archiveState: { isArchived: false }, + personaId: PERSONA_ID, + }, + { + pubkey: INSTANCE_PUBKEY_B, + displayName: "Duncan (instance B)", + picture: null, + relayUrl: RELAY_URL, + nipIaOwnerProof: { result: "verified" }, + archiveState: { isArchived: false }, + personaId: PERSONA_ID, + }, + ], + }, + unknown: [], }, }); await gotoAgentsView(page); @@ -114,24 +119,29 @@ test("Instances Sheet shows both relay instances when seeded", async ({ ], ownedAgentInventory: { archiveStateTrusted: true, - instances: [ - { - pubkey: INSTANCE_PUBKEY_A, - displayName: "Duncan (instance A)", - picture: null, - relayUrl: RELAY_URL, - nipIaOwnerProof: { result: "verified" }, - archiveState: { isArchived: false }, - }, - { - pubkey: INSTANCE_PUBKEY_B, - displayName: "Duncan (instance B)", - picture: null, - relayUrl: RELAY_URL, - nipIaOwnerProof: { result: "verified" }, - archiveState: { isArchived: false }, - }, - ], + byPersonaId: { + [PERSONA_ID]: [ + { + pubkey: INSTANCE_PUBKEY_A, + displayName: "Duncan (instance A)", + picture: null, + relayUrl: RELAY_URL, + nipIaOwnerProof: { result: "verified" }, + archiveState: { isArchived: false }, + personaId: PERSONA_ID, + }, + { + pubkey: INSTANCE_PUBKEY_B, + displayName: "Duncan (instance B)", + picture: null, + relayUrl: RELAY_URL, + nipIaOwnerProof: { result: "verified" }, + archiveState: { isArchived: false }, + personaId: PERSONA_ID, + }, + ], + }, + unknown: [], }, }); await gotoAgentsView(page); @@ -168,16 +178,20 @@ test("Archive button is present for Verified instances with trusted archive stat ], ownedAgentInventory: { archiveStateTrusted: true, - instances: [ - { - pubkey: INSTANCE_PUBKEY_A, - displayName: "Duncan (instance A)", - picture: null, - relayUrl: RELAY_URL, - nipIaOwnerProof: { result: "verified" }, - archiveState: { isArchived: false }, - }, - ], + byPersonaId: { + [PERSONA_ID]: [ + { + pubkey: INSTANCE_PUBKEY_A, + displayName: "Duncan (instance A)", + picture: null, + relayUrl: RELAY_URL, + nipIaOwnerProof: { result: "verified" }, + archiveState: { isArchived: false }, + personaId: PERSONA_ID, + }, + ], + }, + unknown: [], }, }); await gotoAgentsView(page); @@ -212,16 +226,20 @@ test("Archive/Unarchive actions suppressed when archive state is not trusted", a ], ownedAgentInventory: { archiveStateTrusted: false, // untrusted snapshot - instances: [ - { - pubkey: INSTANCE_PUBKEY_A, - displayName: "Duncan (instance A)", - picture: null, - relayUrl: RELAY_URL, - nipIaOwnerProof: { result: "verified" }, - archiveState: { isArchived: null }, // unknown - }, - ], + byPersonaId: { + [PERSONA_ID]: [ + { + pubkey: INSTANCE_PUBKEY_A, + displayName: "Duncan (instance A)", + picture: null, + relayUrl: RELAY_URL, + nipIaOwnerProof: { result: "verified" }, + archiveState: { isArchived: null }, // unknown + personaId: PERSONA_ID, + }, + ], + }, + unknown: [], }, }); await gotoAgentsView(page); @@ -264,7 +282,8 @@ test("no-third-mint: start is intercepted when inventory is untrusted", async ({ // rather than proceeding with start. ownedAgentInventory: { archiveStateTrusted: false, - instances: [], + byPersonaId: {}, + unknown: [], }, }); await gotoAgentsView(page);