Files
buzz/desktop/src/testing/e2eBridge.ts
klopez4212 e6c90bb7c4 Polish community rail and mobile pairing (#2972)
## Summary
- align the multi-community rail with the content surface and balance
its visible 10px side gutters
- center Mobile pairing, start sessions on demand, and keep retry states
inside the QR area
- reveal the QR code and copy action with 250ms motion and use the
standard loading spinner

The rail was centered within its own box, but the adjacent sidebar added
another 11px to the visible right gap. Mobile pairing also started
before user intent, which could leave an idle session waiting for EOSE.

## Validation
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop test` — 3,516 passed
- `pnpm -C desktop exec playwright test tests/e2e/community-rail.spec.ts
--project=smoke` — 19 passed
- `pnpm -C desktop exec playwright test
tests/e2e/mobile-pairing-qr.spec.ts --project=smoke` — 1 passed

`pnpm -C desktop check` is currently blocked by the existing
`src-tauri/src/managed_agents/runtime.rs` file-size baseline (2,220
lines; limit 2,216).
2026-07-26 13:20:26 -07:00

11125 lines
348 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 { 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_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;
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;
isActive?: boolean;
sourceTeam?: string | null;
envVars?: Record<string, string>;
runtime?: string | null;
model?: string | null;
provider?: string | null;
namePool?: 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?: {
/** 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;
/** 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;
connectAcpRuntimeResult?: RawConnectAcpRuntimeResult;
connectAcpRuntimeDelayMs?: number;
connectAcpRuntimeError?: string;
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[];
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;
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;
source_team?: 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;
pubkey?: 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,
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,
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),
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 ?? [],
is_builtin: false,
is_active: persona.isActive ?? true,
source_team: persona.sourceTeam ?? null,
env_vars: { ...(persona.envVars ?? {}) },
created_at: now,
updated_at: 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[] = [];
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[] = [];
// 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,
}));
}
// 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) {
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) {
const event: RelayEvent = {
id: crypto.randomUUID().replace(/-/g, ""),
pubkey,
created_at: Math.floor(Date.now() / 1000),
kind: 20002,
tags: [["h", channelId]],
content: "",
sig: "mocksig".repeat(20).slice(0, 128),
};
emitMockLiveEvent(channelId, event);
return event;
}
function toRawForumPost(
event: RelayEvent,
channelId: string,
threadSummary: RawThreadSummary | null,
): RawForumPost {
return {
event_id: event.id,
pubkey: event.pubkey,
content: event.content,
kind: event.kind,
created_at: event.created_at,
channel_id: channelId,
tags: event.tags,
thread_summary: threadSummary,
reactions: null,
};
}
function toRawForumReply(event: RelayEvent, channelId: string): RawForumReply {
const thread = getThreadReferenceFromTags(event.tags);
return {
event_id: event.id,
pubkey: event.pubkey,
content: event.content,
kind: event.kind,
created_at: event.created_at,
channel_id: channelId,
tags: event.tags,
parent_event_id: thread.parentEventId,
root_event_id: thread.rootEventId,
depth:
thread.rootEventId && thread.parentEventId !== thread.rootEventId ? 2 : 1,
broadcast: false,
reactions: null,
};
}
async function handleGetForumPosts(args: {
channelId: string;
limit?: number | null;
before?: number | null;
}): Promise<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, 45001, 45003], "#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;
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 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 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" },
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" },
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" },
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" },
login_hint: undefined,
},
];
return 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));
}
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 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;
};
}): 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,
source_team: null,
env_vars: { ...(args.input.envVars ?? {}) },
created_at: now,
updated_at: now,
};
applyMockPersonaBehavior(persona, args.input.behavior);
mockPersonas.push(persona);
return { ...persona };
}
async function handleUpdatePersona(args: {
input: {
id: string;
displayName: string;
avatarUrl?: string;
systemPrompt: string;
runtime?: string;
model?: string;
provider?: string;
envVars?: Record<string, string>;
behavior?: PersonaBehaviorInput;
};
}): Promise<RawPersona> {
const persona = mockPersonas.find(
(candidate) => candidate.id === args.input.id,
);
if (!persona) {
throw new Error(`agent ${args.input.id} not found`);
}
persona.display_name = args.input.displayName.trim();
persona.avatar_url = args.input.avatarUrl?.trim() || null;
persona.system_prompt = args.input.systemPrompt.trim();
persona.runtime = args.input.runtime?.trim() || null;
persona.model = args.input.model?.trim() || null;
persona.provider = args.input.provider?.trim() || null;
if (args.input.envVars !== undefined) {
// Absent = preserve; present = replace entirely (matches Rust handler).
persona.env_vars = { ...args.input.envVars };
}
applyMockPersonaBehavior(persona, args.input.behavior);
persona.updated_at = new Date().toISOString();
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 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. Choose it from Agent Catalog first.`,
);
}
}
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,
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 };
}
async function handleSearchMessages(
args: {
q: string;
limit?: 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 hits = mockHits
.filter((hit) => {
if (!query) {
return true;
}
return (
hit.content.toLowerCase().includes(query) ||
(hit.channel_name?.toLowerCase().includes(query) ?? false)
);
})
.slice(0, limit);
return {
hits,
found: hits.length,
};
}
// NIP-50 search via POST /query
const limit = args.limit ?? 20;
const events = await relayQuery(config, [
{ kinds: [9, 40002], search: args.q, limit },
]);
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 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;
}
// 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 === 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();
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, pubkey }) => {
const channel = mockChannels.find(
(candidate) => candidate.name === channelName,
);
if (!channel) {
throw new Error(`Mock channel ${channelName} not found.`);
}
return emitMockTypingIndicator(channel.id, pubkey ?? CHARLIE_PUBKEY);
};
window.__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_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 "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,
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 "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 "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);
if (existing) {
existing.display_name =
content.display_name ?? existing.display_name;
existing.system_prompt =
content.system_prompt ?? existing.system_prompt;
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,
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 "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 a minimal PNG-shaped payload so the send flow can proceed
// through upload_media_bytes without a real Rust encode step.
// 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),
);
}
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",
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 (await resolveMockUploadDescriptors(activeConfig))[0];
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;
}