From 27b859584fb3f7e1c12de39a120398db6bcf16ad Mon Sep 17 00:00:00 2001 From: Bryce Del Rio Date: Thu, 30 Jul 2026 05:49:47 +0000 Subject: [PATCH 1/2] fix(desktop): gate the add-member search on the relay-enforced add policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The add-member search dropped every agent-classified candidate not in the local managed store, and the relay's actual authority for third-party adds — the agent's kind:10100 channel_add_policy, enforced server-side in handlers/side_effects.rs (owner_only checked against the stored attested owner, nobody refused, anyone allowed) — was never consulted client-side: the field was not even carried through the RelayAgentInfo conversion. Carry channel_add_policy through the wire types and gate the picker on a predicate aligned with relay enforcement: managed agents always offered (native parity, unchanged); external agents offered when their declared policy is "anyone", or "owner_only" when the viewer is the NIP-OA-verified owner; "nobody" hidden from everyone including the owner (the relay refuses those adds regardless of actor). The picker mirrors the relay's declared policy; enforcement stays server-side, so the rare divergence (the relay enforces stored DB state — first-write-wins owner mapping, last-validated policy — while the client reads the latest published declaration) surfaces as a visible add error rather than a silent gap. In one direction the picker is deliberately STRICTER than the relay: agents with no directory entry stay hidden even though the relay — whose stored policy defaults to 'anyone' — would accept the add. That tightening preserves the stale-identity suppression the managed-list gate was introduced for (#2149): no declaration, no offer. Complements the mention-eligibility fix (#3676): each surface reads its own declaration — respond_to for mentions, channel_add_policy for adds. Unit tests cover the matrix (people / managed / anyone / owner_only as owner, non-owner, unverified / nobody incl. owner / no entry / null policy / pubkey normalization) plus the wire contract on both hops: kind:10100 content → RelayAgentInfo (Rust directory parse) and channel_add_policy → channelAddPolicy (fromRawRelayAgent), where a misnamed field would fail closed and silently hide agents app-wide. Co-Authored-By: Claude Fable 5 Signed-off-by: Bryce Del Rio --- desktop/src-tauri/src/managed_agents/types.rs | 5 + desktop/src-tauri/src/nostr_convert.rs | 4 +- .../lib/agentAutocompleteEligibility.test.mjs | 122 ++++++++++++++++++ .../lib/agentAutocompleteEligibility.ts | 61 +++++++++ .../features/channels/ui/MembersSidebar.tsx | 15 ++- desktop/src/features/pulse/ui/PulseView.tsx | 1 + desktop/src/shared/api/tauri.test.mjs | 51 +++++++- desktop/src/shared/api/tauri.ts | 6 +- desktop/src/shared/api/types.ts | 4 + 9 files changed, 264 insertions(+), 5 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 3d8e0ed02b..7c5be09474 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -207,6 +207,11 @@ pub struct RelayAgentInfo { pub respond_to: Option, #[serde(default)] pub respond_to_allowlist: Vec, + /// The agent's kind:10100 `channel_add_policy` declaration + /// (`anyone` | `owner_only` | `nobody`). Relay-enforced on + /// third-party adds; surfaced so pickers can mirror that enforcement. + #[serde(default)] + pub channel_add_policy: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ManagedAgentRecord { diff --git a/desktop/src-tauri/src/nostr_convert.rs b/desktop/src-tauri/src/nostr_convert.rs index ec4970e0c9..d89991ab48 100644 --- a/desktop/src-tauri/src/nostr_convert.rs +++ b/desktop/src-tauri/src/nostr_convert.rs @@ -910,7 +910,7 @@ mod tests { fn agents_default_sparse_agent_profiles_for_directory_parse() { let e = ev( 10100, - r#"{"channel_add_policy":"owner-only","display_name":"Scout"}"#, + r#"{"channel_add_policy":"owner_only","display_name":"Scout"}"#, vec![], ); let v = agents_from_events(std::slice::from_ref(&e)); @@ -926,6 +926,7 @@ mod tests { assert_eq!(parsed[0].capabilities, Vec::::new()); assert_eq!(parsed[0].status, "offline"); assert_eq!(parsed[0].respond_to, None); + assert_eq!(parsed[0].channel_add_policy.as_deref(), Some("owner_only")); } #[test] @@ -941,6 +942,7 @@ mod tests { parsed[0].respond_to, Some(crate::managed_agents::RespondTo::Anyone) ); + assert_eq!(parsed[0].channel_add_policy, None); } #[test] diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 4e02b7bd68..5bde51892a 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -7,6 +7,7 @@ import { getSharedChannelIds, isAgentIdentityInManagedList, relayAgentIsSharedWithUser, + shouldHideAgentFromAddSearch, shouldHideAgentFromMentions, } from "./agentAutocompleteEligibility.ts"; @@ -306,3 +307,124 @@ test("coalesceAgentAutocompleteCandidates: leaves non-agents alone", () => { assert.deepEqual(coalesce([first, second]), [first, second]); }); + +// ── shouldHideAgentFromAddSearch: mirror relay add enforcement ───────────── + +function addArgs(overrides = {}) { + const { + candidate = {}, + currentPubkey = CURRENT_PUBKEY, + managedAgentPubkeys = new Set(), + relayAgentAddPolicies = new Map(), + } = overrides; + return { + candidate: { + isAgent: true, + ownerPubkey: null, + pubkey: PUB_A, + ...candidate, + }, + currentPubkey, + managedAgentPubkeys, + relayAgentAddPolicies, + }; +} + +function addPolicy(policy) { + return new Map([[PUB_A, policy]]); +} + +test("shouldHideAgentFromAddSearch: never hides people", () => { + assert.equal( + shouldHideAgentFromAddSearch(addArgs({ candidate: { isAgent: false } })), + false, + ); +}); + +test("shouldHideAgentFromAddSearch: always offers managed agents", () => { + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ managedAgentPubkeys: new Set([PUB_A]) }), + ), + false, + ); +}); + +test("shouldHideAgentFromAddSearch: offers agents declaring add-policy anyone", () => { + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ relayAgentAddPolicies: addPolicy("anyone") }), + ), + false, + ); +}); + +test("shouldHideAgentFromAddSearch: owner_only admits only the verified owner", () => { + // The relay refuses owner_only adds from anyone but the stored attested + // owner — the picker must mirror both directions. + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ + candidate: { ownerPubkey: CURRENT_PUBKEY }, + relayAgentAddPolicies: addPolicy("owner_only"), + }), + ), + false, + ); + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ + candidate: { ownerPubkey: OTHER_OWNER_PUBKEY }, + relayAgentAddPolicies: addPolicy("owner_only"), + }), + ), + true, + ); + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ relayAgentAddPolicies: addPolicy("owner_only") }), + ), + true, + ); +}); + +test("shouldHideAgentFromAddSearch: hides nobody-policy agents from everyone including the owner", () => { + // Relay enforcement rejects nobody-policy adds regardless of actor. + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ + candidate: { ownerPubkey: CURRENT_PUBKEY }, + relayAgentAddPolicies: addPolicy("nobody"), + }), + ), + true, + ); +}); + +test("shouldHideAgentFromAddSearch: hides agents with no directory entry or no declared policy", () => { + // No declaration, no offer — preserves #2149's stale-identity guarantee. + assert.equal(shouldHideAgentFromAddSearch(addArgs()), true); + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ relayAgentAddPolicies: addPolicy(null) }), + ), + true, + ); +}); + +test("shouldHideAgentFromAddSearch: normalizes pubkeys before every comparison", () => { + const mixedCase = "Ab".repeat(32); + const normalized = mixedCase.toLowerCase(); + assert.equal( + shouldHideAgentFromAddSearch( + addArgs({ + candidate: { + ownerPubkey: CURRENT_PUBKEY.toUpperCase(), + pubkey: mixedCase, + }, + relayAgentAddPolicies: new Map([[normalized, "owner_only"]]), + }), + ), + false, + ); +}); diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts index e4afe7fea4..8082321749 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -97,6 +97,67 @@ export function shouldHideAgentFromMentions({ return directoryAgentPubkeys.has(normalized); } +/** + * Hide an agent-classified add-search candidate unless the relay would + * accept the add. + * + * The relay enforces the agent's own kind:10100 `channel_add_policy` on + * third-party adds (`buzz-relay` `handlers/side_effects.rs`): `owner_only` + * requires the actor to be the stored attested owner, `nobody` is refused + * for everyone, `anyone` is allowed, and self-adds always pass. The picker + * mirrors the relay's *declared* policy; enforcement stays server-side, so + * the rare divergence (the relay enforces stored DB state, the client reads + * the latest published declaration) surfaces as a visible add error rather + * than a silent gap. One deliberate tightening: + * agent candidates with no directory entry stay hidden. An undeclared + * identity offers no proof it is a live agent anyone intended to be + * addable, which preserves the stale-identity suppression the managed-list + * gate was introduced for (#2149). Ownership uses the NIP-OA-verified + * `ownerPubkey` (the same value the "managed by" surface renders), so + * `owner_only` agents remain addable by their verified owner. + */ +export function shouldHideAgentFromAddSearch({ + candidate, + currentPubkey, + managedAgentPubkeys, + relayAgentAddPolicies, +}: { + candidate: { + isAgent?: boolean; + ownerPubkey?: string | null; + pubkey: string; + }; + currentPubkey: string | null | undefined; + managedAgentPubkeys: ReadonlySet; + relayAgentAddPolicies: ReadonlyMap; +}) { + if (candidate.isAgent !== true) return false; + const pubkey = normalizePubkey(candidate.pubkey); + if (managedAgentPubkeys.has(pubkey)) return false; + if (!relayAgentAddPolicies.has(pubkey)) return true; + switch (relayAgentAddPolicies.get(pubkey)) { + case "anyone": + return false; + case "owner_only": { + const ownerPubkey = candidate.ownerPubkey + ? normalizePubkey(candidate.ownerPubkey) + : null; + const normalizedCurrentPubkey = currentPubkey + ? normalizePubkey(currentPubkey) + : null; + return ( + ownerPubkey === null || + normalizedCurrentPubkey === null || + ownerPubkey !== normalizedCurrentPubkey + ); + } + // "nobody", null, or any unknown value → the relay would refuse (or + // the declaration is unreadable) — do not offer. + default: + return true; + } +} + type AgentAutocompleteCandidate = { pubkey?: string; displayName?: string | null; diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index c6349546a2..ac9efefe87 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -9,7 +9,7 @@ import { import { attachManagedAgentToChannel } from "@/features/agents/channelAgents"; import { coalesceAgentAutocompleteCandidates, - isAgentIdentityInManagedList, + shouldHideAgentFromAddSearch, } from "@/features/agents/lib/agentAutocompleteEligibility"; import { useIsArchivedPredicate } from "@/features/identity-archive/hooks"; import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers"; @@ -265,6 +265,12 @@ export function MembersSidebar({ agent, ]), ); + const relayAgentAddPolicies = new Map( + (relayAgentsQuery.data ?? []).map((agent) => [ + normalizePubkey(agent.pubkey), + agent.channelAddPolicy, + ]), + ); const memberAgentLabels = new Set( rawMembers .filter((member) => member.isAgent === true || member.role === "bot") @@ -282,7 +288,12 @@ export function MembersSidebar({ )) || memberPubkeys.has(pubkey) || isArchivedDiscovery(pubkey) || - !isAgentIdentityInManagedList(candidate, managedAgentPubkeys) + shouldHideAgentFromAddSearch({ + candidate, + currentPubkey, + managedAgentPubkeys, + relayAgentAddPolicies, + }) ) { return; } diff --git a/desktop/src/features/pulse/ui/PulseView.tsx b/desktop/src/features/pulse/ui/PulseView.tsx index 3009076aa8..2a6df1e774 100644 --- a/desktop/src/features/pulse/ui/PulseView.tsx +++ b/desktop/src/features/pulse/ui/PulseView.tsx @@ -107,6 +107,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) { channels: [], channelIds: [], capabilities: [], + channelAddPolicy: null, status: agent.status === "running" || agent.status === "deployed" ? "online" diff --git a/desktop/src/shared/api/tauri.test.mjs b/desktop/src/shared/api/tauri.test.mjs index f4692a7d2a..f6e5f6e3c6 100644 --- a/desktop/src/shared/api/tauri.test.mjs +++ b/desktop/src/shared/api/tauri.test.mjs @@ -54,7 +54,9 @@ const { isRateLimited, resetRateLimitGate } = await import( // Import the production classifier from tauri.ts — tests must exercise the // real function, not a local copy, so a logic change is always caught here. -const { applyTauriRateLimitIfNeeded } = await import("./tauri.ts"); +const { applyTauriRateLimitIfNeeded, fromRawRelayAgent } = await import( + "./tauri.ts" +); function resetGate(startMs = 0) { pendingTimers.clear(); @@ -211,6 +213,53 @@ test("fromRawAcpRuntimeCatalogEntry env round-trips through edit payload shape", ); }); +// ── fromRawRelayAgent: kind:10100 directory wire contract ───────────────────── + +// These tests pin the snake_case → camelCase field mapping for the relay +// agent directory. A misnamed field here fails CLOSED with no error — the +// value silently defaults and eligibility gates (add-search, mentions) hide +// the agent app-wide — so the contract must be covered, not just reviewed. + +function rawRelayAgent(overrides = {}) { + return { + pubkey: "ab".repeat(32), + name: "Scout", + agent_type: "agent", + channels: [], + channel_ids: [], + capabilities: [], + status: "online", + ...overrides, + }; +} + +test("fromRawRelayAgent maps channel_add_policy to channelAddPolicy", () => { + const agent = fromRawRelayAgent( + rawRelayAgent({ channel_add_policy: "owner_only" }), + ); + assert.equal(agent.channelAddPolicy, "owner_only"); +}); + +test("fromRawRelayAgent defaults a missing channel_add_policy to null", () => { + const agent = fromRawRelayAgent(rawRelayAgent()); + assert.equal(agent.channelAddPolicy, null); +}); + +test("fromRawRelayAgent maps respond_to fields and defaults them when absent", () => { + const declared = fromRawRelayAgent( + rawRelayAgent({ + respond_to: "allowlist", + respond_to_allowlist: ["cd".repeat(32)], + }), + ); + assert.equal(declared.respondTo, "allowlist"); + assert.deepStrictEqual(declared.respondToAllowlist, ["cd".repeat(32)]); + + const sparse = fromRawRelayAgent(rawRelayAgent()); + assert.equal(sparse.respondTo, null); + assert.deepStrictEqual(sparse.respondToAllowlist, []); +}); + // ── Teardown ────────────────────────────────────────────────────────────────── test("teardown — restore Date.now", () => { diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts index c57525480e..798ddd6c8d 100644 --- a/desktop/src/shared/api/tauri.ts +++ b/desktop/src/shared/api/tauri.ts @@ -113,6 +113,7 @@ type RawRelayAgent = { status: RelayAgent["status"]; respond_to?: RelayAgent["respondTo"]; respond_to_allowlist?: string[]; + channel_add_policy?: RelayAgent["channelAddPolicy"]; }; export type RawManagedAgent = { pubkey: string; @@ -680,7 +681,9 @@ export async function createAuthEvent(input: { return JSON.parse(eventJson) as RelayEvent; } -function fromRawRelayAgent(agent: RawRelayAgent): RelayAgent { +// Exported for tests — tauri.test.mjs covers the snake_case wire contract +// (a silently dropped field here fails closed and hides agents app-wide). +export function fromRawRelayAgent(agent: RawRelayAgent): RelayAgent { return { pubkey: agent.pubkey, name: agent.name, @@ -691,6 +694,7 @@ function fromRawRelayAgent(agent: RawRelayAgent): RelayAgent { status: agent.status, respondTo: agent.respond_to ?? null, respondToAllowlist: agent.respond_to_allowlist ?? [], + channelAddPolicy: agent.channel_add_policy ?? null, }; } diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index 689c400b03..142adff5b9 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -295,8 +295,12 @@ export type RelayAgent = { status: "online" | "away" | "offline"; respondTo: RespondToMode | null; respondToAllowlist: string[]; + /** kind:10100 `channel_add_policy` — relay-enforced on third-party adds. */ + channelAddPolicy: ChannelAddPolicy | null; }; +export type ChannelAddPolicy = "anyone" | "owner_only" | "nobody"; + export type ManagedAgentRuntimeLifecycle = | "starting" | "listening" From 6964f91d3d6bb9865dcd9e132e169cce466195d9 Mon Sep 17 00:00:00 2001 From: Bryce Del Rio Date: Fri, 31 Jul 2026 08:51:17 +0000 Subject: [PATCH 2/2] test(desktop): e2e coverage for add-search channel_add_policy gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up on #3864: the predicate matrix is unit-tested, but the mock bridge seeds carried no channel_add_policy at all — every mock directory agent resolved to null policy and was hidden from add search, which made the e2e layer blind to this feature (the same fixture-gap class that let the mentions-surface regression hide until #3676's e2e update). Carry channel_add_policy through the bridge seed types with an "anyone" default (matching the relay's stored default), and add paired smoke specs modeled on identity-archive-hide.spec.ts: an agent declaring "nobody" never reaches the picker, and an "anyone" control proves the deny comes from the policy gate rather than a broken search. Swept existing specs for assertions the seed default could disturb: alice is already a member of general (excluded from add search there regardless of policy), charlie stays offered via the managed path with an unchanged testid, the no-directory-entry agents in channels.spec.ts remain hidden under the new gate, and the pagination count query cannot match either default agent's name. Co-Authored-By: Claude Fable 5 Signed-off-by: Bryce Del Rio --- desktop/playwright.config.ts | 1 + desktop/src/testing/e2eBridge.ts | 5 ++ .../tests/e2e/add-search-eligibility.spec.ts | 61 +++++++++++++++++++ desktop/tests/helpers/bridge.ts | 1 + 4 files changed, 68 insertions(+) create mode 100644 desktop/tests/e2e/add-search-eligibility.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 459fa75743..dfce737a22 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -69,6 +69,7 @@ export default defineConfig({ "**/workflows.spec.ts", "**/identity-archive.spec.ts", "**/identity-archive-hide.spec.ts", + "**/add-search-eligibility.spec.ts", "**/relay-connectivity.spec.ts", "**/unread-pill.spec.ts", "**/sidebar-more-unread-overlap.spec.ts", diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 4cfc553df1..3f66c55e51 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -103,6 +103,7 @@ type MockRelayAgentSeed = { capabilities?: string[]; respondTo?: RawRelayAgent["respond_to"]; respondToAllowlist?: string[]; + channelAddPolicy?: RawRelayAgent["channel_add_policy"]; channelNames?: string[]; channelIds?: string[]; status?: PresenceStatus; @@ -726,6 +727,7 @@ type RawRelayAgent = { status: PresenceStatus; respond_to?: "owner-only" | "allowlist" | "anyone"; respond_to_allowlist?: string[]; + channel_add_policy?: "anyone" | "owner_only" | "nobody"; }; type RawManagedAgent = { @@ -2091,6 +2093,7 @@ function resetMockRelayAgents(config?: E2eConfig) { status: seed.status ?? "online", respond_to: seed.respondTo ?? "owner-only", respond_to_allowlist: seed.respondToAllowlist ?? [], + channel_add_policy: seed.channelAddPolicy ?? "anyone", }); } } @@ -2926,6 +2929,7 @@ const defaultMockRelayAgents: RawRelayAgent[] = [ status: "online", respond_to: "anyone", respond_to_allowlist: [], + channel_add_policy: "anyone", }, { pubkey: CHARLIE_PUBKEY, @@ -2937,6 +2941,7 @@ const defaultMockRelayAgents: RawRelayAgent[] = [ status: "away", respond_to: "anyone", respond_to_allowlist: [], + channel_add_policy: "anyone", }, ]; let mockRelayAgents: RawRelayAgent[] = defaultMockRelayAgents.map((agent) => ({ diff --git a/desktop/tests/e2e/add-search-eligibility.spec.ts b/desktop/tests/e2e/add-search-eligibility.spec.ts new file mode 100644 index 0000000000..bff3684aa1 --- /dev/null +++ b/desktop/tests/e2e/add-search-eligibility.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge } from "../helpers/bridge"; + +// External (non-managed) relay agents in the add-member search are gated on +// their kind:10100 `channel_add_policy` — the declaration the relay enforces +// on third-party adds. Paired specs: the deny case proves an opted-out agent +// never reaches the picker; the allow control proves the deny result comes +// from the policy gate, not from a broken search or member-exclusion. + +const NOBODY_AGENT_PUBKEY = "77".repeat(32); +const ANYONE_AGENT_PUBKEY = "99".repeat(32); + +async function openAddMemberSearch(page: import("@playwright/test").Page) { + await page.goto("/"); + await page.getByTestId("channel-agents").click(); + await page.getByTestId("channel-members-trigger").click(); + await expect(page.getByTestId("members-sidebar")).toBeVisible(); +} + +test("add search: agent declaring channel_add_policy nobody is never offered", async ({ + page, +}) => { + await installMockBridge(page, { + relayAgents: [ + { + pubkey: NOBODY_AGENT_PUBKEY, + name: "vega", + channelAddPolicy: "nobody", + channelNames: ["general"], + respondTo: "anyone", + }, + ], + }); + await openAddMemberSearch(page); + await page.getByTestId("channel-management-search-users").fill("vega"); + await expect( + page.getByTestId(`channel-user-search-result-${NOBODY_AGENT_PUBKEY}`), + ).toHaveCount(0); +}); + +test("add search: control — agent declaring channel_add_policy anyone IS offered", async ({ + page, +}) => { + await installMockBridge(page, { + relayAgents: [ + { + pubkey: ANYONE_AGENT_PUBKEY, + name: "lyra", + channelAddPolicy: "anyone", + channelNames: ["general"], + respondTo: "anyone", + }, + ], + }); + await openAddMemberSearch(page); + await page.getByTestId("channel-management-search-users").fill("lyra"); + await expect( + page.getByTestId(`channel-user-search-result-${ANYONE_AGENT_PUBKEY}`), + ).toBeVisible(); +}); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index ca4d62ddd6..f96973a904 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -78,6 +78,7 @@ type MockRelayAgentSeed = { capabilities?: string[]; respondTo?: "owner-only" | "allowlist" | "anyone"; respondToAllowlist?: string[]; + channelAddPolicy?: "anyone" | "owner_only" | "nobody"; channelNames?: string[]; channelIds?: string[]; status?: "online" | "away" | "offline";