import { hexToBytes } from "@noble/hashes/utils.js"; import { mockIPC, mockWindows } from "@tauri-apps/api/mocks"; import { finalizeEvent } from "nostr-tools/pure"; import { parse as yamlParse } from "yaml"; import type { RelayEvent } from "@/shared/api/types"; type TestIdentity = { privateKey: string; pubkey: string; username: string; }; type E2eConfig = { mode?: "mock" | "relay"; mock?: { mintTokenError?: string; seededTokens?: RawMockTokenSeed[]; }; relayHttpUrl?: string; relayWsUrl?: string; identity?: TestIdentity; }; type RawMockTokenSeed = { id: string; name: string; scopes: string[]; channel_ids: string[]; created_at: string; expires_at: string | null; last_used_at: string | null; revoked_at: string | null; token?: string; }; type RawProfile = { pubkey: string; display_name: string | null; avatar_url: string | null; about: string | null; nip05_handle: string | null; }; type RawUserProfileSummary = { display_name: string | null; avatar_url: string | null; nip05_handle: string | null; }; type RawUsersBatchResponse = { profiles: Record; missing: string[]; }; type RawUserSearchResult = { pubkey: string; display_name: string | null; avatar_url: string | null; nip05_handle: string | null; }; type RawSearchUsersResponse = { users: RawUserSearchResult[]; }; type PresenceStatus = "online" | "away" | "offline"; type RawPresenceLookup = Record; type RawSetPresenceResponse = { status: PresenceStatus; ttl_seconds: number; }; type RawOpenDmResponse = { channel_id: string; created: boolean; participants: string[]; }; type RawChannel = { id: string; name: string; channel_type: "stream" | "forum" | "dm"; visibility: "open" | "private"; description: string; topic: string | null; purpose: string | null; member_count: number; last_message_at: string | null; archived_at: string | null; participants: string[]; participant_pubkeys: string[]; ttl_seconds: number | null; ttl_deadline: string | null; }; type RawChannelWithMembership = RawChannel & { is_member: boolean; }; type RawChannelDetail = RawChannel & { created_by: string; created_at: string; updated_at: string; topic_set_by: string | null; topic_set_at: string | null; purpose_set_by: string | null; purpose_set_at: string | null; topic_required: boolean; max_members: number | null; nip29_group_id: string | null; }; type RawChannelMember = { pubkey: string; role: "owner" | "admin" | "member" | "guest" | "bot"; joined_at: string; display_name: string | null; }; type RawChannelMembersResponse = { members: RawChannelMember[]; next_cursor: string | null; }; type RawAddChannelMembersResponse = { added: string[]; errors: Array<{ pubkey: string; error: string; }>; }; type MockChannel = RawChannelDetail & { members: RawChannelMember[]; }; type RawFeedItem = { id: string; kind: number; pubkey: string; content: string; created_at: number; channel_id: string | null; channel_name: string; tags: string[][]; category: "mention" | "needs_action" | "activity" | "agent_activity"; }; type RawHomeFeedResponse = { feed: { mentions: RawFeedItem[]; needs_action: RawFeedItem[]; activity: RawFeedItem[]; agent_activity: RawFeedItem[]; }; meta: { since: number; total: number; generated_at: number; }; }; type RawThreadSummary = { reply_count: number; descendant_count: number; last_reply_at: number | null; participants: string[]; }; type RawForumPost = { event_id: string; pubkey: string; content: string; kind: number; created_at: number; channel_id: string; tags: string[][]; thread_summary: RawThreadSummary | null; reactions: unknown; }; type RawForumPostsResponse = { messages: RawForumPost[]; next_cursor: number | null; }; type RawForumReply = { event_id: string; pubkey: string; content: string; kind: number; created_at: number; channel_id: string; tags: string[][]; parent_event_id: string | null; root_event_id: string | null; depth: number; broadcast: boolean; reactions: unknown; }; type RawForumThreadResponse = { root: RawForumPost; replies: RawForumReply[]; total_replies: number; next_cursor: string | null; }; type RawUserNote = { id: string; pubkey: string; created_at: number; content: string; }; type RawUserNotesCursor = { before: number; before_id: string; }; type RawUserNotesResponse = { notes: RawUserNote[]; next_cursor: RawUserNotesCursor | null; }; type RawSearchHit = { event_id: string; content: string; kind: number; pubkey: string; channel_id: string | null; channel_name: string | null; created_at: number; score: number; }; type RawSearchResponse = { hits: RawSearchHit[]; found: number; }; type RawSendChannelMessageResponse = { event_id: string; parent_event_id: string | null; root_event_id: string | null; depth: number; created_at: number; }; type RawToken = { id: string; name: string; scopes: string[]; channel_ids: string[]; created_at: string; expires_at: string | null; last_used_at: string | null; revoked_at: string | null; }; type RawListTokensResponse = { tokens: RawToken[]; }; type RawMintTokenResponse = RawToken & { token: string; }; type RawRevokeAllTokensResponse = { revoked_count: number; }; type RawRelayAgent = { pubkey: string; name: string; agent_type: string; channels: string[]; channel_ids: string[]; capabilities: string[]; status: PresenceStatus; }; type RawManagedAgent = { pubkey: string; name: string; persona_id: string | null; relay_url: string; acp_command: string; agent_command: string; agent_args: string[]; mcp_command: string; turn_timeout_seconds: number; idle_timeout_seconds: number | null; max_turn_duration_seconds: number | null; parallelism: number; system_prompt: string | null; model: string | null; has_api_token: boolean; status: "running" | "stopped" | "deployed" | "not_deployed"; pid: number | null; created_at: string; updated_at: string; last_started_at: string | null; last_stopped_at: string | null; last_exit_code: number | null; last_error: string | null; log_path: string; start_on_app_launch: boolean; backend: | { type: "local" } | { type: "provider"; id: string; config: Record }; backend_agent_id: string | null; }; type RawCreateManagedAgentResponse = { agent: RawManagedAgent; private_key_nsec: string; api_token: string | null; profile_sync_error: string | null; spawn_error: string | null; }; type RawMintManagedAgentTokenResponse = { agent: RawManagedAgent; token: string; }; type RawManagedAgentLog = { content: string; log_path: string; }; type RawAcpProvider = { id: string; label: string; command: string; binary_path: string; default_args: string[]; }; type RawCommandAvailability = { command: string; resolved_path: string | null; available: boolean; }; type RawManagedAgentPrereqs = { admin: RawCommandAvailability; acp: RawCommandAvailability; mcp: RawCommandAvailability; }; type RawPersona = { id: string; display_name: string; avatar_url: string | null; system_prompt: string; is_builtin: boolean; is_active: boolean; created_at: string; updated_at: string; }; type RawTeam = { id: string; name: string; description: string | null; persona_ids: string[]; created_at: string; updated_at: string; }; type MockToken = RawToken & { token: string; }; type MockManagedAgent = RawManagedAgent & { private_key_nsec: string; api_token: string | null; log_lines: string[]; }; type WsHandler = (message: unknown) => void; const GLOBAL_MOCK_SUBSCRIPTION = "*"; type MockSocket = { handler: WsHandler; subscriptions: Map; }; declare global { interface Window { __SPROUT_E2E__?: E2eConfig; __SPROUT_E2E_COMMANDS__?: string[]; __SPROUT_E2E_WEBVIEW_ZOOM__?: number; __SPROUT_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: { channelName: string; }) => boolean; __SPROUT_E2E_EMIT_MOCK_MESSAGE__?: (input: { channelName: string; content: string; pubkey?: string; }) => RelayEvent; __SPROUT_E2E_EMIT_MOCK_TYPING__?: (input: { channelName: string; pubkey?: string; }) => RelayEvent; __SPROUT_E2E_INVOKE_MOCK_COMMAND__?: ( command: string, payload?: Record, ) => Promise; __SPROUT_E2E_PUSH_MOCK_FEED_ITEM__?: (item: RawFeedItem) => RawFeedItem; } } const DEFAULT_RELAY_HTTP_URL = "http://localhost:3000"; const DEFAULT_RELAY_WS_URL = "ws://localhost:3000"; const DEFAULT_MOCK_IDENTITY = { pubkey: "deadbeef".repeat(8), display_name: "npub1mock...", }; const DEFAULT_REAL_IDENTITY = { privateKey: "3dbaebadb5dfd777ff25149ee230d907a15a9e1294b40b830661e65bb42f6c03", pubkey: "e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34", username: "tyler", } satisfies TestIdentity; const ALICE_PUBKEY = "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"; const BOB_PUBKEY = "bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"; const CHARLIE_PUBKEY = "554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"; const OUTSIDER_PUBKEY = "df8e91b86fda13a9a67896df77232f7bdab2ba9c3e165378e1ba3d24c13a328e"; const MOCK_IDENTITY_PUBKEY = DEFAULT_MOCK_IDENTITY.pubkey; const MOCK_PRESENCE_TTL_SECONDS = 90; const mockDisplayNames = new Map([ [MOCK_IDENTITY_PUBKEY, DEFAULT_MOCK_IDENTITY.display_name], [ALICE_PUBKEY, "alice"], [BOB_PUBKEY, "bob"], [CHARLIE_PUBKEY, "charlie"], [OUTSIDER_PUBKEY, "outsider"], [DEFAULT_REAL_IDENTITY.pubkey, DEFAULT_REAL_IDENTITY.username], ]); function isoMinutesAgo(minutesAgo: number): string { return new Date(Date.now() - minutesAgo * 60_000).toISOString(); } function cloneMembers(members: RawChannelMember[]): RawChannelMember[] { return members.map((member) => ({ ...member })); } function toRawChannel(channel: MockChannel): RawChannelWithMembership { return { id: channel.id, name: channel.name, channel_type: channel.channel_type, visibility: channel.visibility, description: channel.description, topic: channel.topic, purpose: channel.purpose, member_count: channel.member_count, last_message_at: channel.last_message_at, archived_at: channel.archived_at, participants: [...channel.participants], participant_pubkeys: [...channel.participant_pubkeys], ttl_seconds: channel.ttl_seconds ?? null, ttl_deadline: channel.ttl_deadline ?? null, is_member: channel.members.some( (member) => member.pubkey === MOCK_IDENTITY_PUBKEY || member.pubkey === DEFAULT_REAL_IDENTITY.pubkey, ), }; } function toRawChannelDetail(channel: MockChannel): RawChannelDetail { return { ...toRawChannel(channel), created_by: channel.created_by, created_at: channel.created_at, updated_at: channel.updated_at, topic_set_by: channel.topic_set_by, topic_set_at: channel.topic_set_at, purpose_set_by: channel.purpose_set_by, purpose_set_at: channel.purpose_set_at, topic_required: channel.topic_required, max_members: channel.max_members, nip29_group_id: channel.nip29_group_id, }; } function createMockMember( pubkey: string, role: RawChannelMember["role"], joinedMinutesAgo: number, ): RawChannelMember { return { pubkey, role, joined_at: isoMinutesAgo(joinedMinutesAgo), display_name: mockDisplayNames.get(pubkey) ?? null, }; } function createMockChannel( seed: Omit< MockChannel, | "created_at" | "member_count" | "members" | "updated_at" | "participant_pubkeys" | "participants" | "ttl_seconds" | "ttl_deadline" > & { created_minutes_ago: number; members: RawChannelMember[]; participant_pubkeys?: string[]; participants?: string[]; ttl_seconds?: number | null; ttl_deadline?: string | null; updated_minutes_ago?: number; }, ): MockChannel { return { ...seed, created_at: isoMinutesAgo(seed.created_minutes_ago), member_count: seed.members.length, members: cloneMembers(seed.members), participant_pubkeys: [...(seed.participant_pubkeys ?? [])], participants: [...(seed.participants ?? [])], ttl_seconds: seed.ttl_seconds ?? null, ttl_deadline: seed.ttl_deadline ?? null, updated_at: isoMinutesAgo( seed.updated_minutes_ago ?? seed.created_minutes_ago, ), }; } function syncMockChannel(channel: MockChannel) { channel.member_count = channel.members.length; if (channel.channel_type !== "dm") { return; } channel.participant_pubkeys = channel.members.map((member) => member.pubkey); channel.participants = channel.members.map( (member) => member.display_name ?? member.pubkey.slice(0, 8), ); } function touchMockChannel(channel: MockChannel) { channel.updated_at = new Date().toISOString(); } function normalizeParticipantPubkeys(pubkeys: string[]) { return [...new Set(pubkeys.map((pubkey) => pubkey.toLowerCase()))].sort(); } function findMockDmByParticipantPubkeys(pubkeys: string[]) { const normalizedPubkeys = normalizeParticipantPubkeys(pubkeys); return ( mockChannels.find((channel) => { if (channel.channel_type !== "dm") { return false; } const channelPubkeys = normalizeParticipantPubkeys( channel.participant_pubkeys, ); return ( channelPubkeys.length === normalizedPubkeys.length && channelPubkeys.every( (pubkey, index) => pubkey === normalizedPubkeys[index], ) ); }) ?? null ); } function getMockIdentity() { return { pubkey: MOCK_IDENTITY_PUBKEY, displayName: DEFAULT_MOCK_IDENTITY.display_name, }; } function cloneProfile(profile: RawProfile): RawProfile { return { ...profile }; } function cloneToken(token: RawToken): RawToken { return { ...token, channel_ids: [...token.channel_ids], scopes: [...token.scopes], }; } function cloneMintedToken(token: MockToken): RawMintTokenResponse { return { ...cloneToken(token), token: token.token, }; } function cloneRelayAgent(agent: RawRelayAgent): RawRelayAgent { return { ...agent, channels: [...agent.channels], channel_ids: [...agent.channel_ids], capabilities: [...agent.capabilities], }; } function cloneManagedAgent(agent: MockManagedAgent): RawManagedAgent { return { pubkey: agent.pubkey, name: agent.name, persona_id: agent.persona_id, relay_url: agent.relay_url, acp_command: agent.acp_command, agent_command: agent.agent_command, agent_args: [...agent.agent_args], mcp_command: agent.mcp_command, turn_timeout_seconds: agent.turn_timeout_seconds, idle_timeout_seconds: agent.idle_timeout_seconds ?? null, max_turn_duration_seconds: agent.max_turn_duration_seconds ?? null, parallelism: agent.parallelism, system_prompt: agent.system_prompt, model: agent.model, has_api_token: agent.has_api_token, status: agent.status, pid: agent.pid, created_at: agent.created_at, updated_at: agent.updated_at, last_started_at: agent.last_started_at, last_stopped_at: agent.last_stopped_at, last_exit_code: agent.last_exit_code, last_error: agent.last_error, log_path: agent.log_path, start_on_app_launch: agent.start_on_app_launch, backend: agent.backend ?? { type: "local" as const }, backend_agent_id: agent.backend_agent_id ?? null, }; } function toMockToken(seed: RawMockTokenSeed): MockToken { return { ...cloneToken(seed), token: seed.token ?? `spr_tok_mock_${seed.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 24)}`, }; } function resetMockTokens(config: E2eConfig | undefined) { mockTokens = (config?.mock?.seededTokens ?? []).map(toMockToken); mockMintTokenError = config?.mock?.mintTokenError ?? null; } function resetMockManagedAgents() { mockManagedAgents = []; syncMockRelayAgentsFromManagedAgents(); } function resetMockPersonas() { const now = new Date().toISOString(); mockPersonas = [ { id: "builtin:solo", display_name: "Solo", avatar_url: null, system_prompt: "You are Solo.", is_builtin: true, is_active: false, created_at: now, updated_at: now, }, { id: "builtin:ralph", display_name: "Ralph", avatar_url: null, system_prompt: "You are Ralph.", is_builtin: true, is_active: false, created_at: now, updated_at: now, }, { id: "builtin:scout", display_name: "Scout", avatar_url: null, system_prompt: "You are Scout.", is_builtin: true, is_active: false, created_at: now, updated_at: now, }, { id: "builtin:reviewer", display_name: "Reviewer", avatar_url: null, system_prompt: "You are Reviewer.", is_builtin: true, is_active: false, created_at: now, updated_at: now, }, ]; } function resetMockTeams() { mockTeams = []; } function getMockProfileByPubkey(pubkey: string): RawProfile | null { const normalizedPubkey = pubkey.toLowerCase(); const existing = mockProfiles.get(normalizedPubkey); if (existing) { return existing; } if (!mockDisplayNames.has(normalizedPubkey)) { return null; } return { pubkey: normalizedPubkey, display_name: mockDisplayNames.get(normalizedPubkey) ?? null, avatar_url: null, about: null, nip05_handle: null, }; } function listMockProfiles(): RawProfile[] { const pubkeys = new Set([ ...mockProfiles.keys(), ...mockDisplayNames.keys(), DEFAULT_REAL_IDENTITY.pubkey, ]); return [...pubkeys] .map((pubkey) => getMockProfileByPubkey(pubkey)) .filter((profile): profile is RawProfile => profile !== null); } function listMockChannels(): RawChannelWithMembership[] { return mockChannels.map(toRawChannel); } function getMockChannel(channelId: string): MockChannel { const channel = mockChannels.find((candidate) => candidate.id === channelId); if (!channel) { throw new Error(`Channel ${channelId} not found.`); } return channel; } function getMockMemberPubkey(config: E2eConfig | undefined): string { return getIdentity(config)?.pubkey ?? getMockIdentity().pubkey; } function getMockMemberDisplayName(config: E2eConfig | undefined): string { return getIdentity(config)?.username ?? getMockIdentity().displayName; } function createCurrentMember( config: E2eConfig | undefined, role: RawChannelMember["role"], ): RawChannelMember { return { pubkey: getMockMemberPubkey(config), role, joined_at: new Date().toISOString(), display_name: getMockMemberDisplayName(config), }; } const mockChannels: MockChannel[] = [ createMockChannel({ id: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", name: "general", channel_type: "stream", visibility: "open", description: "General discussion for everyone", topic: "Company-wide updates", purpose: "Coordinate day-to-day work and unblock the team.", last_message_at: isoMinutesAgo(5), archived_at: null, created_by: MOCK_IDENTITY_PUBKEY, topic_set_by: MOCK_IDENTITY_PUBKEY, topic_set_at: isoMinutesAgo(90), purpose_set_by: MOCK_IDENTITY_PUBKEY, purpose_set_at: isoMinutesAgo(80), topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 1440, updated_minutes_ago: 5, members: [ createMockMember(MOCK_IDENTITY_PUBKEY, "owner", 1440), createMockMember(ALICE_PUBKEY, "admin", 1200), createMockMember(BOB_PUBKEY, "member", 960), ], }), createMockChannel({ id: "9dae0116-799b-5071-a0a8-fdd30a91a35d", name: "random", channel_type: "stream", visibility: "open", description: "Off-topic, fun stuff", topic: null, purpose: null, last_message_at: null, archived_at: null, created_by: ALICE_PUBKEY, topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 1400, updated_minutes_ago: 1400, members: [ createMockMember(ALICE_PUBKEY, "owner", 1400), createMockMember(MOCK_IDENTITY_PUBKEY, "member", 1300), createMockMember(BOB_PUBKEY, "member", 1000), ], }), createMockChannel({ id: "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e", name: "design", channel_type: "stream", visibility: "open", description: "Design system and UX discussions", topic: null, purpose: null, last_message_at: isoMinutesAgo(120), archived_at: null, created_by: ALICE_PUBKEY, topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 1350, updated_minutes_ago: 120, members: [ createMockMember(ALICE_PUBKEY, "owner", 1350), createMockMember(BOB_PUBKEY, "member", 1100), ], }), createMockChannel({ id: "c6f3a9b2-4d55-5a23-bf78-5b9e2g3c5d6f", name: "sales", channel_type: "stream", visibility: "open", description: "Sales team coordination and pipeline updates", topic: "Q1 targets", purpose: null, last_message_at: isoMinutesAgo(30), archived_at: null, created_by: BOB_PUBKEY, topic_set_by: BOB_PUBKEY, topic_set_at: isoMinutesAgo(200), purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 1300, updated_minutes_ago: 30, members: [ createMockMember(BOB_PUBKEY, "owner", 1300), createMockMember(CHARLIE_PUBKEY, "member", 900), ], }), createMockChannel({ id: "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9", name: "engineering", channel_type: "stream", visibility: "open", description: "Engineering discussions", topic: "Desktop release train", purpose: "Track implementation details and release readiness.", last_message_at: isoMinutesAgo(42), archived_at: null, created_by: ALICE_PUBKEY, topic_set_by: ALICE_PUBKEY, topic_set_at: isoMinutesAgo(120), purpose_set_by: ALICE_PUBKEY, purpose_set_at: isoMinutesAgo(130), topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 1320, updated_minutes_ago: 42, members: [ createMockMember(ALICE_PUBKEY, "owner", 1320), createMockMember(MOCK_IDENTITY_PUBKEY, "member", 1180), createMockMember(BOB_PUBKEY, "member", 900), ], }), createMockChannel({ id: "94a444a4-c0a3-5966-ab05-530c6ddc2301", name: "agents", channel_type: "stream", visibility: "open", description: "AI agent testing and collaboration", topic: "Coordination board", purpose: "Track agent work and relay activity.", last_message_at: isoMinutesAgo(15), archived_at: null, created_by: MOCK_IDENTITY_PUBKEY, topic_set_by: MOCK_IDENTITY_PUBKEY, topic_set_at: isoMinutesAgo(60), purpose_set_by: MOCK_IDENTITY_PUBKEY, purpose_set_at: isoMinutesAgo(65), topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 1000, updated_minutes_ago: 15, members: [ createMockMember(MOCK_IDENTITY_PUBKEY, "owner", 1000), createMockMember(CHARLIE_PUBKEY, "bot", 800), ], }), createMockChannel({ id: "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11", name: "watercooler", channel_type: "forum", visibility: "open", description: "Casual forum for async discussions", topic: null, purpose: null, last_message_at: null, archived_at: null, created_by: ALICE_PUBKEY, topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 900, updated_minutes_ago: 900, members: [ createMockMember(ALICE_PUBKEY, "owner", 900), createMockMember(MOCK_IDENTITY_PUBKEY, "member", 750), ], }), createMockChannel({ id: "1be1dcdb-4c31-5a8c-81de-ac102552ca10", name: "announcements", channel_type: "forum", visibility: "private", description: "Company announcements", topic: "Leadership updates", purpose: "Read-only announcements for the workspace.", last_message_at: null, archived_at: null, created_by: ALICE_PUBKEY, topic_set_by: ALICE_PUBKEY, topic_set_at: isoMinutesAgo(200), purpose_set_by: ALICE_PUBKEY, purpose_set_at: isoMinutesAgo(210), topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 880, updated_minutes_ago: 200, members: [ createMockMember(ALICE_PUBKEY, "owner", 880), createMockMember(MOCK_IDENTITY_PUBKEY, "guest", 700), ], }), createMockChannel({ id: "f48efb06-0c93-5025-aac9-2e646bb6bfa8", name: "alice-tyler", channel_type: "dm", visibility: "private", description: "DM between alice and tyler", topic: null, purpose: null, last_message_at: null, archived_at: null, created_by: ALICE_PUBKEY, topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: 2, nip29_group_id: null, created_minutes_ago: 720, updated_minutes_ago: 720, participants: ["alice", "tyler"], participant_pubkeys: [ALICE_PUBKEY, DEFAULT_REAL_IDENTITY.pubkey], members: [ createMockMember(ALICE_PUBKEY, "member", 720), createMockMember(DEFAULT_REAL_IDENTITY.pubkey, "member", 720), ], }), createMockChannel({ id: "7eb9f239-9393-50b0-bd76-d85eef0511c7", name: "bob-tyler", channel_type: "dm", visibility: "private", description: "DM between bob and tyler", topic: null, purpose: null, last_message_at: null, archived_at: null, created_by: BOB_PUBKEY, topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: 2, nip29_group_id: null, created_minutes_ago: 700, updated_minutes_ago: 700, participants: ["bob", "tyler"], participant_pubkeys: [BOB_PUBKEY, DEFAULT_REAL_IDENTITY.pubkey], members: [ createMockMember(BOB_PUBKEY, "member", 700), createMockMember(DEFAULT_REAL_IDENTITY.pubkey, "member", 700), ], }), ]; const mockMessages = new Map(); const mockSockets = new Map(); const realSockets = new Map(); let mockTokens: MockToken[] = []; let mockMintTokenError: string | null = null; let mockManagedAgents: MockManagedAgent[] = []; let mockPersonas: RawPersona[] = []; let mockTeams: RawTeam[] = []; let mockRelayAgents: RawRelayAgent[] = [ { pubkey: ALICE_PUBKEY, name: "alice", agent_type: "goose", channels: ["general", "agents"], channel_ids: [ "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", "94a444a4-c0a3-5966-ab05-530c6ddc2301", ], capabilities: ["search", "summaries", "workflows"], status: "online", }, { pubkey: CHARLIE_PUBKEY, name: "charlie", agent_type: "codex", channels: ["general"], channel_ids: ["9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"], capabilities: ["code", "reviews"], status: "away", }, ]; // ── Workflow mocks ───────────────────────────────────────────────────────── type MockWorkflow = { id: string; name: string; owner_pubkey: string; channel_id: string | null; definition: Record; status: "active" | "disabled" | "archived"; created_at: number; updated_at: number; }; type RawWorkflowTraceEntry = { step_id: string; status: string; output?: Record; started_at?: number | null; completed_at?: number | null; error?: string | null; }; type RawWorkflowRun = { id: string; workflow_id: string; status: | "pending" | "running" | "completed" | "failed" | "cancelled" | "waiting_approval"; current_step: number | null; execution_trace: RawWorkflowTraceEntry[]; started_at: number | null; completed_at: number | null; error_message: string | null; created_at: number; }; const mockWorkflows: MockWorkflow[] = []; let mockWorkflowRuns: RawWorkflowRun[] = []; let mockWorkflowIdCounter = 0; function resetMockWorkflows() { mockWorkflows.length = 0; mockWorkflowRuns = []; mockWorkflowIdCounter = 0; } function parseWorkflowDefinition( yamlDefinition: string, ): Record { const parsed = yamlParse(yamlDefinition); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("Workflow definition must be a YAML object"); } return parsed as Record; } function handleGetChannelWorkflows(args: { channelId: string }) { return mockWorkflows.filter((w) => w.channel_id === args.channelId); } function handleGetWorkflow(args: { workflowId: string }) { const workflow = mockWorkflows.find((w) => w.id === args.workflowId); if (!workflow) throw new Error(`Workflow ${args.workflowId} not found`); return workflow; } function handleCreateWorkflow(args: { channelId: string; yamlDefinition: string; }) { mockWorkflowIdCounter += 1; const now = Math.floor(Date.now() / 1000); const definition = parseWorkflowDefinition(args.yamlDefinition); const name = typeof definition.name === "string" ? definition.name : `workflow_${mockWorkflowIdCounter}`; const workflow: MockWorkflow = { id: `mock-wf-${mockWorkflowIdCounter}`, name, owner_pubkey: MOCK_IDENTITY_PUBKEY, channel_id: args.channelId, definition, status: "active", created_at: now, updated_at: now, }; mockWorkflows.push(workflow); const trigger = definition.trigger as Record | undefined; return { ...workflow, webhook_secret: trigger?.on === "webhook" ? `mock-webhook-secret-${mockWorkflowIdCounter}` : undefined, }; } function handleUpdateWorkflow(args: { workflowId: string; yamlDefinition: string; }) { const workflow = mockWorkflows.find((w) => w.id === args.workflowId); if (!workflow) throw new Error(`Workflow ${args.workflowId} not found`); const definition = parseWorkflowDefinition(args.yamlDefinition); if (typeof definition.name === "string") workflow.name = definition.name; workflow.definition = definition; workflow.updated_at = Math.floor(Date.now() / 1000); const trigger = definition.trigger as Record | undefined; return { ...workflow, webhook_secret: trigger?.on === "webhook" ? `mock-webhook-secret-${workflow.id}` : undefined, }; } function handleDeleteWorkflow(args: { workflowId: string }) { const index = mockWorkflows.findIndex((w) => w.id === args.workflowId); if (index === -1) throw new Error(`Workflow ${args.workflowId} not found`); mockWorkflows.splice(index, 1); mockWorkflowRuns = mockWorkflowRuns.filter( (run) => run.workflow_id !== args.workflowId, ); } function buildMockWorkflowRun(workflow: MockWorkflow): RawWorkflowRun { const createdAt = Math.floor(Date.now() / 1000); const rawSteps = Array.isArray(workflow.definition.steps) ? workflow.definition.steps : []; const executionTrace = rawSteps.map((candidate, index) => { const step = candidate && typeof candidate === "object" ? (candidate as Record) : {}; const startedAt = createdAt + index; const completedAt = startedAt + 1; const output: Record = {}; if (typeof step.action === "string") { output.action = step.action; } if (typeof step.name === "string" && step.name.trim().length > 0) { output.name = step.name; } if (typeof step.text === "string" && step.text.trim().length > 0) { output.preview = step.text; } return { step_id: typeof step.id === "string" && step.id.trim().length > 0 ? step.id : `step_${index + 1}`, status: "completed", output, started_at: startedAt, completed_at: completedAt, error: null, }; }); const startedAt = executionTrace.length > 0 ? (executionTrace[0].started_at ?? createdAt) : createdAt; const lastTraceEntry = executionTrace[executionTrace.length - 1]; const completedAt = executionTrace.length > 0 ? (lastTraceEntry?.completed_at ?? createdAt) : createdAt; return { id: `mock-run-${Date.now()}`, workflow_id: workflow.id, status: "completed", current_step: null, execution_trace: executionTrace, started_at: startedAt, completed_at: completedAt, error_message: null, created_at: createdAt, }; } function handleTriggerWorkflow(args: { workflowId: string }) { const workflow = mockWorkflows.find((w) => w.id === args.workflowId); if (!workflow) throw new Error(`Workflow ${args.workflowId} not found`); const run = buildMockWorkflowRun(workflow); mockWorkflowRuns = [run, ...mockWorkflowRuns]; return { run_id: run.id, workflow_id: workflow.id, status: run.status, }; } function handleGetWorkflowRuns(args: { limit?: number | null; workflowId: string; }) { const runs = mockWorkflowRuns.filter( (run) => run.workflow_id === args.workflowId, ); return args.limit ? runs.slice(0, args.limit) : runs; } function handleGetRunApprovals(_args: { workflowId: string; runId: string }) { return []; } const mockProfiles = new Map([ [ MOCK_IDENTITY_PUBKEY, { pubkey: MOCK_IDENTITY_PUBKEY, display_name: DEFAULT_MOCK_IDENTITY.display_name, avatar_url: null, about: null, nip05_handle: null, }, ], ]); const mockPresence = new Map([ [MOCK_IDENTITY_PUBKEY, "offline"], [DEFAULT_REAL_IDENTITY.pubkey, "offline"], [ALICE_PUBKEY, "online"], [BOB_PUBKEY, "away"], [CHARLIE_PUBKEY, "online"], [OUTSIDER_PUBKEY, "offline"], ]); const mockFeedOverrides: RawHomeFeedResponse["feed"] = { mentions: [], needs_action: [], activity: [], agent_activity: [], }; let installed = false; let nextSocketId = 1; function syncMockRelayAgentsFromManagedAgents() { const baseAgents = mockRelayAgents.filter( (agent) => !mockManagedAgents.some((managed) => managed.pubkey === agent.pubkey), ); const managedAgentsAsRelay: RawRelayAgent[] = mockManagedAgents.map( (agent) => { const memberships = getManagedAgentRelayMembership(agent.pubkey); return { pubkey: agent.pubkey, name: agent.name, agent_type: agent.agent_command, channels: memberships.channels, channel_ids: memberships.channelIds, capabilities: ["messages", "channels", "mcp"], status: agent.status === "running" || agent.status === "deployed" ? "online" : "offline", }; }, ); mockRelayAgents = [...baseAgents, ...managedAgentsAsRelay]; } function getManagedAgentRelayMembership(pubkey: string) { const memberships = mockChannels.filter((channel) => channel.members.some((member) => member.pubkey === pubkey), ); return { channelIds: memberships.map((channel) => channel.id), channels: memberships.map((channel) => channel.name), }; } function getConfig(): E2eConfig | undefined { return window.__SPROUT_E2E__; } function isRelayMode(config: E2eConfig | undefined): boolean { return config?.mode === "relay"; } function getRelayHttpUrl(config: E2eConfig | undefined): string { return config?.relayHttpUrl ?? DEFAULT_RELAY_HTTP_URL; } function getRelayWsUrl(config: E2eConfig | undefined): string { return config?.relayWsUrl ?? DEFAULT_RELAY_WS_URL; } function getIdentity(config: E2eConfig | undefined): TestIdentity | undefined { if (!isRelayMode(config)) { return undefined; } return config?.identity ?? DEFAULT_REAL_IDENTITY; } function ensureMockProfile(config: E2eConfig | undefined): RawProfile { const pubkey = getMockMemberPubkey(config); const existing = mockProfiles.get(pubkey); if (existing) { return existing; } const profile = { pubkey, display_name: getMockMemberDisplayName(config), avatar_url: null, about: null, nip05_handle: null, }; mockProfiles.set(pubkey, profile); return profile; } function applyMockDisplayName(pubkey: string, displayName: string | null) { if (displayName) { mockDisplayNames.set(pubkey, displayName); } else { mockDisplayNames.delete(pubkey); } for (const channel of mockChannels) { for (const member of channel.members) { if (member.pubkey === pubkey) { member.display_name = displayName; } } syncMockChannel(channel); } } function getMockPresenceStatus(pubkey: string): PresenceStatus { return mockPresence.get(pubkey.toLowerCase()) ?? "offline"; } function setMockPresenceStatus(pubkey: string, status: PresenceStatus) { mockPresence.set(pubkey.toLowerCase(), status); } function resolveHandler(handler: unknown): WsHandler { if (typeof handler === "function") { return handler as WsHandler; } if ( typeof handler === "object" && handler !== null && "onmessage" in handler && typeof handler.onmessage === "function" ) { return handler.onmessage as WsHandler; } throw new Error("Invalid websocket message handler."); } function sendWsText(handler: WsHandler, payload: unknown[]) { handler({ type: "Text", data: JSON.stringify(payload), }); } function sendWsClose(handler: WsHandler) { handler({ type: "Close", }); } function getChannelIdFromTags(tags: string[][]): string | undefined { return tags.find((tag) => tag[0] === "h")?.[1]; } function getThreadReferenceFromTags(tags: string[][]) { const eventTags = tags.filter( (tag) => tag[0] === "e" && typeof tag[1] === "string", ); if (eventTags.length === 0) { return { parentEventId: null, rootEventId: null, }; } const rootTag = eventTags.find((tag) => tag[3] === "root"); const replyTag = [...eventTags].reverse().find((tag) => tag[3] === "reply") ?? null; if (!replyTag) { return { parentEventId: null, rootEventId: null, }; } return { parentEventId: replyTag[1] ?? null, rootEventId: rootTag?.[1] ?? replyTag[1] ?? null, }; } function appendMentionTags( tags: string[][], mentionPubkeys: string[] | undefined, selfPubkey: string, ) { const selfLower = selfPubkey.toLowerCase(); const seen = new Set([selfLower]); for (const pk of mentionPubkeys ?? []) { const lower = pk.toLowerCase(); if (seen.has(lower)) { continue; } seen.add(lower); tags.push(["p", lower]); } } function buildTopLevelMessageTags( channelId: string, mentionPubkeys: string[] | undefined, selfPubkey: string, ) { const tags: string[][] = [["h", channelId]]; appendMentionTags(tags, mentionPubkeys, selfPubkey); return tags; } function buildReplyMessageTags( channelId: string, authorPubkey: string, parentEventId: string, rootEventId: string, mentionPubkeys: string[] | undefined, ) { // Preserve the reply tag ordering that the desktop message hooks already // expect locally: author p, h, mention ps, then thread e-tags. const tags: string[][] = [ ["p", authorPubkey], ["h", channelId], ]; appendMentionTags(tags, mentionPubkeys, authorPubkey); if (parentEventId === rootEventId) { tags.push(["e", rootEventId, "", "reply"]); return tags; } tags.push(["e", rootEventId, "", "root"]); tags.push(["e", parentEventId, "", "reply"]); return tags; } function getMockMessageStore(channelId: string): RelayEvent[] { const existing = mockMessages.get(channelId); if (existing) { return existing; } const seeded: RelayEvent[] = channelId === "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50" ? [ { id: "mock-general-welcome", pubkey: DEFAULT_MOCK_IDENTITY.pubkey, created_at: Math.floor(Date.now() / 1000), kind: 9, tags: [["h", channelId]], content: "Welcome to #general", sig: "mocksig".repeat(20).slice(0, 128), }, ] : channelId === "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11" ? [ { id: "mock-forum-release-thread", pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", created_at: Math.floor(Date.now() / 1000) - 90 * 60, kind: 45001, tags: [["h", channelId]], content: "Release checklist: async feedback thread.", sig: "mocksig".repeat(20).slice(0, 128), }, { id: "mock-forum-release-reply", pubkey: ALICE_PUBKEY, created_at: Math.floor(Date.now() / 1000) - 80 * 60, kind: 45003, tags: buildReplyMessageTags( channelId, ALICE_PUBKEY, "mock-forum-release-thread", "mock-forum-release-thread", undefined, ), content: "Looks good to me. We should ship it.", sig: "mocksig".repeat(20).slice(0, 128), }, ] : []; mockMessages.set(channelId, seeded); return seeded; } function emitMockHistory(socket: MockSocket, subId: string, channelId: string) { const events = getMockMessageStore(channelId); for (const event of events) { sendWsText(socket.handler, ["EVENT", subId, event]); } sendWsText(socket.handler, ["EOSE", subId]); } function emitMockLiveEvent(channelId: string, event: RelayEvent) { for (const socket of mockSockets.values()) { for (const [subId, subscribedChannelId] of socket.subscriptions) { if ( subscribedChannelId === channelId || subscribedChannelId === GLOBAL_MOCK_SUBSCRIPTION ) { sendWsText(socket.handler, ["EVENT", subId, event]); } } } } function hasMockLiveSubscription(channelId: string) { for (const socket of mockSockets.values()) { for (const subscribedChannelId of socket.subscriptions.values()) { if ( subscribedChannelId === channelId || subscribedChannelId === GLOBAL_MOCK_SUBSCRIPTION ) { return true; } } } return false; } function recordMockMessage(channelId: string, event: RelayEvent) { const history = getMockMessageStore(channelId); history.push(event); const channel = mockChannels.find((candidate) => candidate.id === channelId); if (!channel) { return; } channel.last_message_at = new Date(event.created_at * 1_000).toISOString(); touchMockChannel(channel); } function emitMockChannelMessage( channelId: string, content: string, pubkey?: string, ) { const event = createMockEvent(9, content, [["h", channelId]], pubkey); recordMockMessage(channelId, event); emitMockLiveEvent(channelId, event); return event; } function emitMockTypingIndicator(channelId: string, pubkey: string) { const event: RelayEvent = { id: crypto.randomUUID().replace(/-/g, ""), pubkey, created_at: Math.floor(Date.now() / 1000), kind: 20002, tags: [["h", channelId]], content: "", sig: "mocksig".repeat(20).slice(0, 128), }; emitMockLiveEvent(channelId, event); return event; } function toRawForumPost( event: RelayEvent, channelId: string, threadSummary: RawThreadSummary | null, ): RawForumPost { return { event_id: event.id, pubkey: event.pubkey, content: event.content, kind: event.kind, created_at: event.created_at, channel_id: channelId, tags: event.tags, thread_summary: threadSummary, reactions: null, }; } function toRawForumReply(event: RelayEvent, channelId: string): RawForumReply { const thread = getThreadReferenceFromTags(event.tags); return { event_id: event.id, pubkey: event.pubkey, content: event.content, kind: event.kind, created_at: event.created_at, channel_id: channelId, tags: event.tags, parent_event_id: thread.parentEventId, root_event_id: thread.rootEventId, depth: thread.rootEventId && thread.parentEventId !== thread.rootEventId ? 2 : 1, broadcast: false, reactions: null, }; } async function handleGetForumPosts(args: { channelId: string; limit?: number | null; before?: number | null; }): Promise { const events = getMockMessageStore(args.channelId); const posts = events .filter((event) => event.kind === 45001) .filter((event) => (args.before ? event.created_at < args.before : true)) .sort((left, right) => right.created_at - left.created_at) .slice(0, args.limit ?? 50) .map((event) => { const replies = events.filter((candidate) => { if (candidate.kind !== 45003) { return false; } const thread = getThreadReferenceFromTags(candidate.tags); return (thread.rootEventId ?? thread.parentEventId) === event.id; }); return toRawForumPost(event, args.channelId, { reply_count: replies.length, descendant_count: replies.length, last_reply_at: replies.length > 0 ? replies[replies.length - 1].created_at : null, participants: [...new Set(replies.map((reply) => reply.pubkey))], }); }); return { messages: posts, next_cursor: null, }; } async function handleGetForumThread(args: { channelId: string; eventId: string; }): Promise { const events = getMockMessageStore(args.channelId); const root = events.find( (event) => event.id === args.eventId && event.kind === 45001, ); if (!root) { throw new Error(`Mock forum thread not found: ${args.eventId}`); } const replies = events .filter((event) => event.kind === 45003) .filter((event) => { const thread = getThreadReferenceFromTags(event.tags); return (thread.rootEventId ?? thread.parentEventId) === root.id; }) .sort((left, right) => left.created_at - right.created_at) .map((event) => toRawForumReply(event, args.channelId)); return { root: toRawForumPost(root, args.channelId, { reply_count: replies.length, descendant_count: replies.length, last_reply_at: replies.length > 0 ? replies[replies.length - 1].created_at : null, participants: [...new Set(replies.map((reply) => reply.pubkey))], }), replies, total_replies: replies.length, next_cursor: null, }; } function getMockUserNotes(pubkey: string): RawUserNote[] { const now = Math.floor(Date.now() / 1000); if (pubkey === DEFAULT_MOCK_IDENTITY.pubkey) { return [ { id: "mock-note-launch", pubkey, created_at: now - 20 * 60, content: "Shipped the new desktop sidebar polish today.", }, { id: "mock-note-forum", pubkey, created_at: now - 3 * 60 * 60, content: "Forum threads feel like the right home for slower decisions.", }, ]; } if (pubkey === ALICE_PUBKEY) { return [ { id: "mock-alice-note-release", pubkey, created_at: now - 45 * 60, content: "Release checklist is ready for async feedback.", }, { id: "mock-alice-note-design", pubkey, created_at: now - 5 * 60 * 60, content: "Trying a lighter forum layout for longer-form notes.", }, ]; } return []; } async function handleGetUserNotes( args: { pubkey: string; limit?: number | null; before?: number | null; beforeId?: string | null; }, config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { const notes = getMockUserNotes(args.pubkey) .filter((note) => (args.before ? note.created_at < args.before : true)) .sort((left, right) => right.created_at - left.created_at) .slice(0, args.limit ?? 50); return { notes, next_cursor: null, }; } const url = new URL( `/api/users/${args.pubkey}/notes`, getRelayHttpUrl(config), ); if (args.limit !== undefined && args.limit !== null) { url.searchParams.set("limit", String(args.limit)); } if (args.before !== undefined && args.before !== null) { url.searchParams.set("before", String(args.before)); } if (args.beforeId) { url.searchParams.set("before_id", args.beforeId); } const response = await fetch(url, { headers: { "X-Pubkey": identity.pubkey, }, }); await assertOk(response); return response.json(); } function createMockEvent( kind: number, content: string, tags: string[][], pubkey = DEFAULT_MOCK_IDENTITY.pubkey, ): RelayEvent { return { id: crypto.randomUUID().replace(/-/g, ""), pubkey, created_at: Math.floor(Date.now() / 1000), kind, tags, content, sig: "mocksig".repeat(20).slice(0, 128), }; } async function signWithIdentity( identity: TestIdentity, template: { kind: number; content: string; tags: string[][]; }, ) { const secretKey = hexToBytes(identity.privateKey); return finalizeEvent( { kind: template.kind, content: template.content, tags: template.tags, created_at: Math.floor(Date.now() / 1000), }, secretKey, ); } async function assertOk(response: Response) { if (response.ok) { return; } const body = await response.text(); throw new Error(body || `Request failed with ${response.status}`); } function getRelayIdentity(config: E2eConfig | undefined): TestIdentity { const identity = getIdentity(config); if (!identity) { throw new Error("Relay identity required."); } return identity; } async function relayJsonRequest( config: E2eConfig | undefined, path: string, init: RequestInit = {}, ): Promise { const identity = getRelayIdentity(config); const headers = new Headers(init.headers); headers.set("X-Pubkey", identity.pubkey); if (init.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(`${getRelayHttpUrl(config)}${path}`, { ...init, headers, }); await assertOk(response); return response.json() as Promise; } async function relayEmptyRequest( config: E2eConfig | undefined, path: string, init: RequestInit = {}, ) { const identity = getRelayIdentity(config); const headers = new Headers(init.headers); headers.set("X-Pubkey", identity.pubkey); if (init.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(`${getRelayHttpUrl(config)}${path}`, { ...init, headers, }); await assertOk(response); } async function submitSignedEvent( config: E2eConfig | undefined, template: { kind: number; content: string; tags: string[][] }, ): Promise<{ event_id: string; accepted: boolean; message: string }> { const identity = getRelayIdentity(config); const signed = await signWithIdentity(identity, template); return relayJsonRequest(config, "/api/events", { method: "POST", body: JSON.stringify(signed), }); } async function handleGetChannels(config: E2eConfig | undefined) { const identity = getIdentity(config); if (!identity) { return listMockChannels(); } return relayJsonRequest(config, "/api/channels"); } async function handleGetProfile(config: E2eConfig | undefined) { const identity = getIdentity(config); if (!identity) { return cloneProfile(ensureMockProfile(config)); } return relayJsonRequest(config, "/api/users/me/profile"); } async function handleUpdateProfile( args: { displayName?: string; avatarUrl?: string; about?: string; nip05Handle?: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const profile = ensureMockProfile(config); const nextDisplayName = args.displayName?.trim(); const nextAvatarUrl = args.avatarUrl?.trim(); const nextAbout = args.about?.trim(); const nextNip05Handle = args.nip05Handle?.trim(); if (nextDisplayName && nextDisplayName !== profile.display_name) { profile.display_name = nextDisplayName; applyMockDisplayName(profile.pubkey, nextDisplayName); } if (nextAvatarUrl && nextAvatarUrl !== profile.avatar_url) { profile.avatar_url = nextAvatarUrl; } if (nextAbout && nextAbout !== profile.about) { profile.about = nextAbout; } if ( typeof nextNip05Handle === "string" && nextNip05Handle !== profile.nip05_handle ) { profile.nip05_handle = nextNip05Handle.length > 0 ? nextNip05Handle : null; } return cloneProfile(profile); } // Read-merge-write: fetch current profile, merge, sign kind:0. const current = await relayJsonRequest( config, "/api/users/me/profile", ); const profileContent = JSON.stringify({ display_name: args.displayName ?? current.display_name ?? undefined, name: current.display_name ?? undefined, picture: args.avatarUrl ?? current.avatar_url ?? undefined, about: args.about ?? current.about ?? undefined, nip05: args.nip05Handle ?? current.nip05_handle ?? undefined, }); await submitSignedEvent(config, { kind: 0, content: profileContent, tags: [], }); return relayJsonRequest(config, "/api/users/me/profile"); } async function handleGetUserProfile( args: { pubkey?: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const pubkey = (args.pubkey ?? getMockMemberPubkey(config)).toLowerCase(); const profile = getMockProfileByPubkey(pubkey); if (!profile) { throw new Error(`User ${pubkey} not found.`); } return cloneProfile(profile); } const path = args.pubkey ? `/api/users/${args.pubkey}/profile` : "/api/users/me/profile"; return relayJsonRequest(config, path); } async function handleGetUsersBatch( args: { pubkeys: string[]; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const profiles: RawUsersBatchResponse["profiles"] = {}; const missing: string[] = []; for (const pubkey of args.pubkeys) { const normalizedPubkey = pubkey.toLowerCase(); const profile = getMockProfileByPubkey(normalizedPubkey); if (!profile) { missing.push(pubkey); continue; } profiles[normalizedPubkey] = { display_name: profile.display_name, avatar_url: profile.avatar_url, nip05_handle: profile.nip05_handle, }; } return { profiles, missing, }; } return relayJsonRequest(config, "/api/users/batch", { method: "POST", body: JSON.stringify({ pubkeys: args.pubkeys, }), }); } async function handleSearchUsers( args: { query: string; limit?: number; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const normalizedQuery = args.query.trim().toLowerCase(); if (normalizedQuery.length === 0) { return { users: [] } satisfies RawSearchUsersResponse; } const results = listMockProfiles() .filter((profile) => { const displayName = profile.display_name?.toLowerCase() ?? ""; const nip05Handle = profile.nip05_handle?.toLowerCase() ?? ""; const pubkey = profile.pubkey.toLowerCase(); return ( displayName.includes(normalizedQuery) || nip05Handle.includes(normalizedQuery) || pubkey.includes(normalizedQuery) ); }) .sort((left, right) => { const leftName = left.display_name ?? left.nip05_handle ?? left.pubkey; const rightName = right.display_name ?? right.nip05_handle ?? right.pubkey; return leftName.localeCompare(rightName); }) .slice(0, args.limit ?? 8) .map((profile) => ({ pubkey: profile.pubkey, display_name: profile.display_name, avatar_url: profile.avatar_url, nip05_handle: profile.nip05_handle, })); return { users: results, } satisfies RawSearchUsersResponse; } const searchParams = new URLSearchParams(); searchParams.set("q", args.query); searchParams.set("limit", String(args.limit ?? 8)); return relayJsonRequest( config, `/api/users/search?${searchParams.toString()}`, ); } async function handleGetPresence( args: { pubkeys: string[]; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { return Object.fromEntries( args.pubkeys.map((pubkey) => [ pubkey.toLowerCase(), getMockPresenceStatus(pubkey), ]), ) satisfies RawPresenceLookup; } if (args.pubkeys.length === 0) { return {} satisfies RawPresenceLookup; } const searchParams = new URLSearchParams(); searchParams.set("pubkeys", args.pubkeys.join(",")); return relayJsonRequest( config, `/api/presence?${searchParams.toString()}`, ); } async function handleSetPresence( args: { status: PresenceStatus; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { setMockPresenceStatus(getMockMemberPubkey(config), args.status); return { status: args.status, ttl_seconds: args.status === "offline" ? 0 : MOCK_PRESENCE_TTL_SECONDS, } satisfies RawSetPresenceResponse; } return relayJsonRequest(config, "/api/presence", { method: "PUT", body: JSON.stringify({ status: args.status, }), }); } async function handleCreateChannel( args: { name: string; channelType: "stream" | "forum"; visibility: "open" | "private"; description?: string; ttlSeconds?: number; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); const ttlDeadline = typeof args.ttlSeconds === "number" ? new Date(Date.now() + args.ttlSeconds * 1_000).toISOString() : null; if (!identity) { const owner = createCurrentMember(config, "owner"); const channel = createMockChannel({ id: crypto.randomUUID(), name: args.name, channel_type: args.channelType, visibility: args.visibility, description: args.description ?? "", topic: null, purpose: null, last_message_at: null, archived_at: null, created_by: owner.pubkey, topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, ttl_seconds: args.ttlSeconds ?? null, ttl_deadline: ttlDeadline, topic_required: false, max_members: null, nip29_group_id: null, created_minutes_ago: 0, updated_minutes_ago: 0, members: [owner], }); mockChannels.push(channel); return toRawChannel(channel); } const channelId = crypto.randomUUID(); const tags: string[][] = [ ["h", channelId], ["name", args.name], ["channel_type", args.channelType], ["visibility", args.visibility], ]; if (args.description) { tags.push(["about", args.description]); } if (typeof args.ttlSeconds === "number") { tags.push(["ttl", String(args.ttlSeconds)]); } await submitSignedEvent(config, { kind: 9007, content: "", tags }); // Fetch the created channel to return the expected shape. const channels = await relayJsonRequest( config, "/api/channels", ); const created = channels.find((ch) => ch.name === args.name); if (!created) { throw new Error(`Channel "${args.name}" not found after creation`); } return created; } async function handleOpenDm( args: { pubkeys: string[]; }, config: E2eConfig | undefined, ) { const normalizedPubkeys = normalizeParticipantPubkeys(args.pubkeys); if (normalizedPubkeys.length === 0) { throw new Error("Select at least one person to start a DM."); } const currentPubkey = getMockMemberPubkey(config).toLowerCase(); const participantPubkeys = normalizeParticipantPubkeys([ currentPubkey, ...normalizedPubkeys.filter((pubkey) => pubkey !== currentPubkey), ]); const existingChannel = findMockDmByParticipantPubkeys(participantPubkeys); if (existingChannel) { return toRawChannel(existingChannel); } const identity = getIdentity(config); if (!identity) { const members = participantPubkeys.map((pubkey) => createMockMember(pubkey, "member", 0), ); const channel = createMockChannel({ id: crypto.randomUUID(), name: participantPubkeys.length === 2 ? "DM" : `Group DM (${participantPubkeys.length})`, channel_type: "dm", visibility: "private", description: "Direct message conversation", topic: null, purpose: null, last_message_at: null, archived_at: null, created_by: getMockMemberPubkey(config), topic_set_by: null, topic_set_at: null, purpose_set_by: null, purpose_set_at: null, topic_required: false, max_members: participantPubkeys.length, nip29_group_id: null, created_minutes_ago: 0, updated_minutes_ago: 0, members, }); syncMockChannel(channel); mockChannels.push(channel); return toRawChannel(channel); } const response = await relayJsonRequest( config, "/api/dms", { method: "POST", body: JSON.stringify({ pubkeys: normalizedPubkeys, }), }, ); return relayJsonRequest( config, `/api/channels/${response.channel_id}`, ); } async function handleHideDm( args: { channelId: string }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const index = mockChannels.findIndex( (channel) => channel.id === args.channelId, ); if (index === -1) { throw new Error(`DM ${args.channelId} not found.`); } // Remove from mock list (simulates hiding from sidebar). mockChannels.splice(index, 1); return; } await relayEmptyRequest(config, `/api/dms/${args.channelId}/hide`, { method: "POST", }); } async function handleGetChannelDetails( args: { channelId: string }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { return toRawChannelDetail(getMockChannel(args.channelId)); } return relayJsonRequest( config, `/api/channels/${args.channelId}`, ); } async function handleGetChannelMembers( args: { channelId: string }, config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); return { members: cloneMembers(channel.members), next_cursor: null, }; } return relayJsonRequest( config, `/api/channels/${args.channelId}/members`, ); } async function handleUpdateChannel( args: { channelId: string; name?: string; description?: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); if (args.name !== undefined) { channel.name = args.name; } if (args.description !== undefined) { channel.description = args.description; } touchMockChannel(channel); return toRawChannelDetail(channel); } const tags: string[][] = [["h", args.channelId]]; if (args.name !== undefined) { tags.push(["name", args.name]); } if (args.description !== undefined) { tags.push(["about", args.description]); } await submitSignedEvent(config, { kind: 9002, content: "", tags }); return relayJsonRequest( config, `/api/channels/${args.channelId}`, ); } async function handleSetChannelTopic( args: { channelId: string; topic: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); const nextTopic = args.topic.trim(); channel.topic = nextTopic.length > 0 ? nextTopic : null; channel.topic_set_by = getMockMemberPubkey(config); channel.topic_set_at = new Date().toISOString(); touchMockChannel(channel); return; } await submitSignedEvent(config, { kind: 9002, content: "", tags: [ ["h", args.channelId], ["topic", args.topic], ], }); } async function handleSetChannelPurpose( args: { channelId: string; purpose: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); const nextPurpose = args.purpose.trim(); channel.purpose = nextPurpose.length > 0 ? nextPurpose : null; channel.purpose_set_by = getMockMemberPubkey(config); channel.purpose_set_at = new Date().toISOString(); touchMockChannel(channel); return; } await submitSignedEvent(config, { kind: 9002, content: "", tags: [ ["h", args.channelId], ["purpose", args.purpose], ], }); } async function handleArchiveChannel( args: { channelId: string }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); channel.archived_at = new Date().toISOString(); touchMockChannel(channel); return; } await submitSignedEvent(config, { kind: 9002, content: "", tags: [ ["h", args.channelId], ["archived", "true"], ], }); } async function handleUnarchiveChannel( args: { channelId: string }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); channel.archived_at = null; touchMockChannel(channel); return; } await submitSignedEvent(config, { kind: 9002, content: "", tags: [ ["h", args.channelId], ["archived", "false"], ], }); } async function handleDeleteChannel( args: { channelId: string }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const index = mockChannels.findIndex( (channel) => channel.id === args.channelId, ); if (index === -1) { throw new Error(`Channel ${args.channelId} not found.`); } mockChannels.splice(index, 1); mockMessages.delete(args.channelId); return; } await submitSignedEvent(config, { kind: 9008, content: "", tags: [["h", args.channelId]], }); } async function handleAddChannelMembers( args: { channelId: string; pubkeys: string[]; role?: RawChannelMember["role"]; }, config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); const added: string[] = []; const errors: RawAddChannelMembersResponse["errors"] = []; for (const pubkey of args.pubkeys) { if (channel.members.some((member) => member.pubkey === pubkey)) { errors.push({ pubkey, error: "Already a member.", }); continue; } channel.members.push({ pubkey, role: args.role ?? "member", joined_at: new Date().toISOString(), display_name: mockDisplayNames.get(pubkey) ?? null, }); added.push(pubkey); } syncMockChannel(channel); touchMockChannel(channel); syncMockRelayAgentsFromManagedAgents(); return { added, errors, }; } const added: string[] = []; const errors: RawAddChannelMembersResponse["errors"] = []; for (const pubkey of args.pubkeys) { try { const tags: string[][] = [ ["h", args.channelId], ["p", pubkey], ]; if (args.role) { tags.push(["role", args.role]); } await submitSignedEvent(config, { kind: 9000, content: "", tags }); added.push(pubkey); } catch (e) { errors.push({ pubkey, error: String(e) }); } } return { added, errors }; } async function handleRemoveChannelMember( args: { channelId: string; pubkey: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); channel.members = channel.members.filter( (member) => member.pubkey !== args.pubkey, ); syncMockChannel(channel); touchMockChannel(channel); syncMockRelayAgentsFromManagedAgents(); return; } await submitSignedEvent(config, { kind: 9001, content: "", tags: [ ["h", args.channelId], ["p", args.pubkey], ], }); } async function handleJoinChannel( args: { channelId: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); const currentPubkey = getMockMemberPubkey(config); if (channel.members.some((member) => member.pubkey === currentPubkey)) { return; } channel.members.push(createCurrentMember(config, "member")); syncMockChannel(channel); touchMockChannel(channel); return; } await submitSignedEvent(config, { kind: 9021, content: "", tags: [["h", args.channelId]], }); } async function handleLeaveChannel( args: { channelId: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); const currentPubkey = getMockMemberPubkey(config); channel.members = channel.members.filter( (member) => member.pubkey !== currentPubkey, ); syncMockChannel(channel); touchMockChannel(channel); return; } await submitSignedEvent(config, { kind: 9022, content: "", tags: [["h", args.channelId]], }); } async function handleGetFeed( args: { since?: number; limit?: number; types?: string; }, config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { const now = Math.floor(Date.now() / 1000); const limit = args.limit ?? 50; const wantedTypes = args.types ?.split(",") .map((value) => value.trim()) .filter((value) => value.length > 0) ?? []; const includeType = (type: string) => wantedTypes.length === 0 || wantedTypes.includes(type); const defaultFeed: RawHomeFeedResponse["feed"] = { mentions: [ { id: "mock-feed-mention", kind: 9, pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", content: "Please review the release checklist.", created_at: now - 90, channel_id: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", channel_name: "general", tags: [ ["e", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"], ["p", DEFAULT_MOCK_IDENTITY.pubkey], ], category: "mention" as const, }, ], needs_action: [ { id: "mock-feed-reminder", kind: 40007, pubkey: "0000000000000000000000000000000000000000000000000000000000000000", content: "Reminder: update the launch plan before lunch.", created_at: now - 15 * 60, channel_id: "94a444a4-c0a3-5966-ab05-530c6ddc2301", channel_name: "agents", tags: [ ["e", "94a444a4-c0a3-5966-ab05-530c6ddc2301"], ["p", DEFAULT_MOCK_IDENTITY.pubkey], ], category: "needs_action" as const, }, ], activity: [ { id: "mock-feed-self-activity", kind: 9, pubkey: DEFAULT_MOCK_IDENTITY.pubkey, content: "I posted a note about the launch checklist.", created_at: now - 25 * 60, channel_id: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", channel_name: "general", tags: [["e", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"]], category: "activity" as const, }, { id: "mock-feed-activity", kind: 9, pubkey: "bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260", content: "Engineering shipped the desktop build.", created_at: now - 42 * 60, channel_id: "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9", channel_name: "engineering", tags: [["e", "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9"]], category: "activity" as const, }, ], agent_activity: [ { id: "mock-feed-agent", kind: 43003, pubkey: "db0b028cd36f4d3e36c8300cce87252c1f7fc9495ffecc53f393fcac341ffd36", content: "Agent progress: channel index complete.", created_at: now - 2 * 60 * 60, channel_id: "94a444a4-c0a3-5966-ab05-530c6ddc2301", channel_name: "agents", tags: [["e", "94a444a4-c0a3-5966-ab05-530c6ddc2301"]], category: "agent_activity" as const, }, ], }; const mergeFeedCategory = ( category: keyof RawHomeFeedResponse["feed"], ): RawFeedItem[] => includeType(category) ? [...mockFeedOverrides[category], ...defaultFeed[category]] .sort((left, right) => right.created_at - left.created_at) .slice(0, limit) : []; const mentions = mergeFeedCategory("mentions"); const needsAction = mergeFeedCategory("needs_action"); const activity = mergeFeedCategory("activity"); const agentActivity = mergeFeedCategory("agent_activity"); return { feed: { mentions, needs_action: needsAction, activity, agent_activity: agentActivity, }, meta: { since: args.since ?? now - 7 * 24 * 60 * 60, total: mentions.length + needsAction.length + activity.length + agentActivity.length, generated_at: now, }, }; } const url = new URL("/api/feed", getRelayHttpUrl(config)); if (args.since !== undefined) { url.searchParams.set("since", String(args.since)); } if (args.limit !== undefined) { url.searchParams.set("limit", String(args.limit)); } if (args.types) { url.searchParams.set("types", args.types); } const response = await fetch(url, { headers: { "X-Pubkey": identity.pubkey, }, }); await assertOk(response); return response.json(); } async function handleListTokens( config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { return { tokens: mockTokens.map(cloneToken), }; } return relayJsonRequest(config, "/api/tokens"); } async function handleMintToken( args: { name: string; scopes: string[]; channelIds?: string[]; expiresInDays?: number; }, config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { if (mockMintTokenError) { throw mockMintTokenError; } const now = new Date(); const token: MockToken = { id: crypto.randomUUID(), name: args.name, scopes: [...args.scopes], channel_ids: [...(args.channelIds ?? [])], created_at: now.toISOString(), expires_at: typeof args.expiresInDays === "number" ? new Date( now.getTime() + args.expiresInDays * 24 * 60 * 60 * 1_000, ).toISOString() : null, last_used_at: null, revoked_at: null, token: `spr_tok_mock_${crypto.randomUUID().replace(/-/g, "")}`, }; mockTokens.unshift(token); return cloneMintedToken(token); } return relayJsonRequest(config, "/api/tokens", { method: "POST", body: JSON.stringify({ name: args.name, scopes: args.scopes, channel_ids: args.channelIds, expires_in_days: args.expiresInDays, }), }); } async function handleRevokeToken( args: { tokenId: string }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const token = mockTokens.find((candidate) => candidate.id === args.tokenId); if (!token) { throw new Error(`Token ${args.tokenId} not found.`); } token.revoked_at = new Date().toISOString(); return; } await relayEmptyRequest(config, `/api/tokens/${args.tokenId}`, { method: "DELETE", }); } async function handleRevokeAllTokens( config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { const now = new Date().toISOString(); let revokedCount = 0; for (const token of mockTokens) { if (token.revoked_at) { continue; } token.revoked_at = now; revokedCount += 1; } return { revoked_count: revokedCount, }; } return relayJsonRequest(config, "/api/tokens", { method: "DELETE", }); } async function handleListRelayAgents(): Promise { syncMockRelayAgentsFromManagedAgents(); return mockRelayAgents.map(cloneRelayAgent); } async function handleDiscoverAcpProviders(): Promise { return [ { id: "goose", label: "Goose", command: "goose", binary_path: "/usr/local/bin/goose", default_args: ["acp"], }, { id: "codex", label: "Codex", command: "codex-acp", binary_path: "/usr/local/bin/codex-acp", default_args: [], }, ]; } async function handleDiscoverManagedAgentPrereqs(args: { input?: { acpCommand?: string; mcpCommand?: string; }; }): Promise { return { admin: { command: "sprout-admin", resolved_path: "/Users/wesb/dev/sprout/target/debug/sprout-admin", available: true, }, acp: { command: args.input?.acpCommand ?? "sprout-acp", resolved_path: "/Users/wesb/dev/sprout/target/debug/sprout-acp", available: true, }, mcp: { command: args.input?.mcpCommand ?? "sprout-mcp-server", resolved_path: "/Users/wesb/dev/sprout/target/debug/sprout-mcp-server", available: true, }, }; } async function handleListManagedAgents(): Promise { return mockManagedAgents.map(cloneManagedAgent); } async function handleListPersonas(): Promise { return mockPersonas.map((persona) => ({ ...persona })); } async function handleCreatePersona(args: { input: { displayName: string; avatarUrl?: string; systemPrompt: string; }; }): Promise { const now = new Date().toISOString(); const persona: RawPersona = { id: crypto.randomUUID(), display_name: args.input.displayName.trim(), avatar_url: args.input.avatarUrl?.trim() || null, system_prompt: args.input.systemPrompt.trim(), is_builtin: false, is_active: true, created_at: now, updated_at: now, }; mockPersonas.push(persona); return { ...persona }; } async function handleUpdatePersona(args: { input: { id: string; displayName: string; avatarUrl?: string; systemPrompt: string; }; }): Promise { const persona = mockPersonas.find( (candidate) => candidate.id === args.input.id, ); if (!persona) { throw new Error(`Persona ${args.input.id} not found.`); } if (persona.is_builtin) { throw new Error("Built-in personas cannot be edited."); } persona.display_name = args.input.displayName.trim(); persona.avatar_url = args.input.avatarUrl?.trim() || null; persona.system_prompt = args.input.systemPrompt.trim(); persona.updated_at = new Date().toISOString(); return { ...persona }; } async function handleDeletePersona(args: { id: string }): Promise { const persona = mockPersonas.find((candidate) => candidate.id === args.id); if (!persona) { throw new Error(`Persona ${args.id} not found.`); } if (persona.is_builtin) { throw new Error("Built-in personas cannot be deleted."); } if (mockTeams.some((team) => team.persona_ids.includes(args.id))) { throw new Error( `${persona.display_name} is still referenced by a team. Remove it from those teams first.`, ); } mockPersonas = mockPersonas.filter((candidate) => candidate.id !== args.id); const now = new Date().toISOString(); for (const agent of mockManagedAgents) { if (agent.persona_id === args.id) { agent.persona_id = null; agent.updated_at = now; } } } async function handleSetPersonaActive(args: { id: string; active: boolean; }): Promise { const persona = mockPersonas.find((candidate) => candidate.id === args.id); if (!persona) { throw new Error(`Persona ${args.id} not found.`); } if (!persona.is_builtin) { throw new Error( "Only built-in personas can be added to or removed from My Agents.", ); } if ( !args.active && mockManagedAgents.some((agent) => agent.persona_id === args.id) ) { throw new Error( `${persona.display_name} is still assigned to a managed agent. Remove or reassign those agents first.`, ); } if ( !args.active && mockTeams.some((team) => team.persona_ids.includes(args.id)) ) { throw new Error( `${persona.display_name} is still referenced by a team. Remove it from those teams first.`, ); } persona.is_active = args.active; persona.updated_at = new Date().toISOString(); return { ...persona }; } function ensureMockPersonaIsActive(personaId: string) { const persona = mockPersonas.find((candidate) => candidate.id === personaId); if (!persona) { throw new Error(`persona ${personaId} not found`); } if (!persona.is_active) { throw new Error( `${persona.display_name} is not in My Agents. Choose it from Persona Catalog first.`, ); } } function ensureMockPersonaIdsAreActive(personaIds: string[]) { for (const personaId of personaIds) { ensureMockPersonaIsActive(personaId); } } async function handleListTeams(): Promise { return mockTeams.map((team) => ({ ...team, persona_ids: [...team.persona_ids], })); } async function handleCreateTeam(args: { input: { name: string; description?: string; personaIds: string[]; }; }): Promise { ensureMockPersonaIdsAreActive(args.input.personaIds); const now = new Date().toISOString(); const team: RawTeam = { id: crypto.randomUUID(), name: args.input.name.trim(), description: args.input.description?.trim() || null, persona_ids: [...args.input.personaIds], created_at: now, updated_at: now, }; mockTeams.push(team); return { ...team, persona_ids: [...team.persona_ids] }; } async function handleUpdateTeam(args: { input: { id: string; name: string; description?: string; personaIds: string[]; }; }): Promise { const team = mockTeams.find((candidate) => candidate.id === args.input.id); if (!team) { throw new Error(`Team ${args.input.id} not found.`); } ensureMockPersonaIdsAreActive(args.input.personaIds); team.name = args.input.name.trim(); team.description = args.input.description?.trim() || null; team.persona_ids = [...args.input.personaIds]; team.updated_at = new Date().toISOString(); return { ...team, persona_ids: [...team.persona_ids] }; } async function handleDeleteTeam(args: { id: string }): Promise { mockTeams = mockTeams.filter((candidate) => candidate.id !== args.id); } async function handleExportTeamToJson(args: { id: string }): Promise { const team = mockTeams.find((candidate) => candidate.id === args.id); if (!team) { throw new Error(`Team ${args.id} not found.`); } const missingPersonaIds = team.persona_ids.filter( (personaId) => !mockPersonas.some((candidate) => candidate.id === personaId), ); if (missingPersonaIds.length > 0) { throw new Error( `Team ${team.name} references missing personas: ${missingPersonaIds.join(", ")}. Repair the team before exporting.`, ); } return true; } async function handleParseTeamFile(): Promise<{ name: string; description: string | null; personas: Array<{ display_name: string; system_prompt: string; avatar_url: string | null; }>; }> { return { name: "Imported Team", description: null, personas: [], }; } async function handleParsePersonaFiles(args: { fileBytes: number[]; fileName: string; }): Promise<{ personas: { display_name: string; system_prompt: string; avatar_data_url: string | null; source_file: string; }[]; skipped: { source_file: string; reason: string }[]; }> { // In test mode, return canned data — we can't actually parse PNG chunks in JS return { personas: [ { display_name: "Imported Persona", system_prompt: "You are an imported test persona.", avatar_data_url: null, source_file: args.fileName, }, ], skipped: [], }; } async function handleExportPersonaToJson(args: { id: string; }): Promise { // In test mode, just verify the persona exists const persona = mockPersonas.find((p) => p.id === args.id); if (!persona) throw new Error(`Persona ${args.id} not found.`); return true; // Simulate successful save } async function handleCreateManagedAgent(args: { input: { name: string; personaId?: string; relayUrl?: string; acpCommand?: string; agentCommand?: string; agentArgs?: string[]; mcpCommand?: string; turnTimeoutSeconds?: number; idleTimeoutSeconds?: number; maxTurnDurationSeconds?: number; parallelism?: number; systemPrompt?: string; avatarUrl?: string; model?: string; mintToken?: boolean; tokenScopes?: string[]; tokenName?: string; spawnAfterCreate?: boolean; startOnAppLaunch?: boolean; backend?: | { type: "local" } | { type: "provider"; id: string; config: Record }; }; }): Promise { if (args.input.personaId) { ensureMockPersonaIsActive(args.input.personaId); } const name = args.input.name.trim(); const now = new Date().toISOString(); const pubkey = crypto .randomUUID() .replace(/-/g, "") .padEnd(64, "0") .slice(0, 64); const token = args.input.mintToken === false ? null : `spr_tok_mock_${crypto.randomUUID().replace(/-/g, "")}`; const managedAgent: MockManagedAgent = { pubkey, name, persona_id: args.input.personaId ?? null, relay_url: args.input.relayUrl ?? DEFAULT_RELAY_WS_URL, acp_command: args.input.acpCommand ?? "sprout-acp", agent_command: args.input.agentCommand ?? "goose", agent_args: args.input.agentArgs && args.input.agentArgs.length > 0 ? [...args.input.agentArgs] : ["acp"], mcp_command: args.input.mcpCommand ?? "sprout-mcp-server", turn_timeout_seconds: args.input.turnTimeoutSeconds ?? 320, idle_timeout_seconds: args.input.idleTimeoutSeconds ?? null, max_turn_duration_seconds: args.input.maxTurnDurationSeconds ?? null, parallelism: args.input.parallelism ?? 1, system_prompt: args.input.systemPrompt?.trim() || null, model: args.input.model?.trim() || null, has_api_token: token !== null, status: args.input.spawnAfterCreate ? "running" : "stopped", pid: args.input.spawnAfterCreate ? 42000 + mockManagedAgents.length : null, created_at: now, updated_at: now, last_started_at: args.input.spawnAfterCreate ? now : null, last_stopped_at: null, last_exit_code: null, last_error: null, log_path: `/tmp/mock-agent-${pubkey}.log`, start_on_app_launch: args.input.startOnAppLaunch ?? true, backend: args.input.backend ?? { type: "local" as const }, backend_agent_id: null, private_key_nsec: `nsec1mock${pubkey.slice(0, 20)}`, api_token: token, log_lines: [ `sprout-acp starting: relay=${args.input.relayUrl ?? DEFAULT_RELAY_WS_URL} agent_pubkey=${pubkey} parallelism=${args.input.parallelism ?? 1}`, args.input.systemPrompt?.trim() ? `system prompt override configured (${args.input.systemPrompt.trim().length} chars)` : "system prompt override not set", args.input.spawnAfterCreate ? "connected to relay at ws://localhost:3000" : "profile created; harness not started", ], }; mockManagedAgents.unshift(managedAgent); syncMockRelayAgentsFromManagedAgents(); return { agent: cloneManagedAgent(managedAgent), private_key_nsec: managedAgent.private_key_nsec, api_token: managedAgent.api_token, profile_sync_error: null, spawn_error: null, }; } function getMockManagedAgent(pubkey: string): MockManagedAgent { const agent = mockManagedAgents.find( (candidate) => candidate.pubkey === pubkey, ); if (!agent) { throw new Error(`Managed agent ${pubkey} not found.`); } return agent; } async function handleStartManagedAgent(args: { pubkey: string; }): Promise { const agent = getMockManagedAgent(args.pubkey); const now = new Date().toISOString(); agent.status = "running"; agent.pid = agent.pid ?? 42000 + mockManagedAgents.indexOf(agent); agent.updated_at = now; agent.last_started_at = now; agent.last_error = null; agent.log_lines.push(`started mock harness at ${now}`); syncMockRelayAgentsFromManagedAgents(); return cloneManagedAgent(agent); } async function handleStopManagedAgent(args: { pubkey: string; }): Promise { const agent = getMockManagedAgent(args.pubkey); const now = new Date().toISOString(); agent.status = "stopped"; agent.pid = null; agent.updated_at = now; agent.last_stopped_at = now; agent.log_lines.push(`stopped mock harness at ${now}`); syncMockRelayAgentsFromManagedAgents(); return cloneManagedAgent(agent); } async function handleDeleteManagedAgent(args: { pubkey: string; forceRemoteDelete?: boolean | null; }): Promise { // Model the backend invariant: reject deletion of deployed remote agents // unless force_remote_delete is true. const agent = mockManagedAgents.find((a) => a.pubkey === args.pubkey); if ( agent && agent.backend.type === "provider" && agent.backend_agent_id != null && !args.forceRemoteDelete ) { throw new Error( "cannot delete a deployed remote agent without force_remote_delete: true", ); } mockManagedAgents = mockManagedAgents.filter( (candidate) => candidate.pubkey !== args.pubkey, ); syncMockRelayAgentsFromManagedAgents(); } async function handleSetManagedAgentStartOnAppLaunch(args: { pubkey: string; startOnAppLaunch: boolean; }): Promise { const agent = getMockManagedAgent(args.pubkey); agent.start_on_app_launch = args.startOnAppLaunch; agent.updated_at = new Date().toISOString(); return cloneManagedAgent(agent); } async function handleMintManagedAgentToken(args: { input: { pubkey: string; tokenName?: string; scopes?: string[]; }; }): Promise { const agent = getMockManagedAgent(args.input.pubkey); const now = new Date().toISOString(); agent.api_token = `spr_tok_mock_${crypto.randomUUID().replace(/-/g, "")}`; agent.has_api_token = true; agent.updated_at = now; agent.log_lines.push( `minted token ${args.input.tokenName ?? `${agent.name}-token`} at ${now}`, ); return { agent: cloneManagedAgent(agent), token: agent.api_token ?? "", }; } async function handleGetManagedAgentLog(args: { pubkey: string; lineCount?: number; }): Promise { const agent = getMockManagedAgent(args.pubkey); const count = args.lineCount ?? 120; return { content: agent.log_lines.slice(-count).join("\n"), log_path: agent.log_path, }; } async function handleUpdateManagedAgent(args: { input: { pubkey: string; name?: string; model?: string | null; systemPrompt?: string | null; }; }): Promise<{ agent: RawManagedAgent; profile_sync_error: string | null }> { const agent = getMockManagedAgent(args.input.pubkey); if (args.input.name !== undefined) { agent.name = args.input.name; } if (args.input.model !== undefined) { agent.model = args.input.model; } if (args.input.systemPrompt !== undefined) { agent.system_prompt = args.input.systemPrompt; } agent.updated_at = new Date().toISOString(); return { agent: cloneManagedAgent(agent), profile_sync_error: null }; } async function handleSearchMessages( args: { q: string; limit?: number; }, config: E2eConfig | undefined, ): Promise { const identity = getIdentity(config); if (!identity) { const query = args.q.trim().toLowerCase(); const limit = args.limit ?? 20; const now = Math.floor(Date.now() / 1000); const mockHits: RawSearchHit[] = [ { event_id: "mock-general-welcome", content: "Welcome to #general", kind: 9, pubkey: DEFAULT_MOCK_IDENTITY.pubkey, channel_id: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", channel_name: "general", created_at: now - 60, score: 8.5, }, { event_id: "mock-engineering-shipped", content: "Engineering shipped the desktop build.", kind: 9, pubkey: "bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260", channel_id: "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9", channel_name: "engineering", created_at: now - 42 * 60, score: 7.2, }, { event_id: "mock-design-critique", content: "Design critique notes for the browse flow.", kind: 9, pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", channel_id: "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e", channel_name: "design", created_at: now - 75 * 60, score: 6.6, }, { event_id: "mock-forum-release-thread", content: "Release checklist: async feedback thread.", kind: 45001, pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", channel_id: "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11", channel_name: "watercooler", created_at: now - 90 * 60, score: 5.8, }, { event_id: "mock-forum-release-reply", content: "Looks good to me. We should ship it.", kind: 45003, pubkey: ALICE_PUBKEY, channel_id: "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11", channel_name: "watercooler", created_at: now - 80 * 60, score: 5.2, }, ]; const hits = mockHits .filter((hit) => { if (!query) { return true; } return ( hit.content.toLowerCase().includes(query) || (hit.channel_name?.toLowerCase().includes(query) ?? false) ); }) .slice(0, limit); return { hits, found: hits.length, }; } const url = new URL("/api/search", getRelayHttpUrl(config)); url.searchParams.set("q", args.q); if (args.limit !== undefined) { url.searchParams.set("limit", String(args.limit)); } const response = await fetch(url, { headers: { "X-Pubkey": identity.pubkey, }, }); await assertOk(response); return response.json(); } async function handleSendChannelMessage( args: { channelId: string; content: string; parentEventId?: string | null; kind?: number | null; mentionPubkeys?: string[]; }, config: E2eConfig | undefined, ): Promise { const kind = args.kind ?? 9; const identity = getIdentity(config); if (!identity) { const createdAt = Math.floor(Date.now() / 1000); const mockPubkey = getMockMemberPubkey(config); if (!args.parentEventId) { const event = createMockEvent( kind, args.content, buildTopLevelMessageTags( args.channelId, args.mentionPubkeys, mockPubkey, ), ); recordMockMessage(args.channelId, event); emitMockLiveEvent(args.channelId, event); return { event_id: event.id, parent_event_id: null, root_event_id: null, depth: 0, created_at: createdAt, }; } const history = getMockMessageStore(args.channelId); const parentEvent = history.find( (event) => event.id === args.parentEventId, ); const parentThread = parentEvent ? getThreadReferenceFromTags(parentEvent.tags) : { parentEventId: null, rootEventId: null, }; const rootEventId = parentThread.rootEventId ?? args.parentEventId; const depth = parentEvent ? (() => { let currentEvent: RelayEvent | undefined = parentEvent; let nextDepth = 1; while (currentEvent) { const reference = getThreadReferenceFromTags(currentEvent.tags); if (!reference.parentEventId) { return nextDepth; } nextDepth += 1; currentEvent = history.find( (event) => event.id === reference.parentEventId, ); } return nextDepth; })() : 1; const event: RelayEvent = { id: crypto.randomUUID().replace(/-/g, ""), pubkey: mockPubkey, created_at: createdAt, kind, tags: buildReplyMessageTags( args.channelId, mockPubkey, args.parentEventId, rootEventId, args.mentionPubkeys, ), content: args.content.trim(), sig: "mocksig".repeat(20).slice(0, 128), }; recordMockMessage(args.channelId, event); emitMockLiveEvent(args.channelId, event); return { event_id: event.id, parent_event_id: args.parentEventId, root_event_id: rootEventId, depth, created_at: createdAt, }; } const relayIdentity = getRelayIdentity(config); const tags = args.parentEventId ? buildReplyMessageTags( args.channelId, relayIdentity.pubkey, args.parentEventId, args.parentEventId, args.mentionPubkeys, ) : buildTopLevelMessageTags( args.channelId, args.mentionPubkeys, relayIdentity.pubkey, ); const result = await submitSignedEvent(config, { kind, content: args.content.trim(), tags, }); return { event_id: result.event_id, parent_event_id: args.parentEventId ?? null, root_event_id: args.parentEventId ?? null, depth: args.parentEventId ? 1 : 0, created_at: Math.floor(Date.now() / 1000), }; } async function handleGetEvent( args: { eventId: string; }, config: E2eConfig | undefined, ) { const identity = getIdentity(config); if (!identity) { const knownEvents: RelayEvent[] = [ ...Array.from(mockMessages.values()).flat(), { id: "mock-engineering-shipped", pubkey: "bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260", created_at: Math.floor(Date.now() / 1000) - 42 * 60, kind: 9, tags: [["e", "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9"]], content: "Engineering shipped the desktop build.", sig: "mocksig".repeat(20).slice(0, 128), }, { id: "mock-design-critique", pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", created_at: Math.floor(Date.now() / 1000) - 75 * 60, kind: 9, tags: [["h", "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e"]], content: "Design critique notes for the browse flow.", sig: "mocksig".repeat(20).slice(0, 128), }, { id: "mock-forum-release-thread", pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", created_at: Math.floor(Date.now() / 1000) - 90 * 60, kind: 45001, tags: [["e", "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11"]], content: "Release checklist: async feedback thread.", sig: "mocksig".repeat(20).slice(0, 128), }, { id: "mock-forum-release-reply", pubkey: ALICE_PUBKEY, created_at: Math.floor(Date.now() / 1000) - 80 * 60, kind: 45003, tags: buildReplyMessageTags( "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11", ALICE_PUBKEY, "mock-forum-release-thread", "mock-forum-release-thread", undefined, ), content: "Looks good to me. We should ship it.", sig: "mocksig".repeat(20).slice(0, 128), }, ]; const event = knownEvents.find((item) => item.id === args.eventId); if (!event) { throw new Error(`Event not found: ${args.eventId}`); } return JSON.stringify(event); } const response = await fetch( `${getRelayHttpUrl(config)}/api/events/${args.eventId}`, { headers: { "X-Pubkey": identity.pubkey, }, }, ); await assertOk(response); return JSON.stringify(await response.json()); } async function connectRealSocket(args: { url?: string; onMessage: unknown }) { const wsId = nextSocketId++; const ws = new WebSocket(args.url ?? DEFAULT_RELAY_WS_URL); const handler = resolveHandler(args.onMessage); realSockets.set(wsId, ws); ws.addEventListener("message", (event) => { handler({ type: "Text", data: event.data, }); }); ws.addEventListener("close", () => { sendWsClose(handler); realSockets.delete(wsId); }); ws.addEventListener("error", () => { handler({ type: "Error", }); }); return await new Promise((resolve) => { ws.addEventListener("open", () => resolve(wsId), { once: true }); ws.addEventListener("error", () => resolve(wsId), { once: true }); }); } async function connectMockSocket(args: { onMessage: unknown }) { const wsId = nextSocketId++; const handler = resolveHandler(args.onMessage); mockSockets.set(wsId, { handler, subscriptions: new Map(), }); window.setTimeout(() => { sendWsText(handler, ["AUTH", `mock-challenge-${wsId}`]); }, 0); return wsId; } async function sendToRealSocket(args: { id: number; message?: { type: "Text" | "Close"; data?: string; }; }) { const socket = realSockets.get(args.id); if (!socket) { return; } if (args.message?.type === "Close") { socket.close(); return; } if (args.message?.type === "Text") { socket.send(args.message.data ?? ""); } } function sendToMockSocket(args: { id: number; message?: { type: "Text" | "Close"; data?: string; }; }) { const socket = mockSockets.get(args.id); if (!socket || !args.message) { return; } if (args.message.type === "Close") { mockSockets.delete(args.id); sendWsClose(socket.handler); return; } if (args.message.type !== "Text" || !args.message.data) { return; } const [type, ...rest] = JSON.parse(args.message.data) as [ string, ...unknown[], ]; if (type === "AUTH") { const event = rest[0] as RelayEvent; sendWsText(socket.handler, ["OK", event.id, true, ""]); return; } if (type === "REQ") { const subId = rest[0] as string; const filter = rest[1] as { "#h"?: string[] }; const channelId = filter["#h"]?.[0]; if (subId.startsWith("live-")) { socket.subscriptions.set(subId, channelId ?? GLOBAL_MOCK_SUBSCRIPTION); sendWsText(socket.handler, ["EOSE", subId]); return; } if (!channelId) { sendWsText(socket.handler, ["EOSE", subId]); return; } emitMockHistory(socket, subId, channelId); return; } if (type === "CLOSE") { const subId = rest[0] as string; socket.subscriptions.delete(subId); return; } if (type === "EVENT") { const event = rest[0] as RelayEvent; const channelId = getChannelIdFromTags(event.tags); if (!channelId) { sendWsText(socket.handler, [ "OK", event.id, false, "Missing channel tag.", ]); return; } recordMockMessage(channelId, event); emitMockLiveEvent(channelId, event); sendWsText(socket.handler, ["OK", event.id, true, ""]); } } function disconnectMockSocket(id: number) { const socket = mockSockets.get(id); if (!socket) { return; } mockSockets.delete(id); sendWsClose(socket.handler); } export function maybeInstallE2eTauriMocks() { if (installed) { return; } const config = getConfig(); if (!config) { return; } resetMockTokens(config); resetMockManagedAgents(); resetMockPersonas(); resetMockTeams(); resetMockWorkflows(); mockWindows("main"); window.__SPROUT_E2E_COMMANDS__ = []; window.__SPROUT_E2E_WEBVIEW_ZOOM__ = 1; window.__SPROUT_E2E_EMIT_MOCK_MESSAGE__ = ({ channelName, content, pubkey, }) => { const channel = mockChannels.find( (candidate) => candidate.name === channelName, ); if (!channel) { throw new Error(`Mock channel ${channelName} not found.`); } return emitMockChannelMessage(channel.id, content, pubkey); }; window.__SPROUT_E2E_EMIT_MOCK_TYPING__ = ({ channelName, pubkey }) => { const channel = mockChannels.find( (candidate) => candidate.name === channelName, ); if (!channel) { throw new Error(`Mock channel ${channelName} not found.`); } return emitMockTypingIndicator(channel.id, pubkey ?? CHARLIE_PUBKEY); }; window.__SPROUT_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__ = ({ channelName }) => { const channel = mockChannels.find( (candidate) => candidate.name === channelName, ); if (!channel) { throw new Error(`Mock channel ${channelName} not found.`); } return hasMockLiveSubscription(channel.id); }; window.__SPROUT_E2E_PUSH_MOCK_FEED_ITEM__ = (item) => { const category = item.category === "mention" ? "mentions" : item.category; mockFeedOverrides[category].unshift(item); window.dispatchEvent(new CustomEvent("sprout:e2e-home-feed-updated")); return item; }; const handleMockCommand = async (command: string, payload: unknown) => { const activeConfig = getConfig(); const identity = getIdentity(activeConfig); window.__SPROUT_E2E_COMMANDS__?.push(command); switch (command) { case "get_identity": if (identity) { return { pubkey: identity.pubkey, display_name: identity.username, }; } return DEFAULT_MOCK_IDENTITY; case "get_profile": return handleGetProfile(activeConfig); case "update_profile": return handleUpdateProfile( payload as Parameters[0], activeConfig, ); case "get_user_profile": return handleGetUserProfile( (payload as Parameters[0]) ?? {}, activeConfig, ); case "get_users_batch": return handleGetUsersBatch( payload as Parameters[0], activeConfig, ); case "get_user_notes": return handleGetUserNotes( payload as Parameters[0], activeConfig, ); case "search_users": return handleSearchUsers( payload as Parameters[0], activeConfig, ); case "get_presence": return handleGetPresence( (payload as Parameters[0]) ?? { pubkeys: [], }, activeConfig, ); case "set_presence": return handleSetPresence( payload as Parameters[0], activeConfig, ); case "get_relay_ws_url": return getRelayWsUrl(activeConfig); case "get_relay_http_url": return getRelayHttpUrl(activeConfig); case "discover_acp_providers": return handleDiscoverAcpProviders(); case "discover_backend_providers": return []; case "probe_backend_provider": return { ok: false, error: "mock: no providers available" }; case "discover_managed_agent_prereqs": return handleDiscoverManagedAgentPrereqs( payload as Parameters[0], ); case "get_channels": return handleGetChannels(activeConfig); case "get_feed": return handleGetFeed( (payload as Parameters[0]) ?? {}, activeConfig, ); case "list_tokens": return handleListTokens(activeConfig); case "mint_token": return handleMintToken( payload as Parameters[0], activeConfig, ); case "revoke_token": return handleRevokeToken( payload as Parameters[0], activeConfig, ); case "revoke_all_tokens": return handleRevokeAllTokens(activeConfig); case "list_relay_agents": return handleListRelayAgents(); case "list_personas": return handleListPersonas(); case "create_persona": return handleCreatePersona( payload as Parameters[0], ); case "update_persona": return handleUpdatePersona( payload as Parameters[0], ); case "delete_persona": return handleDeletePersona( payload as Parameters[0], ); case "set_persona_active": return handleSetPersonaActive( payload as Parameters[0], ); case "list_teams": return handleListTeams(); case "create_team": return handleCreateTeam( payload as Parameters[0], ); case "update_team": return handleUpdateTeam( payload as Parameters[0], ); case "delete_team": return handleDeleteTeam( payload as Parameters[0], ); case "export_team_to_json": return handleExportTeamToJson(payload as { id: string }); case "parse_team_file": return handleParseTeamFile(); case "parse_persona_files": return handleParsePersonaFiles( payload as { fileBytes: number[]; fileName: string }, ); case "export_persona_to_json": return handleExportPersonaToJson(payload as { id: string }); case "list_managed_agents": return handleListManagedAgents(); case "create_managed_agent": return handleCreateManagedAgent( payload as Parameters[0], ); case "start_managed_agent": return handleStartManagedAgent( payload as Parameters[0], ); case "stop_managed_agent": return handleStopManagedAgent( payload as Parameters[0], ); case "set_managed_agent_start_on_app_launch": return handleSetManagedAgentStartOnAppLaunch( payload as Parameters< typeof handleSetManagedAgentStartOnAppLaunch >[0], ); case "delete_managed_agent": return handleDeleteManagedAgent( payload as Parameters[0], ); case "mint_managed_agent_token": return handleMintManagedAgentToken( payload as Parameters[0], ); case "get_managed_agent_log": return handleGetManagedAgentLog( payload as Parameters[0], ); case "get_agent_models": return { agentName: "mock-agent", agentVersion: "0.0.0", models: [], agentDefaultModel: null, selectedModel: null, supportsSwitching: false, }; case "update_managed_agent": return handleUpdateManagedAgent( payload as Parameters[0], ); case "create_channel": return handleCreateChannel( payload as Parameters[0], activeConfig, ); case "open_dm": return handleOpenDm( payload as Parameters[0], activeConfig, ); case "hide_dm": return handleHideDm( payload as Parameters[0], activeConfig, ); case "get_channel_details": return handleGetChannelDetails( payload as Parameters[0], activeConfig, ); case "get_channel_members": return handleGetChannelMembers( payload as Parameters[0], activeConfig, ); case "update_channel": return handleUpdateChannel( payload as Parameters[0], activeConfig, ); case "set_channel_topic": return handleSetChannelTopic( payload as Parameters[0], activeConfig, ); case "set_channel_purpose": return handleSetChannelPurpose( payload as Parameters[0], activeConfig, ); case "archive_channel": return handleArchiveChannel( payload as Parameters[0], activeConfig, ); case "unarchive_channel": return handleUnarchiveChannel( payload as Parameters[0], activeConfig, ); case "delete_channel": return handleDeleteChannel( payload as Parameters[0], activeConfig, ); case "add_channel_members": return handleAddChannelMembers( payload as Parameters[0], activeConfig, ); case "remove_channel_member": return handleRemoveChannelMember( payload as Parameters[0], activeConfig, ); case "join_channel": return handleJoinChannel( payload as Parameters[0], activeConfig, ); case "leave_channel": return handleLeaveChannel( payload as Parameters[0], activeConfig, ); case "search_messages": return handleSearchMessages( payload as Parameters[0], activeConfig, ); case "get_forum_posts": return handleGetForumPosts( payload as Parameters[0], ); case "get_forum_thread": return handleGetForumThread( payload as Parameters[0], ); case "send_channel_message": return handleSendChannelMessage( payload as Parameters[0], activeConfig, ); case "get_event": return handleGetEvent( payload as Parameters[0], activeConfig, ); case "sign_event": if (identity) { return JSON.stringify( await signWithIdentity(identity, { kind: (payload as { kind: number }).kind, content: (payload as { content: string }).content, tags: (payload as { tags: string[][] }).tags, }), ); } return JSON.stringify( createMockEvent( (payload as { kind: number }).kind, (payload as { content: string }).content, (payload as { tags: string[][] }).tags, ), ); case "create_auth_event": if (identity) { return JSON.stringify( await signWithIdentity(identity, { kind: 22242, content: "", tags: [ ["relay", (payload as { relayUrl: string }).relayUrl], ["challenge", (payload as { challenge: string }).challenge], ], }), ); } return JSON.stringify( createMockEvent(22242, "", [ ["relay", (payload as { relayUrl: string }).relayUrl], ["challenge", (payload as { challenge: string }).challenge], ]), ); case "plugin:websocket|connect": if (isRelayMode(activeConfig)) { return connectRealSocket( payload as Parameters[0], ); } return connectMockSocket( payload as Parameters[0], ); case "plugin:websocket|send": if (isRelayMode(activeConfig)) { return sendToRealSocket( payload as Parameters[0], ); } return sendToMockSocket( payload as Parameters[0], ); case "plugin:websocket|disconnect": if (isRelayMode(activeConfig)) { realSockets.get((payload as { id: number }).id)?.close(); realSockets.delete((payload as { id: number }).id); return; } return disconnectMockSocket((payload as { id: number }).id); case "plugin:window|show": case "plugin:window|unminimize": case "plugin:window|set_focus": case "plugin:window|set_badge_count": return null; case "get_channel_workflows": return handleGetChannelWorkflows( payload as Parameters[0], ); case "get_workflow": return handleGetWorkflow( payload as Parameters[0], ); case "create_workflow": return handleCreateWorkflow( payload as Parameters[0], ); case "update_workflow": return handleUpdateWorkflow( payload as Parameters[0], ); case "delete_workflow": return handleDeleteWorkflow( payload as Parameters[0], ); case "trigger_workflow": return handleTriggerWorkflow( payload as Parameters[0], ); case "get_workflow_runs": return handleGetWorkflowRuns( payload as Parameters[0], ); case "get_run_approvals": return handleGetRunApprovals( payload as Parameters[0], ); case "plugin:webview|set_webview_zoom": window.__SPROUT_E2E_WEBVIEW_ZOOM__ = ( payload as { value: number } ).value; return; default: throw new Error(`Unsupported mocked Tauri command: ${command}`); } }; window.__SPROUT_E2E_INVOKE_MOCK_COMMAND__ = (command, payload) => handleMockCommand(command, payload ?? null); mockIPC(handleMockCommand); installed = true; }