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 <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-05 19:24:40 -04:00
parent 342aaf43d3
commit 12e0ad7e25
3 changed files with 116 additions and 96 deletions

View File

@@ -104,13 +104,9 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
[personas, agents],
);
const [collapsed, setCollapsed] = React.useState<Set<string>>(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<AgentPersona | null>(null);
const [instancesSheetPubkeys, setInstancesSheetPubkeys] = React.useState<
ReadonlySet<string>
>(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) {
<div className={IDENTITY_CARD_GRID_CLASS}>
{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"
>
<Server className="h-3.5 w-3.5" />
@@ -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) {
<InstancesSheet
open={instancesSheetOpen}
persona={instancesSheetPersona}
personaAgentPubkeys={instancesSheetPubkeys}
inventory={inventoryQuery.data}
onOpenChange={(o) => {
if (!o) {
setInstancesSheetPersona(null);
setInstancesSheetPubkeys(new Set());
}
if (!o) setInstancesSheetPersona(null);
}}
onOpenProfile={onOpenAgentProfile}
/>

View File

@@ -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: [],
}
);
}

View File

@@ -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);