Files
buzz/desktop/src/testing/e2eBridge.ts
T
2026-07-29 12:41:58 -04:00

11764 lines
369 KiB
TypeScript

import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
import { mockIPC, mockWindows } from "@tauri-apps/api/mocks";
import { decode } from "nostr-tools/nip19";
import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
import { parse as yamlParse } from "yaml";
import {
mergeMockCustomHarnesses,
handleSaveCustomHarness,
handleDeleteCustomHarness,
} from "./e2eBridgeCustomHarnesses.ts";
import { relayClient } from "@/shared/api/relayClient";
import type { ConnectionState } from "@/shared/api/relayClientShared";
import type { ChannelTemplate, RelayEvent } from "@/shared/api/types";
import { getMarkdownParseCount } from "@/shared/ui/markdown/nodeCache";
import { syncAgentTurnsFromEvents } from "@/features/agents/activeAgentTurnsStore";
import { recordTimeoutFromRejection } from "@/features/moderation/lib/timeoutStore";
import {
injectObserverEventsForE2E,
syncAgentObserverEvents,
} from "@/features/agents/observerRelayStore";
import {
CUSTOM_EMOJI_SET_D_TAG,
KIND_EMOJI_SET,
} from "@/shared/api/customEmoji";
import {
KIND_AGENT_OBSERVER_FRAME,
KIND_CHANNEL_THREAD_SUMMARY,
KIND_CHANNEL_WINDOW_BOUNDS,
KIND_DM_VISIBILITY,
KIND_EVENT_REMINDER,
KIND_GIT_ISSUE,
KIND_GIT_PATCH,
KIND_GIT_PR_UPDATE,
KIND_GIT_PULL_REQUEST,
KIND_GIT_STATUS_CLOSED,
KIND_GIT_STATUS_DRAFT,
KIND_GIT_STATUS_MERGED,
KIND_GIT_STATUS_OPEN,
KIND_HUDDLE_STARTED,
KIND_MEMBER_ADDED_NOTIFICATION,
KIND_MEMBER_REMOVED_NOTIFICATION,
KIND_PERSONA,
KIND_REPO_ANNOUNCEMENT,
KIND_REPO_STATE,
KIND_STREAM_MESSAGE_EDIT,
KIND_SYSTEM_MESSAGE,
KIND_TEXT_NOTE,
KIND_USER_STATUS,
} from "@/shared/constants/kinds";
import type {
RawAcpAuthMethodsResult,
RawConnectAcpRuntimeResult,
} from "@/shared/api/tauriAgentAuth";
import type {
RawAcpRuntimeCatalogEntry,
RawInstallRuntimeResult,
RuntimeFileConfigSubset,
} from "@/shared/api/tauri";
import { normalizePubkey } from "@/shared/lib/pubkey";
type TestIdentity = {
privateKey: string;
pubkey: string;
username: string;
};
type MockCommandAvailability = {
available?: boolean;
command?: string;
resolvedPath?: string | null;
};
type MockManagedAgentSeed = {
pubkey: string;
name: string;
avatarUrl?: string | null;
personaId?: string | null;
/** Harness/runtime id pin; `null` = inherit from persona (native default). */
runtime?: string | null;
status?: RawManagedAgent["status"];
channelNames?: string[];
channelIds?: string[];
backend?: RawManagedAgent["backend"];
lastError?: string | null;
lastErrorCode?: number | null;
needsRestart?: boolean;
autoRestartOnConfigChange?: boolean;
respondTo?: RawManagedAgent["respond_to"];
respondToAllowlist?: string[];
};
type MockManagedAgentRuntimeSeed = {
pubkey: string;
relayUrl: string;
lifecycle?: MockManagedAgentRuntimeRow["lifecycle"];
};
type MockRelayAgentSeed = {
pubkey: string;
name: string;
agentType?: string;
capabilities?: string[];
respondTo?: RawRelayAgent["respond_to"];
respondToAllowlist?: string[];
channelNames?: string[];
channelIds?: string[];
status?: PresenceStatus;
};
type MockPersonaSeed = {
id?: string;
displayName: string;
avatarUrl?: string | null;
systemPrompt: string;
updatedAt?: string;
isActive?: boolean;
shared?: boolean;
sourceTeam?: string | null;
envVars?: Record<string, string>;
runtime?: string | null;
model?: string | null;
provider?: string | null;
namePool?: string[];
respondTo?: "owner-only" | "allowlist" | "anyone";
respondToAllowlist?: string[];
};
type MockTeamSeed = {
id?: string;
name: string;
description?: string | null;
personaIds: string[];
};
type MockSearchProfileSeed = {
pubkey: string;
displayName: string | null;
avatarUrl?: string | null;
nip05Handle?: string | null;
about?: string | null;
ownerPubkey?: string | null;
isAgent?: boolean;
};
type E2eConfig = {
mode?: "mock" | "relay";
mock?: {
ttsSettings?: {
version: number;
agentTextToSpeech: boolean;
voicePreferences: string[];
};
/** Advertised HEAD for the first mock project without adding that branch. */
projectHeadBranch?: string;
/** Builderlab account returned by hosted-community onboarding. Null/omitted = signed out. */
builderlabAuth?: {
email?: string;
name?: string;
expiresAt: string;
} | null;
/** Optional policy returned by the native join-policy discovery command. */
joinPolicy?: {
terms_markdown?: string;
privacy_markdown?: string;
age_attestation_required: boolean;
version: string;
} | null;
/** Delay Builderlab login completion so cancellation/retry UI can be tested. */
builderlabLoginDelayMs?: number;
/** Bound Builderlab Nostr identity. Null/omitted = not linked yet. */
builderlabIdentity?: { npub?: string; pubkey_hex?: string } | null;
/** Structured error returned when onboarding tries to bind the local identity. */
builderlabBindError?: { code?: string; message?: string };
/** Communities owned by the mocked Builderlab account. */
builderlabCommunities?: Array<{
id?: string;
name?: string;
slug?: string;
normalized_host?: string;
archived_at?: string | null;
}>;
/** Override the community returned after hosted creation. */
builderlabCreatedCommunity?: {
id?: string;
name?: string;
slug?: string;
normalized_host?: string;
archived_at?: string | null;
};
acpRuntimesCatalog?: RawAcpRuntimeCatalogEntry[];
/** Catalog returned after a successful mocked install. */
acpRuntimesCatalogAfterInstall?: RawAcpRuntimeCatalogEntry[];
/** Catalog responses after install for testing later sign-in completion. */
acpRuntimesCatalogAfterInstallSequence?: RawAcpRuntimeCatalogEntry[][];
/** Catalog responses for successive discovery calls. The final response repeats. */
acpRuntimesCatalogSequence?: RawAcpRuntimeCatalogEntry[][];
acpRuntimesDelayMs?: number;
acpAuthMethods?: Record<string, RawAcpAuthMethodsResult>;
acpAuthMethodsErrors?: Record<string, string>;
acpAuthMethodsError?: string;
/** When set, the `delete_custom_harness` mock command throws with this message. */
deleteCustomHarnessError?: string;
connectAcpRuntimeResult?: RawConnectAcpRuntimeResult;
connectAcpRuntimeDelayMs?: number;
connectAcpRuntimeError?: string;
/** Catalog returned after a successful mocked connect (sign-in). */
acpRuntimesCatalogAfterConnect?: RawAcpRuntimeCatalogEntry[];
activePersonaIds?: string[];
installAcpRuntimeDelayMs?: number;
installAcpRuntimeResult?: RawInstallRuntimeResult;
/** Sequence of results for successive `install_acp_runtime` calls.
* Call N returns results[N]; when exhausted the last entry repeats.
* Takes precedence over `installAcpRuntimeResult`. */
installAcpRuntimeResults?: RawInstallRuntimeResult[];
/** Per-runtime install configuration keyed by runtimeId.
* When a runtimeId matches, its entry overrides the global
* installAcpRuntime* fields for that specific runtime. */
installAcpRuntimeByRuntime?: Record<
string,
{
delayMs?: number;
result?: RawInstallRuntimeResult;
/** Call-order sequence — same semantics as installAcpRuntimeResults. */
results?: RawInstallRuntimeResult[];
}
>;
managedAgentPrereqs?: {
acp?: MockCommandAvailability;
mcp?: MockCommandAvailability;
};
managedAgents?: MockManagedAgentSeed[];
/** Per agent+relay runtime rows for the pair-scoped lifecycle commands
* (`list/start/stop/restart_managed_agent_runtime`). */
managedAgentRuntimes?: MockManagedAgentRuntimeSeed[];
personas?: MockPersonaSeed[];
/** Community catalog replaceable-event heads returned by relay queries. */
personaCatalogEvents?: RelayEvent[];
/** Outcomes for successive explicit persona share publications. */
personaSharePublicationStatuses?: Array<"published" | "queued">;
teams?: MockTeamSeed[];
relayAgents?: MockRelayAgentSeed[];
agentListDelayMs?: number;
agentMemory?: RawAgentMemoryListing | Record<string, RawAgentMemoryListing>;
addChannelMembersDelayMs?: number;
/** Sequenced add-member failures. A string fails that call; null succeeds. */
addChannelMembersErrors?: (string | null)[];
channelMembersReadDelayMs?: number;
createManagedAgentDelayMs?: number;
channelTemplates?: ChannelTemplate[];
channelsReadError?: string;
/** Reject successive mock `get_channels` calls, then resume. */
channelsReadErrors?: (string | null)[];
/** Reject successive mock `create_channel` calls, then resume. */
createChannelErrors?: string[];
/** Reject successive mock `ensure_starter_channels` calls, then resume. */
ensureStarterChannelsErrors?: string[];
/** Reject successive mock `join_channel` calls, then resume. */
joinChannelErrors?: string[];
channelsReadDelayMs?: number;
/** Number of seeded rows in the deep-history fixture. Defaults to 600. */
deepHistoryMessageCount?: number;
feedReadError?: string;
canvasReadError?: string;
/** Delay (ms) for `apply_workspace` so e2e tests can observe the
* community-switch gate. 0/undefined = instant. */
applyCommunityDelayMs?: number;
openDmDelayMs?: number;
sendMessageDelayMs?: number;
/** Close the first channel-window live REQ; its retry is accepted. */
closeChannelLiveSubscriptionOnce?: boolean;
/** Reject successive kind-9 sends with these messages, then resume. */
sendMessageErrors?: string[];
/** Reject successive managed-agent starts, then resume. */
startManagedAgentErrors?: string[];
/** Delay (ms) after snapshotting a thread-replies page so E2E tests can
* deliver live reply/aux events while an older response is in flight. */
threadRepliesDelayMs?: number;
usersBatchDelayMs?: number;
/** Delay (ms) applied to continuation channel-window requests so e2e
* tests can observe the in-flight prepend window. 0/undefined = instant. */
channelWindowDelayMs?: number;
profileReadDelayMs?: number;
profileReadError?: string;
profileUpdateError?: string;
profileUpdateErrors?: string[];
searchProfiles?: MockSearchProfileSeed[];
updateAvailable?: boolean;
updateChannelDelayMs?: number;
updateDownloadDelayMs?: number;
restartDelayMs?: number;
updateVersion?: string;
/** When false, `is_auto_update_supported` returns false (simulates a
* Linux .deb install where Tauri's updater cannot swap the binary).
* Defaults to true for all existing tests. */
autoUpdateSupported?: boolean;
/** Reject `plugin:opener|open_url` to exercise browser-return fallback UI. */
openerError?: string;
/** Delay binding signatures so specs can exercise request supersession. */
nostrBindSignDelayMs?: number;
/** Reject successive mock WebSocket connect attempts, then resume. */
websocketConnectErrors?: string[];
stallWebsocketSends?: boolean;
userSearchDelayMs?: number;
// NIP-IA gate inputs — see tests/helpers/bridge.ts:MockBridgeOptions for
// semantics. These three drive the archive-button gate matrix in
// tests/e2e/identity-archive.spec.ts; they're plumbed into:
// - `list_archived_identities` (archivedIdentities)
// - `resolve_oa_owner` (oaOwnerIsMe)
// - `resetMockRelayMembers` (relayRole)
archivedIdentities?: string[];
// Relay's NIP-11 `self` pubkey (hex) for `get_relay_self`. A DM whose peer
// equals this is treated as a moderation DM (composer disabled). Absent →
// fail open (no mod-DM detection), matching the Rust command's contract.
relaySelf?: string | null;
oaOwnerIsMe?: boolean;
/** Whether the mock relay advertises NIP-43 membership support. Defaults to false. */
relayRequiresMembership?: boolean;
/** Delay EOSE for membership snapshots after delivering the event. */
relayMembershipEoseDelayMs?: number;
relayRole?: "owner" | "admin" | "member" | null;
// Descriptors returned by the mocked `pick_and_upload_media` /
// `upload_media_bytes` commands. Lets a spec drive the attachment flow
// (e.g. a generic PDF) without a real upload pipeline. See
// tests/helpers/bridge.ts:MockBridgeOptions.uploadDescriptors.
uploadDelayMs?: number;
/** Delay (ms) applied to `encode_agent_snapshot_for_send` so E2E tests can
* observe the "preparing" phase before the upload begins. 0/undefined = instant. */
encodeDelayMs?: number;
/** Delay (ms) applied to `get_relay_self` so E2E tests can prove the
* fail-closed race: DMs are withheld while classification is unresolved. */
relaySelfDelayMs?: number;
/** Delay (ms) applied to `start_pairing` so pairing loading UI is observable. */
pairingStartDelayMs?: number;
/**
* Sequenced results for `confirm_team_snapshot_import`. String = throw
* with that message; null = succeed. Call N uses results[N]; last entry
* repeats when exhausted. Follows the `nsecErrors` precedent.
*/
teamSnapshotConfirmErrors?: (string | null)[];
/**
* When true, `preview_team_snapshot_import` returns a preview with
* `hasSourceAllowlist: true` so the allowlist section renders in the
* import dialog.
*/
teamSnapshotPreviewHasSourceAllowlist?: boolean;
/**
* When set to a non-empty string, `fetch_snapshot_bytes` throws with this
* message — lets specs prove malformed/hash/size-mismatch error paths.
*/
snapshotFetchError?: string;
uploadDescriptors?: RawBlobDescriptor[];
// Seed rows returned by `list_save_subscriptions`. Each entry uses the same
// snake_case wire shape the Rust backend returns so tests can drive the
// LocalArchiveSettingsCard without a real SQLite database.
observerArchiveDefaultEnabled?: boolean;
/**
* Delay (ms) applied to `observer_archive_default_enabled` so E2E tests
* can observe the pending-reconciliation state (toggle disabled, no
* archive-manager `list_save_subscriptions` call) before the policy
* resolves. 0/undefined = instant.
*/
observerArchiveDefaultEnabledDelayMs?: number;
/**
* When set, `observer_archive_default_enabled` throws with this message
* instead of resolving — drives the fail-closed `.catch()` path in
* `useObserverArchiveReconciliation` / `LocalArchiveSettingsCard`.
*/
observerArchiveDefaultEnabledError?: string;
agentMetricArchiveDefaultEnabled?: boolean;
saveSubscriptions?: Array<{
scope_type: string;
scope_value: string;
kinds: string; // JSON-encoded integer array, e.g. "[9,40002]"
}>;
// Event IDs that `get_event` should report as definitively not found.
// Causes `useDraftRootStatus` to classify as `deleted`.
deletedEventIds?: string[];
// Pending community deep links (buzz://join / buzz://connect / buzz://add-community) seeded into
// the mocked Rust-side queue. Mirrors the real queue's semantics:
// `take_pending_community_deep_link` peeks the head and
// `acknowledge_pending_community_deep_link` removes by id. Drives the
// pending-invite gate and deep-link drain path in tests.
pendingCommunityDeepLinks?: Array<{
id: string;
kind: "connect" | "join" | "add-community";
relayUrl: string;
code?: string | null;
name?: string | null;
}>;
// When true, `get_identity` returns `lost: true` until `persist_current_identity`
// or `import_identity` is called. Drives the identity-lost recovery UX in tests.
identityLost?: boolean;
// When true, `get_identity` returns `locked: true` until `import_identity` is
// called. Drives the keyring-locked screen in tests.
identityLocked?: boolean;
/**
* Global agent config returned by `get_global_agent_config`. Defaults to
* an empty config (no provider, model, or env vars) if not specified.
* Pass a config with a provider to test Inherit-from-global behavior.
*/
globalAgentConfig?: {
env_vars: Record<string, string>;
provider: string | null;
model: string | null;
preferred_runtime?: string | null;
};
/** File-layer config returned by runtime id. */
runtimeFileConfigs?: Record<string, RuntimeFileConfigSubset | null>;
/** Baked build env returned by the display and key-name Tauri commands. */
bakedBuildEnv?: Array<{
key: string;
masked: boolean;
value: string;
}>;
/** Delay (ms) applied to `get_baked_build_env` so specs can observe
* initial render gating around build defaults. 0/undefined = instant. */
bakedBuildEnvDelayMs?: number;
/** Delay (ms) applied to `set_global_agent_config` so tests can observe
* autosave behaviour while a request is in flight. 0/undefined = instant.
* Alias of `globalConfigSaveDelayMs` (kept for onboarding specs). */
setGlobalAgentConfigDelayMs?: number;
/**
* When set, `get_nsec` throws with this message instead of returning the
* mock nsec string. Use `nsecErrors` for sequenced failure/success.
*/
nsecError?: string;
/**
* Sequenced results for `get_nsec`. Each element is either a string
* (error message) or null (success — returns the default mock nsec).
* Call N uses results[N]; when exhausted the last entry repeats.
*/
nsecErrors?: (string | null)[];
/**
* The `restarted_count` returned by `set_global_agent_config`. Defaults to
* 0 (no agents restarted). Set to a positive integer to drive the
* "Saved. Restarted N agent(s)." status text in AgentDefaultsSettingsCard.
*/
globalConfigRestartedCount?: number;
/**
* The `failed_restart_count` returned by `set_global_agent_config`. Defaults
* to 0. Set to a positive integer to drive the "M failed to restart — check
* the Agents tab." status text in AgentDefaultsSettingsCard.
*/
globalConfigFailedRestartCount?: number;
/**
* Milliseconds to delay the mocked `set_global_agent_config` response.
* Defaults to 0 (resolve immediately). Use to hold a save in flight so a
* spec can interleave edits and exercise the mid-save race handling.
*/
globalConfigSaveDelayMs?: number;
/**
* Override the `discover_agent_models` mock response. When set, returns
* this catalog instead of the default per-harness model list.
*/
discoverAgentModels?: {
models: Array<{
id: string;
name: string | null;
description?: string | null;
}>;
supportsSwitching: boolean;
agentDefaultModel?: string | null;
selectedModel?: string | null;
};
/**
* When set, `discover_agent_models` throws with this message instead of
* returning a catalog.
*/
discoverAgentModelsError?: string;
};
relayHttpUrl?: string;
relayWsUrl?: string;
autoConnectDefaultRelay?: boolean;
identity?: TestIdentity;
};
type RawBlobDescriptor = {
url: string;
sha256: string;
size: number;
type: string;
uploaded: number;
dim?: string;
blurhash?: string;
thumb?: string;
duration?: number;
image?: string;
filename?: string;
};
type RawRelayMember = {
pubkey: string;
role: "owner" | "admin" | "member";
added_by: string | null;
created_at: string;
};
type RawProfile = {
pubkey: string;
display_name: string | null;
/** Kind-0 `name` field, kept separate from `display_name` so mention
* resolution can match either alias. */
name?: string | null;
avatar_url: string | null;
about: string | null;
nip05_handle: string | null;
owner_pubkey: string | null;
is_agent?: boolean;
/** Mirrors the Rust `has_profile_event` flag: true when a real kind:0 event
* backed this profile, false for the synthesized empty fallback. */
has_profile_event: boolean;
};
type RawUserProfileSummary = {
display_name: string | null;
name?: string | null;
avatar_url: string | null;
nip05_handle: string | null;
owner_pubkey: string | null;
is_agent?: boolean;
};
type RawUsersBatchResponse = {
profiles: Record<string, RawUserProfileSummary>;
missing: string[];
};
type RawUserSearchResult = {
pubkey: string;
display_name: string | null;
avatar_url: string | null;
nip05_handle: string | null;
owner_pubkey: string | null;
is_agent?: boolean;
};
type RawSearchUsersResponse = {
users: RawUserSearchResult[];
next_cursor?: string | null;
};
type PresenceStatus = "online" | "away" | "offline";
type RawPresenceLookup = Record<string, PresenceStatus>;
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;
member_pubkeys: string[];
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";
is_agent?: boolean;
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 = Omit<RawChannelDetail, "member_pubkeys"> & {
members: RawChannelMember[];
};
type RawFeedItem = {
id: string;
kind: number;
pubkey: string;
content: string;
created_at: number;
channel_id: string | null;
channel_name: string;
// Mirrors native FeedItemInfo.channel_type (Option<String>): the Tauri
// backend always emits the key, as `null` when unknown.
channel_type?: string | null;
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;
tags: 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 RawRelayAgent = {
pubkey: string;
name: string;
agent_type: string;
channels: string[];
channel_ids: string[];
capabilities: string[];
status: PresenceStatus;
respond_to?: "owner-only" | "allowlist" | "anyone";
respond_to_allowlist?: string[];
};
type RawManagedAgent = {
pubkey: string;
name: string;
persona_id: string | null;
/** Record-level harness/runtime pin (`null` when inheriting from the persona). */
runtime: 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;
avatar_url: string | null;
model: string | null;
provider?: string | null;
env_vars?: Record<string, string>;
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;
last_error_code: number | null;
needs_restart?: boolean;
log_path: string;
start_on_app_launch: boolean;
auto_restart_on_config_change?: boolean;
backend:
| { type: "local" }
| { type: "provider"; id: string; config: Record<string, unknown> };
backend_agent_id: string | null;
respond_to: "owner-only" | "allowlist" | "anyone";
respond_to_allowlist: string[];
};
type RawCreateManagedAgentResponse = {
agent: RawManagedAgent;
private_key_nsec: string;
profile_sync_error: string | null;
spawn_error: string | null;
};
type RawManagedAgentLog = {
content: string;
log_path: string;
};
type RawEngramEntry = {
slug: string;
body: string;
eventId: string;
createdAt: number;
outgoingRefs: string[];
};
type RawAgentMemoryListing = {
core: RawEngramEntry | null;
memories: RawEngramEntry[];
truncated: boolean;
fetchedAt: number;
};
type RawCommandAvailability = {
command: string;
resolved_path: string | null;
available: boolean;
};
type RawManagedAgentPrereqs = {
acp: RawCommandAvailability;
mcp: RawCommandAvailability;
};
type RawPersona = {
id: string;
display_name: string;
avatar_url: string | null;
system_prompt: string;
runtime?: string | null;
model?: string | null;
provider?: string | null;
name_pool?: string[];
is_builtin: boolean;
is_active: boolean;
shared: boolean;
source_team?: string | null;
catalog_source?: { owner_pubkey: string; persona_id: string } | null;
env_vars?: Record<string, string>;
respond_to?: string | null;
respond_to_allowlist?: string[];
parallelism?: number | null;
created_at: string;
updated_at: string;
};
type RawTeam = {
id: string;
name: string;
description: string | null;
persona_ids: string[];
is_builtin: boolean;
source_dir: string | null;
is_symlink: boolean;
symlink_target: string | null;
version: string | null;
created_at: string;
updated_at: string;
};
type MockManagedAgent = RawManagedAgent & {
private_key_nsec: string;
log_lines: string[];
};
// Mirrors the Rust `ManagedAgentRuntimeStatus` camelCase wire shape for the
// pair-scoped lifecycle commands.
type MockManagedAgentRuntimeRow = {
pubkey: string;
relayUrl: string;
localSetup: boolean;
lifecycle:
| "starting"
| "listening"
| "waking"
| "ready"
| "failed"
| "stopped";
pid: number | null;
error: string | null;
logPath: string | null;
};
type WsHandler = (message: unknown) => void;
const GLOBAL_MOCK_SUBSCRIPTION = "*";
type MockSubscription = {
channelId: string;
kinds: number[] | null;
/** `#p` values from the REQ filters, if any — lets specs assert an
* owner-scoped live subscription (e.g. the observer-archive `24200`
* reconciliation gate) independently of channel-scoped ones. */
ownerPubkeys: string[];
};
type MockFilter = {
"#a"?: string[];
"#d"?: string[];
"#e"?: string[];
"#h"?: string[];
"#p"?: string[];
authors?: string[];
ids?: string[];
kinds?: number[];
limit?: number;
since?: number;
until?: number;
};
type MockSocket = {
handler: WsHandler;
subscriptions: Map<string, MockSubscription>;
};
function createMockRelayMembershipEvent(): RelayEvent {
return createMockEvent(
13534,
"",
mockRelayMembers.map((member) => ["member", member.pubkey, member.role]),
"f".repeat(64),
);
}
/**
* Per-user custom emoji sets (kind:30030) the mock WS serves for
* `listCustomEmoji` REQs. The community palette is the client-side UNION of
* every member's own set (d=`buzz:custom-emoji`). We serve TWO member-authored
* sets from distinct pubkeys so the e2e exercises the union/collapse path, not
* a single relay-owned set. `:buzz:` is the stable shortcode exercised by
* custom-emoji.spec.ts (claimed by BOTH members with different URLs, so the
* palette must collapse it to one deterministic winner); `:narf:` proves a
* second member's distinct emoji unions in.
*/
function createMockCustomEmojiSetEvents(): RelayEvent[] {
return [
createMockEvent(
KIND_EMOJI_SET,
"",
[
["d", CUSTOM_EMOJI_SET_D_TAG],
["emoji", "buzz", "https://example.com/e2e/buzz.png"],
// A relay-hosted emoji whose URL matches rewriteRelayUrl()'s pattern,
// used by the reaction guard to assert the proxy rewrite fires.
["emoji", REACTION_EMOJI_SHORTCODE, REACTION_EMOJI_URL],
],
// The current mock identity owns this set, so the settings card's
// "My emoji" section is non-empty and removable.
MOCK_IDENTITY_PUBKEY,
),
createMockEvent(
KIND_EMOJI_SET,
"",
[
["d", CUSTOM_EMOJI_SET_D_TAG],
["emoji", "narf", "https://example.com/e2e/narf.png"],
// member B claims :buzz: with a DIFFERENT url — unionCustomEmoji must
// collapse it to one deterministic winner, never expose two URLs.
["emoji", "buzz", "https://example.com/e2e/buzz-b.png"],
],
"b".repeat(64),
),
];
}
function updateMockRelayMembershipFromAdminEvent(event: RelayEvent): boolean {
const targetPubkey = event.tags
.find((tag) => tag[0] === "p")?.[1]
?.toLowerCase();
if (!targetPubkey) return false;
if (event.kind === 9030) {
const role = event.tags.find((tag) => tag[0] === "role")?.[1] ?? "member";
if (role !== "admin" && role !== "member") return false;
if (mockRelayMembers.some((member) => member.pubkey === targetPubkey)) {
return true;
}
mockRelayMembers.push({
pubkey: targetPubkey,
role,
added_by: event.pubkey,
created_at: new Date().toISOString(),
});
return true;
}
if (event.kind === 9031) {
mockRelayMembers = mockRelayMembers.filter(
(member) => member.pubkey !== targetPubkey,
);
return true;
}
if (event.kind === 9032) {
const role = event.tags.find((tag) => tag[0] === "role")?.[1];
if (role !== "admin" && role !== "member") return false;
mockRelayMembers = mockRelayMembers.map((member) =>
member.pubkey === targetPubkey ? { ...member, role } : member,
);
return true;
}
return false;
}
declare global {
interface Window {
__BUZZ_E2E__?: E2eConfig;
__BUZZ_E2E_COMMANDS__?: string[];
__BUZZ_E2E_COMMAND_PAYLOADS__?: Array<{
command: string;
payload: unknown;
}>;
__BUZZ_E2E_COMMAND_LOG__?: Array<{
command: string;
payload: unknown;
}>;
__BUZZ_E2E_WEBVIEW_ZOOM__?: number;
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
kind?: number;
}) => boolean;
__BUZZ_E2E_HAS_MOCK_OWNER_KIND_SUBSCRIPTION__?: (input: {
ownerPubkey: string;
kind: number;
}) => boolean;
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
parentEventId?: string | null;
pubkey?: string;
kind?: number;
mentionPubkeys?: string[];
extraTags?: string[][];
createdAt?: number;
/** 64-hex id required for the event to be a valid reaction target. */
id?: string;
}) => RelayEvent;
/** Prepend `count` synthetic older messages to a channel's mock store so
* an older-history fetch has something to paginate. Mirrors how the real
* relay backfills history. Returns the created events. */
__BUZZ_E2E_PREPEND_MOCK_HISTORY__?: (input: {
channelName: string;
count: number;
startIndex?: number;
lineCount?: number;
createdAtStart?: number;
emit?: boolean;
}) => RelayEvent[];
__BUZZ_E2E_EMIT_MOCK_TYPING__?: (input: {
channelName: string;
createdAt?: number;
pubkey?: string;
threadHeadId?: string;
}) => RelayEvent;
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: RawFeedItem) => RawFeedItem;
/** Replace an existing feed item by id (or push if not found) and fire the updated event. */
__BUZZ_E2E_REPLACE_MOCK_FEED_ITEM__?: (
oldId: string,
item: RawFeedItem,
) => RawFeedItem;
__BUZZ_E2E_SIGNED_EVENTS__?: Array<{
content: string;
createdAt?: number;
kind: number;
tags: string[][];
}>;
/** Project event kinds rejected once, in order, to exercise retry flows. */
__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__?: number[];
/** Structured merge error returned by the mock native merge command. */
__BUZZ_E2E_PROJECT_MERGE_ERROR__?: {
code: string;
message: string;
recovery: {
action: "open_terminal";
sourceBranch: string;
targetBranch: string;
} | null;
};
/** Overrides the first mock repository owner for delegated-owner tests. */
__BUZZ_E2E_PROJECT_OWNER_OVERRIDE__?: string;
/** Project history kinds rejected with CLOSED for aggregate-query tests. */
__BUZZ_E2E_REJECT_PROJECT_QUERY_KINDS__?: number[];
/** Captured aggregate project-history filters for request-count assertions. */
__BUZZ_E2E_PROJECT_QUERY_FILTERS__?: MockFilter[];
__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__?: {
local_path: string | null;
local_branch: string | null;
local_branches: string[];
local_head: string | null;
local_short_head: string | null;
remote_branch: string | null;
remote_head: string | null;
remote_short_head: string | null;
merge_base: string | null;
ahead_count: number;
behind_count: number;
has_uncommitted_changes: boolean;
has_untracked_files: boolean;
can_push: boolean;
push_block_reason: string | null;
can_pull: boolean;
pull_block_reason: string | null;
};
__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__?: (state: ConnectionState) => void;
__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__?: () => ConnectionState;
__BUZZ_E2E_SET_STALL_WEBSOCKET_SENDS__?: (stall: boolean) => void;
__BUZZ_E2E_DISCONNECT_MOCK_WEBSOCKETS__?: () => number;
__BUZZ_E2E_RESTART_MOCK_WEBSOCKETS__?: () => number;
__BUZZ_E2E_SET_MESH__?: (mesh: {
admitted?: boolean;
models?: Array<{ id: string; name: string | null }>;
denyReason?: string;
/** Seed the runtime slot's lifecycle state (default "off"). */
nodeState?: "off" | "running";
/**
* Seed the runtime slot's role. "client" models this machine CONSUMING a
* peer's compute — it shares the single slot and reports state:"running",
* so the Share toggle must stay off. Drives the toggle-on regression test.
*/
nodeMode?: "serve" | "client" | null;
/** Seed host-side serving usage to exercise the "who's using my compute" indicator. */
servingUsage?: Partial<{
inflight: number;
peakInflight: number;
requestsServed: number;
tokensServed: number;
tokensPerSecond: number;
localAttempts: number;
remoteAttempts: number;
endpointAttempts: number;
peers: number;
}>;
}) => void;
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
agentPubkey: string;
channelId: string;
turnId: string;
kind?: "turn_started" | "turn_completed";
}) => void;
__BUZZ_E2E_SEED_OBSERVER_EVENTS__?: (input: {
agentPubkey: string;
events: Array<{
seq: number;
timestamp: string;
kind: string;
agentIndex: number | null;
channelId: string | null;
sessionId: string | null;
turnId: string | null;
payload: unknown;
}>;
}) => void;
__BUZZ_E2E_EMIT_MOCK_READ_STATE__?: (input: {
clientId: string;
contexts: Record<string, number>;
createdAt: number;
slotId: string;
}) => unknown;
__BUZZ_E2E_SEED_MOCK_REMINDERS__?: (reminders: RelayEvent[]) => void;
__BUZZ_E2E_QUERY_CLIENT__?: {
invalidateQueries: (filters: { queryKey: readonly unknown[] }) => unknown;
};
__BUZZ_E2E_MD_PARSE_COUNT__?: () => number;
/**
* Activate the community timeout store as if a send was rejected with a
* timeout message. Lets E2E tests prove the timeout gate fires before encode.
* Call after page load. Pass expiresAtMs (epoch ms) or 0 for unknown expiry.
*/
__BUZZ_E2E_ACTIVATE_TIMEOUT__?: (expiresAtMs: number) => void;
/**
* Invalidate the channels React Query cache so E2E tests can trigger a
* re-fetch after calling archive_channel / update_channel via
* __BUZZ_E2E_INVOKE_MOCK_COMMAND__. Call after the mutation to make the
* updated channel state visible to subscribers.
*/
__BUZZ_E2E_INVALIDATE_CHANNELS__?: () => Promise<void>;
/**
* Directly mutate a mock channel's properties without going through a
* command handler. Use for E2E regressions that need to change
* channel_type or remove isMember in a single synchronous step, then
* follow up with __BUZZ_E2E_INVALIDATE_CHANNELS__ to flush the cache.
*
* Only the listed fields are writeable; omitted fields are left unchanged.
*/
__BUZZ_E2E_MUTATE_CHANNEL__?: (opts: {
channelId: string;
channelType?: "stream" | "forum" | "dm";
removeMemberPubkey?: string;
}) => void;
/**
* When set to an event ID string, `get_event` calls for that specific ID
* are held in a queue and not resolved until `__BUZZ_E2E_RELEASE_GET_EVENT__()`
* is called. Calls for any other event ID proceed normally. Used by the
* cold-recovery race test to prove mid-flight feedItems updates do not
* cancel the in-flight promise for the cold anchor specifically.
* Set to undefined/null to disable deferral.
*/
__BUZZ_E2E_DEFER_GET_EVENT__?: string | null;
/** Flush all deferred `get_event` calls for the target ID. Each queued
* request is resolved (or rejected) immediately. Returns the number of
* requests released. */
__BUZZ_E2E_RELEASE_GET_EVENT__?: () => number;
/** Count of `get_event` invocations for the current defer-target ID since
* the last time `__BUZZ_E2E_DEFER_GET_EVENT__` was set. */
__BUZZ_E2E_GET_EVENT_CALL_COUNT__?: number;
}
}
const DEFAULT_RELAY_HTTP_URL = "http://localhost:3000";
const DEFAULT_RELAY_WS_URL = "ws://localhost:3000";
// NIP event kinds the mock reaction handlers emit.
const KIND_REACTION = 7; // NIP-25 reaction
const KIND_DELETION = 5; // NIP-09 deletion
const KIND_NIP29_DELETION = 9005;
const CHANNEL_WINDOW_AUX_KINDS = new Set([
KIND_REACTION,
KIND_DELETION,
KIND_NIP29_DELETION,
KIND_STREAM_MESSAGE_EDIT,
]);
const CHANNEL_WINDOW_AUX_DELETION_KINDS = new Set([
KIND_DELETION,
KIND_NIP29_DELETION,
]);
// Fake media-proxy port the mock answers for `get_media_proxy_port`, so
// `rewriteRelayUrl()` produces a real `http://127.0.0.1:<port>/media/...` src
// in e2e (instead of the `buzz-media://` fallback). The reaction guard
// asserts against this exact port.
const MOCK_MEDIA_PROXY_PORT = 54321;
// A relay-hosted custom emoji used by the reaction guard. Its URL matches
// `rewriteRelayUrl()`'s `/media/{64-hex}.{ext}` pattern on the relay origin, so
// reacting with it exercises the proxy rewrite (unlike the `:buzz:` fixture,
// whose external example.com URL passes through unchanged).
const REACTION_EMOJI_SHORTCODE = "react";
const REACTION_EMOJI_SHA = "c".repeat(64);
const REACTION_EMOJI_URL = `${DEFAULT_RELAY_HTTP_URL}/media/${REACTION_EMOJI_SHA}.png`;
// A reaction-target message seeded into `general` with a real 64-hex event id.
// The reaction guard reacts to THIS message: getReactionTargetId() only accepts
// a 64-hex `e` tag, and the other mock seeds (and user-sent messages) use short
// non-hex ids, so they can't be reaction targets. Content is distinctive so the
// test locates its row without relying on seed ordering.
const REACTION_TARGET_EVENT_ID = "d".repeat(64);
const REACTION_TARGET_CONTENT = "React to me with a custom emoji";
// System-message reaction target id (kind:40099 join event). Distinct 64-hex
// id so it is a valid reaction target and never collides with the regular
// REACTION_TARGET_EVENT_ID.
const SYSTEM_REACTION_TARGET_EVENT_ID = "e".repeat(64);
const E2E_IDENTITY_OVERRIDE_STORAGE_KEY = "buzz:e2e-identity-override.v1";
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 PROFILE_ONLY_AGENT_PUBKEY =
"8f83d6b7f3d74f7d933ae3a54dd8c6cc85c7f98e531c16e5a827b953441a8d67";
// A relay-classified bot agent whose declared NIP-OA owner is the mock viewer,
// but which is NOT locally managed. This is the fixture that exercises the
// sidebar's owner-gate path (`viewerIsOwner`), distinct from the local-managed
// path that `mira` (profile-only) and managed-agent fixtures cover.
const OWNED_RELAY_AGENT_PUBKEY =
"a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00";
const MOCK_IDENTITY_PUBKEY = DEFAULT_MOCK_IDENTITY.pubkey;
const STARTER_GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const STARTER_WELCOME_CHANNEL_ID = "5f0b1b3c-2a37-5366-9b8c-31a4b21d8e77";
const STARTER_GENERAL_CHANNEL_NAME = "general";
const STARTER_WELCOME_CHANNEL_NAME = "welcome-everyone";
// Tracks whether `persist_current_identity` or `import_identity` has cleared
// the lost flag set by `mock.identityLost`. Reset to false on each fresh page
// load (module re-evaluation), so tests start in a clean state.
let mockIdentityLostCleared = false;
// Same pattern for `mock.identityLocked`.
let mockIdentityLockedCleared = false;
// ── get_event defer/release seam ────────────────────────────────────────────
// When `window.__BUZZ_E2E_DEFER_GET_EVENT__` is set to a target event ID,
// `handleGetEvent` holds calls for that ID in this queue. All other event IDs
// continue to resolve immediately.
// `window.__BUZZ_E2E_RELEASE_GET_EVENT__()` flushes the queue and returns the
// count of released requests, giving the race test a deterministic way to prove
// that a mid-flight feedItems update does NOT cancel the in-flight promise for
// the specific cold anchor under test.
type DeferredGetEvent = {
resolve: (value: string) => void;
reject: (reason: unknown) => void;
run: () => Promise<string>;
};
let deferredGetEventQueue: DeferredGetEvent[] = [];
const mockDisplayNames = new Map<string, string>([
[MOCK_IDENTITY_PUBKEY, DEFAULT_MOCK_IDENTITY.display_name],
[ALICE_PUBKEY, "alice"],
[BOB_PUBKEY, "bob"],
[CHARLIE_PUBKEY, "charlie"],
[PROFILE_ONLY_AGENT_PUBKEY, "mira"],
[OWNED_RELAY_AGENT_PUBKEY, "nadia"],
[OUTSIDER_PUBKEY, "outsider"],
[DEFAULT_REAL_IDENTITY.pubkey, DEFAULT_REAL_IDENTITY.username],
]);
const mockAgentPubkeys = new Set([
ALICE_PUBKEY,
CHARLIE_PUBKEY,
PROFILE_ONLY_AGENT_PUBKEY,
OWNED_RELAY_AGENT_PUBKEY,
]);
// Kind-0 `name` aliases, distinct from the display name, for exercising the
// alias-tolerant mention resolution path (e.g. a message that says "@bobby"
// while bob's display name is "bob").
const mockKind0Names = new Map<string, string>([[BOB_PUBKEY, "bobby"]]);
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,
config?: E2eConfig,
): RawChannelWithMembership {
const currentPubkey = getMockMemberPubkey(config).toLowerCase();
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,
member_pubkeys: channel.members.map((member) => member.pubkey),
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.toLowerCase() === currentPubkey,
),
};
}
function toRawChannelDetail(
channel: MockChannel,
config?: E2eConfig,
): RawChannelDetail {
return {
...toRawChannel(channel, config),
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,
is_agent: role === "bot" || mockAgentPubkeys.has(pubkey),
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 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,
runtime: agent.runtime ?? null,
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,
avatar_url: agent.avatar_url ?? null,
model: agent.model,
provider: agent.provider ?? null,
env_vars: { ...(agent.env_vars ?? {}) },
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,
last_error_code: agent.last_error_code,
needs_restart: agent.needs_restart ?? false,
log_path: agent.log_path,
start_on_app_launch: agent.start_on_app_launch,
auto_restart_on_config_change: agent.auto_restart_on_config_change ?? true,
backend: agent.backend ?? { type: "local" as const },
backend_agent_id: agent.backend_agent_id ?? null,
respond_to: agent.respond_to ?? "owner-only",
respond_to_allowlist: agent.respond_to_allowlist
? [...agent.respond_to_allowlist]
: [],
};
}
function cloneEngramEntry(entry: RawEngramEntry): RawEngramEntry {
return {
...entry,
outgoingRefs: [...entry.outgoingRefs],
};
}
function cloneAgentMemoryListing(
listing: RawAgentMemoryListing,
): RawAgentMemoryListing {
return {
core: listing.core ? cloneEngramEntry(listing.core) : null,
memories: listing.memories.map(cloneEngramEntry),
truncated: listing.truncated,
fetchedAt: listing.fetchedAt,
};
}
function resetMockRelayMembers(config: E2eConfig | undefined) {
const pubkey = getMockMemberPubkey(config);
// Drive the active identity's role from `mock.relayRole` so the e2e harness
// can exercise the NIP-IA admin gate (owner/admin → true, member/null →
// false). Default stays `owner` to preserve existing test behavior.
const role = config?.mock?.relayRole;
const activeRoleMember =
role === null
? null
: {
pubkey,
role: role ?? "owner",
added_by: null,
created_at: isoMinutesAgo(120),
};
mockRelayMembers = [
...(activeRoleMember ? [activeRoleMember] : []),
{
pubkey: ALICE_PUBKEY,
role: "admin",
added_by: pubkey,
created_at: isoMinutesAgo(90),
},
{
pubkey: BOB_PUBKEY,
role: "member",
added_by: pubkey,
created_at: isoMinutesAgo(60),
},
];
}
function buildMockConfigSurface(pubkey: string): {
runtimeId: string | null;
runtimeLabel: string | null;
isPreSpawn: boolean;
normalized: Record<string, unknown>;
advanced: unknown[];
extensions: unknown[];
sources: Record<string, unknown>;
} {
// Goose running — mixed origins, override on model
const gooseSurface = {
runtimeId: "goose",
runtimeLabel: "Goose",
isPreSpawn: false,
normalized: {
model: {
value: "gpt-4o",
origin: "buzzExplicit",
overriddenValue: "gpt-4o-mini",
overriddenOrigin: "configFile",
isRequired: false,
writeVia: { type: "readOnly" },
},
provider: {
value: "openai",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.provider",
},
},
mode: {
value: "auto",
origin: "envVar",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "respawnWithEnvVar", envKey: "GOOSE_MODE" },
},
thinkingEffort: {
value: "medium",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.thinkingEffort",
},
},
maxOutputTokens: null,
contextLimit: null,
systemPrompt: null,
},
advanced: [
{
key: "active_provider",
label: "active_provider",
value: "openai",
origin: "configFile",
schemaType: { type: "string" },
writeVia: { type: "readOnly" },
},
],
extensions: [
{ name: "developer", kind: "stdio", enabled: true },
{ name: "web_search", kind: "stdio", enabled: true },
{ name: "memory", kind: "stdio", enabled: false },
],
sources: {
acpNative: "available",
acpConfigOptions: "available",
envVars: "available",
configFile: "available",
configFilePath: "~/.config/goose/config.yaml",
mcpConfigFilePath: "~/.config/goose/config.yaml",
},
};
// Claude Code — mostly ACP-sourced
const claudeSurface = {
runtimeId: "claude-code",
runtimeLabel: "Claude Code",
isPreSpawn: false,
normalized: {
model: {
value: "claude-sonnet-4-20250514",
origin: "acpConfigOption",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "acpSetConfigOption", configId: "model" },
},
provider: {
value: "anthropic",
origin: "acpConfigOption",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "acpSetConfigOption", configId: "provider" },
},
mode: {
value: "code",
origin: "acpConfigOption",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "acpSetConfigOption", configId: "mode" },
},
thinkingEffort: {
value: "high",
origin: "acpConfigOption",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "acpSetConfigOption", configId: "thinkingEffort" },
},
maxOutputTokens: {
value: "16384",
origin: "acpConfigOption",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "acpSetConfigOption", configId: "maxOutputTokens" },
},
contextLimit: null,
systemPrompt: null,
},
advanced: [],
extensions: [
{ name: "filesystem", kind: "mcp", enabled: true },
{ name: "github", kind: "mcp", enabled: true },
],
sources: {
acpNative: "available",
acpConfigOptions: "available",
envVars: "notApplicable",
configFile: "available",
configFilePath: "~/.claude/settings.json",
mcpConfigFilePath: "~/.claude.json",
},
};
// Pre-spawn — model from config file, ACP fields pending
const preSpawnSurface = {
runtimeId: "goose",
runtimeLabel: "Goose",
isPreSpawn: true,
normalized: {
model: {
value: "gpt-4o-mini",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "gooseNativeConfigWrite", configKey: "goose.model" },
},
provider: {
value: "openai",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.provider",
},
},
mode: {
value: null,
origin: "acpNativeRead",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "readOnly" },
},
thinkingEffort: {
value: null,
origin: "acpNativeRead",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "readOnly" },
},
maxOutputTokens: null,
contextLimit: null,
systemPrompt: null,
},
advanced: [],
extensions: [{ name: "developer", kind: "stdio", enabled: true }],
sources: {
acpNative: "pending",
acpConfigOptions: "pending",
envVars: "available",
configFile: "available",
configFilePath: "~/.config/goose/config.yaml",
mcpConfigFilePath: "~/.config/goose/config.yaml",
},
};
// Codex — dual-axis mode
const codexSurface = {
runtimeId: "codex",
runtimeLabel: "Codex",
isPreSpawn: false,
normalized: {
model: {
value: "codex-mini",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "gooseNativeConfigWrite", configKey: "goose.model" },
},
provider: {
value: "openai",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.provider",
},
},
mode: {
value: "suggest / auto-edit",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "gooseNativeConfigWrite", configKey: "goose.mode" },
},
thinkingEffort: null,
maxOutputTokens: null,
contextLimit: null,
systemPrompt: null,
},
advanced: [
{
key: "approval_policy",
label: "Approval Policy",
value: "unless-allow-listed",
origin: "configFile",
schemaType: {
type: "enum",
options: ["suggest", "auto-edit", "full-auto", "unless-allow-listed"],
},
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.approval_policy",
},
},
{
key: "sandbox_mode",
label: "Sandbox Mode",
value: "container",
origin: "envVar",
schemaType: {
type: "enum",
options: ["container", "host", "none"],
},
writeVia: { type: "respawnWithEnvVar", envKey: "GOOSE_SANDBOX_MODE" },
},
],
extensions: [
{ name: "filesystem", kind: "mcp", enabled: true },
{ name: "github", kind: "mcp", enabled: true },
],
sources: {
acpNative: "notApplicable",
acpConfigOptions: "notApplicable",
envVars: "available",
configFile: "available",
configFilePath: "~/.codex/config.toml",
mcpConfigFilePath: "~/.codex/config.toml",
},
};
// Live runtime override — a persona-linked agent whose session model was
// switched at runtime. The live model rides over the persona baseline as a
// secondary value WITHOUT strikethrough (the headline runtimeOverride render).
const runtimeOverrideSurface = {
runtimeId: "goose",
runtimeLabel: "Goose",
isPreSpawn: false,
normalized: {
model: {
value: "claude-opus-4-20250514",
origin: "runtimeOverride",
overriddenValue: "gpt-4o",
overriddenOrigin: "personaDefault",
isRequired: false,
writeVia: { type: "acpSetSessionModel" },
},
provider: {
value: "anthropic",
origin: "acpConfigOption",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "acpSetConfigOption", configId: "provider" },
},
mode: {
value: "auto",
origin: "envVar",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "respawnWithEnvVar", envKey: "GOOSE_MODE" },
},
thinkingEffort: {
value: "high",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.thinkingEffort",
},
},
maxOutputTokens: null,
contextLimit: null,
systemPrompt: null,
},
advanced: [],
extensions: [{ name: "web_search", kind: "stdio", enabled: true }],
sources: {
acpNative: "available",
acpConfigOptions: "available",
envVars: "available",
configFile: "available",
configFilePath: "~/.config/goose/config.yaml",
mcpConfigFilePath: "~/.config/goose/config.yaml",
},
};
// Mixed-provenance showcase — top-level rows carry different origins so the
// panel witnesses distinct provenance labels in one frame: "Set in Buzz",
// "Inherited from template", "From config file (...)" and
// "From environment variable (...)".
const multiOriginSurface = {
runtimeId: "goose",
runtimeLabel: "Goose",
isPreSpawn: false,
normalized: {
model: {
value: "gpt-4o",
origin: "buzzExplicit",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "readOnly" },
},
provider: {
value: "openai",
origin: "personaDefault",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "readOnly" },
},
mode: {
value: "auto",
origin: "envVar",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: { type: "respawnWithEnvVar", envKey: "GOOSE_MODE" },
},
thinkingEffort: {
value: "medium",
origin: "configFile",
overriddenValue: null,
overriddenOrigin: null,
isRequired: false,
writeVia: {
type: "gooseNativeConfigWrite",
configKey: "goose.thinkingEffort",
},
},
maxOutputTokens: null,
contextLimit: null,
systemPrompt: null,
},
advanced: [],
extensions: [],
sources: {
acpNative: "available",
acpConfigOptions: "available",
envVars: "available",
configFile: "available",
configFilePath: "~/.config/goose/config.yaml",
mcpConfigFilePath: "~/.config/goose/config.yaml",
},
};
const buzzAgentSurface = {
...gooseSurface,
runtimeId: "buzz-agent",
runtimeLabel: "Buzz Agent",
advanced: [],
extensions: [],
sources: {
...gooseSurface.sources,
configFilePath: null,
mcpConfigFilePath: null,
},
};
// Map well-known test pubkeys to specific fixtures.
// Synthetic agents are intentionally not TEST_IDENTITIES.
const PUBKEY_MULTI_ORIGIN =
"abc1230000000000000000000000000000000000000000000000000000000def";
const PUBKEY_BUZZ_AGENT =
"b0220000000000000000000000000000000000000000000000000000000000a9";
switch (pubkey) {
case ALICE_PUBKEY:
return claudeSurface;
case BOB_PUBKEY:
return preSpawnSurface;
case CHARLIE_PUBKEY:
return codexSurface;
case OUTSIDER_PUBKEY:
return runtimeOverrideSurface;
case PUBKEY_MULTI_ORIGIN:
return multiOriginSurface;
case PUBKEY_BUZZ_AGENT:
return buzzAgentSurface;
default:
return gooseSurface;
}
}
function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent {
const now = new Date().toISOString();
const status = seed.status ?? "stopped";
return {
pubkey: seed.pubkey,
name: seed.name,
persona_id: seed.personaId ?? null,
// Native serde always emits this key (`null` when unpinned) — the bridge
// must mirror the wire shape, not omit the key.
runtime: seed.runtime ?? null,
relay_url: DEFAULT_RELAY_WS_URL,
acp_command: "buzz-acp",
agent_command: "goose",
agent_args: ["acp"],
mcp_command: "",
turn_timeout_seconds: 320,
idle_timeout_seconds: null,
max_turn_duration_seconds: null,
parallelism: 1,
system_prompt: null,
avatar_url: seed.avatarUrl ?? null,
model: null,
env_vars: {},
status,
pid: status === "running" ? 42000 + mockManagedAgents.length : null,
created_at: now,
updated_at: now,
last_started_at: status === "running" ? now : null,
last_stopped_at: status === "stopped" ? now : null,
last_exit_code: null,
last_error: seed.lastError ?? null,
last_error_code: seed.lastErrorCode ?? null,
needs_restart: seed.needsRestart ?? false,
log_path: `/tmp/mock-agent-${seed.pubkey}.log`,
start_on_app_launch: true,
auto_restart_on_config_change: seed.autoRestartOnConfigChange ?? true,
backend: seed.backend ?? { type: "local" },
backend_agent_id: null,
respond_to: seed.respondTo ?? "owner-only",
respond_to_allowlist: seed.respondToAllowlist ?? [],
private_key_nsec: `nsec1mock${seed.pubkey.slice(0, 20)}`,
log_lines: [
`buzz-acp starting: relay=${DEFAULT_RELAY_WS_URL} agent_pubkey=${seed.pubkey} parallelism=1`,
"profile created; harness not started",
],
};
}
function resetMockRelayAgents(config?: E2eConfig) {
mockRelayAgents = defaultMockRelayAgents.map((agent) => ({
...agent,
channels: [...agent.channels],
channel_ids: [...agent.channel_ids],
capabilities: [...agent.capabilities],
respond_to_allowlist: [...(agent.respond_to_allowlist ?? [])],
}));
for (const seed of config?.mock?.relayAgents ?? []) {
const channels = mockChannels.filter((channel) => {
return (
seed.channelIds?.includes(channel.id) ||
seed.channelNames?.includes(channel.name)
);
});
mockRelayAgents.push({
pubkey: seed.pubkey,
name: seed.name,
agent_type: seed.agentType ?? "goose",
channels: channels.map((channel) => channel.name),
channel_ids: channels.map((channel) => channel.id),
capabilities: seed.capabilities ?? ["messages", "channels", "mcp"],
status: seed.status ?? "online",
respond_to: seed.respondTo ?? "owner-only",
respond_to_allowlist: seed.respondToAllowlist ?? [],
});
}
}
function resetMockManagedAgents(config?: E2eConfig) {
mockManagedAgents = [];
mockManagedAgentRuntimes = (config?.mock?.managedAgentRuntimes ?? []).map(
(seed) => ({
pubkey: seed.pubkey,
relayUrl: seed.relayUrl,
localSetup: true,
lifecycle: seed.lifecycle ?? "ready",
pid: seed.lifecycle === "stopped" ? null : 43000,
error: null,
logPath: null,
}),
);
for (const seed of config?.mock?.managedAgents ?? []) {
mockManagedAgents.push(buildSeededManagedAgent(seed));
applyMockDisplayName(seed.pubkey, seed.name);
mockAgentPubkeys.add(seed.pubkey);
mockProfiles.set(seed.pubkey, {
pubkey: seed.pubkey,
display_name: seed.name,
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: MOCK_IDENTITY_PUBKEY,
is_agent: true,
has_profile_event: true,
});
for (const channel of mockChannels) {
const isSeedChannel =
seed.channelIds?.includes(channel.id) ||
seed.channelNames?.includes(channel.name);
if (
!isSeedChannel ||
channel.members.some((member) => member.pubkey === seed.pubkey)
) {
continue;
}
channel.members.push({
pubkey: seed.pubkey,
role: "bot",
is_agent: true,
joined_at: new Date().toISOString(),
display_name: seed.name,
});
syncMockChannel(channel);
touchMockChannel(channel);
}
}
syncMockRelayAgentsFromManagedAgents();
}
function resetMockPersonas(config?: E2eConfig) {
const now = new Date().toISOString();
const activePersonaIds = new Set(config?.mock?.activePersonaIds ?? []);
const builtInPersonas = [
{
id: "builtin:fizz",
display_name: "Fizz",
avatar_url: null,
system_prompt: "You are Fizz.",
},
{
id: "builtin:honey",
display_name: "Honey",
avatar_url: null,
system_prompt: "You are Honey.",
},
{
id: "builtin:bumble",
display_name: "Bumble",
avatar_url: null,
system_prompt: "You are Bumble.",
},
];
mockPersonas = builtInPersonas.map((persona) => ({
id: persona.id,
display_name: persona.display_name,
avatar_url: persona.avatar_url,
system_prompt: persona.system_prompt,
runtime: null,
model: null,
provider: null,
name_pool: [],
is_builtin: true,
is_active: activePersonaIds.has(persona.id),
shared: false,
source_team: null,
created_at: now,
updated_at: now,
}));
for (const persona of config?.mock?.personas ?? []) {
mockPersonas.push({
id: persona.id ?? crypto.randomUUID(),
display_name: persona.displayName,
avatar_url: persona.avatarUrl ?? null,
system_prompt: persona.systemPrompt,
runtime: persona.runtime ?? null,
model: persona.model ?? null,
provider: persona.provider ?? null,
name_pool: persona.namePool ?? [],
respond_to: persona.respondTo ?? null,
respond_to_allowlist:
persona.respondTo === "allowlist"
? [...(persona.respondToAllowlist ?? [])]
: [],
is_builtin: false,
is_active: persona.isActive ?? true,
shared: persona.shared ?? false,
source_team: persona.sourceTeam ?? null,
env_vars: { ...(persona.envVars ?? {}) },
created_at: now,
updated_at: persona.updatedAt ?? now,
});
}
}
function resetMockTeams(config?: E2eConfig) {
const now = new Date().toISOString();
mockTeams = [
{
id: "team-engineering-001",
name: "Engineering",
description: "Core engineering personas",
persona_ids: [],
is_builtin: false,
source_dir: null,
is_symlink: false,
symlink_target: null,
version: null,
created_at: now,
updated_at: now,
},
{
id: "team-research-002",
name: "Research Agents",
description: "Directory-backed research team",
persona_ids: [],
is_builtin: false,
source_dir: "/Users/dev/agents/research",
is_symlink: false,
symlink_target: null,
version: "1.2.0",
created_at: now,
updated_at: now,
},
{
id: "team-platform-003",
name: "Platform Tools",
description: "Symlinked platform team",
persona_ids: [],
is_builtin: false,
source_dir: "/Users/dev/agents/platform",
is_symlink: true,
symlink_target: "/opt/shared-teams/platform",
version: "2.0.1",
created_at: now,
updated_at: now,
},
];
for (const team of config?.mock?.teams ?? []) {
mockTeams.push({
id: team.id ?? crypto.randomUUID(),
name: team.name,
description: team.description ?? null,
persona_ids: [...team.personaIds],
is_builtin: false,
source_dir: null,
is_symlink: false,
symlink_target: null,
version: null,
created_at: now,
updated_at: now,
});
}
}
function seedMockSearchProfiles(config?: E2eConfig) {
for (const seed of config?.mock?.searchProfiles ?? []) {
const pubkey = seed.pubkey.toLowerCase();
const profile = {
pubkey,
display_name: seed.displayName,
avatar_url: seed.avatarUrl ?? null,
about: seed.about ?? null,
nip05_handle: seed.nip05Handle ?? null,
owner_pubkey: seed.ownerPubkey ?? null,
is_agent: seed.isAgent ?? false,
has_profile_event: true,
};
mockProfiles.set(pubkey, profile);
applyMockDisplayName(pubkey, seed.displayName);
if (seed.isAgent) {
mockAgentPubkeys.add(pubkey);
}
}
}
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,
name: mockKind0Names.get(normalizedPubkey) ?? null,
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: null,
is_agent: mockAgentPubkeys.has(normalizedPubkey),
has_profile_event: true,
};
}
function listMockProfiles(): RawProfile[] {
const pubkeys = new Set<string>([
...mockProfiles.keys(),
...mockDisplayNames.keys(),
DEFAULT_REAL_IDENTITY.pubkey,
]);
return [...pubkeys]
.map((pubkey) => getMockProfileByPubkey(pubkey))
.filter((profile): profile is RawProfile => profile !== null);
}
function listMockChannels(config?: E2eConfig): RawChannelWithMembership[] {
return mockChannels.map((channel) => toRawChannel(channel, config));
}
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 getActiveIdentity(config)?.pubkey ?? getMockIdentity().pubkey;
}
function getMockMemberDisplayName(config: E2eConfig | undefined): string {
return getActiveIdentity(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: STARTER_GENERAL_CHANNEL_ID,
name: STARTER_GENERAL_CHANNEL_NAME,
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),
createMockMember(PROFILE_ONLY_AGENT_PUBKEY, "member", 840),
],
}),
createMockChannel({
id: STARTER_WELCOME_CHANNEL_ID,
name: STARTER_WELCOME_CHANNEL_NAME,
channel_type: "stream",
visibility: "open",
description: "Say hi, ask a question, or share what brought you here.",
topic: null,
purpose: null,
last_message_at: null,
archived_at: null,
created_by: MOCK_IDENTITY_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: 1440,
updated_minutes_ago: 1440,
members: [createMockMember(MOCK_IDENTITY_PUBKEY, "owner", 1440)],
}),
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),
],
}),
// Reproduces the all-replies-window regression (NIP-RS Fix A): a busy
// single-thread channel whose top-level root has scrolled past the history
// limit. `last_message_at` is a far-future timestamp standing in for the
// backend's reply-inclusive MAX(created_at) — it is NEWER than any top-level
// message the window can load, so falling back to it would advance the
// channel marker past unread replies. Seeded as its own channel so existing
// channels' unread state is undisturbed.
createMockChannel({
id: "fa11bac0-0000-4000-8000-000000000012",
name: "all-replies",
channel_type: "stream",
visibility: "open",
description: "Single-thread channel with the root past the history limit",
topic: null,
purpose: null,
last_message_at: new Date("2999-01-01T00:00:00.000Z").toISOString(),
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),
],
}),
createMockChannel({
id: "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e",
name: "design",
channel_type: "stream",
visibility: "open",
description: "Design system and UX discussions with engineering partners",
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),
createMockMember(OWNED_RELAY_AGENT_PUBKEY, "member", 600),
],
}),
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 community.",
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: "3c2d9f0a-1b44-5e77-9a21-6f8b0c4d2e91",
name: "secret-projects",
channel_type: "stream",
visibility: "private",
description: "Private project room",
topic: "Skunkworks",
purpose: "Coordinate confidential project work.",
last_message_at: null,
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: 600,
updated_minutes_ago: 120,
members: [
createMockMember(ALICE_PUBKEY, "owner", 600),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 540),
],
}),
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, MOCK_IDENTITY_PUBKEY],
members: [
createMockMember(ALICE_PUBKEY, "member", 720),
createMockMember(MOCK_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, MOCK_IDENTITY_PUBKEY],
members: [
createMockMember(BOB_PUBKEY, "member", 700),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 700),
],
}),
// Generic-named DM — name is "DM" so resolveChannelDisplayLabel must resolve
// the participant display name instead of returning the raw channel name.
// Used by agent-snapshot-send.spec.ts to prove picker/search/memgate/done
// all use the same resolved label from useUsersBatchQuery.
createMockChannel({
id: "d1ec7000-d000-4000-8000-000000000001",
name: "DM",
channel_type: "dm",
visibility: "private",
description: "Generic-named DM with charlie",
topic: null,
purpose: null,
last_message_at: null,
archived_at: null,
created_by: CHARLIE_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: 680,
updated_minutes_ago: 680,
participants: ["charlie", "tyler"],
participant_pubkeys: [CHARLIE_PUBKEY, MOCK_IDENTITY_PUBKEY],
members: [
createMockMember(CHARLIE_PUBKEY, "member", 680),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 680),
],
}),
// Generic-named Group DM — name "Group DM (3)" so resolveChannelDisplayLabel
// must resolve all OTHER participants' display names (bob, charlie).
// Used by agent-snapshot-send.spec.ts group-DM label test.
// NOTE: participants are BOB + CHARLIE (not ALICE, which conflicts with
// ANALYST_PUBKEY in managed-agent tests).
createMockChannel({
id: "d1ec7000-d000-4000-8000-000000000003",
name: "Group DM (3)",
channel_type: "dm",
visibility: "private",
description: "Generic-named group DM with bob and charlie",
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: 3,
nip29_group_id: null,
created_minutes_ago: 660,
updated_minutes_ago: 660,
participants: ["bob", "charlie", "tyler"],
participant_pubkeys: [BOB_PUBKEY, CHARLIE_PUBKEY, MOCK_IDENTITY_PUBKEY],
members: [
createMockMember(BOB_PUBKEY, "member", 660),
createMockMember(CHARLIE_PUBKEY, "member", 660),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 660),
],
}),
// Deep history channel for the load-older-under-virtualization E2E. Seeded
// with more messages than CHANNEL_HISTORY_LIMIT (300) so the initial load
// windows to the newest page and a `fetchOlder` (until-cursor) prepend has
// genuinely older rows to add — exercising the scroll-restore anchor under
// virtualization. Its own channel so existing channels' row-index and unread
// assertions stay undisturbed.
createMockChannel({
id: "feedf00d-0000-4000-8000-000000000007",
name: "deep-history",
channel_type: "stream",
visibility: "open",
description: "Channel with paginated history for load-older tests",
topic: null,
purpose: null,
last_message_at: isoMinutesAgo(1),
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: 2000,
updated_minutes_ago: 1,
members: [
createMockMember(ALICE_PUBKEY, "owner", 2000),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 1900),
],
}),
];
const mockMessages = new Map<string, RelayEvent[]>();
const mockUserStatuses: RelayEvent[] = [];
const mockReminderEvents: RelayEvent[] = [];
const mockPersonaEvents: RelayEvent[] = [];
let mockRelayMembers: RawRelayMember[] = [];
const mockSockets = new Map<number, MockSocket>();
let mockWebsocketSendMutexWedged = false;
let mockClosedChannelLiveSubscription = false;
const realSockets = new Map<number, WebSocket>();
let mockManagedAgents: MockManagedAgent[] = [];
let mockManagedAgentRuntimes: MockManagedAgentRuntimeRow[] = [];
let mockTtsPlaybackSpeed = 1;
// Mutable `save_subscriptions` table mirror — TEST-ONLY.
//
// Cloned from `activeConfig.mock.saveSubscriptions` at install time, then
// mutated by `create_save_subscription` / `delete_save_subscription` /
// `merge_save_subscription_kinds` / `remove_save_subscription_kind` exactly
// as the real SQLite-backed Rust commands would (see `archive/store.rs`).
// This lets E2E specs drive the fresh-internal-repair path (start from `[]`,
// reconcile, observe a kind-24200 row appear) and OSS toggle ON/OFF, neither
// of which an immutable seed can represent.
type MockSaveSubscriptionRow = {
scope_type: string;
scope_value: string;
kinds: string; // JSON-encoded integer array, e.g. "[9,40002]"
};
let mockSaveSubscriptions: MockSaveSubscriptionRow[] = [];
function resetMockSaveSubscriptions(config: E2eConfig | undefined) {
mockSaveSubscriptions = (config?.mock?.saveSubscriptions ?? []).map((s) => ({
...s,
}));
}
function resetMockPersonaCatalogEvents(config: E2eConfig | undefined) {
mockPersonaEvents.length = 0;
for (const event of config?.mock?.personaCatalogEvents ?? []) {
mockPersonaEvents.push({
...event,
tags: event.tags.map((tag) => [...tag]),
});
}
}
// Mesh-compute mock state — TEST-ONLY.
//
// This entire module (e2eBridge.ts) is loaded only when `window.__BUZZ_E2E__`
// is set by the Playwright harness; it never runs in a shipped build. These
// handlers stub the `mesh_*` Tauri commands with the SHAPES the UI expects
// (availability, node status, preset) so the desktop UI flow can be exercised
// in a browser. They deliberately do NOT model real admission, real inference,
// or real mesh routing — those are proven by the Rust layer-2 tests and the
// on-hardware layer-1 example. Do not port any of this into production code.
type MockServingUsage = {
inflight: number;
peakInflight: number;
requestsServed: number;
tokensServed: number;
tokensPerSecond: number;
localAttempts: number;
remoteAttempts: number;
endpointAttempts: number;
peers: number;
};
const ZERO_SERVING_USAGE: MockServingUsage = {
inflight: 0,
peakInflight: 0,
requestsServed: 0,
tokensServed: 0,
tokensPerSecond: 0,
localAttempts: 0,
remoteAttempts: 0,
endpointAttempts: 0,
peers: 0,
};
const mockMeshState: {
admitted: boolean;
models: Array<{ id: string; name: string | null }>;
denyReason: string;
nodeState: "off" | "running";
nodeMode: "serve" | "client" | null;
servingUsage: MockServingUsage;
} = {
admitted: true,
models: [
{ id: "hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M", name: "SmolLM2 135M" },
],
denyReason: "not a relay member",
nodeState: "off",
nodeMode: null,
servingUsage: { ...ZERO_SERVING_USAGE },
};
function resetMockMesh() {
mockMeshState.admitted = true;
mockMeshState.models = [
{ id: "hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M", name: "SmolLM2 135M" },
];
mockMeshState.denyReason = "not a relay member";
mockMeshState.nodeState = "off";
mockMeshState.nodeMode = null;
mockMeshState.servingUsage = { ...ZERO_SERVING_USAGE };
}
let mockPersonas: RawPersona[] = [];
let mockTeams: RawTeam[] = [];
// Listeners registered via the mock __TAURI_INTERNALS__.listen — keyed by event name.
const tauriEventListeners = new Map<string, Set<() => void>>();
const openedExternalUrls: string[] = [];
const defaultMockRelayAgents: 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",
respond_to: "anyone",
respond_to_allowlist: [],
},
{
pubkey: CHARLIE_PUBKEY,
name: "charlie",
agent_type: "codex",
channels: ["general"],
channel_ids: ["9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"],
capabilities: ["code", "reviews"],
status: "away",
respond_to: "anyone",
respond_to_allowlist: [],
},
];
let mockRelayAgents: RawRelayAgent[] = defaultMockRelayAgents.map((agent) => ({
...agent,
channels: [...agent.channels],
channel_ids: [...agent.channel_ids],
capabilities: [...agent.capabilities],
respond_to_allowlist: [...(agent.respond_to_allowlist ?? [])],
}));
// ── Workflow mocks ─────────────────────────────────────────────────────────
type MockWorkflow = {
id: string;
name: string;
owner_pubkey: string;
channel_id: string | null;
definition: Record<string, unknown>;
status: "active" | "disabled" | "archived";
created_at: number;
updated_at: number;
};
type RawWorkflowTraceEntry = {
step_id: string;
status: string;
output?: Record<string, unknown>;
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<string, unknown> {
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<string, unknown>;
}
function handleGetChannelWorkflows(args: { channelId: string }) {
return mockWorkflows.filter((w) => w.channel_id === args.channelId);
}
function handleGetChannelsWorkflows(args: { channelIds: string[] }) {
const ids = new Set(args.channelIds);
return mockWorkflows.filter(
(w) => w.channel_id != null && ids.has(w.channel_id),
);
}
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<string, unknown> | 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<string, unknown> | 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<string, unknown>)
: {};
const startedAt = createdAt + index;
const completedAt = startedAt + 1;
const output: Record<string, unknown> = {};
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<string, RawProfile>([
[
MOCK_IDENTITY_PUBKEY,
{
pubkey: MOCK_IDENTITY_PUBKEY,
display_name: DEFAULT_MOCK_IDENTITY.display_name,
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: null,
is_agent: false,
has_profile_event: true,
},
],
// alice, bob, and charlie are intentionally NOT seeded here — they are
// covered by mockDisplayNames + mockAgentPubkeys and synthesised on demand
// by getMockProfileByPubkey. Static seeds would cause ensureMockProfile to
// return has_profile_event:true when alice/bob/charlie are used as the
// active first-run identity, incorrectly skipping onboarding page 1.
[
PROFILE_ONLY_AGENT_PUBKEY,
{
pubkey: PROFILE_ONLY_AGENT_PUBKEY,
display_name: "mira",
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: MOCK_IDENTITY_PUBKEY,
is_agent: true,
has_profile_event: true,
},
],
[
OWNED_RELAY_AGENT_PUBKEY,
{
pubkey: OWNED_RELAY_AGENT_PUBKEY,
display_name: "nadia",
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: MOCK_IDENTITY_PUBKEY,
is_agent: true,
has_profile_event: true,
},
],
]);
const mockPresence = new Map<string, PresenceStatus>([
[MOCK_IDENTITY_PUBKEY, "offline"],
[DEFAULT_REAL_IDENTITY.pubkey, "offline"],
[ALICE_PUBKEY, "online"],
[BOB_PUBKEY, "away"],
[CHARLIE_PUBKEY, "online"],
[PROFILE_ONLY_AGENT_PUBKEY, "online"],
[OWNED_RELAY_AGENT_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",
respond_to: agent.respond_to,
respond_to_allowlist: [...agent.respond_to_allowlist],
};
},
);
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.__BUZZ_E2E__;
}
function readStoredIdentityOverride(): TestIdentity | undefined {
try {
const rawValue = window.localStorage.getItem(
E2E_IDENTITY_OVERRIDE_STORAGE_KEY,
);
if (!rawValue) {
return undefined;
}
const parsed = JSON.parse(rawValue);
if (
!parsed ||
typeof parsed !== "object" ||
typeof parsed.privateKey !== "string" ||
typeof parsed.pubkey !== "string" ||
typeof parsed.username !== "string"
) {
return undefined;
}
return {
privateKey: parsed.privateKey,
pubkey: parsed.pubkey,
username: parsed.username,
};
} catch {
return undefined;
}
}
function writeStoredIdentityOverride(identity: TestIdentity) {
window.localStorage.setItem(
E2E_IDENTITY_OVERRIDE_STORAGE_KEY,
JSON.stringify(identity),
);
}
function importMockIdentity(nsec: string) {
const decoded = decode(nsec.trim());
if (decoded.type !== "nsec") {
throw new Error("Invalid Nostr private key.");
}
const privateKey = bytesToHex(decoded.data);
const pubkey = getPublicKey(decoded.data);
const username = mockDisplayNames.get(pubkey) ?? "";
const identity = {
privateKey,
pubkey,
username,
};
writeStoredIdentityOverride(identity);
if (!mockProfiles.has(pubkey)) {
mockProfiles.set(pubkey, {
pubkey,
display_name: username || null,
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: null,
// A non-empty username means this identity is registered in
// mockDisplayNames — it has a real mock relay profile (kind:0).
// A truly new identity (no username) has no event yet.
has_profile_event: username.length > 0,
});
}
return {
pubkey,
display_name: username,
};
}
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 getActiveIdentity(config: E2eConfig | undefined) {
return readStoredIdentityOverride() ?? getIdentity(config);
}
function ensureMockProfile(config: E2eConfig | undefined): RawProfile {
const pubkey = getMockMemberPubkey(config);
const existing = mockProfiles.get(pubkey);
if (existing) {
return existing;
}
const displayName = getMockMemberDisplayName(config);
const profile = {
pubkey,
display_name: displayName,
avatar_url: null,
about: null,
nip05_handle: null,
owner_pubkey: null,
// Synthesised fallback: no kind:0 event exists on the relay for this
// identity. Always false regardless of display name so the onboarding
// gate cannot mistake a blank first-run identity for a returning user.
has_profile_event: false,
};
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, code?: number, reason?: string) {
handler({
type: "Close",
data: code === undefined ? undefined : { code, reason: reason ?? "" },
});
}
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,
};
}
/**
* A reply broadcast to the channel timeline carries the exact tag
* `["broadcast", "1"]` (NIP-CW §Top-level Classification).
*/
function isMockBroadcastReply(tags: string[][]): boolean {
return tags.some((tag) => tag[0] === "broadcast" && tag[1] === "1");
}
/**
* Mirror the relay's channel-window row set (buzz-db `thread.rs`, NIP-CW
* §Top-level Classification): an event is a timeline row iff its depth is 0
* (no reply marker → `rootEventId === null`) OR its depth is 1 (its parent is
* the thread root) AND it is broadcast. Depth ≥ 2 replies never surface on the
* timeline. A bare-`rootEventId === null` predicate silently dropped broadcast
* depth-1 replies the real relay serves.
*/
function isMockTopLevelRow(event: RelayEvent): boolean {
const { parentEventId, rootEventId } = getThreadReferenceFromTags(event.tags);
if (rootEventId === null) {
return true;
}
const isDepthOne = parentEventId !== null && parentEventId === rootEventId;
return isDepthOne && isMockBroadcastReply(event.tags);
}
function appendMentionTags(
tags: string[][],
mentionPubkeys: string[] | undefined,
selfPubkey: string,
) {
const selfLower = selfPubkey.toLowerCase();
const seen = new Set<string>([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) - 120,
kind: 9,
tags: [["h", channelId]],
content: "Welcome to #general",
sig: "mocksig".repeat(20).slice(0, 128),
},
// Alice authored — gives e2e specs a non-self profile pane to open
// by clicking the second message-row's author button. Used by
// tests/e2e/identity-archive.spec.ts to exercise the admin / OA /
// none-of-the-above branches of the NIP-IA gate. Both seeds are
// backdated (welcome at -120s, Alice at -60s) so user-sent messages
// in other specs always land after both — preserving
// `message-row.first()` = welcome and `.last()` = sent.
{
id: "mock-general-alice",
pubkey: ALICE_PUBKEY,
created_at: Math.floor(Date.now() / 1000) - 60,
kind: 9,
tags: [["h", channelId]],
content: "Hey team — checking in.",
sig: "mocksig".repeat(20).slice(0, 128),
},
// Reaction-target seed for the custom-emoji reaction guard. Real
// 64-hex id so getReactionTargetId() accepts it as a reaction target
// (the short-id seeds above can't be reacted to). Backdated after the
// other seeds, so it stays at row index >= 2 and never displaces
// first()=welcome / nth(1)=alice that other specs rely on.
{
id: REACTION_TARGET_EVENT_ID,
pubkey: ALICE_PUBKEY,
created_at: Math.floor(Date.now() / 1000) - 45,
kind: 9,
tags: [["h", channelId]],
content: REACTION_TARGET_CONTENT,
sig: "mocksig".repeat(20).slice(0, 128),
},
// System-message reaction target. A kind:40099 join event renders via
// SystemMessageRow (testid `system-message-row`, NOT `message-row`),
// so it never displaces the `message-row` index assertions other
// specs rely on. Real 64-hex id so getReactionTargetId() accepts it
// as a reaction target — this is the surface the original "react to a
// system message" bug lived on. Backdated like the other seeds.
{
id: SYSTEM_REACTION_TARGET_EVENT_ID,
pubkey: ALICE_PUBKEY,
created_at: Math.floor(Date.now() / 1000) - 30,
kind: KIND_SYSTEM_MESSAGE,
tags: [["h", channelId]],
content: JSON.stringify({
type: "member_joined",
actor: ALICE_PUBKEY,
target: ALICE_PUBKEY,
}),
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),
},
// Filler replies so the thread overflows the panel viewport — the
// deep-link target (mock-forum-release-deeplink) sits below the fold
// at open, proving scrollIntoView lands an offscreen content-
// visibility row. Named IDs above are untouched.
...Array.from({ length: 24 }, (_, index) => ({
id:
index === 23
? "mock-forum-release-deeplink"
: `mock-forum-release-filler-${index}`,
pubkey: ALICE_PUBKEY,
created_at: Math.floor(Date.now() / 1000) - (79 - index) * 60,
kind: 45003,
tags: buildReplyMessageTags(
channelId,
ALICE_PUBKEY,
"mock-forum-release-thread",
"mock-forum-release-thread",
undefined,
),
content:
index === 23
? "Deep-link target: confirmed the rollout plan end to end."
: `Follow-up note #${index + 1} on the release checklist.`,
sig: "mocksig".repeat(20).slice(0, 128),
})),
]
: channelId === "94a444a4-c0a3-5966-ab05-530c6ddc2301"
? [
// Charlie is a `bot` member of #agents (see channel seed), so this
// message renders with role="bot" — the surface whose avatar opens
// a managed-agent profile panel / hover popover with active-turn
// badges. #agents has no message-row index assertions, so seeding
// here is safe for existing specs.
{
id: "mock-agents-charlie",
pubkey: CHARLIE_PUBKEY,
created_at: Math.floor(Date.now() / 1000) - 90,
kind: 9,
tags: [["h", channelId]],
content: "Indexing the channel catalog now.",
sig: "mocksig".repeat(20).slice(0, 128),
},
// Owned remote relay agent: declared-owned by the mock viewer,
// present in the relay registry, but NOT locally managed. This
// keeps the profile Runtime-tab owner gate honest.
{
id: "mock-agents-owned-relay-nadia",
pubkey: OWNED_RELAY_AGENT_PUBKEY,
created_at: Math.floor(Date.now() / 1000) - 85,
kind: 9,
tags: [["h", channelId]],
content: "Indexing remotely for my owner.",
sig: "mocksig".repeat(20).slice(0, 128),
},
// Seed one message per managed agent that is a member of #agents.
// This lets e2e specs open the profile panel by clicking the
// agent's avatar in a message-row (the same pattern as charlie).
...mockManagedAgents
.filter((agent) =>
mockChannels
.find((ch) => ch.id === channelId)
?.members.some((m) => m.pubkey === agent.pubkey),
)
.map((agent, index) => ({
id: `mock-agents-managed-${agent.pubkey.slice(0, 8)}`,
pubkey: agent.pubkey,
created_at: Math.floor(Date.now() / 1000) - 80 + index,
kind: 9 as const,
tags: [["h", channelId]],
content: `${agent.name} reporting in.`,
sig: "mocksig".repeat(20).slice(0, 128),
})),
]
: channelId === "feedf00d-0000-4000-8000-000000000007"
? (() => {
const count = getConfig()?.mock?.deepHistoryMessageCount ?? 600;
return Array.from({ length: count }, (_, index) => ({
id: `mock-deep-history-${index}`,
pubkey: index % 2 === 0 ? ALICE_PUBKEY : MOCK_IDENTITY_PUBKEY,
created_at:
Math.floor(Date.now() / 1000) - (count - index) * 60,
kind: 9,
tags: [["h", channelId]],
content:
count > 600
? `Deep history message #${index}\n${"variable wrapped history ".repeat((index % 12) + 1)}`
: `Deep history message #${index}`,
sig: "mocksig".repeat(20).slice(0, 128),
}));
})()
: [];
mockMessages.set(channelId, seeded);
return seeded;
}
function prependMockHistory(input: {
channelName: string;
count: number;
startIndex?: number;
lineCount?: number;
createdAtStart?: number;
emit?: boolean;
}) {
const channel = mockChannels.find(
(candidate) => candidate.name === input.channelName,
);
if (!channel) {
throw new Error(`Unknown mock channel: ${input.channelName}`);
}
const store = getMockMessageStore(channel.id);
const earliestCreatedAt = store.reduce(
(earliest, event) => Math.min(earliest, event.created_at),
Math.floor(Date.now() / 1000),
);
const createdAtStart =
input.createdAtStart ?? earliestCreatedAt - input.count - 1;
const startIndex = input.startIndex ?? 0;
const lineCount = input.lineCount ?? 1;
const events = Array.from({ length: input.count }, (_, offset) => {
const index = startIndex + offset;
const body = Array.from(
{ length: lineCount },
(_unused, lineIndex) => `mock older ${index} line ${lineIndex + 1}`,
).join("\n");
return createMockEvent(
9,
body,
[["h", channel.id]],
ALICE_PUBKEY,
createdAtStart + offset,
`mock-older-${channel.name}-${index}`.replace(/[^a-zA-Z0-9]/g, ""),
);
});
store.unshift(...events);
store.sort((left, right) => left.created_at - right.created_at);
if (input.emit) {
for (const event of events) {
emitMockLiveEvent(channel.id, event);
}
}
return events;
}
function emitMockHistory(
socket: MockSocket,
subId: string,
channelId: string,
filter: MockFilter,
) {
const events = getMockMessageStore(channelId)
.filter((event) => {
if (filter.kinds && !filter.kinds.includes(event.kind)) {
return false;
}
if (filter.since !== undefined && event.created_at < filter.since) {
return false;
}
if (filter.until !== undefined && event.created_at > filter.until) {
return false;
}
return true;
})
// Relay order is `created_at DESC, id ASC` — match it (both the WS history
// page and the `get_channel_messages_before` keyset are backed by that one
// order in production, so the mock must be self-consistent too, else a
// same-second slice returned here won't line up with the keyset's tiebreak
// and the dense-second escape hatch can't prove completeness). Bare `until`
// still can't advance past a second denser than one page; the composite
// keyset is the escape hatch.
.sort(
(left, right) =>
right.created_at - left.created_at || left.id.localeCompare(right.id),
)
.slice(0, filter.limit ?? 50)
.sort(
(left, right) =>
left.created_at - right.created_at || left.id.localeCompare(right.id),
);
const emit = () => {
for (const event of events) {
sendWsText(socket.handler, ["EVENT", subId, event]);
}
sendWsText(socket.handler, ["EOSE", subId]);
};
emit();
}
function emitMockLiveEvent(channelId: string, event: RelayEvent) {
for (const socket of mockSockets.values()) {
for (const [subId, subscription] of socket.subscriptions) {
if (
(subscription.channelId === channelId ||
subscription.channelId === GLOBAL_MOCK_SUBSCRIPTION) &&
(!subscription.kinds || subscription.kinds.includes(event.kind))
) {
sendWsText(socket.handler, ["EVENT", subId, event]);
}
}
}
}
function emitMockGlobalEvent(event: RelayEvent) {
if (
event.kind === KIND_PERSONA &&
event.pubkey.toLowerCase() !== MOCK_IDENTITY_PUBKEY.toLowerCase() &&
!personaHasExactSharedTag(event)
) {
return;
}
for (const socket of mockSockets.values()) {
for (const [subId, subscription] of socket.subscriptions) {
if (subscription.kinds && !subscription.kinds.includes(event.kind)) {
continue;
}
sendWsText(socket.handler, ["EVENT", subId, event]);
}
}
}
function hasMockLiveSubscription(channelId: string, kind?: number) {
for (const socket of mockSockets.values()) {
for (const subscription of socket.subscriptions.values()) {
if (
(subscription.channelId === channelId ||
subscription.channelId === GLOBAL_MOCK_SUBSCRIPTION) &&
(kind === undefined ||
!subscription.kinds ||
subscription.kinds.includes(kind))
) {
return true;
}
}
}
return false;
}
/**
* True iff a live REQ subscription is open with an `#p` filter containing
* `ownerPubkey` and a `kinds` filter containing `kind`. Used to assert the
* observer-archive reconciliation gate actually opens an owner-scoped live
* filter (not just that `list_save_subscriptions` was called) once the
* gate resolves.
*/
function hasMockOwnerKindSubscription(ownerPubkey: string, kind: number) {
for (const socket of mockSockets.values()) {
for (const subscription of socket.subscriptions.values()) {
if (
subscription.ownerPubkeys.includes(ownerPubkey) &&
(subscription.kinds?.includes(kind) ?? false)
) {
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 resetMockUserStatuses() {
mockUserStatuses.length = 0;
}
// Mocked Rust-side pending deep-link queue (see desktop/src-tauri/src/deep_link.rs).
let mockPendingCommunityDeepLinks: Array<{
id: string;
kind: string;
relayUrl: string;
code: string | null;
name: string | null;
}> = [];
function resetMockPendingCommunityDeepLinks(config: E2eConfig | null) {
mockPendingCommunityDeepLinks = (
config?.mock?.pendingCommunityDeepLinks ?? []
).map((pending) => ({
...pending,
code: pending.code ?? null,
name: pending.name ?? null,
}));
}
function recordMockUserStatus(event: RelayEvent) {
const dTag = event.tags.find((tag) => tag[0] === "d")?.[1];
if (dTag) {
const index = mockUserStatuses.findIndex(
(stored) =>
stored.pubkey.toLowerCase() === event.pubkey.toLowerCase() &&
stored.tags.some((tag) => tag[0] === "d" && tag[1] === dTag),
);
if (index >= 0) {
mockUserStatuses.splice(index, 1);
}
}
mockUserStatuses.push(event);
}
function filterMockUserStatuses(filter: MockFilter) {
const authors = filter.authors?.map((author) => author.toLowerCase());
const dTags = filter["#d"];
return mockUserStatuses
.filter((event) => {
if (authors && !authors.includes(event.pubkey.toLowerCase())) {
return false;
}
if (
dTags &&
!event.tags.some((tag) => tag[0] === "d" && dTags.includes(tag[1]))
) {
return false;
}
return true;
})
.sort((a, b) => b.created_at - a.created_at);
}
function emitMockChannelMessage(
channelId: string,
content: string,
parentEventId?: string | null,
pubkey?: string,
kind?: number,
mentionPubkeys?: string[],
extraTags?: string[][],
createdAt?: number,
id?: string,
) {
const eventKind = kind ?? 9;
if (!parentEventId) {
const tags = buildTopLevelMessageTags(
channelId,
mentionPubkeys,
pubkey ?? DEFAULT_MOCK_IDENTITY.pubkey,
);
if (extraTags) tags.push(...extraTags);
const event = createMockEvent(
eventKind,
content,
tags,
pubkey,
createdAt,
id,
);
recordMockMessage(channelId, event);
emitMockLiveEvent(channelId, event);
return event;
}
const history = getMockMessageStore(channelId);
const parentEvent =
history.find((event) => event.id === parentEventId) ?? null;
const parentThread = parentEvent
? getThreadReferenceFromTags(parentEvent.tags)
: {
parentEventId: null,
rootEventId: null,
};
const rootEventId = parentThread.rootEventId ?? parentEventId;
const authorPubkey = pubkey ?? DEFAULT_MOCK_IDENTITY.pubkey;
const tags = buildReplyMessageTags(
channelId,
authorPubkey,
parentEventId,
rootEventId,
mentionPubkeys,
);
if (extraTags) tags.push(...extraTags);
const event = createMockEvent(
eventKind,
content,
tags,
authorPubkey,
createdAt,
id,
);
recordMockMessage(channelId, event);
emitMockLiveEvent(channelId, event);
return event;
}
function emitMockTypingIndicator(
channelId: string,
pubkey: string,
threadHeadId?: string,
createdAt?: number,
) {
const event: RelayEvent = {
id: crypto.randomUUID().replace(/-/g, ""),
pubkey,
created_at: createdAt ?? Math.floor(Date.now() / 1000),
kind: 20002,
tags: [
["h", channelId],
...(threadHeadId ? [["e", threadHeadId, "", "reply"]] : []),
],
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<RawForumPostsResponse> {
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<RawForumThreadResponse> {
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,
};
}
type RawThreadCursor = {
created_at: number;
event_id: string;
};
type RawThreadRepliesResponse = {
events: RelayEvent[];
next_cursor: RawThreadCursor | null;
};
/**
* Mirror of the desktop `get_thread_replies` command: return the full reply
* subtree under a root, chronological (oldest first), excluding the root itself,
* with gap-free `(created_at, event_id)` keyset paging.
*
* The event-id tiebreak is load-bearing — same-second replies must all page
* through even when they cross a page boundary. This lets a Playwright spec
* assert the paged union equals the whole subtree, matching the relay contract.
*/
async function handleGetThreadReplies(
args: {
rootEventId: string;
channelId?: string | null;
limit?: number | null;
depthLimit?: number | null;
cursor?: RawThreadCursor | null;
},
config: E2eConfig | undefined,
): Promise<RawThreadRepliesResponse> {
const cap = Math.min(args.limit ?? 200, 500);
const filter: MockFilter & Record<string, unknown> = {
"#e": [args.rootEventId],
depth_limit: args.depthLimit ?? 64,
kinds: [...TIMELINE_KINDS],
limit: cap,
};
if (args.channelId) {
filter["#h"] = [args.channelId];
}
if (args.cursor) {
filter.thread_cursor = args.cursor.created_at;
filter.thread_cursor_id = args.cursor.event_id;
}
const identity = getIdentity(config);
if (
!isPGatedFilterAuthorized(
filter,
identity?.pubkey ?? getMockMemberPubkey(config),
)
) {
throw new Error(P_GATED_REJECTION_MESSAGE);
}
let subtree: RelayEvent[];
if (!identity) {
// Mock store: walk the reply forest transitively from the root so nested
// replies (reply-to-a-reply) are included, matching thread_metadata depth.
const events = args.channelId
? getMockMessageStore(args.channelId)
: Array.from(mockMessages.values()).flat();
const byId = new Map(events.map((event) => [event.id, event]));
const root = byId.get(args.rootEventId);
const collected: RelayEvent[] = [];
const included = new Set<string>();
if (!root) {
subtree = collected;
} else {
const frontier = new Set<string>([root.id]);
for (;;) {
let added = false;
for (const event of events) {
if (included.has(event.id)) {
continue;
}
const ref = getThreadReferenceFromTags(event.tags);
if (!ref.parentEventId || !frontier.has(ref.parentEventId)) {
continue;
}
included.add(event.id);
collected.push(event);
frontier.add(event.id);
added = true;
}
if (!added) {
break;
}
}
subtree = collected;
}
} else {
// Config mode: exercise the real bridge thread path over /query.
const events = await relayQuery(config, [filter]);
const nextCursor =
events.length >= cap
? {
created_at: events[events.length - 1].created_at,
event_id: events[events.length - 1].id,
}
: null;
return { events, next_cursor: nextCursor };
}
// Mock mode paging: sort by the composite key, then slice strictly after the
// cursor so same-second ties can never be skipped across a page boundary.
subtree.sort(
(left, right) =>
left.created_at - right.created_at || left.id.localeCompare(right.id),
);
let start = 0;
if (args.cursor) {
const cursor = args.cursor;
start = subtree.findIndex(
(event) =>
event.created_at > cursor.created_at ||
(event.created_at === cursor.created_at &&
event.id.localeCompare(cursor.event_id) > 0),
);
if (start < 0) {
start = subtree.length;
}
}
const page = subtree.slice(start, start + cap);
const nextCursor =
page.length >= cap && start + cap < subtree.length
? {
created_at: page[page.length - 1].created_at,
event_id: page[page.length - 1].id,
}
: null;
const delayMs = config?.mock?.threadRepliesDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
return { events: page, next_cursor: nextCursor };
}
const TIMELINE_KINDS = new Set([
9,
40002,
40008,
40099,
43001,
43002,
43003,
43004,
43005,
43006,
KIND_HUDDLE_STARTED,
]);
const KIND_GIFT_WRAP = 1059;
const P_GATED_KINDS = new Set([
KIND_AGENT_OBSERVER_FRAME,
KIND_MEMBER_ADDED_NOTIFICATION,
KIND_MEMBER_REMOVED_NOTIFICATION,
KIND_GIFT_WRAP,
KIND_DM_VISIBILITY,
]);
const P_GATED_REJECTION_MESSAGE =
"restricted: p-gated kinds require #p tag matching your pubkey";
function filterKinds(filter: { kinds?: unknown }): number[] | undefined {
return Array.isArray(filter.kinds)
? filter.kinds.filter((kind): kind is number => typeof kind === "number")
: undefined;
}
function filterCanMatchPGated(filter: { kinds?: unknown }) {
const kinds = filterKinds(filter);
return !kinds || kinds.some((kind) => P_GATED_KINDS.has(kind));
}
function filterHasOwnPTag(filter: { "#p"?: unknown }, pubkey: string) {
const values = filter["#p"];
return (
Array.isArray(values) &&
values.length > 0 &&
values.every((value) => value === pubkey)
);
}
function isPGatedFilterAuthorized(
filter: { "#p"?: unknown; ids?: unknown; kinds?: unknown },
pubkey: string,
) {
if (!filterCanMatchPGated(filter)) {
return true;
}
const kinds = filterKinds(filter);
const ids = filter.ids;
const explicitlyDmVisibility = kinds?.includes(KIND_DM_VISIBILITY);
if (!explicitlyDmVisibility && Array.isArray(ids) && ids.length > 0) {
return true;
}
return filterHasOwnPTag(filter, pubkey);
}
type RawChannelMessagesPageResponse = {
events: RelayEvent[];
next_cursor: RawThreadCursor | null;
};
/**
* Mirror of the desktop `get_channel_messages_before` command: return one
* keyset page of *top-level* channel history strictly older than a composite
* `(before, before_id)` cursor, newest first (relay order `created_at DESC,
* id ASC`).
*
* This is the dense-second escape hatch — the id tiebreak is load-bearing so a
* single `created_at` second denser than one WS page can still be paged
* through: within a tied second the relay advances via `id > before_id`. Lets a
* Playwright spec assert the keyset union reaches every top-level message even
* when a second holds more than one page.
*/
async function handleGetChannelMessagesBefore(
args: {
channelId: string;
before: number;
beforeId?: string | null;
limit?: number | null;
},
config: E2eConfig | undefined,
): Promise<RawChannelMessagesPageResponse> {
const cap = Math.min(args.limit ?? 200, 500);
const identity = getIdentity(config);
let events: RelayEvent[];
if (!identity) {
// Mock store: top-level timeline events for this channel.
events = getMockMessageStore(args.channelId).filter((event) => {
if (!TIMELINE_KINDS.has(event.kind)) {
return false;
}
return isMockTopLevelRow(event);
});
} else {
// Config mode: exercise the real bridge keyset over /query.
const filter: Record<string, unknown> = {
"#h": [args.channelId],
kinds: [...TIMELINE_KINDS],
until: args.before,
limit: cap,
};
if (args.beforeId) {
filter.before_id = args.beforeId;
}
const page = await relayQuery(config, [filter]);
const nextCursor =
page.length >= cap
? {
created_at: page[page.length - 1].created_at,
event_id: page[page.length - 1].id,
}
: null;
return { events: page, next_cursor: nextCursor };
}
// Mock mode paging: relay order (created_at DESC, id ASC), then take the
// slice strictly older than the composite cursor. Strictly-older means
// `created_at < before OR (created_at === before AND id > before_id)` — the
// id tiebreak walks *forward* through a tied second under ASC id order.
events.sort(
(left, right) =>
right.created_at - left.created_at || left.id.localeCompare(right.id),
);
const before = args.before;
const beforeId = args.beforeId ?? null;
const older = events.filter((event) => {
if (event.created_at < before) {
return true;
}
if (event.created_at === before && beforeId !== null) {
return event.id.localeCompare(beforeId) > 0;
}
return false;
});
const page = older.slice(0, cap);
const nextCursor =
page.length >= cap
? {
created_at: page[page.length - 1].created_at,
event_id: page[page.length - 1].id,
}
: null;
return { events: page, next_cursor: nextCursor };
}
function getEventTargets(event: RelayEvent) {
return event.tags.flatMap((tag) =>
tag[0] === "e" && typeof tag[1] === "string" ? [tag[1]] : [],
);
}
function buildMockChannelWindowAux(
events: RelayEvent[],
rows: RelayEvent[],
): RelayEvent[] {
const collectHop = (kinds: Set<number>, targetIds: Set<string>) =>
events.filter(
(event) =>
kinds.has(event.kind) &&
getEventTargets(event).some((target) => targetIds.has(target)),
);
const firstHop = collectHop(
CHANNEL_WINDOW_AUX_KINDS,
new Set(rows.map((row) => row.id)),
);
const secondHop = collectHop(
CHANNEL_WINDOW_AUX_DELETION_KINDS,
new Set(firstHop.map((event) => event.id)),
);
const byId = new Map(firstHop.map((event) => [event.id, event]));
for (const event of secondHop) byId.set(event.id, event);
return [...byId.values()];
}
function buildMockChannelThreadSummary(
channelId: string,
root: RelayEvent,
events: RelayEvent[],
): RelayEvent | null {
const replies = events.filter((event) => {
const thread = getThreadReferenceFromTags(event.tags);
return thread.rootEventId === root.id;
});
if (replies.length === 0) return null;
const directReplies = replies.filter(
(event) => getThreadReferenceFromTags(event.tags).parentEventId === root.id,
);
const participants = [
...new Set(
replies
.sort(
(left, right) =>
right.created_at - left.created_at ||
left.id.localeCompare(right.id),
)
.map((event) => event.pubkey),
),
].slice(0, 10);
const lastReplyAt = Math.max(...replies.map((event) => event.created_at));
return {
id: `mock-window-summary-${root.id}`,
pubkey: DEFAULT_MOCK_IDENTITY.pubkey,
created_at: Math.floor(Date.now() / 1000),
kind: KIND_CHANNEL_THREAD_SUMMARY,
tags: [
["e", root.id],
["d", root.id],
["h", channelId],
],
content: JSON.stringify({
reply_count: directReplies.length,
descendant_count: replies.length,
last_reply_at: lastReplyAt,
participants,
}),
sig: "mocksig".repeat(20).slice(0, 128),
};
}
/**
* Build the single kind-39006 bounds event a channel window response must carry.
* The `d` tag key must match `expectedBoundsKey` in channelWindowResponse.ts:
* `<channel>:head` at the frontier, else `<channel>:<created_at>:<event_id>` of
* the request cursor (lower-cased). `has_more`/`next_cursor` must agree — the
* parser rejects a bounds event where they disagree.
*/
function buildMockChannelWindowBounds(
args: {
channelId: string;
cursor?: { created_at: number; event_id: string } | null;
},
hasMore: boolean,
nextCursor: { created_at: number; id: string } | null,
): RelayEvent {
const suffix = args.cursor
? `${args.cursor.created_at}:${args.cursor.event_id.toLowerCase()}`
: "head";
const boundsKey = `${args.channelId.toLowerCase()}:${suffix}`;
return {
id: `mock-window-bounds-${boundsKey}`,
pubkey: DEFAULT_MOCK_IDENTITY.pubkey,
created_at: Math.floor(Date.now() / 1000),
kind: KIND_CHANNEL_WINDOW_BOUNDS,
tags: [["d", boundsKey]],
content: JSON.stringify({ has_more: hasMore, next_cursor: nextCursor }),
sig: "mocksig".repeat(20).slice(0, 128),
};
}
/**
* one server-assembled channel window over the `/query` bridge. Emits the flat
* event array the relay assembles — top-level rows (newest first), then the aux
* closure, then relay-signed `39005` summaries and exactly one `39006` bounds
* event carrying `has_more` + `next_cursor`. The client derives its cursor and
* exhaustion solely from `39006`, never from the rows, so this handler returns
* the raw array unchanged.
*
* This is the window read-model surface the overhaul introduced; without it the
* relay-mode bridge has no handler and the timeline renders empty.
*/
async function handleGetChannelWindow(
args: {
channelId: string;
limitRows?: number | null;
cursor?: { created_at: number; event_id: string } | null;
},
config: E2eConfig | undefined,
): Promise<RelayEvent[]> {
const execute = async () => {
const cap = Math.min(args.limitRows ?? 50, 200);
const identity = getIdentity(config);
if (!identity) {
// Mock store: server-assembled channel window over the mock event store,
// mirroring the relay path's shape so callers (parseChannelWindowResponse)
// parse both modes identically. Top-level timeline rows in relay order,
// then exactly one kind-39006 bounds event.
const events = getMockMessageStore(args.channelId);
const candidates = events
.filter(
(event) => TIMELINE_KINDS.has(event.kind) && isMockTopLevelRow(event),
)
.sort(
(left, right) =>
right.created_at - left.created_at ||
left.id.localeCompare(right.id),
);
// Honor the composite (until, before_id) cursor exactly like the relay's
// keyset: keep only rows strictly older than the cursor under the
// (created_at DESC, id ASC) order — older created_at, or the same second
// with a strictly greater id.
const cursor = args.cursor;
const afterCursor = cursor
? candidates.filter(
(event) =>
event.created_at < cursor.created_at ||
(event.created_at === cursor.created_at &&
event.id > cursor.event_id),
)
: candidates;
const rows = afterCursor.slice(0, cap);
// Exhaustion probe mirrors the relay's limit+1: more rows past the cursor
// than the page cap means another page exists. next_cursor is the last
// retained row.
const hasMore = afterCursor.length > cap;
const lastRow = rows[rows.length - 1];
const nextCursor =
hasMore && lastRow
? { created_at: lastRow.created_at, id: lastRow.id }
: null;
const aux = buildMockChannelWindowAux(events, rows);
const summaries = rows.flatMap((row) => {
const summary = buildMockChannelThreadSummary(
args.channelId,
row,
events,
);
return summary ? [summary] : [];
});
return [
...rows,
...aux,
...summaries,
buildMockChannelWindowBounds(args, hasMore, nextCursor),
];
}
// Relay mode: mirror build_channel_window_filter exactly — top-level dispatch
// with summaries + aux, composite (until, before_id) cursor (both or neither).
const filter: Record<string, unknown> = {
"#h": [args.channelId],
kinds: [...TIMELINE_KINDS],
limit: cap,
top_level: true,
include_summaries: true,
include_aux: true,
};
if (args.cursor) {
filter.until = args.cursor.created_at;
filter.before_id = args.cursor.event_id;
}
return relayQuery(config, [filter]);
};
if (!args.cursor) {
return execute();
}
const probe = window as unknown as {
__CHANNEL_WINDOW_FETCH_COUNT__?: number;
__CHANNEL_WINDOW_INFLIGHT__?: number;
__CHANNEL_WINDOW_INFLIGHT_PEAK__?: number;
};
probe.__CHANNEL_WINDOW_FETCH_COUNT__ =
(probe.__CHANNEL_WINDOW_FETCH_COUNT__ ?? 0) + 1;
const delayMs = getConfig()?.mock?.channelWindowDelayMs ?? 0;
if (delayMs <= 0) {
return execute();
}
probe.__CHANNEL_WINDOW_INFLIGHT__ =
(probe.__CHANNEL_WINDOW_INFLIGHT__ ?? 0) + 1;
probe.__CHANNEL_WINDOW_INFLIGHT_PEAK__ = Math.max(
probe.__CHANNEL_WINDOW_INFLIGHT_PEAK__ ?? 0,
probe.__CHANNEL_WINDOW_INFLIGHT__,
);
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
try {
return await execute();
} finally {
probe.__CHANNEL_WINDOW_INFLIGHT__ =
(probe.__CHANNEL_WINDOW_INFLIGHT__ ?? 1) - 1;
}
}
function getMockUserNotes(pubkey: string): RawUserNote[] {
const now = Math.floor(Date.now() / 1000);
if (pubkey === DEFAULT_MOCK_IDENTITY.pubkey) {
// Two named notes plus generated filler so the Pulse feed overflows the
// viewport — required to exercise windowed scroll + sticky-composer offset.
const named: RawUserNote[] = [
{
id: "mock-note-launch",
pubkey,
created_at: now - 20 * 60,
content: "Shipped the new desktop sidebar polish today.",
tags: [],
},
{
id: "mock-note-forum",
pubkey,
created_at: now - 3 * 60 * 60,
content: "Forum threads feel like the right home for slower decisions.",
tags: [],
},
];
const filler: RawUserNote[] = Array.from({ length: 28 }, (_, index) => ({
id: `mock-note-filler-${index}`,
pubkey,
created_at: now - (4 + index) * 60 * 60,
content: `Pulse update #${index + 1}: tracking virtualization rollout across desktop surfaces.`,
tags: [],
}));
return [...named, ...filler];
}
if (pubkey === ALICE_PUBKEY) {
return [
{
id: "mock-alice-note-release",
pubkey,
created_at: now - 45 * 60,
content: "Release checklist is ready for async feedback.",
tags: [],
},
{
id: "mock-alice-note-design",
pubkey,
created_at: now - 5 * 60 * 60,
content: "Trying a lighter forum layout for longer-form notes.",
tags: [],
},
];
}
return [];
}
async function handleGetUserNotes(
args: {
pubkey: string;
limit?: number | null;
before?: number | null;
beforeId?: string | null;
},
config: E2eConfig | undefined,
): Promise<RawUserNotesResponse> {
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,
};
}
// Query kind:1 notes for the user
const limit = args.limit ?? 50;
const filter: Record<string, unknown> = {
kinds: [1],
authors: [args.pubkey],
limit,
};
if (args.before !== undefined && args.before !== null) {
filter.until = args.before;
}
const events = await relayQuery(config, [filter]);
const notes = events.map((ev) => ({
id: ev.id,
pubkey: ev.pubkey,
content: ev.content,
created_at: ev.created_at,
tags: ev.tags,
}));
return { notes, next_cursor: null };
}
async function handleGetGlobalNotes(
args: { limit?: number | null; before?: number | null } | null,
config: E2eConfig | undefined,
): Promise<RawUserNotesResponse> {
const notes = [
...getMockUserNotes(DEFAULT_MOCK_IDENTITY.pubkey),
...getMockUserNotes(ALICE_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);
if (!getIdentity(config)) {
return { notes, next_cursor: null };
}
const events = await relayQuery(config, [
{ kinds: [1], limit: args?.limit ?? 50, until: args?.before ?? undefined },
]);
return {
notes: events.map((ev) => ({
id: ev.id,
pubkey: ev.pubkey,
content: ev.content,
created_at: ev.created_at,
tags: ev.tags,
})),
next_cursor: null,
};
}
function handleGetNotesTimeline(args: {
pubkeys?: string[];
limitPerUser?: number | null;
}) {
const pubkeys = args.pubkeys ?? [];
const limitPerUser = args.limitPerUser ?? 10;
const notes = pubkeys
.flatMap((pubkey) => getMockUserNotes(pubkey).slice(0, limitPerUser))
.sort((left, right) => right.created_at - left.created_at);
return { notes, next_cursor: null };
}
function handleGetNote(args: { noteId?: string }) {
const noteId = args.noteId;
return (
[
...getMockUserNotes(DEFAULT_MOCK_IDENTITY.pubkey),
...getMockUserNotes(ALICE_PUBKEY),
].find((note) => note.id === noteId) ?? null
);
}
function handleGetNoteReactions() {
return [];
}
function handleGetLikedNotes(): RawUserNotesResponse {
return { notes: [], next_cursor: null };
}
// A random 64-hex event id, matching the shape of real Nostr event ids
// (sha256 → 64 hex). Most mock events use the 32-hex `createMockEvent` default,
// but kind:7 reactions need a real 64-hex id: the timeline's deletion path only
// accepts 64-hex `e` tags (getDeletionTargets in formatTimelineMessages.ts), so
// a kind:5 targeting a 32-hex reaction id would be silently ignored and the
// reaction pill would never clear on toggle-off.
// --- Mock projects (NIP-34 repo announcements + git activity) ---
// Deterministic fixtures so the Projects view (cards, stat pills, and the
// contribution heatmap) renders with data in screenshots and e2e specs.
const MOCK_PROJECT_SEEDS = [
{
dtag: "buzz",
name: "buzz",
description:
"Relay, desktop, and mobile clients for the Buzz community platform.",
owner: MOCK_IDENTITY_PUBKEY,
contributors: [ALICE_PUBKEY, BOB_PUBKEY, CHARLIE_PUBKEY],
activityLevel: 4,
},
{
dtag: "relay-tools",
name: "relay-tools",
description: "Operator tooling and admin CLI for relay deployments.",
owner: ALICE_PUBKEY,
contributors: [MOCK_IDENTITY_PUBKEY, BOB_PUBKEY],
activityLevel: 2,
},
{
dtag: "design-system",
name: "design-system",
description: "Shared UI tokens, typography ramps, and component library.",
owner: BOB_PUBKEY,
contributors: [ALICE_PUBKEY],
activityLevel: 1,
},
] as const;
const MOCK_PROJECT_SUBJECTS = [
"Fix reconnect backoff jitter",
"Polish overview cards",
"Add contribution heatmap",
"Refactor filter matching",
"Speed up event dedup",
"Handle empty clone URLs",
"Update onboarding copy",
"Tighten p-gate checks",
];
const MOCK_PROJECT_KINDS = new Set<number>([
KIND_REPO_ANNOUNCEMENT,
KIND_REPO_STATE,
KIND_GIT_PATCH,
KIND_GIT_PULL_REQUEST,
KIND_GIT_PR_UPDATE,
KIND_GIT_ISSUE,
KIND_GIT_STATUS_OPEN,
KIND_GIT_STATUS_MERGED,
KIND_GIT_STATUS_CLOSED,
KIND_GIT_STATUS_DRAFT,
]);
function mulberry32(seed: number) {
let state = seed;
return () => {
state |= 0;
state = (state + 0x6d2b79f5) | 0;
let t = Math.imul(state ^ (state >>> 15), 1 | state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
let mockProjectEventStore: RelayEvent[] | null = null;
const MOCK_PROJECT_BRANCHES_KEY = "buzz-e2e-project-branches";
function readMockProjectBranches(): Record<string, Record<string, string>> {
try {
return JSON.parse(
window.sessionStorage.getItem(MOCK_PROJECT_BRANCHES_KEY) ?? "{}",
);
} catch {
return {};
}
}
function writeMockProjectBranch(
dtag: string,
branch: string,
commit: string | null,
) {
const projects = readMockProjectBranches();
const branches = { ...(projects[dtag] ?? {}) };
if (commit) {
branches[branch] = commit;
} else {
delete branches[branch];
}
projects[dtag] = branches;
window.sessionStorage.setItem(
MOCK_PROJECT_BRANCHES_KEY,
JSON.stringify(projects),
);
}
function buildMockProjectEvents(): RelayEvent[] {
const events: RelayEvent[] = [];
const daySeconds = 86_400;
const now = Math.floor(Date.now() / 1000);
const historyDays = 26 * 7;
for (const [projectIndex, seed] of MOCK_PROJECT_SEEDS.entries()) {
const owner =
projectIndex === 0
? (window.__BUZZ_E2E_PROJECT_OWNER_OVERRIDE__ ?? seed.owner)
: seed.owner;
const repoAddress = `${KIND_REPO_ANNOUNCEMENT}:${owner}:${seed.dtag}`;
const authors = [seed.owner, ...seed.contributors];
const random = mulberry32(projectIndex + 1);
events.push(
createMockEvent(
KIND_REPO_ANNOUNCEMENT,
seed.description,
[
["d", seed.dtag],
["name", seed.name],
["description", seed.description],
["clone", `https://relay.example.com/git/${owner}/${seed.dtag}`],
...seed.contributors.map((pubkey) => ["p", pubkey]),
],
owner,
now - (historyDays + 30 + projectIndex) * daySeconds,
`mock-project-${seed.dtag}`.replace(/[^a-zA-Z0-9]/g, ""),
),
);
events.push(
createMockEvent(
KIND_REPO_STATE,
"",
[
["d", seed.dtag],
[
"HEAD",
`ref: refs/heads/${
projectIndex === 0
? (getConfig()?.mock?.projectHeadBranch ?? "main")
: "main"
}`,
],
["refs/heads/main", "0123456789abcdef0123456789abcdef01234567"],
["refs/tags/v1.0.0", "0123456789abcdef0123456789abcdef01234567"],
...Object.entries(readMockProjectBranches()[seed.dtag] ?? {}).map(
([branch, commit]) => [`refs/heads/${branch}`, commit],
),
],
getConfig()?.mock?.relaySelf ?? owner,
now - projectIndex,
`mock-repo-state-${seed.dtag}`.replace(/[^a-zA-Z0-9]/g, ""),
),
);
for (let dayOffset = historyDays; dayOffset >= 0; dayOffset -= 1) {
// Roughly half the days are quiet; busy days scale with activityLevel.
if (random() < 0.5) continue;
const dayEventCount = 1 + Math.floor(random() * seed.activityLevel);
for (let index = 0; index < dayEventCount; index += 1) {
const createdAt =
now - dayOffset * daySeconds - Math.floor(random() * 10) * 3_600;
const author = authors[Math.floor(random() * authors.length)];
const subject =
MOCK_PROJECT_SUBJECTS[
Math.floor(random() * MOCK_PROJECT_SUBJECTS.length)
];
const commitHash = `${seed.dtag}${dayOffset}x${index}`
.padEnd(40, "0")
.slice(0, 40);
const roll = random();
const kind =
roll < 0.7
? KIND_GIT_PATCH
: roll < 0.85
? KIND_GIT_PULL_REQUEST
: KIND_GIT_ISSUE;
const tags = [
["a", repoAddress],
["subject", subject],
...(kind === KIND_GIT_ISSUE ? [] : [["c", commitHash]]),
...(kind === KIND_GIT_PULL_REQUEST
? [
["h", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"],
["branch-name", `feature/mock-${dayOffset}-${index}`],
[
"clone",
`https://relay.example.com/git/${owner}/${seed.dtag}`,
],
]
: []),
];
events.push(createMockEvent(kind, subject, tags, author, createdAt));
}
}
}
return events;
}
function getMockProjectEventStore(): RelayEvent[] {
mockProjectEventStore ??= buildMockProjectEvents();
return mockProjectEventStore;
}
/** Project-scoped publishes (PR/issue comments, NIP-34 status events) carry
* a repo-address `a` tag instead of a channel `h` tag — store them with the
* seeded project events so refetches see them. */
function isMockProjectScopedEvent(event: RelayEvent): boolean {
const hasRepoAddressTag = event.tags.some(
(tag) => tag[0] === "a" && (tag[1] ?? "").startsWith("30617:"),
);
return (
hasRepoAddressTag &&
(event.kind === 1 || MOCK_PROJECT_KINDS.has(event.kind))
);
}
function filterMockProjectEvents(filter: MockFilter): RelayEvent[] {
const authors = filter.authors?.map((author) => author.toLowerCase());
return getMockProjectEventStore()
.filter((event) => {
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
if (authors && !authors.includes(event.pubkey.toLowerCase())) {
return false;
}
if (
filter["#d"] &&
!event.tags.some(
(tag) => tag[0] === "d" && filter["#d"]?.includes(tag[1]),
)
) {
return false;
}
if (
filter["#a"] &&
!event.tags.some(
(tag) => tag[0] === "a" && filter["#a"]?.includes(tag[1]),
)
) {
return false;
}
return true;
})
.sort((left, right) => right.created_at - left.created_at)
.slice(0, filter.limit ?? 500);
}
function mockEventId(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
}
function createMockEvent(
kind: number,
content: string,
tags: string[][],
pubkey = DEFAULT_MOCK_IDENTITY.pubkey,
createdAt = Math.floor(Date.now() / 1000),
id = crypto.randomUUID().replace(/-/g, ""),
): RelayEvent {
return {
id,
pubkey,
created_at: createdAt,
kind,
tags,
content,
sig: "mocksig".repeat(20).slice(0, 128),
};
}
async function signWithIdentity(
identity: TestIdentity,
template: {
kind: number;
content: string;
createdAt?: number;
tags: string[][];
},
) {
const secretKey = hexToBytes(identity.privateKey);
return finalizeEvent(
{
kind: template.kind,
content: template.content,
tags: template.tags,
created_at: template.createdAt ?? 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<T>(
config: E2eConfig | undefined,
path: string,
init: RequestInit = {},
): Promise<T> {
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<T>;
}
/**
* Query the relay via POST /query (pure Nostr HTTP bridge).
* Returns an array of raw Nostr events matching the filters.
*/
async function relayQuery(
config: E2eConfig | undefined,
filters: Array<Record<string, unknown>>,
): Promise<RelayEvent[]> {
const identity = getRelayIdentity(config);
if (
!filters.every((filter) =>
isPGatedFilterAuthorized(filter, identity.pubkey),
)
) {
throw new Error(P_GATED_REJECTION_MESSAGE);
}
const response = await fetch(`${getRelayHttpUrl(config)}/query`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Pubkey": identity.pubkey,
},
body: JSON.stringify(filters),
});
await assertOk(response);
return response.json() as Promise<RelayEvent[]>;
}
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, "/events", {
method: "POST",
body: JSON.stringify(signed),
});
}
async function handleGetChannels(config: E2eConfig | undefined) {
const channelsReadDelayMs = config?.mock?.channelsReadDelayMs ?? 0;
if (channelsReadDelayMs > 0) {
await new Promise((resolve) =>
window.setTimeout(resolve, channelsReadDelayMs),
);
}
const channelsReadError =
config?.mock?.channelsReadErrors?.shift() ??
config?.mock?.channelsReadError;
if (channelsReadError) {
throw new Error(channelsReadError);
}
const identity = getIdentity(config);
if (!identity) {
return listMockChannels(config);
}
// Pure Nostr: query kind:39002 (membership) for our pubkey, extract channel
// UUIDs from d-tags, then query kind:39000 (metadata) for those channels.
const memberEvents = await relayQuery(config, [
{ kinds: [39002], "#p": [identity.pubkey], limit: 1000 },
]);
const channelIds = [
...new Set(
memberEvents.flatMap((ev) =>
(ev.tags ?? [])
.filter((t: string[]) => t[0] === "d")
.map((t: string[]) => t[1]),
),
),
];
// Also fetch ALL open channel metadata (for channel browser — shows joinable channels)
const allMetaEvents = await relayQuery(config, [
{ kinds: [39000], limit: 200 },
]);
// Merge: use all metadata events, mark membership
const memberSet = new Set(channelIds);
const metaEvents = allMetaEvents;
// NIP-DV: query the viewer's latest DM visibility snapshot (kind:30622).
// The snapshot is `#p`-gated to its owner, so we query by `#p`=my pubkey.
// Its `h` tags are the DM channel ids to hide from the sidebar.
const visibilityEvents = await relayQuery(config, [
{ kinds: [KIND_DM_VISIBILITY], "#p": [identity.pubkey], limit: 1 },
]);
const latestVisibility = visibilityEvents.reduce<RelayEvent | null>(
(latest, ev) =>
!latest || ev.created_at > latest.created_at ? ev : latest,
null,
);
const hiddenDms = new Set(
((latestVisibility?.tags ?? []) as string[][])
.filter((t) => t[0] === "h")
.map((t) => t[1]),
);
// Convert kind:39000 events to the RawChannel shape the frontend expects.
return metaEvents
.map((ev) => {
const tags = (ev.tags ?? []) as string[][];
const getTag = (name: string) =>
tags.find((t) => t[0] === name)?.[1] ?? null;
const channelId = getTag("d") ?? "";
const channelType = getTag("t") ?? "stream";
const isPrivate = tags.some((t) => t[0] === "private");
const isArchived = tags.some(
(t) => t[0] === "archived" && t[1] === "true",
);
// Get participant pubkeys from the membership event for this channel
const memberEvent = memberEvents.find((me) =>
(me.tags ?? []).some(
(t: string[]) => t[0] === "d" && t[1] === channelId,
),
);
const pTags = memberEvent
? ((memberEvent.tags ?? []) as string[][])
.filter((t) => t[0] === "p")
.map((t) => t[1])
: [];
return {
id: channelId,
name: getTag("name") ?? "",
description: getTag("about") ?? "",
channel_type: channelType as "stream" | "forum" | "dm",
visibility: (isPrivate ? "private" : "open") as "open" | "private",
topic: getTag("topic") ?? null,
purpose: getTag("purpose") ?? null,
member_count: pTags.length,
last_message_at: null,
archived_at: isArchived ? new Date().toISOString() : null,
participants: pTags,
participant_pubkeys: pTags,
ttl_seconds: getTag("ttl") ? Number(getTag("ttl")) : null,
ttl_deadline: getTag("ttl_deadline") ?? null,
is_member: memberSet.has(channelId),
};
})
.filter((c) => c.channel_type !== "dm" || !hiddenDms.has(c.id));
}
async function handleGetProfile(config: E2eConfig | undefined) {
const identity = getIdentity(config);
if (!identity) {
const profileReadDelayMs = config?.mock?.profileReadDelayMs ?? 0;
if (profileReadDelayMs > 0) {
await new Promise<void>((resolve) => {
window.setTimeout(resolve, profileReadDelayMs);
});
}
const profileReadError = config?.mock?.profileReadError;
if (profileReadError) {
throw new Error(profileReadError);
}
return cloneProfile(ensureMockProfile(config));
}
// Pure Nostr: query kind:0 (profile metadata) for our pubkey.
const events = await relayQuery(config, [
{ kinds: [0], authors: [identity.pubkey], limit: 1 },
]);
if (events.length === 0) {
return {
pubkey: identity.pubkey,
display_name: null,
about: null,
avatar_url: null,
nip05_handle: null,
owner_pubkey: null,
has_profile_event: false,
};
}
const content = JSON.parse(events[0].content ?? "{}");
return {
pubkey: identity.pubkey,
display_name: content.display_name ?? content.name ?? null,
about: content.about ?? null,
avatar_url: content.picture ?? null,
nip05_handle: content.nip05 ?? null,
owner_pubkey: null,
has_profile_event: true,
};
}
async function handleUpdateProfile(
args: {
displayName?: string;
avatarUrl?: string;
about?: string;
nip05Handle?: string;
},
config: E2eConfig | undefined,
) {
const identity = getIdentity(config);
if (!identity) {
const profileUpdateError = config?.mock?.profileUpdateError;
const profileUpdateErrors = config?.mock?.profileUpdateErrors;
const nextProfileUpdateError = profileUpdateErrors?.shift();
if (nextProfileUpdateError) {
throw new Error(nextProfileUpdateError);
}
if (profileUpdateError) {
if (config?.mock) {
config.mock.profileUpdateError = undefined;
}
throw new Error(profileUpdateError);
}
const profile = ensureMockProfile(config);
const hasDisplayNameUpdate = typeof args.displayName === "string";
const hasAvatarUrlUpdate = typeof args.avatarUrl === "string";
const hasAboutUpdate = typeof args.about === "string";
const hasNip05HandleUpdate = typeof args.nip05Handle === "string";
const nextDisplayName = args.displayName?.trim() ?? "";
const nextAvatarUrl = args.avatarUrl?.trim() ?? "";
const nextAbout = args.about?.trim() ?? "";
const nextNip05Handle = args.nip05Handle?.trim() ?? "";
if (hasDisplayNameUpdate && nextDisplayName !== profile.display_name) {
profile.display_name = nextDisplayName || null;
applyMockDisplayName(profile.pubkey, profile.display_name);
}
if (hasAvatarUrlUpdate && nextAvatarUrl !== profile.avatar_url) {
profile.avatar_url = nextAvatarUrl || null;
}
if (hasAboutUpdate && nextAbout !== profile.about) {
profile.about = nextAbout || null;
}
if (hasNip05HandleUpdate && nextNip05Handle !== profile.nip05_handle) {
profile.nip05_handle = nextNip05Handle || null;
}
return cloneProfile(profile);
}
// Read-merge-write: fetch current profile, merge, sign kind:0.
const currentEvents = await relayQuery(config, [
{ kinds: [0], authors: [identity.pubkey], limit: 1 },
]);
const currentContent = currentEvents[0]
? JSON.parse(currentEvents[0].content ?? "{}")
: {};
const profileContent = JSON.stringify({
display_name: args.displayName ?? currentContent.display_name ?? undefined,
name: currentContent.display_name ?? undefined,
picture: args.avatarUrl ?? currentContent.picture ?? undefined,
about: args.about ?? currentContent.about ?? undefined,
nip05: args.nip05Handle ?? currentContent.nip05 ?? undefined,
});
await submitSignedEvent(config, {
kind: 0,
content: profileContent,
tags: [],
});
// Return the updated profile in RawProfile shape
const updated = JSON.parse(profileContent);
return {
pubkey: identity.pubkey,
display_name: updated.display_name ?? null,
about: updated.about ?? null,
avatar_url: updated.picture ?? null,
nip05_handle: updated.nip05 ?? null,
owner_pubkey: null,
has_profile_event: true,
};
}
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 targetPubkey = args.pubkey ?? identity.pubkey;
const events = await relayQuery(config, [
{ kinds: [0], authors: [targetPubkey], limit: 1 },
]);
if (events.length === 0) {
return {
pubkey: targetPubkey,
display_name: null,
about: null,
avatar_url: null,
nip05_handle: null,
owner_pubkey: null,
has_profile_event: false,
};
}
const content = JSON.parse(events[0].content ?? "{}");
return {
pubkey: targetPubkey,
display_name: content.display_name ?? content.name ?? null,
about: content.about ?? null,
avatar_url: content.picture ?? null,
nip05_handle: content.nip05 ?? null,
owner_pubkey: null,
has_profile_event: true,
};
}
async function handleGetUsersBatch(
args: {
pubkeys: string[];
},
config: E2eConfig | undefined,
) {
const usersBatchDelayMs = config?.mock?.usersBatchDelayMs ?? 0;
if (usersBatchDelayMs > 0) {
await new Promise<void>((resolve) => {
window.setTimeout(resolve, usersBatchDelayMs);
});
}
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,
name: profile.name ?? null,
avatar_url: profile.avatar_url,
nip05_handle: profile.nip05_handle,
owner_pubkey: profile.owner_pubkey,
is_agent: profile.is_agent ?? false,
};
}
return {
profiles,
missing,
};
}
const events = await relayQuery(config, [
{ kinds: [0], authors: args.pubkeys, limit: args.pubkeys.length },
]);
const profiles: RawUsersBatchResponse["profiles"] = {};
const found = new Set<string>();
for (const ev of events) {
const pk = ev.pubkey?.toLowerCase() ?? "";
found.add(pk);
const content = JSON.parse(ev.content ?? "{}");
profiles[pk] = {
display_name: content.display_name ?? content.name ?? null,
name: content.name ?? null,
avatar_url: content.picture ?? null,
nip05_handle: content.nip05 ?? null,
owner_pubkey:
((ev.tags ?? []) as string[][]).find(
(tag) => Array.isArray(tag) && tag[0] === "auth" && tag.length === 4,
)?.[1] ?? null,
is_agent: Array.isArray(ev.tags)
? ev.tags.some(
(tag) =>
Array.isArray(tag) && tag[0] === "auth" && tag.length === 4,
)
: false,
};
}
for (const pubkey of args.pubkeys) {
const normalizedPubkey = pubkey.toLowerCase();
if (found.has(normalizedPubkey)) {
continue;
}
const profile = getMockProfileByPubkey(normalizedPubkey);
if (!profile) {
continue;
}
found.add(normalizedPubkey);
profiles[normalizedPubkey] = {
display_name: profile.display_name,
name: profile.name ?? null,
avatar_url: profile.avatar_url,
nip05_handle: profile.nip05_handle,
owner_pubkey: profile.owner_pubkey,
is_agent: profile.is_agent ?? false,
};
}
const missing = args.pubkeys.filter((p) => !found.has(p.toLowerCase()));
return { profiles, missing };
}
async function handleSearchUsers(
args: {
query: string;
limit?: number;
cursor?: string | null;
},
config: E2eConfig | undefined,
) {
const userSearchDelayMs = config?.mock?.userSearchDelayMs ?? 0;
if (userSearchDelayMs > 0) {
await new Promise<void>((resolve) => {
window.setTimeout(resolve, userSearchDelayMs);
});
}
const identity = getIdentity(config);
if (!identity) {
const normalizedQuery = args.query.trim().toLowerCase();
const limit = args.limit ?? 8;
const page = Math.max(Number(args.cursor ?? 1) || 1, 1);
const allResults = listMockProfiles()
.filter((profile) => {
if (normalizedQuery.length === 0) {
return true;
}
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);
});
const results = allResults
.slice((page - 1) * limit, page * limit)
.map((profile) => ({
pubkey: profile.pubkey,
display_name: profile.display_name,
avatar_url: profile.avatar_url,
nip05_handle: profile.nip05_handle,
owner_pubkey: profile.owner_pubkey,
is_agent: profile.is_agent ?? false,
}));
return {
users: results,
next_cursor: page * limit < allResults.length ? String(page + 1) : null,
} satisfies RawSearchUsersResponse;
}
// NIP-50 search on kind:0 profiles
const limit = args.limit ?? 8;
const normalizedQuery = args.query.trim();
const page = Math.max(Number(args.cursor ?? 1) || 1, 1);
const filter =
normalizedQuery.length === 0
? { kinds: [0], limit, page }
: { kinds: [0], search: args.query, limit, page };
const events = await relayQuery(config, [filter]);
const users = events.map((ev) => {
const content = JSON.parse(ev.content ?? "{}");
return {
pubkey: ev.pubkey ?? "",
display_name: content.display_name ?? content.name ?? null,
avatar_url: content.picture ?? null,
nip05_handle: content.nip05 ?? null,
owner_pubkey:
((ev.tags ?? []) as string[][]).find(
(tag) => Array.isArray(tag) && tag[0] === "auth" && tag.length === 4,
)?.[1] ?? null,
is_agent: Array.isArray(ev.tags)
? ev.tags.some(
(tag) =>
Array.isArray(tag) && tag[0] === "auth" && tag.length === 4,
)
: false,
};
});
return {
users,
next_cursor: users.length >= limit ? String(page + 1) : null,
};
}
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;
}
// Presence is ephemeral (kind:20001) — mock returns from in-memory map.
const events = await relayQuery(config, [
{ kinds: [20001], authors: args.pubkeys, limit: args.pubkeys.length },
]);
const result: RawPresenceLookup = {};
for (const ev of events) {
// Synthesized presence events have ["p", subject_pubkey] tag
const pTag = ((ev.tags ?? []) as string[][]).find((t) => t[0] === "p");
const pk = pTag?.[1] ?? ev.pubkey ?? "";
result[pk.toLowerCase()] = (ev.content ?? "offline") as PresenceStatus;
}
// Fill missing pubkeys with "offline"
for (const pk of args.pubkeys) {
if (!result[pk.toLowerCase()]) {
result[pk.toLowerCase()] = "offline";
}
}
return result;
}
async function handleEnsureStarterChannels(
config: E2eConfig | undefined,
): Promise<RawChannelWithMembership[]> {
const starterChannelError =
config?.mock?.ensureStarterChannelsErrors?.shift();
if (starterChannelError) {
throw new Error(starterChannelError);
}
const currentPubkey = getMockMemberPubkey(config);
const ensureMember = (channel: MockChannel) => {
if (
channel.members.some(
(member) =>
normalizePubkey(member.pubkey) === normalizePubkey(currentPubkey),
)
) {
return;
}
channel.members.push(createCurrentMember(config, "member"));
syncMockChannel(channel);
touchMockChannel(channel);
};
for (const channelName of [
STARTER_GENERAL_CHANNEL_NAME,
STARTER_WELCOME_CHANNEL_NAME,
]) {
const channel = mockChannels.find(
(candidate) =>
candidate.name === channelName &&
candidate.channel_type === "stream" &&
candidate.visibility === "open" &&
candidate.archived_at === null,
);
if (!channel) {
throw new Error(`Starter channel ${channelName} not found.`);
}
ensureMember(channel);
}
return listMockChannels(config);
}
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 createChannelError = config?.mock?.createChannelErrors?.shift();
if (createChannelError) {
throw new Error(createChannelError);
}
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, config);
}
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 via pure Nostr query.
// The relay emits kind:39000 as a side effect of kind:9007.
const metaEvents = await relayQuery(config, [
{ kinds: [39000], "#d": [channelId], limit: 1 },
]);
const ev = metaEvents[0];
if (!ev) {
throw new Error(`Channel "${args.name}" not found after creation`);
}
const evTags = (ev.tags ?? []) as string[][];
const getTag = (name: string) =>
evTags.find((t) => t[0] === name)?.[1] ?? null;
return {
id: channelId,
name: getTag("name") ?? args.name,
description: getTag("about") ?? args.description ?? null,
channel_type: args.channelType,
visibility: args.visibility,
topic: null,
purpose: null,
role: "owner",
archived_at: null,
ttl_seconds: args.ttlSeconds ?? null,
ttl_deadline: ttlDeadline,
created_at: ev.created_at
? new Date(ev.created_at * 1000).toISOString()
: new Date().toISOString(),
};
}
async function handleOpenDm(
args: {
pubkeys: string[];
},
config: E2eConfig | undefined,
) {
const delayMs = config?.mock?.openDmDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
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, config);
}
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, config);
}
// Submit kind:41010 (DM open) with p-tags for participants
const tags = normalizedPubkeys.map((pk) => ["p", pk]);
const result = await submitSignedEvent(config, {
kind: 41010,
content: "",
tags,
});
// Parse channel_id from response message
const respJson = JSON.parse(result.message.replace("response:", "") || "{}");
const channelId = respJson.channel_id ?? "";
// Fetch channel metadata
const metaEvents = await relayQuery(config, [
{ kinds: [39000], "#d": [channelId], limit: 1 },
]);
const ev = metaEvents[0];
const evTags = (ev?.tags ?? []) as string[][];
const getTag = (name: string) =>
evTags.find((t) => t[0] === name)?.[1] ?? null;
return {
id: channelId,
name: getTag("name") ?? "DM",
description: null,
channel_type: "dm",
visibility: "private",
topic: null,
purpose: null,
role: "member",
archived_at: null,
ttl_seconds: null,
ttl_deadline: null,
created_at: ev?.created_at
? new Date(ev.created_at * 1000).toISOString()
: new Date().toISOString(),
};
}
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;
}
// Submit kind:41012 (DM hide) with h-tag
await submitSignedEvent(config, {
kind: 41012,
content: "",
tags: [["h", args.channelId]],
});
}
async function handleGetChannelDetails(
args: { channelId: string },
config: E2eConfig | undefined,
) {
const identity = getIdentity(config);
if (!identity) {
return toRawChannelDetail(getMockChannel(args.channelId), config);
}
const metaEvents = await relayQuery(config, [
{ kinds: [39000], "#d": [args.channelId], limit: 1 },
]);
const ev = metaEvents[0];
const evTags = (ev?.tags ?? []) as string[][];
const getTag = (name: string) =>
evTags.find((t) => t[0] === name)?.[1] ?? null;
// Get members for member_count
const memberEvents = await relayQuery(config, [
{ kinds: [39002], "#d": [args.channelId], limit: 1 },
]);
const memberTags = ((memberEvents[0]?.tags ?? []) as string[][]).filter(
(t) => t[0] === "p",
);
return {
id: args.channelId,
name: getTag("name") ?? "",
description: getTag("about") ?? null,
channel_type: getTag("t") ?? "stream",
visibility: evTags.some((t) => t[0] === "private") ? "private" : "open",
topic: getTag("topic") ?? null,
purpose: getTag("purpose") ?? null,
member_count: memberTags.length,
role: "member",
archived_at: evTags.some((t) => t[0] === "archived" && t[1] === "true")
? new Date().toISOString()
: null,
ttl_seconds: getTag("ttl") ? Number(getTag("ttl")) : null,
ttl_deadline: getTag("ttl_deadline") ?? null,
created_at: ev?.created_at
? new Date(ev.created_at * 1000).toISOString()
: new Date().toISOString(),
};
}
async function handleGetChannelMembers(
args: { channelId: string },
config: E2eConfig | undefined,
): Promise<RawChannelMembersResponse> {
const delayMs = config?.mock?.channelMembersReadDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
const identity = getIdentity(config);
if (!identity) {
const channel = getMockChannel(args.channelId);
return {
members: cloneMembers(channel.members),
next_cursor: null,
};
}
const memberEvents = await relayQuery(config, [
{ kinds: [39002], "#d": [args.channelId], limit: 1 },
]);
const memberTags = ((memberEvents[0]?.tags ?? []) as string[][]).filter(
(t) => t[0] === "p",
);
const members = memberTags.map((t) => ({
pubkey: t[1],
role: (t[3] ?? t[2] ?? "member") as
| "owner"
| "admin"
| "member"
| "guest"
| "bot",
is_agent: (t[3] ?? t[2]) === "bot",
display_name: null,
avatar_url: null,
joined_at: new Date().toISOString(),
}));
return { members, next_cursor: null };
}
async function handleUpdateChannel(
args: {
channelId: string;
name?: string;
description?: string;
visibility?: "open" | "private";
ttlSeconds?: number | null;
},
config: E2eConfig | undefined,
) {
const delayMs = config?.mock?.updateChannelDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
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;
}
if (args.visibility !== undefined) {
channel.visibility = args.visibility;
}
if (args.ttlSeconds !== undefined) {
channel.ttl_seconds = args.ttlSeconds;
channel.ttl_deadline =
args.ttlSeconds === null
? null
: new Date(Date.now() + args.ttlSeconds * 1000).toISOString();
}
touchMockChannel(channel);
return toRawChannelDetail(channel, config);
}
const tags: string[][] = [["h", args.channelId]];
if (args.name !== undefined) {
tags.push(["name", args.name]);
}
if (args.description !== undefined) {
tags.push(["about", args.description]);
}
if (args.visibility !== undefined) {
tags.push(["visibility", args.visibility]);
}
if (args.ttlSeconds !== undefined) {
tags.push(["ttl", args.ttlSeconds === null ? "" : String(args.ttlSeconds)]);
}
await submitSignedEvent(config, { kind: 9002, content: "", tags });
// Re-fetch updated metadata
const metaEvents = await relayQuery(config, [
{ kinds: [39000], "#d": [args.channelId], limit: 1 },
]);
const ev = metaEvents[0];
const evTags = (ev?.tags ?? []) as string[][];
const getTag = (name: string) =>
evTags.find((t) => t[0] === name)?.[1] ?? null;
const ttlTag = getTag("ttl");
const ttlSeconds = ttlTag === null || ttlTag === "" ? null : Number(ttlTag);
return {
id: args.channelId,
name: getTag("name") ?? "",
description: getTag("about") ?? null,
channel_type: getTag("t") ?? "stream",
visibility: getTag("visibility") ?? "open",
topic: getTag("topic") ?? null,
purpose: getTag("purpose") ?? null,
member_count: 0,
role: "owner",
archived_at: null,
ttl_seconds: ttlSeconds,
ttl_deadline:
ttlSeconds === null
? null
: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
created_at: ev?.created_at
? new Date(ev.created_at * 1000).toISOString()
: new Date().toISOString(),
};
}
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],
],
});
}
type MockUpdaterChannel = {
onmessage?: (event: { event: "Finished" }) => void;
};
function notifyUpdaterFinished(payload: unknown) {
const channel = (payload as { onEvent?: MockUpdaterChannel } | null)?.onEvent;
channel?.onmessage?.({ event: "Finished" });
}
function handleUpdaterCheck(config: E2eConfig | undefined) {
if (!config?.mock?.updateAvailable) {
return null;
}
const version = config.mock.updateVersion ?? "0.3.18";
return {
rid: 42,
currentVersion: "0.3.17",
version,
date: "2026-06-12T00:00:00Z",
body: `Mock update ${version}`,
rawJson: null,
};
}
async function handleUpdaterDownload(
payload: unknown,
config: E2eConfig | undefined,
) {
const delayMs = config?.mock?.updateDownloadDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
notifyUpdaterFinished(payload);
return 43;
}
function handleUpdaterInstall() {
return null;
}
async function handleRestart(config: E2eConfig | undefined) {
const delayMs = config?.mock?.restartDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
return null;
}
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<RawAddChannelMembersResponse> {
const addChannelMembersDelayMs = config?.mock?.addChannelMembersDelayMs ?? 0;
if (addChannelMembersDelayMs > 0) {
await new Promise((resolve) =>
window.setTimeout(resolve, addChannelMembersDelayMs),
);
}
const configuredErrors = config?.mock?.addChannelMembersErrors;
if (configuredErrors && configuredErrors.length > 0) {
const index = Math.min(
addChannelMembersCallCount,
configuredErrors.length - 1,
);
addChannelMembersCallCount += 1;
const error = configuredErrors[index];
if (error) {
return {
added: [],
errors: args.pubkeys.map((pubkey) => ({ pubkey, error })),
};
}
}
const identity = getIdentity(config);
if (!identity) {
const channel = getMockChannel(args.channelId);
const added: string[] = [];
const errors: RawAddChannelMembersResponse["errors"] = [];
const existingPubkeys = new Set(
channel.members.map((member) => normalizePubkey(member.pubkey)),
);
for (const pubkey of args.pubkeys) {
const normalizedPubkey = normalizePubkey(pubkey);
if (existingPubkeys.has(normalizedPubkey)) {
errors.push({
pubkey,
error: "Already a member.",
});
continue;
}
existingPubkeys.add(normalizedPubkey);
added.push(pubkey);
}
// DM participant sets are immutable. Adding a member creates or reuses a
// separate DM for the expanded set instead of mutating the source channel.
const targetChannel =
channel.channel_type === "dm" && added.length > 0
? getMockChannel(
(
await handleOpenDm(
{
pubkeys: [
...channel.members.map((member) => member.pubkey),
...added,
],
},
config,
)
).id,
)
: channel;
for (const pubkey of added) {
const existingMember = targetChannel.members.find(
(member) => normalizePubkey(member.pubkey) === normalizePubkey(pubkey),
);
if (existingMember) {
existingMember.role = args.role ?? "member";
existingMember.is_agent =
args.role === "bot" ||
mockAgentPubkeys.has(pubkey) ||
mockManagedAgents.some((agent) => agent.pubkey === pubkey);
existingMember.display_name = mockDisplayNames.get(pubkey) ?? null;
continue;
}
targetChannel.members.push({
pubkey,
role: args.role ?? "member",
is_agent:
args.role === "bot" ||
mockAgentPubkeys.has(pubkey) ||
mockManagedAgents.some((agent) => agent.pubkey === pubkey),
joined_at: new Date().toISOString(),
display_name: mockDisplayNames.get(pubkey) ?? null,
});
}
syncMockChannel(targetChannel);
touchMockChannel(targetChannel);
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 joinChannelError = config?.mock?.joinChannelErrors?.shift();
if (joinChannelError) {
throw new Error(joinChannelError);
}
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<RawHomeFeedResponse> {
const feedReadError = config?.mock?.feedReadError;
if (feedReadError) {
throw new Error(feedReadError);
}
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 currentPubkey = getMockMemberPubkey(config).toLowerCase();
const defaultFeed: RawHomeFeedResponse["feed"] =
currentPubkey === ALICE_PUBKEY
? {
mentions: [
{
id: "mock-feed-alice-mention",
kind: 9,
pubkey: BOB_PUBKEY,
content: "Alice, can you sanity-check the new design mocks?",
created_at: now - 90,
channel_id: "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e",
channel_name: "design",
tags: [
["e", "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e"],
["p", ALICE_PUBKEY],
],
category: "mention" as const,
},
],
needs_action: [
{
id: "mock-feed-alice-reminder",
kind: 40007,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000000",
content: "Reminder: post the engineering launch note.",
created_at: now - 15 * 60,
channel_id: "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9",
channel_name: "engineering",
tags: [
["e", "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9"],
["p", ALICE_PUBKEY],
],
category: "needs_action" as const,
},
],
activity: [
{
id: "mock-feed-alice-self-activity",
kind: 9,
pubkey: ALICE_PUBKEY,
content: "I posted the latest design review summary.",
created_at: now - 25 * 60,
channel_id: "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e",
channel_name: "design",
tags: [["e", "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e"]],
category: "activity" as const,
},
{
id: "mock-feed-alice-activity",
kind: 9,
pubkey: BOB_PUBKEY,
content: "Engineering signed off on 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-alice-agent",
kind: 43003,
pubkey:
"db0b028cd36f4d3e36c8300cce87252c1f7fc9495ffecc53f393fcac341ffd36",
content: "Agent progress: design review summary complete.",
created_at: now - 2 * 60 * 60,
channel_id: "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9",
channel_name: "engineering",
tags: [["e", "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9"]],
category: "agent_activity" as const,
},
],
}
: currentPubkey === DEFAULT_REAL_IDENTITY.pubkey.toLowerCase()
? {
mentions: [
{
id: "mock-feed-tyler-mention",
kind: 9,
pubkey: ALICE_PUBKEY,
content: "Tyler, can you review the DM onboarding copy?",
created_at: now - 90,
channel_id: "f48efb06-0c93-5025-aac9-2e646bb6bfa8",
channel_name: "alice-tyler",
tags: [
["e", "f48efb06-0c93-5025-aac9-2e646bb6bfa8"],
["p", DEFAULT_REAL_IDENTITY.pubkey],
],
category: "mention" as const,
},
],
needs_action: [
{
id: "mock-feed-tyler-reminder",
kind: 40007,
pubkey:
"0000000000000000000000000000000000000000000000000000000000000000",
content: "Reminder: answer Bob in the launch DM thread.",
created_at: now - 15 * 60,
channel_id: "7eb9f239-9393-50b0-bd76-d85eef0511c7",
channel_name: "bob-tyler",
tags: [
["e", "7eb9f239-9393-50b0-bd76-d85eef0511c7"],
["p", DEFAULT_REAL_IDENTITY.pubkey],
],
category: "needs_action" as const,
},
],
activity: [
{
id: "mock-feed-tyler-self-activity",
kind: 9,
pubkey: DEFAULT_REAL_IDENTITY.pubkey,
content: "I sent the follow-up in the Alice DM.",
created_at: now - 25 * 60,
channel_id: "f48efb06-0c93-5025-aac9-2e646bb6bfa8",
channel_name: "alice-tyler",
tags: [["e", "f48efb06-0c93-5025-aac9-2e646bb6bfa8"]],
category: "activity" as const,
},
],
agent_activity: [
{
id: "mock-feed-tyler-agent",
kind: 43003,
pubkey:
"db0b028cd36f4d3e36c8300cce87252c1f7fc9495ffecc53f393fcac341ffd36",
content: "Agent progress: DM summary complete.",
created_at: now - 2 * 60 * 60,
channel_id: "f48efb06-0c93-5025-aac9-2e646bb6bfa8",
channel_name: "alice-tyler",
tags: [["e", "f48efb06-0c93-5025-aac9-2e646bb6bfa8"]],
category: "agent_activity" as const,
},
],
}
: {
mentions: [
{
id: "mock-feed-mention",
kind: 9,
pubkey: ALICE_PUBKEY,
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", currentPubkey],
],
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", currentPubkey],
],
category: "needs_action" as const,
},
],
activity: [
{
id: "mock-feed-self-activity",
kind: 9,
pubkey: currentPubkey,
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: BOB_PUBKEY,
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,
},
};
}
// Feed is composed of multiple queries: mentions (#p), activity, approvals.
// For e2e, return a minimal feed structure with mentions.
const limit = args.limit ?? 50;
const mentionEvents = await relayQuery(config, [
{
kinds: [
9,
40002,
1,
45001,
45003,
KIND_GIT_PULL_REQUEST,
KIND_GIT_PR_UPDATE,
KIND_GIT_ISSUE,
KIND_GIT_STATUS_OPEN,
KIND_GIT_STATUS_MERGED,
KIND_GIT_STATUS_CLOSED,
KIND_GIT_STATUS_DRAFT,
],
"#p": [identity.pubkey],
limit,
},
]);
// Look up channel names for feed items
const channelIdsInFeed = [
...new Set(
mentionEvents
.map(
(ev) =>
((ev.tags ?? []) as string[][]).find((t) => t[0] === "h")?.[1],
)
.filter(Boolean) as string[],
),
];
const channelNameMap = new Map<string, string>();
if (channelIdsInFeed.length > 0) {
const metaEvents = await relayQuery(config, [
{
kinds: [39000],
"#d": channelIdsInFeed,
limit: channelIdsInFeed.length,
},
]);
for (const me of metaEvents) {
const d = ((me.tags ?? []) as string[][]).find((t) => t[0] === "d")?.[1];
const name = ((me.tags ?? []) as string[][]).find(
(t) => t[0] === "name",
)?.[1];
if (d && name) channelNameMap.set(d, name);
}
}
const items = mentionEvents.map((ev) => {
const chId =
((ev.tags ?? []) as string[][]).find((t) => t[0] === "h")?.[1] ?? null;
return {
id: ev.id ?? "",
pubkey: ev.pubkey ?? "",
content: ev.content ?? "",
created_at: ev.created_at ?? 0,
kind: ev.kind ?? 9,
tags: (ev.tags ?? []) as string[][],
channel_id: chId,
channel_name: chId ? (channelNameMap.get(chId) ?? "") : "",
// Native-shaped: get_feed emits channel_type: null (Option<String>),
// never omits the key. Keeping the bridge faithful here is what lets
// the DM dedupe e2e catch null-vs-undefined regressions at the API
// conversion seam.
channel_type: null,
category: "mention" as const,
};
});
return {
feed: {
mentions: items,
needs_action: [],
activity: [],
agent_activity: [],
},
meta: {
since: Math.floor(Date.now() / 1000) - 7 * 86400,
total: items.length,
generated_at: Math.floor(Date.now() / 1000),
},
};
}
async function delayAgentList(config: E2eConfig | undefined) {
const agentListDelayMs = config?.mock?.agentListDelayMs ?? 0;
if (agentListDelayMs > 0) {
await new Promise<void>((resolve) => {
window.setTimeout(resolve, agentListDelayMs);
});
}
}
async function handleListRelayAgents(
config: E2eConfig | undefined,
): Promise<RawRelayAgent[]> {
await delayAgentList(config);
syncMockRelayAgentsFromManagedAgents();
return mockRelayAgents.map(cloneRelayAgent);
}
function withMockRuntimeConfigMetadata(
runtime: RawAcpRuntimeCatalogEntry,
): RawAcpRuntimeCatalogEntry {
return {
...runtime,
node_required: runtime.node_required ?? false,
auth_status: runtime.auth_status ?? { status: "unknown" },
model_env_var:
"model_env_var" in runtime
? runtime.model_env_var
: runtime.id === "buzz-agent"
? "BUZZ_AGENT_MODEL"
: runtime.id === "goose"
? "GOOSE_MODEL"
: null,
provider_env_var:
"provider_env_var" in runtime
? runtime.provider_env_var
: runtime.id === "buzz-agent"
? "BUZZ_AGENT_PROVIDER"
: runtime.id === "goose"
? "GOOSE_PROVIDER"
: null,
thinking_env_var:
"thinking_env_var" in runtime
? runtime.thinking_env_var
: runtime.id === "buzz-agent"
? "BUZZ_AGENT_THINKING_EFFORT"
: runtime.id === "goose"
? "GOOSE_THINKING_EFFORT"
: null,
};
}
let runtimeCatalogDiscoveryCount = 0;
let mockInstallCompleted = false;
let mockConnectCompleted = false;
async function handleDiscoverAcpRuntimes(
config: E2eConfig | undefined,
): Promise<RawAcpRuntimeCatalogEntry[]> {
const delayMs = config?.mock?.acpRuntimesDelayMs ?? 0;
if (delayMs > 0) {
await new Promise<void>((resolve) => {
window.setTimeout(resolve, delayMs);
});
}
const afterInstallSequence =
config?.mock?.acpRuntimesCatalogAfterInstallSequence;
if (mockInstallCompleted && afterInstallSequence?.length) {
const index = Math.min(
runtimeCatalogDiscoveryCount,
afterInstallSequence.length - 1,
);
runtimeCatalogDiscoveryCount += 1;
return afterInstallSequence[index].map(withMockRuntimeConfigMetadata);
}
const afterInstall = config?.mock?.acpRuntimesCatalogAfterInstall;
if (mockInstallCompleted && afterInstall) {
return afterInstall.map(withMockRuntimeConfigMetadata);
}
const afterConnect = config?.mock?.acpRuntimesCatalogAfterConnect;
if (mockConnectCompleted && afterConnect) {
return afterConnect.map(withMockRuntimeConfigMetadata);
}
const sequence = config?.mock?.acpRuntimesCatalogSequence;
if (sequence && sequence.length > 0) {
const index = Math.min(runtimeCatalogDiscoveryCount, sequence.length - 1);
runtimeCatalogDiscoveryCount += 1;
return sequence[index].map(withMockRuntimeConfigMetadata);
}
const configured = config?.mock?.acpRuntimesCatalog;
if (configured) {
return mergeMockCustomHarnesses(
configured.map(withMockRuntimeConfigMetadata),
);
}
const defaultCatalog: RawAcpRuntimeCatalogEntry[] = [
{
id: "goose",
label: "Goose",
avatar_url: "",
availability: "available",
command: "goose",
binary_path: "/usr/local/bin/goose",
default_args: ["acp"],
mcp_command: null,
install_hint: "Install Goose via the official install script.",
install_instructions_url: "https://block.github.io/goose/",
can_auto_install: true,
requires_external_cli: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
source: "builtin",
login_hint: undefined,
},
{
id: "claude",
label: "Claude Code",
avatar_url: "",
availability: "adapter_missing",
command: null,
binary_path: null,
default_args: [],
mcp_command: null,
install_hint: "Install the Claude Code ACP adapter via npm.",
install_instructions_url:
"https://www.npmjs.com/package/@anthropic-ai/claude-agent-acp",
can_auto_install: true,
requires_external_cli: true,
underlying_cli_path: "/usr/local/bin/claude",
node_required: false,
auth_status: { status: "unknown" },
source: "builtin",
login_hint: undefined,
},
{
id: "codex",
label: "Codex",
avatar_url: "",
availability: "not_installed",
command: null,
binary_path: null,
default_args: [],
mcp_command: null,
install_hint:
"The codex-acp adapter must be built from source. See the GitHub repo.",
install_instructions_url: "https://github.com/openai/codex",
can_auto_install: false,
requires_external_cli: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "unknown" },
source: "builtin",
login_hint: undefined,
},
{
id: "buzz-agent",
label: "Buzz Agent",
avatar_url: "",
availability: "available",
command: "buzz-agent",
binary_path: "/usr/local/bin/buzz-agent",
default_args: [],
mcp_command: "buzz-dev-mcp",
install_hint: "Ships with the Buzz desktop app.",
install_instructions_url: "https://github.com/block/buzz",
can_auto_install: false,
requires_external_cli: false,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
source: "builtin",
login_hint: undefined,
},
];
return mergeMockCustomHarnesses(
defaultCatalog.map(withMockRuntimeConfigMetadata),
);
}
async function handleDiscoverAcpAuthMethods(
args: { runtimeId?: string },
config: E2eConfig | undefined,
): Promise<RawAcpAuthMethodsResult> {
const globalError = config?.mock?.acpAuthMethodsError;
if (globalError) {
throw new Error(globalError);
}
const runtimeId = args.runtimeId ?? "";
const perRuntimeError = config?.mock?.acpAuthMethodsErrors?.[runtimeId];
if (perRuntimeError) {
throw new Error(perRuntimeError);
}
const configured = config?.mock?.acpAuthMethods?.[runtimeId];
if (configured) {
return configured;
}
return { methods: [] };
}
async function handleConnectAcpRuntime(
_args: { request?: { runtimeId?: string; methodId?: string } },
config: E2eConfig | undefined,
): Promise<RawConnectAcpRuntimeResult> {
const error = config?.mock?.connectAcpRuntimeError;
if (error) {
throw new Error(error);
}
const delayMs = config?.mock?.connectAcpRuntimeDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
mockConnectCompleted = true;
return config?.mock?.connectAcpRuntimeResult ?? { launched: true };
}
// Per-page install call counter. Reset each test run because this module is
// re-evaluated via addInitScript, so the counter starts at 0 for every test.
let installCallCount = 0;
/** Per-runtime call counters for `installAcpRuntimeByRuntime` sequences. */
const installCallCountByRuntime: Record<string, number> = {};
let addChannelMembersCallCount = 0;
let mockGlobalAgentConfig: {
env_vars: Record<string, string>;
provider: string | null;
model: string | null;
preferred_runtime?: string | null;
} | null = null;
// Per-page get_nsec call counter for sequenced error testing.
let nsecCallCount = 0;
// Per-page explicit catalog publication outcomes.
let personaSharePublicationCallCount = 0;
// Per-page confirm_team_snapshot_import call counter for sequenced error testing.
let teamSnapshotConfirmCallCount = 0;
async function handleInstallAcpRuntime(
args: {
runtimeId?: string;
},
config: E2eConfig | undefined,
): Promise<RawInstallRuntimeResult> {
const runtimeId = args.runtimeId ?? "";
const perRuntime = config?.mock?.installAcpRuntimeByRuntime?.[runtimeId];
if (perRuntime) {
const delayMs = perRuntime.delayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
const seq = perRuntime.results;
if (seq && seq.length > 0) {
const idx = Math.min(
installCallCountByRuntime[runtimeId] ?? 0,
seq.length - 1,
);
installCallCountByRuntime[runtimeId] = idx + 1;
const result = seq[idx];
if (result.success) mockInstallCompleted = true;
return result;
}
if (perRuntime.result) {
if (perRuntime.result.success) mockInstallCompleted = true;
return perRuntime.result;
}
}
const delayMs = config?.mock?.installAcpRuntimeDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
const sequence = config?.mock?.installAcpRuntimeResults;
if (sequence && sequence.length > 0) {
const idx = Math.min(installCallCount, sequence.length - 1);
installCallCount++;
const result = sequence[idx];
if (result.success) mockInstallCompleted = true;
return result;
}
const configured = config?.mock?.installAcpRuntimeResult;
if (configured) {
if (configured.success) mockInstallCompleted = true;
return configured;
}
mockInstallCompleted = true;
return {
success: true,
steps: [
{
step: "adapter",
command: `mock install ${args.runtimeId ?? "unknown"}`,
success: true,
stdout: "mock: installed successfully",
stderr: "",
exit_code: 0,
},
],
restarted_count: 0,
failed_restart_count: 0,
};
}
async function handleDiscoverManagedAgentPrereqs(
args: {
input?: {
acpCommand?: string;
mcpCommand?: string;
};
},
config: E2eConfig | undefined,
): Promise<RawManagedAgentPrereqs> {
const configuredPrereqs = config?.mock?.managedAgentPrereqs;
return {
acp: {
command:
configuredPrereqs?.acp?.command ?? args.input?.acpCommand ?? "buzz-acp",
resolved_path:
configuredPrereqs?.acp?.resolvedPath ??
"/Users/wesb/dev/buzz/target/debug/buzz-acp",
available: configuredPrereqs?.acp?.available ?? true,
},
mcp: {
command: configuredPrereqs?.mcp?.command ?? args.input?.mcpCommand ?? "",
resolved_path: configuredPrereqs?.mcp?.resolvedPath ?? "",
available: configuredPrereqs?.mcp?.available ?? true,
},
};
}
async function handleListManagedAgents(
config: E2eConfig | undefined,
): Promise<RawManagedAgent[]> {
await delayAgentList(config);
return mockManagedAgents.map(cloneManagedAgent);
}
function isAgentMemoryListing(
value: RawAgentMemoryListing | Record<string, RawAgentMemoryListing>,
): value is RawAgentMemoryListing {
return (
"memories" in value &&
Array.isArray(value.memories) &&
"truncated" in value &&
"fetchedAt" in value
);
}
async function handleGetAgentMemory(
args: { agentPubkey?: string },
config: E2eConfig | undefined,
): Promise<RawAgentMemoryListing> {
const pubkey = args.agentPubkey?.toLowerCase();
if (!pubkey) {
throw new Error("mock get_agent_memory: missing agent pubkey");
}
const isManagedAgent = mockManagedAgents.some(
(agent) => agent.pubkey.toLowerCase() === pubkey,
);
if (!isManagedAgent) {
throw new Error(`mock get_agent_memory: unmanaged agent ${pubkey}`);
}
const configuredMemory = config?.mock?.agentMemory;
if (!configuredMemory) {
return {
core: null,
memories: [],
truncated: false,
fetchedAt: Math.floor(Date.now() / 1000),
};
}
if (isAgentMemoryListing(configuredMemory)) {
return cloneAgentMemoryListing(configuredMemory);
}
const listing =
configuredMemory[pubkey] ?? configuredMemory[args.agentPubkey ?? ""];
return listing
? cloneAgentMemoryListing(listing)
: {
core: null,
memories: [],
truncated: false,
fetchedAt: Math.floor(Date.now() / 1000),
};
}
async function handleListPersonas(): Promise<RawPersona[]> {
return mockPersonas.map((persona) => ({ ...persona }));
}
type PersonaBehaviorInput = {
respondTo?: "owner-only" | "allowlist" | "anyone";
respondToAllowlist?: string[];
parallelism?: number;
};
/** Mirrors `apply_persona_behavior`: replace all four as a unit. */
function applyMockPersonaBehavior(
persona: RawPersona,
behavior: PersonaBehaviorInput | undefined,
) {
if (behavior === undefined) {
return;
}
persona.respond_to = behavior.respondTo ?? null;
persona.respond_to_allowlist =
behavior.respondTo === "allowlist"
? [...(behavior.respondToAllowlist ?? [])]
: [];
persona.parallelism = behavior.parallelism ?? null;
}
async function handleCreatePersona(args: {
input: {
displayName: string;
avatarUrl?: string;
systemPrompt: string;
runtime?: string;
model?: string;
provider?: string;
envVars?: Record<string, string>;
behavior?: PersonaBehaviorInput;
catalogSource?: { ownerPubkey: string; personaId: string };
};
}): Promise<RawPersona> {
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(),
runtime: args.input.runtime?.trim() || null,
model: args.input.model?.trim() || null,
provider: args.input.provider?.trim() || null,
is_builtin: false,
is_active: true,
shared: false,
source_team: null,
// Mirrors `CatalogSource::normalized`: the coordinate a catalog copy keeps
// so the catalog can tell an already-added foreign entry from a new one.
catalog_source: args.input.catalogSource
? {
owner_pubkey: args.input.catalogSource.ownerPubkey
.trim()
.toLowerCase(),
persona_id: args.input.catalogSource.personaId.trim(),
}
: null,
env_vars: { ...(args.input.envVars ?? {}) },
created_at: now,
updated_at: now,
};
applyMockPersonaBehavior(persona, args.input.behavior);
mockPersonas.push(persona);
upsertMockPersonaEvent(persona);
return { ...persona };
}
type MockUpdatePersonaInput = {
id: string;
displayName: string;
avatarUrl?: string;
systemPrompt: string;
runtime?: string;
model?: string;
provider?: string;
envVars?: Record<string, string>;
behavior?: PersonaBehaviorInput;
};
async function handleUpdatePersona(args: {
input: MockUpdatePersonaInput;
}): Promise<RawPersona> {
return { ...applyMockPersonaUpdate(args.input) };
}
/**
* Save an edit to the mock persona store, exactly like `update_persona_with`,
* and return the live record so a caller can publish it.
*
* Deliberately does NOT publish a catalog event: the real `update_persona`
* only enqueues a pending head for the out-of-band flush loop, so nothing has
* reached the relay by the time the command returns. Publishing here would
* make a UI that never calls `update_persona_and_publish` look like it kept
* the "Save and publish" promise.
*/
function applyMockPersonaUpdate(input: MockUpdatePersonaInput): RawPersona {
const persona = mockPersonas.find((candidate) => candidate.id === input.id);
if (!persona) {
throw new Error(`agent ${input.id} not found`);
}
persona.display_name = input.displayName.trim();
persona.avatar_url = input.avatarUrl?.trim() || null;
persona.system_prompt = input.systemPrompt.trim();
persona.runtime = input.runtime?.trim() || null;
persona.model = input.model?.trim() || null;
persona.provider = input.provider?.trim() || null;
if (input.envVars !== undefined) {
// Absent = preserve; present = replace entirely (matches Rust handler).
persona.env_vars = { ...input.envVars };
}
applyMockPersonaBehavior(persona, input.behavior);
persona.updated_at = new Date().toISOString();
for (const callback of tauriEventListeners.get("agents-data-changed") ?? []) {
callback();
}
return persona;
}
async function handleDeletePersona(args: { id: string }): Promise<void> {
const persona = mockPersonas.find((candidate) => candidate.id === args.id);
if (!persona) {
throw new Error(`agent ${args.id} not found`);
}
if (persona.is_builtin) {
throw new Error("Built-in agents 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<RawPersona> {
const persona = mockPersonas.find((candidate) => candidate.id === args.id);
if (!persona) {
throw new Error(`agent ${args.id} not found`);
}
if (!persona.is_builtin) {
throw new Error(
"Only built-in agents 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 personaHasExactSharedTag(event: RelayEvent): boolean {
const tags = event.tags.filter((tag) => tag[0] === "shared");
return tags.length === 1 && tags[0]?.length === 2 && tags[0]?.[1] === "true";
}
function upsertMockPersonaRelayEvent(event: RelayEvent): void {
const sourceId = event.tags.find((tag) => tag[0] === "d")?.[1];
if (!sourceId) return;
const existingIndex = mockPersonaEvents.findIndex(
(candidate) =>
candidate.pubkey.toLowerCase() === event.pubkey.toLowerCase() &&
candidate.tags.some((tag) => tag[0] === "d" && tag[1] === sourceId),
);
if (existingIndex >= 0) {
mockPersonaEvents.splice(existingIndex, 1);
}
mockPersonaEvents.push(event);
}
function upsertMockPersonaEvent(persona: RawPersona): void {
const event: RelayEvent = {
id: mockEventId(),
pubkey: MOCK_IDENTITY_PUBKEY,
created_at: Math.floor(Date.now() / 1_000),
kind: KIND_PERSONA,
tags: [["d", persona.id], ...(persona.shared ? [["shared", "true"]] : [])],
content: JSON.stringify({
display_name: persona.display_name,
system_prompt: persona.system_prompt,
avatar_url: persona.avatar_url,
runtime: persona.runtime ?? null,
model: persona.model ?? null,
provider: persona.provider ?? null,
name_pool: persona.name_pool ?? [],
respond_to: persona.respond_to ?? null,
respond_to_allowlist: persona.respond_to_allowlist ?? [],
parallelism: persona.parallelism ?? null,
}),
sig: "0".repeat(128),
};
upsertMockPersonaRelayEvent(event);
emitMockGlobalEvent(event);
}
type MockPersonaPublicationResult = {
persona: RawPersona;
publicationStatus: "published" | "queued";
relayMessage?: string;
};
/**
* Publish a persona's catalog head and report the relay outcome, like
* `publish_prepared_persona`. A `queued` outcome must NOT make the event
* visible to catalog readers — that is the whole distinction the UI reports.
*/
function publishMockPersonaHead(
persona: RawPersona,
config: E2eConfig | undefined,
): MockPersonaPublicationResult {
const publicationStatus =
config?.mock?.personaSharePublicationStatuses?.[
personaSharePublicationCallCount++
] ?? "published";
if (publicationStatus === "published") {
upsertMockPersonaEvent(persona);
}
return {
persona: { ...persona },
publicationStatus,
...(publicationStatus === "queued"
? { relayMessage: "relay unreachable: could not connect to relay" }
: {}),
};
}
async function handleSetPersonaShared(
args: {
id: string;
shared: boolean;
},
config?: E2eConfig,
): Promise<MockPersonaPublicationResult> {
const persona = mockPersonas.find((candidate) => candidate.id === args.id);
if (!persona) {
throw new Error(`agent ${args.id} not found`);
}
if (persona.is_builtin) {
throw new Error("Built-in agents cannot be shared to the catalog.");
}
persona.shared = args.shared;
persona.updated_at = new Date().toISOString();
return publishMockPersonaHead(persona, config);
}
/** Mirrors `update_persona_and_publish`: save the edit, then await the relay. */
async function handleUpdatePersonaAndPublish(
args: { input: MockUpdatePersonaInput },
config?: E2eConfig,
): Promise<MockPersonaPublicationResult> {
return publishMockPersonaHead(applyMockPersonaUpdate(args.input), config);
}
function ensureMockPersonaIsActive(personaId: string) {
const persona = mockPersonas.find((candidate) => candidate.id === personaId);
if (!persona) {
throw new Error(`agent ${personaId} not found`);
}
if (!persona.is_active) {
throw new Error(`${persona.display_name} is not in My Agents.`);
}
}
function ensureMockPersonaIdsAreActive(personaIds: string[]) {
for (const personaId of personaIds) {
ensureMockPersonaIsActive(personaId);
}
}
async function handleListTeams(): Promise<RawTeam[]> {
return mockTeams.map((team) => ({
...team,
persona_ids: [...team.persona_ids],
}));
}
async function handleCreateTeam(args: {
input: {
name: string;
description?: string;
personaIds: string[];
};
}): Promise<RawTeam> {
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],
is_builtin: false,
source_dir: null,
is_symlink: false,
symlink_target: null,
version: null,
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<RawTeam> {
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<void> {
const team = mockTeams.find((candidate) => candidate.id === args.id);
if (team?.is_builtin) {
throw new Error("Built-in teams cannot be deleted.");
}
mockTeams = mockTeams.filter((candidate) => candidate.id !== args.id);
}
async function handleExportTeamToJson(args: { id: string }): Promise<boolean> {
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 handlePickTeamDirectory(): Promise<string | null> {
return "/Users/dev/agents/new-team";
}
async function handleInstallTeamFromDirectory(args: {
path: string;
symlink: boolean;
}): Promise<RawTeam> {
const now = new Date().toISOString();
const team: RawTeam = {
id: crypto.randomUUID(),
name: "Installed Team",
description: null,
persona_ids: [],
is_builtin: false,
source_dir: args.path,
is_symlink: args.symlink,
symlink_target: args.symlink ? args.path : null,
version: null,
created_at: now,
updated_at: now,
};
mockTeams.push(team);
return { ...team, persona_ids: [...team.persona_ids] };
}
async function handleSyncTeamDirectory(args: { teamId: string }): Promise<{
personas_added: string[];
personas_removed: string[];
personas_updated: string[];
metadata_changed: boolean;
}> {
const team = mockTeams.find((candidate) => candidate.id === args.teamId);
if (!team) {
throw new Error(`Team ${args.teamId} not found.`);
}
return {
personas_added: [],
personas_removed: [],
personas_updated: [],
metadata_changed: false,
};
}
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 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;
provider?: string;
envVars?: Record<string, string>;
spawnAfterCreate?: boolean;
startOnAppLaunch?: boolean;
backend?:
| { type: "local" }
| { type: "provider"; id: string; config: Record<string, unknown> };
respondTo?: "owner-only" | "allowlist" | "anyone";
respondToAllowlist?: string[];
};
},
config: E2eConfig | undefined,
): Promise<RawCreateManagedAgentResponse> {
const delayMs = config?.mock?.createManagedAgentDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
if (args.input.personaId) {
ensureMockPersonaIsActive(args.input.personaId);
}
// Mint-parity with resolve_mint_behavioral_defaults: an explicit input
// wins; otherwise the linked definition's stored quad applies (mode+list
// travel together); otherwise the schema default.
const linkedPersona = args.input.personaId
? (mockPersonas.find((persona) => persona.id === args.input.personaId) ??
null)
: null;
const mintRespondTo =
args.input.respondTo ??
(linkedPersona?.respond_to as RawManagedAgent["respond_to"] | null) ??
"owner-only";
const mintRespondToAllowlist =
args.input.respondTo !== undefined
? (args.input.respondToAllowlist ?? [])
: (linkedPersona?.respond_to_allowlist ?? []);
const mintParallelism =
args.input.parallelism ?? linkedPersona?.parallelism ?? 1;
const personaAvatarUrl =
args.input.personaId === undefined
? null
: (mockPersonas.find((persona) => persona.id === args.input.personaId)
?.avatar_url ?? null);
const avatarUrl = args.input.avatarUrl?.trim() || personaAvatarUrl;
const name = args.input.name.trim();
const now = new Date().toISOString();
const pubkey = crypto
.randomUUID()
.replace(/-/g, "")
.padEnd(64, "0")
.slice(0, 64);
const agentCommand = args.input.agentCommand ?? "buzz-agent";
const agentArgs =
args.input.agentArgs && args.input.agentArgs.length > 0
? [...args.input.agentArgs]
: agentCommand === "goose"
? ["acp"]
: [];
const managedAgent: MockManagedAgent = {
pubkey,
name,
persona_id: args.input.personaId ?? null,
// Create never pins a harness id — the record inherits from the persona.
runtime: null,
relay_url: args.input.relayUrl ?? DEFAULT_RELAY_WS_URL,
acp_command: args.input.acpCommand ?? "buzz-acp",
agent_command: agentCommand,
agent_args: agentArgs,
mcp_command: args.input.mcpCommand ?? "",
turn_timeout_seconds: args.input.turnTimeoutSeconds ?? 320,
idle_timeout_seconds: args.input.idleTimeoutSeconds ?? null,
max_turn_duration_seconds: args.input.maxTurnDurationSeconds ?? null,
parallelism: mintParallelism,
system_prompt: args.input.systemPrompt?.trim() || null,
avatar_url: avatarUrl,
model: args.input.model?.trim() || linkedPersona?.model || null,
provider: args.input.provider?.trim() || linkedPersona?.provider || null,
env_vars: { ...(args.input.envVars ?? {}) },
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,
last_error_code: null,
log_path: `/tmp/mock-agent-${pubkey}.log`,
start_on_app_launch: args.input.startOnAppLaunch ?? true,
auto_restart_on_config_change: true,
backend: args.input.backend ?? { type: "local" as const },
backend_agent_id: null,
respond_to: mintRespondTo,
respond_to_allowlist: [...mintRespondToAllowlist],
private_key_nsec: `nsec1mock${pubkey.slice(0, 20)}`,
log_lines: [
`buzz-acp starting: relay=${args.input.relayUrl ?? DEFAULT_RELAY_WS_URL} agent_pubkey=${pubkey} parallelism=${mintParallelism}`,
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);
if (args.input.spawnAfterCreate && managedAgent.backend.type === "local") {
// The real create command spawns a pair runtime on the agent's effective
// relay (`start_local_agent_with_preflight`), so mirror that row here.
upsertMockManagedAgentRuntime(pubkey, managedAgent.relay_url, "ready");
}
applyMockDisplayName(pubkey, name);
mockAgentPubkeys.add(pubkey);
mockProfiles.set(pubkey, {
pubkey,
display_name: name,
avatar_url: avatarUrl,
about: args.input.systemPrompt?.trim() || null,
nip05_handle: null,
owner_pubkey: MOCK_IDENTITY_PUBKEY,
is_agent: true,
has_profile_event: true,
});
syncMockRelayAgentsFromManagedAgents();
return {
agent: cloneManagedAgent(managedAgent),
private_key_nsec: managedAgent.private_key_nsec,
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;
}
function upsertMockManagedAgentRuntime(
pubkey: string,
relayUrl: string,
lifecycle: MockManagedAgentRuntimeRow["lifecycle"],
): MockManagedAgentRuntimeRow {
let row = mockManagedAgentRuntimes.find(
(candidate) =>
candidate.pubkey === pubkey && candidate.relayUrl === relayUrl,
);
if (!row) {
row = {
pubkey,
relayUrl,
localSetup: true,
lifecycle,
pid: null,
error: null,
logPath: null,
};
mockManagedAgentRuntimes.push(row);
}
row.lifecycle = lifecycle;
row.pid =
lifecycle === "stopped"
? null
: (row.pid ?? 43000 + mockManagedAgentRuntimes.length);
row.error = null;
return row;
}
// Pair-scoped lifecycle, mirroring `runtime_commands.rs`: the command touches
// exactly one agent+relay runtime and rejects non-local agents.
function handleManagedAgentRuntimeAction(
action: "start" | "stop" | "restart",
args: { pubkey: string; relayUrl: string },
): MockManagedAgentRuntimeRow {
const agent = getMockManagedAgent(args.pubkey);
if (agent.backend.type !== "local") {
throw new Error("managed runtime pairs require a local agent");
}
return {
...upsertMockManagedAgentRuntime(
args.pubkey,
args.relayUrl,
action === "stop" ? "stopped" : "ready",
),
};
}
function isRelayMeshManagedAgent(agent: MockManagedAgent): boolean {
return agent.backend.type === "local" && agent.provider === "relay-mesh";
}
async function handleStartManagedAgent(
args: {
pubkey: string;
},
config?: E2eConfig,
): Promise<RawManagedAgent> {
const startError = config?.mock?.startManagedAgentErrors?.shift();
if (startError) {
throw new Error(startError);
}
const agent = getMockManagedAgent(args.pubkey);
if (isRelayMeshManagedAgent(agent)) {
// Model the backend start preflight (ensure_relay_mesh_for_record): a
// saved relay-mesh agent re-resolves a live serve target for its model
// and only fails when no peer currently serves it.
const modelId = agent.model ?? "auto";
const hasLiveTarget =
mockMeshState.admitted &&
(modelId === "auto" ||
mockMeshState.models.some((model) => model.id === modelId));
if (!hasLiveTarget) {
throw new Error(
"Buzz shared compute cannot start because no live member is serving this model.",
);
}
}
const now = new Date().toISOString();
if (agent.backend.type === "provider") {
agent.status = "deployed";
agent.pid = null;
agent.backend_agent_id =
agent.backend_agent_id ?? `mock-provider-${agent.pubkey.slice(0, 12)}`;
} else {
agent.status = "running";
agent.pid = agent.pid ?? 42000 + mockManagedAgents.indexOf(agent);
// The real command spawns a pair runtime keyed by the agent's effective
// relay (`start_managed_agent_process`), so mirror that row here.
upsertMockManagedAgentRuntime(agent.pubkey, agent.relay_url, "ready");
}
agent.updated_at = now;
agent.last_started_at = now;
agent.last_error = null;
setMockPresenceStatus(agent.pubkey, "online");
agent.log_lines.push(
agent.backend.type === "provider"
? `deployed mock provider harness at ${now}`
: `started mock harness at ${now}`,
);
syncMockRelayAgentsFromManagedAgents();
return cloneManagedAgent(agent);
}
async function handleStopManagedAgent(args: {
pubkey: string;
}): Promise<RawManagedAgent> {
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;
// Legacy agent-wide stop deliberately drains EVERY pair runtime for the
// agent (`runtime/stop.rs`) — mirror that so specs can prove the sidebar
// no longer routes through it.
for (const row of mockManagedAgentRuntimes) {
if (row.pubkey === args.pubkey) {
row.lifecycle = "stopped";
row.pid = null;
}
}
setMockPresenceStatus(agent.pubkey, "offline");
agent.log_lines.push(`stopped mock harness at ${now}`);
syncMockRelayAgentsFromManagedAgents();
return cloneManagedAgent(agent);
}
async function handleDeleteManagedAgent(args: {
pubkey: string;
forceRemoteDelete?: boolean | null;
}): Promise<void> {
// 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<RawManagedAgent> {
const agent = getMockManagedAgent(args.pubkey);
agent.start_on_app_launch = args.startOnAppLaunch;
agent.updated_at = new Date().toISOString();
return cloneManagedAgent(agent);
}
async function handleSetManagedAgentAutoRestart(args: {
pubkey: string;
autoRestartOnConfigChange: boolean;
}): Promise<RawManagedAgent> {
const agent = getMockManagedAgent(args.pubkey);
agent.auto_restart_on_config_change = args.autoRestartOnConfigChange;
agent.updated_at = new Date().toISOString();
return cloneManagedAgent(agent);
}
async function handleGetManagedAgentLog(args: {
pubkey: string;
lineCount?: number;
}): Promise<RawManagedAgentLog> {
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;
envVars?: Record<string, string>;
respondTo?: "owner-only" | "allowlist" | "anyone";
respondToAllowlist?: string[];
};
}): 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;
}
if (args.input.envVars !== undefined) {
agent.env_vars = { ...args.input.envVars };
}
if (args.input.respondTo !== undefined) {
agent.respond_to = args.input.respondTo;
}
if (args.input.respondToAllowlist !== undefined) {
agent.respond_to_allowlist = args.input.respondToAllowlist;
}
agent.updated_at = new Date().toISOString();
return { agent: cloneManagedAgent(agent), profile_sync_error: null };
}
/**
* Mock-mode `search_messages` predicate, mirroring the relay's filter contract.
*
* `since`/`until` are NIP-01 bounds and both inclusive — the relay keeps events
* where `since <= created_at <= until` (`crates/buzz-core/src/filter.rs`). The
* `before:` operator's exclusivity is encoded upstream in
* `parseSearchOperators`, which subtracts a second; the mock must not subtract
* it a second time.
*
* Exported so the boundary behavior is unit-testable without a browser.
*/
export function mockSearchHitMatches(
hit: Pick<
RawSearchHit,
"channel_id" | "pubkey" | "created_at" | "content" | "channel_name"
>,
filters: {
/** Lowercased FTS query; empty matches everything. */
query: string;
channelId?: string;
authorSet: Set<string> | null;
since?: number;
until?: number;
},
): boolean {
if (filters.channelId && hit.channel_id !== filters.channelId) {
return false;
}
if (filters.authorSet && !filters.authorSet.has(hit.pubkey.toLowerCase())) {
return false;
}
if (filters.since != null && hit.created_at < filters.since) {
return false;
}
if (filters.until != null && hit.created_at > filters.until) {
return false;
}
if (!filters.query) {
return true;
}
return (
hit.content.toLowerCase().includes(filters.query) ||
(hit.channel_name?.toLowerCase().includes(filters.query) ?? false)
);
}
async function handleSearchMessages(
args: {
q: string;
limit?: number;
channelId?: string;
authors?: string[];
since?: number;
until?: number;
},
config: E2eConfig | undefined,
): Promise<RawSearchResponse> {
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,
},
];
for (const [channelId, events] of mockMessages) {
const channel = mockChannels.find(
(candidate) => candidate.id === channelId,
);
for (const event of events) {
mockHits.push({
event_id: event.id,
content: event.content,
kind: event.kind,
pubkey: event.pubkey,
channel_id: channelId,
channel_name: channel?.name ?? null,
created_at: event.created_at,
score: 1,
});
}
}
const authorSet = args.authors?.length
? new Set(args.authors.map((author) => author.toLowerCase()))
: null;
const hits = mockHits
.filter((hit) =>
mockSearchHitMatches(hit, {
query,
channelId: args.channelId,
authorSet,
since: args.since,
until: args.until,
}),
)
.slice(0, limit);
return {
hits,
found: hits.length,
};
}
// NIP-50 search via POST /query — forward operator pushdown fields.
const limit = args.limit ?? 20;
const filter: Record<string, unknown> = {
kinds: [9, 40002, 45001, 45003],
search: args.q,
limit,
};
if (args.channelId) {
filter["#h"] = [args.channelId];
}
if (args.authors?.length) {
filter.authors = args.authors;
}
if (args.since != null) {
filter.since = args.since;
}
if (args.until != null) {
filter.until = args.until;
}
const events = await relayQuery(config, [filter]);
const hits = events.map((ev) => ({
event_id: ev.id ?? "",
pubkey: ev.pubkey ?? "",
content: ev.content ?? "",
created_at: ev.created_at ?? 0,
kind: ev.kind ?? 9,
tags: ev.tags ?? [],
sig: ev.sig ?? "",
channel_id:
((ev.tags ?? []) as string[][]).find((t) => t[0] === "h")?.[1] ?? null,
channel_name: null,
score: 1.0,
}));
return { hits, found: hits.length };
}
/**
* Descriptors returned by the mocked upload commands. A spec can override via
* `MockBridgeOptions.uploadDescriptors`; otherwise we return a single generic
* PDF so the file-attachment flow (chip → send → FileCard) can be exercised
* out of the box.
*/
async function resolveMockUploadDescriptors(
config: E2eConfig | undefined,
): Promise<RawBlobDescriptor[]> {
const delayMs = config?.mock?.uploadDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
const configured = config?.mock?.uploadDescriptors;
// `undefined` means "not configured" → default PDF. An explicit `[]` is a
// valid override (e.g. modelling a picker cancel / no-files-selected), so it
// must pass through rather than fall back to the default.
if (configured !== undefined) return configured;
return [
{
url: `https://mock.relay/media/${"a".repeat(64)}.pdf`,
sha256: "a".repeat(64),
size: 12345,
type: "application/pdf",
uploaded: Math.floor(Date.now() / 1000),
filename: "quarterly-report.pdf",
},
];
}
async function resolveMockUploadDescriptorForBytes(
args: { data: number[]; filename?: string | null },
config: E2eConfig | undefined,
): Promise<RawBlobDescriptor> {
const configured = config?.mock?.uploadDescriptors;
if (configured !== undefined) {
const descriptors = await resolveMockUploadDescriptors(config);
const descriptor = descriptors[0];
if (!descriptor) throw new Error("mock upload returned no descriptor");
return descriptor;
}
const bytes = Uint8Array.from(args.data);
const digest = await crypto.subtle.digest("SHA-256", bytes);
const sha256 = Array.from(new Uint8Array(digest), (value) =>
value.toString(16).padStart(2, "0"),
).join("");
const filename = args.filename ?? "upload.bin";
const isAgentJson = filename.toLowerCase().endsWith(".agent.json");
return {
url: `https://mock.relay/media/${sha256}${isAgentJson ? ".json" : ".bin"}`,
sha256,
size: bytes.length,
type: isAgentJson ? "application/json" : "application/octet-stream",
uploaded: Math.floor(Date.now() / 1000),
filename,
};
}
async function handleSendChannelMessage(
args: {
channelId: string;
content: string;
parentEventId?: string | null;
kind?: number | null;
mentionPubkeys?: string[];
mediaTags?: string[][] | null;
emojiTags?: string[][] | null;
},
config: E2eConfig | undefined,
): Promise<RawSendChannelMessageResponse> {
const kind = args.kind ?? 9;
const sendMessageDelayMs = config?.mock?.sendMessageDelayMs ?? 0;
if (sendMessageDelayMs > 0) {
await new Promise((resolve) =>
window.setTimeout(resolve, sendMessageDelayMs),
);
}
// NIP-92 imeta attachments. The real relay echoes these back on the stored
// event; mirror that here so attachment renderers (FileCard, images, video)
// have the imeta tags they key on. `null`/empty → no extra tags.
const mediaTags = args.mediaTags ?? [];
// NIP-30 custom-emoji tags ride their own validated arg server-side; the
// relay echoes them back on the stored event too, so mirror that here so the
// emoji renderer keeps resolving `:shortcode:` after the round-trip.
const emojiTags = args.emojiTags ?? [];
// Both kinds end up on the stored event's tag set, just like the real relay.
const extraTags = [...mediaTags, ...emojiTags];
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,
),
...extraTags,
]);
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: mockEventId(),
pubkey: mockPubkey,
created_at: createdAt,
kind,
tags: [
...buildReplyMessageTags(
args.channelId,
mockPubkey,
args.parentEventId,
rootEventId,
args.mentionPubkeys,
),
...extraTags,
],
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: [...tags, ...extraTags],
});
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 handleSendManagedAgentChannelMessage(
args: {
agentPubkey: string;
channelId: string;
content: string;
marker?: string | null;
markerScope?: "agent" | "channel" | null;
mentionPubkeys?: string[] | null;
parentEventId?: string | null;
additionalMarkers?: string[] | null;
},
_config: E2eConfig | undefined,
): Promise<RawSendChannelMessageResponse> {
const agent = getMockManagedAgent(args.agentPubkey);
const marker = args.marker?.trim();
if (marker) {
const existing = getMockMessageStore(args.channelId).find(
(event) =>
(args.markerScope === "channel" || event.pubkey === agent.pubkey) &&
event.tags.some((tag) => tag[0] === "client" && tag[1] === marker),
);
if (existing) {
return {
event_id: existing.id,
parent_event_id: args.parentEventId ?? null,
root_event_id: args.parentEventId ?? null,
depth: args.parentEventId ? 1 : 0,
created_at: existing.created_at,
};
}
}
const createdAt = Math.floor(Date.now() / 1000);
const tags = args.parentEventId
? buildReplyMessageTags(
args.channelId,
agent.pubkey,
args.parentEventId,
args.parentEventId,
args.mentionPubkeys ?? undefined,
)
: buildTopLevelMessageTags(
args.channelId,
args.mentionPubkeys ?? undefined,
agent.pubkey,
);
for (const clientMarker of [marker, ...(args.additionalMarkers ?? [])]) {
if (clientMarker?.trim()) tags.push(["client", clientMarker.trim()]);
}
const event = createMockEvent(
9,
args.content.trim(),
tags,
agent.pubkey,
createdAt,
);
recordMockMessage(args.channelId, event);
emitMockLiveEvent(args.channelId, event);
return {
event_id: event.id,
parent_event_id: args.parentEventId ?? null,
root_event_id: args.parentEventId ?? null,
depth: args.parentEventId ? 1 : 0,
created_at: createdAt,
};
}
/**
* Mock the `delete_message` Tauri command. Removes the event from the
* in-memory mock store so the query-cache invalidation in
* `useDeleteMessageMutation.onSuccess` (which filters by eventId) finds
* nothing to keep, and the row disappears from the timeline.
*/
function handleDeleteMessage(args: {
channelId: string;
eventId: string;
}): void {
const history = mockMessages.get(args.channelId);
if (history) {
const index = history.findIndex((ev) => ev.id === args.eventId);
if (index !== -1) history.splice(index, 1);
}
}
/**
* Mock the `edit_message` Tauri command. Mirrors the real Rust command
* (`build_message_edit`): emit a kind:40003 edit event carrying `["e", target]`
* plus the new content, media (imeta) tags, and NIP-30 emoji tags. The timeline
* (`formatTimelineMessages`) scans for these edit events and overlays the new
* content + media/emoji tags onto the original via `applyEditTagOverlay`, so
* recording + emitting the edit event is all the bridge needs to do — the same
* path the real relay drives. `null`/empty tag args → no extra tags.
*/
async function handleEditMessage(
args: {
channelId: string;
eventId: string;
content: string;
mediaTags?: string[][] | null;
emojiTags?: string[][] | null;
},
config: E2eConfig | undefined,
): Promise<void> {
const mediaTags = args.mediaTags ?? [];
const emojiTags = args.emojiTags ?? [];
const extraTags = [...mediaTags, ...emojiTags];
const tags = [["h", args.channelId], ["e", args.eventId], ...extraTags];
const content = args.content.trim();
const identity = getIdentity(config);
if (!identity) {
const editEvent = createMockEvent(
KIND_STREAM_MESSAGE_EDIT,
content,
tags,
getMockMemberPubkey(config),
);
recordMockMessage(args.channelId, editEvent);
emitMockLiveEvent(args.channelId, editEvent);
return;
}
await submitSignedEvent(config, {
kind: KIND_STREAM_MESSAGE_EDIT,
content,
tags,
});
}
/** Locate the channel a stored mock event lives in (reactions carry no channel arg). */
function findMockEventChannel(eventId: string): string | undefined {
for (const [channelId, events] of mockMessages) {
if (events.some((event) => event.id === eventId)) {
return channelId;
}
}
return undefined;
}
/**
* Mock the `add_reaction` Tauri command. Mirrors the real Rust command: a
* kind:7 whose content is the emoji, plus — for a custom emoji — the NIP-30
* `["emoji", shortcode, url]` tag (shortcode normalized to match the relay).
* Recorded into the target's channel store and emitted live so the timeline's
* reaction aggregation renders the pill (the channel subscription includes
* kind:7). Unicode reactions carry no emoji tag, like the real command.
*/
async function handleAddReaction(
args: { eventId: string; emoji: string; emojiUrl?: string | null },
config: E2eConfig | undefined,
): Promise<void> {
const channelId = findMockEventChannel(args.eventId);
if (!channelId) {
throw new Error(`mock add_reaction: unknown target event ${args.eventId}`);
}
const emoji = args.emoji.trim();
// Real add_reaction events carry only the target `e` tag. Channel live
// subscriptions already know which channel matched and restore that context
// before merging the event into the timeline cache.
const tags: string[][] = [["e", args.eventId]];
if (args.emojiUrl) {
const shortcode = emoji.replace(/^:+/, "").replace(/:+$/, "").toLowerCase();
tags.push(["emoji", shortcode, args.emojiUrl]);
}
const event = createMockEvent(
KIND_REACTION,
emoji,
tags,
getMockMemberPubkey(config),
Math.floor(Date.now() / 1000),
// 64-hex id so the kind:5 deletion emitted by remove_reaction is accepted
// by the timeline (getDeletionTargets requires a 64-hex `e` tag).
mockEventId(),
);
recordMockMessage(channelId, event);
emitMockLiveEvent(channelId, event);
}
/**
* Mock the `remove_reaction` Tauri command. Finds the active member's own
* kind:7 for this target+emoji, removes it from the store, and emits a kind:5
* deletion so the timeline drops the reaction (the real command deletes via a
* kind:5 too).
*/
async function handleRemoveReaction(
args: { eventId: string; emoji: string },
config: E2eConfig | undefined,
): Promise<void> {
const channelId = findMockEventChannel(args.eventId);
if (!channelId) {
return;
}
const myPubkey = getMockMemberPubkey(config).toLowerCase();
const emoji = args.emoji.trim();
const store = getMockMessageStore(channelId);
const reaction = store.find(
(event) =>
event.kind === KIND_REACTION &&
event.pubkey.toLowerCase() === myPubkey &&
event.content.trim() === emoji &&
event.tags.some((t) => t[0] === "e" && t[1] === args.eventId),
);
if (!reaction) {
return;
}
const index = store.indexOf(reaction);
store.splice(index, 1);
const deletion = createMockEvent(
KIND_DELETION,
"",
[["e", reaction.id]],
getMockMemberPubkey(config),
);
recordMockMessage(channelId, deletion);
emitMockLiveEvent(channelId, deletion);
}
async function handleGetEvent(
args: {
eventId: string;
},
config: E2eConfig | undefined,
) {
// Defer/release seam: when __BUZZ_E2E_DEFER_GET_EVENT__ is set to this
// event's ID, hold this call in the queue until __BUZZ_E2E_RELEASE_GET_EVENT__()
// is called. Only the target ID is deferred; all other IDs resolve normally.
// This keeps ancestor-lookup and context loads from being stalled or counted.
if (
window.__BUZZ_E2E_DEFER_GET_EVENT__ &&
window.__BUZZ_E2E_DEFER_GET_EVENT__ === args.eventId
) {
// Increment the count only for calls that are actually deferred.
window.__BUZZ_E2E_GET_EVENT_CALL_COUNT__ =
(window.__BUZZ_E2E_GET_EVENT_CALL_COUNT__ ?? 0) + 1;
return new Promise<string>((resolve, reject) => {
deferredGetEventQueue.push({
resolve,
reject,
run: () => resolveGetEvent(args, config),
});
});
}
return resolveGetEvent(args, config);
}
async function resolveGetEvent(
args: {
eventId: string;
},
config: E2eConfig | undefined,
) {
const identity = getIdentity(config);
if (!identity) {
// Allow test specs to mark specific event IDs as definitively deleted.
if (config?.mock?.deletedEventIds?.includes(args.eventId)) {
throw new Error("event not found");
}
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: [["h", "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);
}
// Query single event by ID via POST /query
const events = await relayQuery(config, [{ ids: [args.eventId], limit: 1 }]);
if (events.length === 0) {
throw new Error(`Event not found: ${args.eventId}`);
}
return JSON.stringify(events[0]);
}
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<number>((resolve) => {
ws.addEventListener("open", () => resolve(wsId), { once: true });
ws.addEventListener("error", () => resolve(wsId), { once: true });
});
}
async function connectMockSocket(args: { onMessage: unknown }) {
const connectError = getConfig()?.mock?.websocketConnectErrors?.shift();
if (connectError) {
throw new Error(connectError);
}
if (mockWebsocketSendMutexWedged) {
return new Promise<number>(() => {});
}
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 (
getConfig()?.mock?.stallWebsocketSends &&
args.message?.type !== "Close"
) {
mockWebsocketSendMutexWedged = true;
return new Promise<void>(() => {});
}
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 filters = rest.slice(1) as MockFilter[];
if (
!filters.every((filter) =>
isPGatedFilterAuthorized(filter, MOCK_IDENTITY_PUBKEY),
)
) {
sendWsText(socket.handler, ["CLOSED", subId, P_GATED_REJECTION_MESSAGE]);
return;
}
if (subId.startsWith("live-")) {
// Collect channel IDs from all filters in the REQ
const channelIds = new Set<string>();
const kinds = new Set<number>();
const ownerPubkeys = new Set<string>();
for (const f of filters) {
const cid = f["#h"]?.[0];
if (cid) channelIds.add(cid);
for (const kind of f.kinds ?? []) {
kinds.add(kind);
}
for (const p of f["#p"] ?? []) {
ownerPubkeys.add(p);
}
}
const onlyChannelId =
channelIds.size === 1
? (channelIds.values().next().value as string)
: undefined;
if (
getConfig()?.mock?.closeChannelLiveSubscriptionOnce &&
!mockClosedChannelLiveSubscription &&
onlyChannelId &&
kinds.has(KIND_CHANNEL_THREAD_SUMMARY)
) {
mockClosedChannelLiveSubscription = true;
sendWsText(socket.handler, ["CLOSED", subId, "rate-limited"]);
return;
}
socket.subscriptions.set(subId, {
channelId: onlyChannelId ?? GLOBAL_MOCK_SUBSCRIPTION,
kinds: kinds.size > 0 ? [...kinds] : null,
ownerPubkeys: [...ownerPubkeys],
});
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
const filter = rest[1] as MockFilter;
if (filter.kinds?.includes(13534)) {
sendWsText(socket.handler, [
"EVENT",
subId,
createMockRelayMembershipEvent(),
]);
const eoseDelayMs = getConfig()?.mock?.relayMembershipEoseDelayMs ?? 0;
if (eoseDelayMs > 0) {
window.setTimeout(
() => sendWsText(socket.handler, ["EOSE", subId]),
eoseDelayMs,
);
} else {
sendWsText(socket.handler, ["EOSE", subId]);
}
return;
}
if (filter.kinds?.includes(KIND_EMOJI_SET)) {
// Honor `authors` so `fetchOwnEmoji` (authors:[me]) sees only the
// caller's set, while the union fetch (no authors) sees every member's —
// matching the real relay and the own-vs-community split in the UI.
const authors = filter.authors?.map((a) => a.toLowerCase());
for (const emojiEvent of createMockCustomEmojiSetEvents()) {
if (authors && !authors.includes(emojiEvent.pubkey.toLowerCase())) {
continue;
}
sendWsText(socket.handler, ["EVENT", subId, emojiEvent]);
}
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
if (filter.kinds?.includes(KIND_USER_STATUS)) {
for (const statusEvent of filterMockUserStatuses(filter)) {
sendWsText(socket.handler, ["EVENT", subId, statusEvent]);
}
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
if (filter.kinds?.includes(KIND_EVENT_REMINDER)) {
const authors = filter.authors?.map((a) => a.toLowerCase());
for (const event of mockReminderEvents) {
if (authors && !authors.includes(event.pubkey.toLowerCase())) continue;
sendWsText(socket.handler, ["EVENT", subId, event]);
}
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
if (filter.kinds?.includes(KIND_PERSONA)) {
const authors = filter.authors?.map((author) => author.toLowerCase());
const sourceIds = filter["#d"];
for (const event of mockPersonaEvents) {
if (authors && !authors.includes(event.pubkey.toLowerCase())) continue;
if (
event.pubkey.toLowerCase() !== MOCK_IDENTITY_PUBKEY.toLowerCase() &&
!personaHasExactSharedTag(event)
) {
continue;
}
const sourceId = event.tags.find((tag) => tag[0] === "d")?.[1];
if (sourceIds && (!sourceId || !sourceIds.includes(sourceId))) continue;
sendWsText(socket.handler, ["EVENT", subId, event]);
}
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
// Project queries: NIP-34 kinds, or kind:1 comments scoped by repo `a`
// tag (PR/issue discussions, approvals, review requests).
if (
filter.kinds?.some((kind) => MOCK_PROJECT_KINDS.has(kind)) ||
(filter.kinds?.includes(1) && filter["#a"])
) {
window.__BUZZ_E2E_PROJECT_QUERY_FILTERS__ ??= [];
window.__BUZZ_E2E_PROJECT_QUERY_FILTERS__.push(filter);
const rejectedKinds =
window.__BUZZ_E2E_REJECT_PROJECT_QUERY_KINDS__ ?? [];
if (filter.kinds?.some((kind) => rejectedKinds.includes(kind))) {
sendWsText(socket.handler, [
"CLOSED",
subId,
"mock project query failure",
]);
return;
}
for (const event of filterMockProjectEvents(filter)) {
sendWsText(socket.handler, ["EVENT", subId, event]);
}
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
const channelId = filter["#h"]?.[0];
if (!channelId) {
// Aux-backfill filters (reactions/deletions) are `#e`-keyed with no
// channel tag — serve them across all channel stores like the relay.
const referencedIds = filter["#e"];
if (referencedIds && referencedIds.length > 0) {
const targets = new Set(referencedIds);
for (const events of mockMessages.values()) {
for (const event of events) {
if (filter.kinds && !filter.kinds.includes(event.kind)) {
continue;
}
if (
event.tags.some(
(tag) => tag[0] === "e" && tag[1] && targets.has(tag[1]),
)
) {
sendWsText(socket.handler, ["EVENT", subId, event]);
}
}
}
}
sendWsText(socket.handler, ["EOSE", subId]);
return;
}
emitMockHistory(socket, subId, channelId, filter);
return;
}
if (type === "CLOSE") {
const subId = rest[0] as string;
socket.subscriptions.delete(subId);
return;
}
if (type === "EVENT") {
const event = rest[0] as RelayEvent;
if ([9030, 9031, 9032].includes(event.kind)) {
const accepted = updateMockRelayMembershipFromAdminEvent(event);
sendWsText(socket.handler, [
"OK",
event.id,
accepted,
accepted ? "" : "Invalid relay admin event.",
]);
return;
}
if (event.kind === 9033) {
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
if (event.kind === 30078) {
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
if (event.kind === KIND_EVENT_REMINDER) {
// Upsert by d-tag (replaceable event)
const dTag = event.tags.find((t) => t[0] === "d")?.[1];
if (dTag) {
const idx = mockReminderEvents.findIndex(
(e) =>
e.pubkey.toLowerCase() === event.pubkey.toLowerCase() &&
e.tags.some((t) => t[0] === "d" && t[1] === dTag),
);
if (idx >= 0) mockReminderEvents.splice(idx, 1);
}
mockReminderEvents.push(event);
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
if (event.kind === KIND_PERSONA) {
const sourceId = event.tags.find((tag) => tag[0] === "d")?.[1];
if (!sourceId) {
sendWsText(socket.handler, [
"OK",
event.id,
false,
"invalid: persona event missing d tag.",
]);
return;
}
const sharedTags = event.tags.filter((tag) => tag[0] === "shared");
if (
sharedTags.length > 1 ||
(sharedTags.length === 1 && !personaHasExactSharedTag(event))
) {
sendWsText(socket.handler, [
"OK",
event.id,
false,
'invalid: shared tag must be exactly ["shared","true"].',
]);
return;
}
upsertMockPersonaRelayEvent(event);
emitMockGlobalEvent(event);
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
if (event.kind === 20001) {
const status = event.content;
if (status === "online" || status === "away" || status === "offline") {
setMockPresenceStatus(event.pubkey, status);
}
emitMockGlobalEvent(event);
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
if (event.kind === KIND_USER_STATUS) {
const hasGeneralDTag = event.tags.some(
(tag) => tag[0] === "d" && tag[1] === "general",
);
if (!hasGeneralDTag) {
sendWsText(socket.handler, [
"OK",
event.id,
false,
"invalid: user status missing d tag.",
]);
return;
}
recordMockUserStatus(event);
emitMockGlobalEvent(event);
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
if (isMockProjectScopedEvent(event)) {
if (event.pubkey !== DEFAULT_MOCK_IDENTITY.pubkey) {
sendWsText(socket.handler, [
"OK",
event.id,
false,
"invalid: event pubkey does not match authenticated identity",
]);
return;
}
const rejectionIndex =
window.__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__?.indexOf(event.kind) ??
-1;
if (rejectionIndex >= 0) {
window.__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__?.splice(
rejectionIndex,
1,
);
sendWsText(socket.handler, [
"OK",
event.id,
false,
"mock project event rejection",
]);
return;
}
getMockProjectEventStore().push(event);
sendWsText(socket.handler, ["OK", event.id, true, ""]);
return;
}
const channelId = getChannelIdFromTags(event.tags);
if (!channelId) {
sendWsText(socket.handler, [
"OK",
event.id,
false,
"Missing channel tag.",
]);
return;
}
const sendMessageError =
event.kind === 9 ? getConfig()?.mock?.sendMessageErrors?.shift() : null;
if (sendMessageError) {
sendWsText(socket.handler, ["OK", event.id, false, sendMessageError]);
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;
}
mockClosedChannelLiveSubscription = false;
mockGlobalAgentConfig = config.mock?.globalAgentConfig
? { ...config.mock.globalAgentConfig }
: null;
resetMockRelayMembers(config);
resetMockRelayAgents(config);
resetMockManagedAgents(config);
resetMockPersonas(config);
resetMockTeams(config);
seedMockSearchProfiles(config);
resetMockWorkflows();
resetMockMesh();
resetMockUserStatuses();
resetMockPersonaCatalogEvents(config);
resetMockSaveSubscriptions(config);
resetMockPendingCommunityDeepLinks(config);
mockWebsocketSendMutexWedged = false;
mockWindows("main");
window.__BUZZ_E2E_COMMANDS__ = [];
window.__BUZZ_E2E_COMMAND_PAYLOADS__ = [];
window.__BUZZ_E2E_COMMAND_LOG__ = [];
window.__BUZZ_E2E_SIGNED_EVENTS__ = [];
window.__BUZZ_E2E_WEBVIEW_ZOOM__ = 1;
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ = ({
channelName,
content,
parentEventId,
pubkey,
kind,
mentionPubkeys,
extraTags,
createdAt,
id,
}) => {
const channel = mockChannels.find(
(candidate) => candidate.name === channelName,
);
if (!channel) {
throw new Error(`Mock channel ${channelName} not found.`);
}
return emitMockChannelMessage(
channel.id,
content,
parentEventId,
pubkey,
kind,
mentionPubkeys,
extraTags,
createdAt,
id,
);
};
window.__BUZZ_E2E_PREPEND_MOCK_HISTORY__ = prependMockHistory;
window.__BUZZ_E2E_EMIT_MOCK_TYPING__ = ({
channelName,
createdAt,
pubkey,
threadHeadId,
}) => {
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,
threadHeadId,
createdAt,
);
};
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__ = ({ channelName, kind }) => {
const channel = mockChannels.find(
(candidate) => candidate.name === channelName,
);
if (!channel) {
throw new Error(`Mock channel ${channelName} not found.`);
}
return hasMockLiveSubscription(channel.id, kind);
};
window.__BUZZ_E2E_HAS_MOCK_OWNER_KIND_SUBSCRIPTION__ = ({
ownerPubkey,
kind,
}) => hasMockOwnerKindSubscription(ownerPubkey, kind);
window.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ = (item) => {
const category = item.category === "mention" ? "mentions" : item.category;
mockFeedOverrides[category].unshift(item);
window.dispatchEvent(new CustomEvent("buzz:e2e-home-feed-updated"));
return item;
};
window.__BUZZ_E2E_REPLACE_MOCK_FEED_ITEM__ = (oldId, item) => {
const category = item.category === "mention" ? "mentions" : item.category;
// Remove the old item from every category bucket (it may have been in a
// different bucket or the same one).
for (const bucket of Object.values(mockFeedOverrides)) {
const idx = (bucket as RawFeedItem[]).findIndex((r) => r.id === oldId);
if (idx !== -1) {
(bucket as RawFeedItem[]).splice(idx, 1);
break;
}
}
// Insert the replacement at the front of the correct bucket.
mockFeedOverrides[category].unshift(item);
window.dispatchEvent(new CustomEvent("buzz:e2e-home-feed-updated"));
return item;
};
window.__BUZZ_E2E_MD_PARSE_COUNT__ = getMarkdownParseCount;
window.__BUZZ_E2E_ACTIVATE_TIMEOUT__ = (expiresAtMs: number) => {
const expiresAtSec = expiresAtMs > 0 ? Math.floor(expiresAtMs / 1000) : 0;
const msg =
expiresAtSec > 0
? `restricted: you are timed out until ${expiresAtSec}`
: "restricted: you are timed out until 0";
recordTimeoutFromRejection(msg);
};
window.__BUZZ_E2E_INVALIDATE_CHANNELS__ = async () => {
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
};
window.__BUZZ_E2E_MUTATE_CHANNEL__ = ({
channelId,
channelType,
removeMemberPubkey,
}) => {
const channel = mockChannels.find((ch) => ch.id === channelId);
if (!channel) return;
if (channelType !== undefined) {
channel.channel_type = channelType;
}
if (removeMemberPubkey !== undefined) {
channel.members = channel.members.filter(
(m) => m.pubkey !== removeMemberPubkey,
);
syncMockChannel(channel);
}
touchMockChannel(channel);
};
// get_event defer/release seam — reset counter and queue on each install.
window.__BUZZ_E2E_GET_EVENT_CALL_COUNT__ = 0;
window.__BUZZ_E2E_DEFER_GET_EVENT__ = null;
deferredGetEventQueue = [];
window.__BUZZ_E2E_RELEASE_GET_EVENT__ = () => {
const queued = deferredGetEventQueue.splice(0);
for (const entry of queued) {
entry.run().then(entry.resolve, entry.reject);
}
// Disable deferral and reset counter after release so the seam is inert
// for the remainder of the test (no stray defers from context loads).
window.__BUZZ_E2E_DEFER_GET_EVENT__ = null;
window.__BUZZ_E2E_GET_EVENT_CALL_COUNT__ = 0;
return queued.length;
};
window.__BUZZ_E2E_EMIT_MOCK_READ_STATE__ = ({
clientId,
contexts,
createdAt,
slotId,
}) => {
const blob = JSON.stringify({
v: 1,
client_id: clientId,
contexts,
});
const event = createMockEvent(
30078,
blob,
[
["d", `read-state:${slotId}`],
["t", "read-state"],
],
getMockMemberPubkey(config),
createdAt,
);
emitMockLiveEvent(GLOBAL_MOCK_SUBSCRIPTION, event);
return event;
};
window.__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__ = (state) => {
// Directly emit a connection state change on the relay client singleton,
// for tests that need to drive degraded relay UI without waiting for the
// real auth-timeout + reconnect-debounce cycle (~10 s). Reaches the
// TS-private emitter via a cast so the production class carries no
// test-only seam.
(
relayClient as unknown as {
connectionStateEmitter: { set: (s: ConnectionState) => void };
}
).connectionStateEmitter.set(state);
};
window.__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__ = () =>
relayClient.getConnectionState();
window.__BUZZ_E2E_SEED_MOCK_REMINDERS__ = (reminders) => {
mockReminderEvents.length = 0;
for (const r of reminders) {
mockReminderEvents.push(r);
}
};
window.__BUZZ_E2E_SET_STALL_WEBSOCKET_SENDS__ = (stall) => {
const config = getConfig();
if (!config?.mock) return;
config.mock.stallWebsocketSends = stall;
if (!stall) mockWebsocketSendMutexWedged = false;
};
window.__BUZZ_E2E_DISCONNECT_MOCK_WEBSOCKETS__ = () => {
const socketIds = [...mockSockets.keys()];
for (const socketId of socketIds) disconnectMockSocket(socketId);
return socketIds.length;
};
window.__BUZZ_E2E_RESTART_MOCK_WEBSOCKETS__ = () => {
const sockets = [...mockSockets.values()];
mockSockets.clear();
for (const socket of sockets) {
sendWsClose(socket.handler, 1012, "relay restarting");
}
return sockets.length;
};
// Tests vary mesh admission and models to exercise provider discovery and
// the managed-agent start preflight.
window.__BUZZ_E2E_SET_MESH__ = (mesh) => {
if (mesh.admitted !== undefined) mockMeshState.admitted = mesh.admitted;
if (mesh.models !== undefined) mockMeshState.models = mesh.models;
if (mesh.denyReason !== undefined)
mockMeshState.denyReason = mesh.denyReason;
if (mesh.nodeState !== undefined) mockMeshState.nodeState = mesh.nodeState;
if (mesh.nodeMode !== undefined) mockMeshState.nodeMode = mesh.nodeMode;
if (mesh.servingUsage !== undefined)
mockMeshState.servingUsage = {
...mockMeshState.servingUsage,
...mesh.servingUsage,
};
};
let seedTurnSeq = Date.now();
window.__BUZZ_E2E_SEED_ACTIVE_TURNS__ = ({
agentPubkey,
channelId,
turnId,
kind = "turn_started",
}) => {
seedTurnSeq += 1;
const event = {
seq: seedTurnSeq,
timestamp: new Date().toISOString(),
kind,
agentIndex: 0,
channelId,
sessionId: null,
turnId,
payload: null,
};
syncAgentTurnsFromEvents(agentPubkey, [event]);
syncAgentObserverEvents(agentPubkey, [event]);
};
window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__ = ({ agentPubkey, events }) => {
injectObserverEventsForE2E(agentPubkey, events);
};
const meshNodeStatus = (
state: "off" | "running",
mode: "serve" | "client" | null,
) => ({
state,
mode,
health: { status: "ok" as const, reason: null },
apiBaseUrl: state === "running" ? "http://127.0.0.1:9337/v1" : null,
consoleUrl: null,
modelId: mockMeshState.models[0]?.id ?? null,
modelName: mockMeshState.models[0]?.name ?? null,
inviteToken: state === "running" ? "mock-endpoint-addr" : null,
endpointId: state === "running" ? "mock-endpoint-id" : null,
deviceId: state === "running" ? "mock-endpoint-id" : null,
deviceName: state === "running" ? "Mock desktop" : null,
});
const handleMockCommand = async (command: string, payload: unknown) => {
const activeConfig = getConfig();
const identity = getActiveIdentity(activeConfig);
window.__BUZZ_E2E_COMMANDS__?.push(command);
const loggedPayload = (() => {
try {
return JSON.parse(JSON.stringify(payload ?? null));
} catch {
return null;
}
})();
window.__BUZZ_E2E_COMMAND_PAYLOADS__?.push({
command,
payload: loggedPayload,
});
window.__BUZZ_E2E_COMMAND_LOG__?.push({ command, payload });
switch (command) {
case "get_tts_settings":
return (
activeConfig?.mock?.ttsSettings ?? {
version: 1,
agentTextToSpeech: true,
voicePreferences: ["pocket:mary"],
}
);
case "list_voice_registry":
return [
[
"anna",
"Anna",
"anna.wav",
"p228_023_enhanced.wav",
"0a6de25cf12bf1540beb85979f306a92be81fecc051c547c5395e7e5237a3856",
],
[
"vera",
"Vera",
"vera.wav",
"p229_023_enhanced.wav",
"309cf91a895830f15842b398f69a4962cb1f7e0bfab10e25dd27838e826c204b",
],
[
"fantine",
"Fantine",
"fantine.wav",
"p244_023_enhanced.wav",
"5f07d4e2a3f20a15572aae885156b43ef3fc12ef3812996fd135680d9956448b",
],
[
"charles",
"Charles",
"charles.wav",
"p254_023_enhanced.wav",
"6b681a429198f16e378d53bccb08d06939da7b00144a7696111d4f8f76be7756",
],
[
"paul",
"Paul",
"paul.wav",
"p259_023_enhanced.wav",
"7aba504fe0b3b16478b69ed27ce6007e3cb42b0c1915b5f1c6a6024ae37d679b",
],
[
"eponine",
"Eponine",
"eponine.wav",
"p262_023_enhanced.wav",
"a13c27fb47627b05223691a0ef2974358a18c886e6c2f9d2762ff1d02c20926b",
],
[
"azelma",
"Azelma",
"azelma.wav",
"p303_023_enhanced.wav",
"60e3d26cdf2efdec5df712152c839928f4d5522821e6554ae11fd96c57ab1026",
],
[
"george",
"George",
"george.wav",
"p315_023_enhanced.wav",
"29a41f93bf5236e5b21501091d7774c255d5f3d4e62fa4f9fdf0a92a793c84ae",
],
[
"mary",
"Mary",
"reference_sample.wav",
"p333_023_enhanced.wav",
"a35b0468382218e9f37a9a7494d1e4b74deaf18d7ced22265b4e325bb55c183f",
],
[
"jane",
"Jane",
"jane.wav",
"p339_023_enhanced.wav",
"2f12e7f155eb3118f55425394f1b049e5b1b67bdc9b3932c8ba4521420aeb84a",
],
[
"michael",
"Michael",
"michael.wav",
"p360_023_enhanced.wav",
"b6743e9195e5e3fd34fe9d1633ae93f7ffab787b249e45f6467d7d6f7a6ee6ad",
],
[
"eve",
"Eve",
"eve.wav",
"p361_023_enhanced.wav",
"396e7cbd066b0f3fb6d67fa26e7904076958239d736d4390f15b5fe88feb14cd",
],
].map(
([id, displayName, referenceFile, upstreamFile, contentHash]) => ({
key: `pocket:${id}`,
displayName,
backend: "pocket",
backendName: "Pocket TTS",
availability: "bundled",
fallbackKey: id === "mary" ? null : "pocket:mary",
referenceFile,
provenance: {
source: "bundled",
contentHash,
license: "CC-BY-4.0",
sourceUrl: `https://huggingface.co/kyutai/tts-voices/blob/323332d33f997de8394f24a193e1a76df720e01a/vctk/${upstreamFile}`,
},
}),
);
case "set_tts_enabled": {
const enabled = (payload as { enabled?: boolean })?.enabled;
if (typeof enabled !== "boolean")
throw new Error("Missing text-to-speech enabled state");
const settings = {
version: 1,
agentTextToSpeech: enabled,
voicePreferences: activeConfig?.mock?.ttsSettings
?.voicePreferences ?? ["pocket:mary"],
};
if (activeConfig) {
activeConfig.mock ??= {};
activeConfig.mock.ttsSettings = settings;
}
return settings;
}
case "set_pocket_voice": {
const voiceKey = (payload as { voiceKey?: string })?.voiceKey;
if (!voiceKey) throw new Error("Missing Pocket voice key");
const current = activeConfig?.mock?.ttsSettings ?? {
version: 1,
agentTextToSpeech: true,
voicePreferences: ["pocket:mary"],
};
const firstPocketIndex = current.voicePreferences.findIndex((key) =>
key.startsWith("pocket:"),
);
const preferences = current.voicePreferences.filter(
(key) => !key.startsWith("pocket:"),
);
preferences.splice(
firstPocketIndex < 0 ? preferences.length : firstPocketIndex,
0,
voiceKey,
);
const settings = { ...current, voicePreferences: preferences };
if (activeConfig) {
activeConfig.mock ??= {};
activeConfig.mock.ttsSettings = settings;
}
return settings;
}
case "preview_pocket_voice":
return null;
case "get_tts_playback_speed":
return mockTtsPlaybackSpeed;
case "set_tts_playback_speed":
mockTtsPlaybackSpeed = (payload as { speed: number }).speed;
return null;
case "get_builderlab_auth":
return activeConfig?.mock?.builderlabAuth ?? null;
case "start_builderlab_login": {
const delayMs = activeConfig?.mock?.builderlabLoginDelayMs ?? 0;
if (delayMs > 0)
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
const nextAuth = activeConfig?.mock?.builderlabAuth ?? {
email: "owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
};
if (activeConfig?.mock) activeConfig.mock.builderlabAuth = nextAuth;
return nextAuth;
}
case "cancel_builderlab_login":
return null;
case "clear_builderlab_auth":
if (activeConfig?.mock) activeConfig.mock.builderlabAuth = null;
return null;
case "get_builderlab_nostr_identity":
return activeConfig?.mock?.builderlabIdentity
? { identity: activeConfig.mock.builderlabIdentity }
: { error: { code: "missing_mapping", setup_needed: true } };
case "bind_builderlab_nostr_identity": {
if (activeConfig?.mock?.builderlabBindError)
return { error: activeConfig.mock.builderlabBindError };
const activeIdentity = identity ?? DEFAULT_MOCK_IDENTITY;
const nextIdentity = {
pubkey_hex: activeIdentity.pubkey,
npub: `npub1${activeIdentity.pubkey}`,
};
if (activeConfig?.mock)
activeConfig.mock.builderlabIdentity = nextIdentity;
return { identity: nextIdentity };
}
case "delete_builderlab_nostr_identity":
if (activeConfig?.mock) activeConfig.mock.builderlabIdentity = null;
return {};
case "list_builderlab_communities":
return {
communities: activeConfig?.mock?.builderlabCommunities ?? [],
};
case "check_builderlab_community_name":
return {
available: true,
normalized_host: `${(payload as { name?: string })?.name ?? "community"}.communities.buzz.xyz`,
};
case "create_builderlab_community": {
const name = (payload as { name?: string })?.name ?? "community";
return {
community: activeConfig?.mock?.builderlabCreatedCommunity ?? {
id: `hosted-${name}`,
name,
normalized_host: `${name}.communities.buzz.xyz`,
},
};
}
case "mesh_installed_models":
return mockMeshState.models;
case "mesh_node_status":
return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
case "mesh_serving_usage":
return mockMeshState.servingUsage;
case "mesh_start_node": {
const req = (
payload as { request?: { mode?: "serve" | "client" } } | null
)?.request;
mockMeshState.nodeState = "running";
mockMeshState.nodeMode = req?.mode ?? "serve";
return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
}
case "mesh_stop_node":
// Mirror the backend contract: "stop sharing" must never tear down a
// client (consume) node occupying the single slot. Leave it running.
if (mockMeshState.nodeMode === "client") {
return meshNodeStatus(
mockMeshState.nodeState,
mockMeshState.nodeMode,
);
}
mockMeshState.nodeState = "off";
mockMeshState.nodeMode = null;
return meshNodeStatus("off", null);
case "get_identity": {
const isLost =
!mockIdentityLostCleared && activeConfig?.mock?.identityLost === true;
const isLocked =
!mockIdentityLockedCleared &&
activeConfig?.mock?.identityLocked === true;
if (identity) {
return {
pubkey: identity.pubkey,
display_name: identity.username,
lost: false,
locked: false,
};
}
return { ...DEFAULT_MOCK_IDENTITY, lost: isLost, locked: isLocked };
}
case "sign_nostr_identity_binding": {
const request = payload as {
challengeId: string;
expiresAt: string;
nonce: string;
origin: string;
verificationCode: string;
};
const signDelayMs = activeConfig?.mock?.nostrBindSignDelayMs ?? 0;
if (signDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, signDelayMs));
}
const activeIdentity = identity ?? DEFAULT_MOCK_IDENTITY;
return JSON.stringify({
id: "e2e-signed-nostr-binding",
pubkey: activeIdentity.pubkey,
created_at: 0,
kind: 24243,
tags: [
["challenge_id", request.challengeId],
["nonce", request.nonce],
["verification_code", request.verificationCode],
["origin", request.origin],
["expires_at", request.expiresAt],
],
content: "",
sig: "e2e-signed-nostr-binding",
});
}
case "sign_out":
// Production wipes local state and restarts the app. In the browser
// harness there is nothing to wipe; resolving is enough — specs
// assert invocation via __BUZZ_E2E_COMMANDS__ and the pending UI.
return;
case "get_nsec": {
const nsecSequence = activeConfig?.mock?.nsecErrors;
if (nsecSequence && nsecSequence.length > 0) {
const idx = Math.min(nsecCallCount, nsecSequence.length - 1);
nsecCallCount++;
const entry = nsecSequence[idx];
if (entry !== null) {
throw new Error(entry);
}
return "nsec1mock000000000000000000000000000000000000000000000000000000";
}
const nsecError = activeConfig?.mock?.nsecError;
if (nsecError) {
throw new Error(nsecError);
}
return "nsec1mock000000000000000000000000000000000000000000000000000000";
}
case "persist_current_identity": {
// Persist the ephemeral key: clears only the lost flag. The locked flag
// is cleared only by import_identity; production rejects
// persist_current_identity when the identity is in the locked state.
mockIdentityLostCleared = true;
const currentPubkey = identity?.pubkey ?? DEFAULT_MOCK_IDENTITY.pubkey;
const currentDisplayName =
identity?.username ?? DEFAULT_MOCK_IDENTITY.display_name;
return {
pubkey: currentPubkey,
display_name: currentDisplayName,
lost: false,
locked: false,
};
}
case "import_identity":
mockIdentityLostCleared = true;
mockIdentityLockedCleared = true;
return importMockIdentity(
(payload as { nsec?: string } | null)?.nsec ?? "",
);
case "validate_repos_dir":
// The browser harness has no host filesystem to validate. Treat the
// seeded empty/default path as valid so Add Community can continue to
// relay-policy discovery.
return;
case "fetch_join_policy":
return activeConfig?.mock?.joinPolicy ?? null;
case "apply_workspace": {
const applyDelayMs = activeConfig?.mock?.applyCommunityDelayMs ?? 0;
if (applyDelayMs > 0) {
return new Promise((resolve) =>
window.setTimeout(resolve, applyDelayMs),
);
}
return;
}
case "get_profile":
return handleGetProfile(activeConfig);
case "update_profile":
return handleUpdateProfile(
payload as Parameters<typeof handleUpdateProfile>[0],
activeConfig,
);
case "update_profile_at_relay":
return handleUpdateProfile(
{
avatarUrl: (payload as { avatarUrl: string }).avatarUrl,
},
activeConfig,
);
case "get_user_profile":
return handleGetUserProfile(
(payload as Parameters<typeof handleGetUserProfile>[0]) ?? {},
activeConfig,
);
case "get_users_batch":
return handleGetUsersBatch(
payload as Parameters<typeof handleGetUsersBatch>[0],
activeConfig,
);
case "get_user_notes":
return handleGetUserNotes(
payload as Parameters<typeof handleGetUserNotes>[0],
activeConfig,
);
case "get_global_notes":
return handleGetGlobalNotes(
payload as Parameters<typeof handleGetGlobalNotes>[0],
activeConfig,
);
case "get_notes_timeline":
return handleGetNotesTimeline(
payload as Parameters<typeof handleGetNotesTimeline>[0],
);
case "get_note":
return handleGetNote(payload as Parameters<typeof handleGetNote>[0]);
case "get_note_reactions":
return handleGetNoteReactions();
case "get_liked_notes":
return handleGetLikedNotes();
case "search_users":
return handleSearchUsers(
payload as Parameters<typeof handleSearchUsers>[0],
activeConfig,
);
case "get_presence":
return handleGetPresence(
(payload as Parameters<typeof handleGetPresence>[0]) ?? {
pubkeys: [],
},
activeConfig,
);
case "get_os_idle_seconds":
// e2e runs headless with no OS idle API; the presence hook falls back
// to in-app activity tracking.
return null;
case "get_git_identity":
// Matches the "Thomas P" author on a mock snapshot commit so the
// viewer-identity avatar attribution is exercised in e2e.
return { name: "Thomas P", email: "thomasp@example.com" };
case "get_project_repo_snapshot":
return {
latest_commit: {
hash: "0123456789abcdef0123456789abcdef01234567",
short_hash: "0123456",
author_name: "Brain",
author_email: "brain@example.com",
timestamp: Math.floor(Date.now() / 1000) - 600,
subject: "Add Trello board workflow details",
},
commits: [
{
hash: "0123456789abcdef0123456789abcdef01234567",
short_hash: "0123456",
author_name: "Brain",
author_email: "brain@example.com",
timestamp: Math.floor(Date.now() / 1000) - 600,
subject: "Add Trello board workflow details",
},
{
hash: "123456789abcdef0123456789abcdef012345678",
short_hash: "1234567",
author_name: "Thomas P",
author_email: "thomasp@example.com",
timestamp: Math.floor(Date.now() / 1000) - 1_800,
subject: "Point project repository details at active branch",
},
{
hash: "23456789abcdef0123456789abcdef0123456789",
short_hash: "2345678",
author_name: "Brain",
author_email: "brain@example.com",
timestamp: Math.floor(Date.now() / 1000) - 3_600,
subject: "Make project repository-first",
},
{
hash: "3456789abcdef0123456789abcdef0123456789a",
short_hash: "3456789",
author_name: "Git Importer",
author_email: "git-importer@example.com",
timestamp: Math.floor(Date.now() / 1000) - 7_200,
subject: "Merge remote project history into local community",
},
],
contributors: [
{
name: "Brain",
email: "brain@example.com",
commit_count: 8,
last_commit_at: Math.floor(Date.now() / 1000) - 600,
},
{
name: "Thomas P",
email: "thomasp@example.com",
commit_count: 3,
last_commit_at: Math.floor(Date.now() / 1000) - 1_800,
},
{
name: "Git Importer",
email: "git-importer@example.com",
commit_count: 1,
last_commit_at: Math.floor(Date.now() / 1000) - 7_200,
},
],
files: [
{
path: "desktop/src/features/projects/ui/ProjectDetailScreen.tsx",
kind: "blob",
size: 18420,
preview_content:
'export function ProjectDetailScreen() {\n return <CommunityTabs defaultValue="files" />;\n}\n',
},
{
path: "desktop/src/features/projects/ui/ProjectsView.tsx",
kind: "blob",
size: 16412,
preview_content:
"export function ProjectsView() {\n return <ProjectsToolbar />;\n}\n",
},
{
path: "desktop/src/features/projects/hooks.ts",
kind: "blob",
size: 9520,
preview_content:
"export function useProjectRepoSnapshotQuery(project) {\n return useQuery({ queryKey: [project.id, 'repo-snapshot'] });\n}\n",
},
{
path: "crates/buzz-relay/src/api/git/transport.rs",
kind: "blob",
size: 33120,
preview_content:
"// Smart HTTP git transport\n// Handles upload-pack and receive-pack for Buzz git repos.\n",
},
],
};
case "get_project_local_repo_snapshot":
return null;
case "get_project_repo_diff":
return {
additions: 27,
deletions: 4,
commit_body: [
"See the [project guide](https://example.com/project-guide).",
"",
"![Architecture](/buzz.svg)",
"",
"![Demo](https://example.com/project-demo.mp4)",
].join("\n"),
files: [
{
path: "desktop/src/features/projects/ui/ProjectDetailScreen.tsx",
additions: 18,
deletions: 3,
patch: [
"@@ -1,6 +1,8 @@",
' import { Tabs } from "@/shared/ui/tabs";',
"",
"-function CommunityTabs() {",
"+function CommunityTabs({ selectedCommitHash }) {",
'+ const [selectedTab, setSelectedTab] = useState("overview");',
"+",
" return (",
' <Tabs value="overview">',
" <ProjectTabsList />",
].join("\n"),
truncated: false,
},
{
path: "desktop/src/features/projects/hooks.ts",
additions: 9,
deletions: 1,
patch: [
"@@ -10,4 +10,12 @@",
" export function useProjectQuery(projectId) {",
" return useQuery({ queryKey: [projectId] });",
" }",
"+",
"+export function useProjectCommitDiffQuery(project, hash) {",
'+ return useQuery({ queryKey: [project?.id, "commit-diff", hash] });',
"+}",
].join("\n"),
truncated: false,
},
],
};
case "get_project_local_repo_diff":
return null;
case "get_project_repo_sync_status":
return (
window.__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__ ?? {
local_path: null,
local_branch: null,
local_branches: [],
local_head: null,
local_short_head: null,
remote_branch: "main",
remote_head: "0123456789abcdef0123456789abcdef01234567",
remote_short_head: "0123456",
merge_base: "0123456789abcdef0123456789abcdef01234567",
ahead_count: 0,
behind_count: 0,
has_uncommitted_changes: false,
has_untracked_files: false,
can_push: false,
push_block_reason: "No local checkout found.",
can_pull: false,
pull_block_reason: "No local checkout found.",
}
);
case "list_project_local_repositories":
return [];
case "push_project_local_repository": {
const input = payload as { branchName?: string | null };
const status = window.__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__;
const branch = input.branchName ?? status?.remote_branch ?? "main";
const commit =
status?.local_head ?? "0123456789abcdef0123456789abcdef01234567";
if (status) {
status.remote_branch = branch;
status.remote_head = commit;
status.remote_short_head = commit.slice(0, 7);
status.ahead_count = 0;
status.can_push = false;
status.push_block_reason = "Local branch is already pushed.";
}
return {
pushed: true,
message: `Pushed ${branch} to remote.`,
branch,
commit,
merge_base:
status?.merge_base ?? "0123456789abcdef0123456789abcdef01234567",
};
}
case "pull_project_local_repository":
return {
pulled: true,
message: "Pulled main from remote.",
};
case "clone_project_repository": {
const path = "/tmp/buzz/REPOS/mock-project";
const commit = "0123456789abcdef0123456789abcdef01234567";
window.__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__ = {
local_path: path,
local_branch: "main",
local_branches: ["main"],
local_head: commit,
local_short_head: commit.slice(0, 7),
remote_branch: "main",
remote_head: commit,
remote_short_head: commit.slice(0, 7),
merge_base: commit,
ahead_count: 0,
behind_count: 0,
has_uncommitted_changes: false,
has_untracked_files: false,
can_push: false,
push_block_reason: "Local branch is already pushed.",
can_pull: false,
pull_block_reason: "Local branch is up to date.",
};
return {
path,
cloned: true,
message: "Cloned repository.",
};
}
case "create_project_remote_branch": {
const input = payload as {
cloneUrl: string;
expectedCommit: string;
newBranch: string;
sourceBranch: string;
};
const dtag = new URL(input.cloneUrl).pathname
.split("/")
.filter(Boolean)
.at(-1)
?.replace(/\.git$/, "");
const repoState = getMockProjectEventStore().find(
(event) =>
event.kind === KIND_REPO_STATE &&
event.tags.some((tag) => tag[0] === "d" && tag[1] === dtag),
);
if (repoState) {
repoState.tags = repoState.tags.filter(
(tag) => tag[0] !== `refs/heads/${input.newBranch}`,
);
repoState.tags.push([
`refs/heads/${input.newBranch}`,
input.expectedCommit,
]);
repoState.created_at = Math.floor(Date.now() / 1000);
}
if (dtag) {
writeMockProjectBranch(dtag, input.newBranch, input.expectedCommit);
}
return {
branch: input.newBranch,
commit: input.expectedCommit,
message: `Created branch ${input.newBranch} from ${input.sourceBranch}.`,
};
}
case "delete_project_remote_branch": {
const input = payload as {
branch: string;
cloneUrl: string;
expectedCommit: string;
};
const dtag = new URL(input.cloneUrl).pathname
.split("/")
.filter(Boolean)
.at(-1)
?.replace(/\.git$/, "");
const repoState = getMockProjectEventStore().find(
(event) =>
event.kind === KIND_REPO_STATE &&
event.tags.some((tag) => tag[0] === "d" && tag[1] === dtag),
);
if (repoState) {
repoState.tags = repoState.tags.filter(
(tag) => tag[0] !== `refs/heads/${input.branch}`,
);
repoState.created_at = Math.floor(Date.now() / 1000);
}
if (dtag) {
writeMockProjectBranch(dtag, input.branch, null);
}
return {
branch: input.branch,
commit: input.expectedCommit,
message: `Deleted branch ${input.branch}.`,
};
}
case "sign_project_pull_request_status": {
const { input } = payload as {
input: {
createdAt: number;
pullRequestAuthor: string;
pullRequestId: string;
repoAddress: string;
status: "open" | "draft" | "closed";
targetOwner: string;
};
};
const kind = {
open: KIND_GIT_STATUS_OPEN,
draft: KIND_GIT_STATUS_DRAFT,
closed: KIND_GIT_STATUS_CLOSED,
}[input.status];
const recipientPubkeys = Array.from(
new Set(
[input.targetOwner, input.pullRequestAuthor].map((pubkey) =>
pubkey.trim().toLowerCase(),
),
),
);
const event = createMockEvent(
kind,
"",
[
["e", input.pullRequestId, "", "root"],
["a", input.repoAddress],
...recipientPubkeys.map((pubkey) => ["p", pubkey]),
],
input.targetOwner,
input.createdAt,
);
window.__BUZZ_E2E_SIGNED_EVENTS__?.push(event);
getMockProjectEventStore().push(event);
return null;
}
case "sign_project_pull_request_review_request": {
const { input } = payload as {
input: {
pullRequestId: string;
repoAddress: string;
reviewerLabel: string;
reviewers: string[];
targetOwner: string;
};
};
const event = createMockEvent(
KIND_TEXT_NOTE,
`Requested a review from ${input.reviewerLabel}`,
[
["e", input.pullRequestId, "", "root"],
["a", input.repoAddress],
...input.reviewers.map((reviewer) => ["p", reviewer]),
["t", "review-request"],
],
input.targetOwner,
);
window.__BUZZ_E2E_SIGNED_EVENTS__?.push({
content: event.content,
kind: event.kind,
tags: event.tags,
});
getMockProjectEventStore().push(event);
return null;
}
case "publish_project_pull_request_merged_status": {
const { input } = payload as {
input: { statusEvent: string; targetOwner: string };
};
const event = JSON.parse(input.statusEvent) as RelayEvent;
if (event.pubkey !== input.targetOwner) {
throw new Error("mock merged status owner mismatch");
}
getMockProjectEventStore().push(event);
return null;
}
case "merge_project_pull_request": {
const { input } = payload as {
input: {
expectedCommit: string;
pullRequestAuthor: string;
pullRequestId: string;
repoAddress: string;
sourceBranch: string;
statusCreatedAt: number;
targetBranch: string;
targetOwner: string;
};
};
const normalizedTargetOwner = input.targetOwner.toLowerCase();
const canSignAsOwner =
(identity?.pubkey ?? MOCK_IDENTITY_PUBKEY).toLowerCase() ===
normalizedTargetOwner ||
mockManagedAgents.some(
(agent) => agent.pubkey.toLowerCase() === normalizedTargetOwner,
);
if (!canSignAsOwner) {
throw new Error(
"Only the repository owner or the owner of its managed agent can merge pull requests.",
);
}
if (window.__BUZZ_E2E_PROJECT_MERGE_ERROR__) {
throw window.__BUZZ_E2E_PROJECT_MERGE_ERROR__;
}
const mergeCommit = "abcdef0123456789abcdef0123456789abcdef01";
const statusEvent = createMockEvent(
KIND_GIT_STATUS_MERGED,
"",
[
["e", input.pullRequestId, "", "root"],
["a", input.repoAddress],
["p", input.targetOwner],
["p", input.pullRequestAuthor],
["merge-commit", mergeCommit],
["r", mergeCommit],
],
input.targetOwner,
input.statusCreatedAt,
);
window.__BUZZ_E2E_SIGNED_EVENTS__?.push({
content: statusEvent.content,
kind: statusEvent.kind,
tags: statusEvent.tags,
});
const rejectionIndex =
window.__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__?.indexOf(
statusEvent.kind,
) ?? -1;
let statusPublicationError: string | null = null;
if (rejectionIndex >= 0) {
window.__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__?.splice(
rejectionIndex,
1,
);
statusPublicationError = "mock project event rejection";
} else {
getMockProjectEventStore().push(statusEvent);
}
return {
message: "Merged feature into main.",
merge_commit: mergeCommit,
status_event: JSON.stringify(statusEvent),
status_publication_error: statusPublicationError,
};
}
case "open_project_merge_recovery_terminal": {
const { input } = payload as {
input: { expectedCommit: string };
};
return {
path: "/tmp/buzz/REPOS/buzz",
cloned: false,
recoveryRef: `refs/buzz/merge-recovery/${input.expectedCommit}`,
targetRef: `refs/buzz/merge-recovery-target/${"f".repeat(40)}`,
};
}
case "open_project_terminal":
return {
path: "/tmp/buzz/REPOS/buzz",
cloned: false,
};
case "get_relay_ws_url":
return getRelayWsUrl(activeConfig);
case "get_default_relay_url":
return getRelayWsUrl(activeConfig);
case "auto_connect_default_relay_enabled":
return activeConfig?.autoConnectDefaultRelay ?? false;
case "get_legacy_workspace_storage":
return {
workspaces: null,
activeWorkspaceId: null,
onboardingCompletions: [],
};
case "take_pending_community_deep_link":
// Mirrors the Rust queue: peek the head; acknowledge removes it.
return mockPendingCommunityDeepLinks[0] ?? null;
case "acknowledge_pending_community_deep_link": {
const { id } = payload as { id: string };
const index = mockPendingCommunityDeepLinks.findIndex(
(pending) => pending.id === id,
);
if (index === -1) {
return false;
}
mockPendingCommunityDeepLinks.splice(index, 1);
return true;
}
case "get_relay_http_url":
return getRelayHttpUrl(activeConfig);
case "relay_requires_membership":
return activeConfig?.mock?.relayRequiresMembership ?? false;
case "discover_acp_providers":
return handleDiscoverAcpRuntimes(activeConfig);
case "save_custom_harness":
return handleSaveCustomHarness(
payload as Parameters<typeof handleSaveCustomHarness>[0],
);
case "delete_custom_harness":
return handleDeleteCustomHarness(
payload as Parameters<typeof handleDeleteCustomHarness>[0],
activeConfig,
);
case "discover_acp_auth_methods":
return handleDiscoverAcpAuthMethods(
payload as { runtimeId?: string },
activeConfig,
);
case "connect_acp_runtime":
return handleConnectAcpRuntime(
payload as { request?: { runtimeId?: string; methodId?: string } },
activeConfig,
);
case "install_acp_runtime":
return handleInstallAcpRuntime(
payload as { runtimeId?: string },
activeConfig,
);
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<typeof handleDiscoverManagedAgentPrereqs>[0],
activeConfig,
);
case "get_channels":
return handleGetChannels(activeConfig);
case "get_feed":
return handleGetFeed(
(payload as Parameters<typeof handleGetFeed>[0]) ?? {},
activeConfig,
);
case "list_relay_agents":
return handleListRelayAgents(activeConfig);
case "list_personas":
return handleListPersonas();
case "create_persona":
return handleCreatePersona(
payload as Parameters<typeof handleCreatePersona>[0],
);
case "update_persona":
return handleUpdatePersona(
payload as Parameters<typeof handleUpdatePersona>[0],
);
case "update_persona_and_publish":
return handleUpdatePersonaAndPublish(
payload as Parameters<typeof handleUpdatePersonaAndPublish>[0],
activeConfig,
);
case "delete_persona":
return handleDeletePersona(
payload as Parameters<typeof handleDeletePersona>[0],
);
case "reconcile_inbound_persona_event": {
const nostrEvent = JSON.parse(
(payload as { eventJson: string }).eventJson,
) as {
kind: number;
tags: string[][];
content: string;
created_at: number;
};
if (nostrEvent.kind === 30175) {
// Persona upsert — parse content and upsert into mockPersonas by d-tag
const dTag = nostrEvent.tags.find((t) => t[0] === "d")?.[1];
if (dTag) {
const content = JSON.parse(nostrEvent.content) as {
display_name?: string;
system_prompt?: string;
};
const now = new Date().toISOString();
const existing = mockPersonas.find((p) => p.id === dTag);
const shared = nostrEvent.tags.some(
(tag) =>
tag.length === 2 && tag[0] === "shared" && tag[1] === "true",
);
if (existing) {
existing.display_name =
content.display_name ?? existing.display_name;
existing.system_prompt =
content.system_prompt ?? existing.system_prompt;
existing.shared = shared;
existing.updated_at = now;
} else {
mockPersonas.push({
id: dTag,
display_name: content.display_name ?? dTag,
avatar_url: null,
system_prompt: content.system_prompt ?? "",
is_builtin: false,
is_active: true,
shared,
env_vars: {},
created_at: now,
updated_at: now,
});
}
}
} else if (nostrEvent.kind === 5) {
// Tombstone — extract d-tag from a-tag "30175:<pubkey>:<d_tag>" and remove
const aTagValue = nostrEvent.tags.find((t) => t[0] === "a")?.[1];
if (aTagValue) {
const dTag = aTagValue.split(":")[2];
if (dTag) {
mockPersonas = mockPersonas.filter((p) => p.id !== dTag);
}
}
}
// Mirror the real Rust backend: emit "agents-data-changed" after reconcile.
for (const cb of tauriEventListeners.get("agents-data-changed") ?? []) {
cb();
}
return undefined;
}
case "set_persona_active":
return handleSetPersonaActive(
payload as Parameters<typeof handleSetPersonaActive>[0],
);
case "set_persona_shared":
return handleSetPersonaShared(
payload as Parameters<typeof handleSetPersonaShared>[0],
activeConfig,
);
case "list_teams":
return handleListTeams();
case "list_channel_templates":
return (activeConfig?.mock?.channelTemplates ?? []).map((template) => ({
id: template.id,
name: template.name,
description: template.description,
channel_type: template.channelType,
visibility: template.visibility,
canvas_template: template.canvasTemplate,
agents: template.agents,
is_builtin: template.isBuiltin,
created_at: template.createdAt,
updated_at: template.updatedAt,
}));
case "create_team":
return handleCreateTeam(
payload as Parameters<typeof handleCreateTeam>[0],
);
case "update_team":
return handleUpdateTeam(
payload as Parameters<typeof handleUpdateTeam>[0],
);
case "delete_team":
return handleDeleteTeam(
payload as Parameters<typeof handleDeleteTeam>[0],
);
case "export_team_to_json":
return handleExportTeamToJson(payload as { id: string });
case "pick_team_directory":
return handlePickTeamDirectory();
case "install_team_from_directory":
return handleInstallTeamFromDirectory(
payload as Parameters<typeof handleInstallTeamFromDirectory>[0],
);
case "sync_team_directory":
return handleSyncTeamDirectory(
payload as Parameters<typeof handleSyncTeamDirectory>[0],
);
case "parse_team_file":
return handleParseTeamFile();
case "export_agent_snapshot":
// Mimics the save-to-disk path: report success without a real dialog.
// Specs assert invocation via __BUZZ_E2E_COMMANDS__.
return true;
case "encode_agent_snapshot_for_send": {
// Return the requested wire format so both message sharing (PNG) and
// community catalog publication (JSON) exercise their real branches.
// Optional encodeDelayMs lets specs observe the "preparing" phase before
// the upload begins.
const encodeDelayMs = activeConfig?.mock?.encodeDelayMs ?? 0;
if (encodeDelayMs > 0) {
await new Promise((resolve) =>
window.setTimeout(resolve, encodeDelayMs),
);
}
const input = payload as {
id: string;
memoryLevel: "none" | "core" | "everything";
format: "json" | "png";
};
if (input.format === "json") {
const persona = mockPersonas.find(
(candidate) => candidate.id === input.id,
);
const snapshot = {
format: "buzz-agent-snapshot",
version: 1,
definition: {
name: persona?.display_name ?? "E2E Agent",
sourceIsBuiltIn: persona?.is_builtin ?? false,
systemPrompt: persona?.system_prompt ?? "",
runtime: persona?.runtime ?? null,
model: persona?.model ?? null,
provider: persona?.provider ?? null,
respondTo: persona?.respond_to ?? null,
respondToAllowlist: persona?.respond_to_allowlist ?? [],
namePool: persona?.name_pool ?? [],
},
profile: {
displayName: persona?.display_name ?? "E2E Agent",
avatarUrl: persona?.avatar_url ?? null,
},
memory: {
level: input.memoryLevel,
entries: [],
},
};
const fileBytes = Array.from(
new TextEncoder().encode(JSON.stringify(snapshot)),
);
return {
fileBytes,
fileName: "e2e-agent.agent.json",
};
}
return {
fileBytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
fileName: "e2e-agent.agent.png",
};
}
case "preview_agent_snapshot_import": {
// Return a minimal preview — no writes performed.
return {
displayName: "Imported Agent",
isBuiltIn: true,
model: "claude-opus-4-5",
runtime: "goose",
systemPrompt: null,
avatarUrl: null,
memoryLevel: "none",
memoryEntryCount: 0,
hasSourceAllowlist: false,
sourceAllowlistCount: 0,
};
}
case "confirm_agent_snapshot_import": {
// Return a successful import result with fresh synthetic keys.
const importResult = {
displayName: "Imported Agent",
newPubkey:
"e2e000000000000000000000000000000000000000000000000000000000000ff",
personaId: `e2e-persona-${Date.now()}`,
memoryWritten: 0,
memoryTotal: 0,
memoryErrors: [],
profileSyncError: null,
};
return importResult;
}
case "export_team_snapshot":
// Mimics the save-to-disk path: report success without a real dialog.
return true;
case "encode_team_snapshot_for_send": {
// Return a minimal PNG-shaped payload so the send flow can proceed
// through upload_media_bytes without a real Rust encode step.
const encodeDelayMs = activeConfig?.mock?.encodeDelayMs ?? 0;
if (encodeDelayMs > 0) {
await new Promise((resolve) =>
window.setTimeout(resolve, encodeDelayMs),
);
}
return {
fileBytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
fileName: "e2e-team.team.png",
};
}
case "preview_team_snapshot_import": {
// Return a minimal preview — no writes performed.
const previewHasAllowlist =
activeConfig?.mock?.teamSnapshotPreviewHasSourceAllowlist ?? false;
return {
name: "Imported Team",
description: null,
instructions: null,
members: [
{
displayName: "Team Member",
systemPrompt: null,
avatarUrl: null,
hasSourceAllowlist: previewHasAllowlist,
sourceAllowlistCount: previewHasAllowlist ? 3 : 0,
},
],
hasSourceAllowlist: previewHasAllowlist,
};
}
case "confirm_team_snapshot_import": {
// Sequenced failure injection: fail once, then succeed (retry test).
const confirmSequence = activeConfig?.mock?.teamSnapshotConfirmErrors;
if (confirmSequence && confirmSequence.length > 0) {
const idx = Math.min(
teamSnapshotConfirmCallCount,
confirmSequence.length - 1,
);
teamSnapshotConfirmCallCount++;
const entry = confirmSequence[idx];
if (entry !== null) {
throw new Error(entry);
}
}
// Return a successful import result with fresh synthetic keys.
// The nested `team` uses snake_case (Rust TeamRecord has no rename_all);
// the outer struct and members use camelCase (their Rust types do).
const importTs = Date.now();
return {
team: {
id: `e2e-team-${importTs}`,
name: "Imported Team",
description: null,
persona_ids: [`e2e-persona-${importTs}`],
instructions: null,
is_builtin: false,
source_dir: null,
is_symlink: false,
symlink_target: null,
version: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
personaIds: [`e2e-persona-${importTs}`],
members: [
{
displayName: "Team Member",
pubkey:
"e2e000000000000000000000000000000000000000000000000000000000000ee",
personaId: `e2e-persona-${importTs}`,
memoryWritten: 0,
memoryTotal: 0,
memoryErrors: [],
profileSyncError: null,
},
],
};
}
case "list_managed_agents":
return handleListManagedAgents(activeConfig);
case "get_agent_memory":
return handleGetAgentMemory(
(payload as Parameters<typeof handleGetAgentMemory>[0]) ?? {},
activeConfig,
);
case "create_managed_agent":
return handleCreateManagedAgent(
payload as Parameters<typeof handleCreateManagedAgent>[0],
activeConfig,
);
case "start_managed_agent":
return handleStartManagedAgent(
payload as Parameters<typeof handleStartManagedAgent>[0],
activeConfig,
);
case "stop_managed_agent":
return handleStopManagedAgent(
payload as Parameters<typeof handleStopManagedAgent>[0],
);
case "list_managed_agent_runtimes":
return mockManagedAgentRuntimes.map((row) => ({ ...row }));
case "start_managed_agent_runtime":
return handleManagedAgentRuntimeAction(
"start",
payload as { pubkey: string; relayUrl: string },
);
case "stop_managed_agent_runtime":
return handleManagedAgentRuntimeAction(
"stop",
payload as { pubkey: string; relayUrl: string },
);
case "restart_managed_agent_runtime":
return handleManagedAgentRuntimeAction(
"restart",
payload as { pubkey: string; relayUrl: string },
);
case "reconcile_managed_agent_runtimes":
// Post-create bootstrap reconcile: no new pairs in the mock world.
return [];
case "set_agent_managed_profiles":
return undefined;
case "set_managed_agent_auto_restart":
return handleSetManagedAgentAutoRestart(
payload as Parameters<typeof handleSetManagedAgentAutoRestart>[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<typeof handleDeleteManagedAgent>[0],
);
case "get_managed_agent_log":
return handleGetManagedAgentLog(
payload as Parameters<typeof handleGetManagedAgentLog>[0],
);
case "get_agent_models":
return {
agentName: "mock-agent",
agentVersion: "0.0.0",
models: [],
agentDefaultModel: null,
selectedModel: null,
supportsSwitching: false,
};
case "discover_agent_models": {
const discoverError = activeConfig?.mock?.discoverAgentModelsError;
if (discoverError) {
throw new Error(discoverError);
}
const discoverOverride = activeConfig?.mock?.discoverAgentModels;
if (discoverOverride) {
return {
agentName: "mock-agent",
agentVersion: "0.0.0",
models: discoverOverride.models.map((model) => ({
id: model.id,
name: model.name,
description: model.description ?? null,
})),
agentDefaultModel: discoverOverride.agentDefaultModel ?? null,
selectedModel: discoverOverride.selectedModel ?? null,
supportsSwitching: discoverOverride.supportsSwitching,
};
}
const input = (
payload as {
input?: { agentCommand?: string; provider?: string };
} | null
)?.input;
const agentCommand = input?.agentCommand?.trim() ?? "";
const provider = input?.provider?.trim() ?? "";
const openAiModels = [
{ id: "gpt-5.5", name: "GPT-5.5", description: null },
{ id: "gpt-5.4", name: "GPT-5.4", description: null },
{ id: "gpt-5.4-mini", name: "GPT-5.4 mini", description: null },
{ id: "gpt-5.4-nano", name: "GPT-5.4 nano", description: null },
];
const anthropicModels = [
{
id: "goose-claude-4-6-opus",
name: "Claude Opus 4.6",
description: null,
},
{
id: "goose-claude-4-6-sonnet",
name: "Claude Sonnet 4.6",
description: null,
},
];
const claudeRuntimeModels = [
{
id: "claude-sonnet-4-20250514",
name: "Claude Sonnet 4",
description: null,
},
{
id: "claude-opus-4-20250514",
name: "Claude Opus 4",
description: null,
},
];
const codexRuntimeModels = [
{ id: "gpt-5.5", name: "GPT-5.5", description: null },
{ id: "gpt-5.5[low]", name: "GPT-5.5 (low)", description: null },
{
id: "gpt-5.5[medium]",
name: "GPT-5.5 (medium)",
description: null,
},
{ id: "gpt-5.5[high]", name: "GPT-5.5 (high)", description: null },
];
if (provider === "relay-mesh") {
if (!mockMeshState.admitted) {
throw new Error(mockMeshState.denyReason);
}
if (mockMeshState.models.length === 0) {
throw new Error(
"no Buzz shared compute serving members are available",
);
}
}
const models =
provider === "relay-mesh"
? mockMeshState.models.map((model) => ({
id: model.id,
name: model.name,
description: null,
}))
: provider === "openai"
? openAiModels
: provider === "anthropic"
? anthropicModels
: agentCommand.includes("claude")
? claudeRuntimeModels
: agentCommand.includes("codex")
? codexRuntimeModels
: [...anthropicModels, ...openAiModels];
return {
agentName: "mock-agent",
agentVersion: "0.0.0",
models,
agentDefaultModel: agentCommand.includes("codex")
? "gpt-5.5[high]"
: null,
selectedModel: null,
supportsSwitching: true,
};
}
case "get_agent_config_surface": {
const configArgs = payload as { pubkey: string };
return buildMockConfigSurface(configArgs.pubkey);
}
case "get_runtime_file_config": {
const runtimeId = (payload as { runtimeId?: string } | null | undefined)
?.runtimeId;
if (!runtimeId) return null;
return config.mock?.runtimeFileConfigs?.[runtimeId] ?? null;
}
case "get_global_agent_config": {
// Return the mutable persisted mock value, seeded from the test config.
return (
mockGlobalAgentConfig ?? {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
}
);
}
case "set_global_agent_config": {
// Echo back the submitted config as the saved value (mirrors the
// backend's strip-on-write pass in tests where all values are already
// non-empty). The invoke payload wraps it as { config }.
const savedConfig = (
payload as {
config: {
env_vars: Record<string, string>;
provider: string | null;
model: string | null;
preferred_runtime: string | null;
};
}
).config;
// Optional configurable delay so specs can hold a save in flight and
// interleave edits (mid-save race + autosave-coalescing coverage).
// Two aliases: onboarding specs use setGlobalAgentConfigDelayMs,
// settings-card specs use globalConfigSaveDelayMs.
const saveDelayMs =
config?.mock?.globalConfigSaveDelayMs ??
activeConfig?.mock?.setGlobalAgentConfigDelayMs ??
0;
if (saveDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, saveDelayMs));
}
mockGlobalAgentConfig = savedConfig;
// In the E2E environment there are no running agents to restart, so
// the counts default to 0 unless a spec drives them explicitly.
return {
config: savedConfig,
restarted_count: config?.mock?.globalConfigRestartedCount ?? 0,
failed_restart_count:
config?.mock?.globalConfigFailedRestartCount ?? 0,
};
}
case "get_baked_build_env": {
const bakedEnvDelayMs = config?.mock?.bakedBuildEnvDelayMs ?? 0;
if (bakedEnvDelayMs > 0) {
await new Promise((resolve) =>
window.setTimeout(resolve, bakedEnvDelayMs),
);
}
return config?.mock?.bakedBuildEnv ?? [];
}
case "get_baked_build_env_keys":
return (config?.mock?.bakedBuildEnv ?? []).map((entry) => entry.key);
case "update_managed_agent":
return handleUpdateManagedAgent(
payload as Parameters<typeof handleUpdateManagedAgent>[0],
);
case "create_channel":
return handleCreateChannel(
payload as Parameters<typeof handleCreateChannel>[0],
activeConfig,
);
case "ensure_starter_channels":
return handleEnsureStarterChannels(activeConfig);
case "open_dm":
return handleOpenDm(
payload as Parameters<typeof handleOpenDm>[0],
activeConfig,
);
case "hide_dm":
return handleHideDm(
payload as Parameters<typeof handleHideDm>[0],
activeConfig,
);
case "get_channel_details":
return handleGetChannelDetails(
payload as Parameters<typeof handleGetChannelDetails>[0],
activeConfig,
);
case "get_channel_members":
return handleGetChannelMembers(
payload as Parameters<typeof handleGetChannelMembers>[0],
activeConfig,
);
case "update_channel":
return handleUpdateChannel(
(payload as { input: Parameters<typeof handleUpdateChannel>[0] })
.input,
activeConfig,
);
case "set_channel_topic":
return handleSetChannelTopic(
payload as Parameters<typeof handleSetChannelTopic>[0],
activeConfig,
);
case "set_channel_purpose":
return handleSetChannelPurpose(
payload as Parameters<typeof handleSetChannelPurpose>[0],
activeConfig,
);
case "archive_channel":
return handleArchiveChannel(
payload as Parameters<typeof handleArchiveChannel>[0],
activeConfig,
);
case "unarchive_channel":
return handleUnarchiveChannel(
payload as Parameters<typeof handleUnarchiveChannel>[0],
activeConfig,
);
case "delete_channel":
return handleDeleteChannel(
payload as Parameters<typeof handleDeleteChannel>[0],
activeConfig,
);
case "add_channel_members":
return handleAddChannelMembers(
payload as Parameters<typeof handleAddChannelMembers>[0],
activeConfig,
);
case "remove_channel_member":
return handleRemoveChannelMember(
payload as Parameters<typeof handleRemoveChannelMember>[0],
activeConfig,
);
case "join_channel":
return handleJoinChannel(
payload as Parameters<typeof handleJoinChannel>[0],
activeConfig,
);
case "leave_channel":
return handleLeaveChannel(
payload as Parameters<typeof handleLeaveChannel>[0],
activeConfig,
);
case "search_messages":
return handleSearchMessages(
payload as Parameters<typeof handleSearchMessages>[0],
activeConfig,
);
case "get_forum_posts":
return handleGetForumPosts(
payload as Parameters<typeof handleGetForumPosts>[0],
);
case "get_forum_thread":
return handleGetForumThread(
payload as Parameters<typeof handleGetForumThread>[0],
);
case "get_thread_replies":
return handleGetThreadReplies(
payload as Parameters<typeof handleGetThreadReplies>[0],
activeConfig,
);
case "get_channel_messages_before":
return handleGetChannelMessagesBefore(
payload as Parameters<typeof handleGetChannelMessagesBefore>[0],
activeConfig,
);
case "get_channel_window":
return handleGetChannelWindow(
payload as Parameters<typeof handleGetChannelWindow>[0],
activeConfig,
);
case "send_channel_message":
return handleSendChannelMessage(
payload as Parameters<typeof handleSendChannelMessage>[0],
activeConfig,
);
case "has_managed_agent_channel_message_marker": {
const args = payload as {
channelId: string;
marker: string;
agentPubkey?: string | null;
markerScope?: "agent" | "channel" | null;
};
return getMockMessageStore(args.channelId).some(
(event) =>
(args.markerScope === "channel" ||
event.pubkey === args.agentPubkey) &&
event.tags.some(
(tag) => tag[0] === "client" && tag[1] === args.marker,
),
);
}
case "send_managed_agent_channel_message":
return handleSendManagedAgentChannelMessage(
payload as Parameters<typeof handleSendManagedAgentChannelMessage>[0],
activeConfig,
);
case "delete_message":
handleDeleteMessage(
payload as Parameters<typeof handleDeleteMessage>[0],
);
return null;
case "edit_message":
return handleEditMessage(
payload as Parameters<typeof handleEditMessage>[0],
activeConfig,
);
case "add_reaction":
return handleAddReaction(
payload as Parameters<typeof handleAddReaction>[0],
activeConfig,
);
case "remove_reaction":
return handleRemoveReaction(
payload as Parameters<typeof handleRemoveReaction>[0],
activeConfig,
);
case "get_media_proxy_port":
return MOCK_MEDIA_PROXY_PORT;
case "pick_and_upload_media":
return await resolveMockUploadDescriptors(activeConfig);
case "pick_and_upload_image":
return (await resolveMockUploadDescriptors(activeConfig))[0] ?? null;
case "upload_media_bytes":
return resolveMockUploadDescriptorForBytes(
payload as { data: number[]; filename?: string | null },
activeConfig,
);
case "fetch_media_bytes": {
// The real command fetches relay media through Rust reqwest and
// replies with raw bytes (`tauri::ipc::Response` → ArrayBuffer). In
// E2E the browser fetch suffices — specs serve the URL via page.route.
const response = await fetch((payload as { url: string }).url);
if (!response.ok) throw new Error(`fetch failed: ${response.status}`);
return await response.arrayBuffer();
}
case "fetch_snapshot_bytes": {
// The real command fetches + validates a snapshot attachment in memory
// (size cap, SHA-256, decode). In E2E the bridge returns a minimal
// valid .agent.json payload so the import flow can proceed without a
// real relay. A non-null snapshotFetchError config forces a rejection.
const err = activeConfig?.mock?.snapshotFetchError;
if (err) throw new Error(err);
const jsonBytes = Array.from(
new TextEncoder().encode(
JSON.stringify({
format: "buzz-agent-snapshot",
version: 1,
definition: { system_prompt: "E2E imported agent prompt." },
profile: { display_name: "Imported Agent" },
memory: { level: "none", entries: [] },
}),
),
);
// Return as ArrayBuffer to mirror the real Tauri ipc::Response.
const buf = new ArrayBuffer(jsonBytes.length);
new Uint8Array(buf).set(jsonBytes);
return buf;
}
case "download_image":
case "save_png_data_url":
case "download_file":
// The save dialog can't run headlessly; report a successful save so the
// FileCard / image-menu click handlers resolve. Specs assert the
// command was invoked via `__BUZZ_E2E_COMMANDS__`, not the dialog.
return true;
case "copy_image_to_clipboard":
return;
case "copy_text_to_clipboard":
await navigator.clipboard.writeText((payload as { text: string }).text);
return;
case "get_event":
return handleGetEvent(
payload as Parameters<typeof handleGetEvent>[0],
activeConfig,
);
case "sign_event":
window.__BUZZ_E2E_SIGNED_EVENTS__?.push({
content: (payload as { content: string }).content,
createdAt: (payload as { createdAt?: number }).createdAt,
kind: (payload as { kind: number }).kind,
tags: (payload as { tags: string[][] }).tags,
});
if (identity) {
return JSON.stringify(
await signWithIdentity(identity, {
kind: (payload as { kind: number }).kind,
content: (payload as { content: string }).content,
createdAt: (payload as { createdAt?: number }).createdAt,
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,
DEFAULT_MOCK_IDENTITY.pubkey,
(payload as { createdAt?: number }).createdAt,
),
);
case "nip44_encrypt_to_self":
return (payload as { plaintext: string }).plaintext;
case "nip44_decrypt_from_self":
return (payload as { ciphertext: string }).ciphertext;
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<typeof connectRealSocket>[0],
);
}
return connectMockSocket(
payload as Parameters<typeof connectMockSocket>[0],
);
case "plugin:websocket|disconnect": {
const { id } = payload as { id: number };
if (isRelayMode(activeConfig)) {
realSockets.get(id)?.close();
realSockets.delete(id);
} else {
const socket = mockSockets.get(id);
mockSockets.delete(id);
if (socket) sendWsClose(socket.handler);
}
return null;
}
case "plugin:websocket|disconnect_all":
for (const socket of realSockets.values()) socket.close();
realSockets.clear();
for (const socket of mockSockets.values()) sendWsClose(socket.handler);
mockSockets.clear();
mockWebsocketSendMutexWedged = false;
return null;
case "plugin:websocket|send":
if (isRelayMode(activeConfig)) {
return sendToRealSocket(
payload as Parameters<typeof sendToRealSocket>[0],
);
}
return sendToMockSocket(
payload as Parameters<typeof sendToMockSocket>[0],
);
case "plugin:opener|open_url":
if (activeConfig?.mock?.openerError) {
throw new Error(activeConfig.mock.openerError);
}
openedExternalUrls.push(String((payload as { url: string | URL }).url));
return null;
case "get_e2e_opened_external_urls":
return [...openedExternalUrls];
case "clear_e2e_opened_external_urls":
openedExternalUrls.length = 0;
return null;
case "plugin:window|show":
case "plugin:window|unminimize":
case "plugin:window|set_focus":
case "plugin:window|set_badge_count":
case "plugin:window|set_badge_label":
return null;
case "plugin:updater|check":
return handleUpdaterCheck(activeConfig);
case "plugin:updater|download":
return handleUpdaterDownload(payload, activeConfig);
case "plugin:updater|install":
return handleUpdaterInstall();
case "is_auto_update_supported":
// Default true so all existing tests continue to use the auto-update
// path. Set mock.autoUpdateSupported: false to simulate a .deb install.
return activeConfig?.mock?.autoUpdateSupported !== false;
case "relay_reconnect_hook":
return null;
case "relay_reconnect_hook_configured":
return false;
case "plugin:resources|close":
return null;
case "plugin:process|restart":
return handleRestart(activeConfig);
case "get_channel_workflows":
return handleGetChannelWorkflows(
payload as Parameters<typeof handleGetChannelWorkflows>[0],
);
case "get_channels_workflows":
return handleGetChannelsWorkflows(
payload as Parameters<typeof handleGetChannelsWorkflows>[0],
);
case "get_workflow":
return handleGetWorkflow(
payload as Parameters<typeof handleGetWorkflow>[0],
);
case "create_workflow":
return handleCreateWorkflow(
payload as Parameters<typeof handleCreateWorkflow>[0],
);
case "update_workflow":
return handleUpdateWorkflow(
payload as Parameters<typeof handleUpdateWorkflow>[0],
);
case "delete_workflow":
return handleDeleteWorkflow(
payload as Parameters<typeof handleDeleteWorkflow>[0],
);
case "trigger_workflow":
return handleTriggerWorkflow(
payload as Parameters<typeof handleTriggerWorkflow>[0],
);
case "get_workflow_runs":
return handleGetWorkflowRuns(
payload as Parameters<typeof handleGetWorkflowRuns>[0],
);
case "get_run_approvals":
return handleGetRunApprovals(
payload as Parameters<typeof handleGetRunApprovals>[0],
);
case "plugin:webview|set_webview_zoom":
window.__BUZZ_E2E_WEBVIEW_ZOOM__ = (payload as { value: number }).value;
return;
case "plugin:event|listen":
// Tauri event system (pairing, huddle) — no-op in e2e, return unlisten fn ID
return Math.floor(Math.random() * 1_000_000);
case "start_pairing": {
const delayMs = activeConfig?.mock?.pairingStartDelayMs ?? 0;
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
return "nostrpair://8f4b8db31967ce14fef970a1ff1e8eecf19a430aa1c83875e2f5be68dcac0f1a?relay=wss%3A%2F%2Frelay.example.com&secret=87d5a8cfd5807a0cb44f728b67d88d6dcb8daf99be137c158f21a50c1e913c0a&v=1";
}
case "cancel_pairing":
case "confirm_pairing_sas":
return null;
// ── NIP-IA identity archival ────────────────────────────────────────
// These mocks drive the archive-button gate matrix in
// tests/e2e/identity-archive.spec.ts. Defaults keep the button hidden
// for non-self viewees so the negative case is the unsurprising one.
case "resolve_oa_owner": {
const isMe = activeConfig?.mock?.oaOwnerIsMe ?? false;
const owner = isMe
? (identity?.pubkey ?? DEFAULT_MOCK_IDENTITY.pubkey)
: "ff".repeat(32);
return { owner, is_me: isMe };
}
case "list_archived_identities": {
const archived = activeConfig?.mock?.archivedIdentities ?? [];
return { archived };
}
case "get_relay_self":
if ((activeConfig?.mock?.relaySelfDelayMs ?? 0) > 0) {
await new Promise((resolve) =>
window.setTimeout(
resolve,
activeConfig?.mock?.relaySelfDelayMs ?? 0,
),
);
}
return activeConfig?.mock?.relaySelf ?? null;
case "archive_identity":
case "unarchive_identity":
// The spec only verifies UI state, not the submitted request shape;
// returning null mirrors the Rust submit_event success path.
return null;
case "set_canvas":
return { ok: true, event_id: mockEventId() };
case "get_canvas": {
const canvasReadError = activeConfig?.mock?.canvasReadError;
if (canvasReadError) {
throw new Error(canvasReadError);
}
// Return the no-canvas success shape — content null means no canvas set.
return { content: null, updated_at: null, author: null };
}
// ── Local-save archive ──────────────────────────────────────────────
// These stubs drive the LocalArchiveSettingsCard in screenshot / UI tests
// without requiring a real SQLite backend. `mockSaveSubscriptions` is a
// mutable clone of `activeConfig.mock.saveSubscriptions` (reset on
// install); create/merge/delete/remove mutate it with the same
// union / delete-row-when-empty semantics as the real Rust commands
// (see `archive/store.rs::merge_owner_p_kinds` / `remove_owner_p_kind`)
// so specs can drive fresh-internal-repair and toggle ON/OFF flows.
case "list_save_subscriptions": {
const win = window as unknown as Record<string, unknown>;
if (!win.__BUZZ_E2E_IPC_COUNTERS__) {
win.__BUZZ_E2E_IPC_COUNTERS__ = {};
}
const ipcCounters = win.__BUZZ_E2E_IPC_COUNTERS__ as Record<
string,
number
>;
ipcCounters.list_save_subscriptions =
(ipcCounters.list_save_subscriptions ?? 0) + 1;
const ident = activeConfig?.identity ?? DEFAULT_MOCK_IDENTITY;
return mockSaveSubscriptions.map((s) => ({
identity_pubkey: ident.pubkey,
relay_url: DEFAULT_RELAY_WS_URL,
scope_type: s.scope_type,
scope_value: s.scope_value,
kinds: s.kinds,
created_at: Math.floor(Date.now() / 1000),
}));
}
case "create_save_subscription": {
const req = payload as {
scopeType: string;
scopeValue: string;
kinds: number[];
};
const kindsJson = JSON.stringify(req.kinds);
const existing = mockSaveSubscriptions.find(
(s) =>
s.scope_type === req.scopeType && s.scope_value === req.scopeValue,
);
if (existing) {
existing.kinds = kindsJson;
} else {
mockSaveSubscriptions.push({
scope_type: req.scopeType,
scope_value: req.scopeValue,
kinds: kindsJson,
});
}
return null;
}
case "delete_save_subscription": {
const req = payload as { scopeType: string; scopeValue: string };
const before = mockSaveSubscriptions.length;
mockSaveSubscriptions = mockSaveSubscriptions.filter(
(s) =>
!(
s.scope_type === req.scopeType && s.scope_value === req.scopeValue
),
);
return mockSaveSubscriptions.length < before;
}
case "archive_events":
// Returns the ArchiveBatchResult shape the UI expects.
return { persisted: 0, dropped: 0 };
case "observer_archive_default_enabled": {
const delayMs =
activeConfig?.mock?.observerArchiveDefaultEnabledDelayMs;
if (delayMs && delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
const error = activeConfig?.mock?.observerArchiveDefaultEnabledError;
if (error) {
throw new Error(error);
}
return activeConfig?.mock?.observerArchiveDefaultEnabled ?? false;
}
case "agent_metric_archive_default_enabled":
return activeConfig?.mock?.agentMetricArchiveDefaultEnabled ?? false;
case "set_prevent_sleep_active":
return null;
case "plugin:window|is_fullscreen":
return false;
case "merge_save_subscription_kinds": {
// Mirrors `merge_owner_p_kinds`: union `kind` into the owner_p row's
// kinds, creating the row if it doesn't exist yet.
const { kind } = payload as { kind: number };
const ident = activeConfig?.identity ?? DEFAULT_MOCK_IDENTITY;
const row = mockSaveSubscriptions.find(
(s) => s.scope_type === "owner_p" && s.scope_value === ident.pubkey,
);
if (row) {
const kinds: number[] = JSON.parse(row.kinds);
if (!kinds.includes(kind)) {
row.kinds = JSON.stringify([...kinds, kind]);
}
} else {
mockSaveSubscriptions.push({
scope_type: "owner_p",
scope_value: ident.pubkey,
kinds: JSON.stringify([kind]),
});
}
return null;
}
case "remove_save_subscription_kind": {
// Mirrors `remove_owner_p_kind`: remove `kind` from the owner_p row's
// kinds, deleting the row entirely once its kinds list is empty.
const { kind } = payload as { kind: number };
const ident = activeConfig?.identity ?? DEFAULT_MOCK_IDENTITY;
const row = mockSaveSubscriptions.find(
(s) => s.scope_type === "owner_p" && s.scope_value === ident.pubkey,
);
if (row) {
const kinds: number[] = JSON.parse(row.kinds).filter(
(k: number) => k !== kind,
);
if (kinds.length === 0) {
mockSaveSubscriptions = mockSaveSubscriptions.filter(
(s) => s !== row,
);
} else {
row.kinds = JSON.stringify(kinds);
}
}
return null;
}
default:
throw new Error(`Unsupported mocked Tauri command: ${command}`);
}
};
window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ = (command, payload) =>
handleMockCommand(command, payload ?? null);
mockIPC(handleMockCommand, { shouldMockEvents: true });
// Wire up __TAURI_INTERNALS__.listen so tests can subscribe to backend-emitted
// events (e.g. "agents-data-changed"). mockIPC already ensures __TAURI_INTERNALS__
// exists; we just add the listen property without clobbering invoke.
(
window as unknown as {
__TAURI_INTERNALS__: {
listen?: (event: string, cb: () => void) => Promise<() => void>;
};
}
).__TAURI_INTERNALS__.listen = async (event: string, cb: () => void) => {
let listeners = tauriEventListeners.get(event);
if (!listeners) {
listeners = new Set();
tauriEventListeners.set(event, listeners);
}
listeners.add(cb);
return () => {
tauriEventListeners.get(event)?.delete(cb);
};
};
installed = true;
}