mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): repair e2e bridge for native unread and persona catalog
Teach the E2E Tauri mock the pack's new renderer<->backend contracts and fix two production seams the repaired suites then exposed: - Stateful observed-unread mock: scope open/ingest now derive real per-channel projections (count, badgeCount, appBadgeCount, topLevelUnread, highPriorityUnread) with monotonic channel_latest anchors, mirroring observed_unread.rs instead of returning empty rows. - Persona catalog mock: add the fetch_persona_catalog case, with validation mirroring the Rust validator's emoji semantics (FE0F/ZWJ). - unread_catch_up mock mirrors Rust authored-root discovery: self-authored top-level events are returned as discovered.authored so thread-activity membership matches the native contract; replies stay excluded. - Production: suppress the onPruned notification for true empty projection deltas, breaking a marker-ingest -> no-op delta -> notify -> re-render -> re-ingest feedback loop that saturated the main thread (ingest sequence doubled to 8192 within ~5s). Snapshot and snapshotRequired paths still notify unconditionally; regression tests cover both the no-op suppression and snapshot recovery. - Production: the desktop app-dot fallback now reads topLevelUnreadChannelIds, so thread-preview-only unread no longer lights the app badge while the sidebar thread indicators still do. Marker ingests for genuine projection changes remain fire-per-effect-run; this is bounded now that the no-op cycle is broken. Co-authored-by: Tyler Longwell <tlongwell@squareup.com> Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
This commit is contained in:
committed by
Eva
co-authored by
Tyler Longwell
parent
b955f528b6
commit
625ee7b72a
@@ -641,7 +641,7 @@ export function AppShell() {
|
||||
useAppShellLifecycleEffects({
|
||||
desktopBadgeEnabled: !isHuddleRoom,
|
||||
homeBadgeCountExcludingHighPriority,
|
||||
unreadChannelIds,
|
||||
topLevelUnreadChannelIds,
|
||||
unreadChannelNotificationCount,
|
||||
});
|
||||
// Dispatch `buzz://` deep links only from the main window; the companion is dedicated to its active Huddle route.
|
||||
|
||||
@@ -8,14 +8,14 @@ import { useRelayResumeTriggers } from "@/shared/api/useRelayResumeTriggers";
|
||||
type AppShellLifecycleEffectsOptions = {
|
||||
desktopBadgeEnabled: boolean;
|
||||
homeBadgeCountExcludingHighPriority: number;
|
||||
unreadChannelIds: ReadonlySet<string>;
|
||||
topLevelUnreadChannelIds: ReadonlySet<string>;
|
||||
unreadChannelNotificationCount: number;
|
||||
};
|
||||
|
||||
export function useAppShellLifecycleEffects({
|
||||
desktopBadgeEnabled,
|
||||
homeBadgeCountExcludingHighPriority,
|
||||
unreadChannelIds,
|
||||
topLevelUnreadChannelIds,
|
||||
unreadChannelNotificationCount,
|
||||
}: AppShellLifecycleEffectsOptions) {
|
||||
// Event-driven reconnect: network online / focus / visibility short-circuit
|
||||
@@ -82,12 +82,12 @@ export function useAppShellLifecycleEffects({
|
||||
void setDesktopAppBadge(
|
||||
count
|
||||
? { kind: "count", count }
|
||||
: { kind: unreadChannelIds.size ? "dot" : "none" },
|
||||
: { kind: topLevelUnreadChannelIds.size ? "dot" : "none" },
|
||||
);
|
||||
}, [
|
||||
desktopBadgeEnabled,
|
||||
homeBadgeCountExcludingHighPriority,
|
||||
unreadChannelIds,
|
||||
topLevelUnreadChannelIds,
|
||||
unreadChannelNotificationCount,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,86 @@ async function settle() {
|
||||
});
|
||||
}
|
||||
|
||||
test("native no-op marker delta does not notify the renderer", async () => {
|
||||
installFreshStorage();
|
||||
let harness;
|
||||
let notifications = 0;
|
||||
const rig = installNativeRig();
|
||||
try {
|
||||
harness = await mountHook(
|
||||
{
|
||||
...DEFAULT_PROPS,
|
||||
pubkey: "pk-no-op-marker",
|
||||
getTs: () => NOW_S,
|
||||
onPruned: () => {
|
||||
notifications += 1;
|
||||
},
|
||||
},
|
||||
makeRefs(),
|
||||
);
|
||||
await settle();
|
||||
assert.equal(notifications, 1, "opening the native snapshot notifies once");
|
||||
|
||||
harness.api.syncMarkers(["channel-empty"]);
|
||||
await settle();
|
||||
|
||||
assert.equal(
|
||||
rig.requests("observed_unread_ingest").length,
|
||||
1,
|
||||
"the marker must still advance the native revision and ack sequence",
|
||||
);
|
||||
assert.equal(
|
||||
notifications,
|
||||
1,
|
||||
"an empty projection delta must not trigger a renderer feedback render",
|
||||
);
|
||||
} finally {
|
||||
await harness?.unmount();
|
||||
rig.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("native snapshotRequired response still reopens and notifies", async () => {
|
||||
installFreshStorage();
|
||||
let harness;
|
||||
let notifications = 0;
|
||||
const scope = { pubkey: "pk-snapshot-required", relayUrl: RELAY };
|
||||
const rig = installNativeRig();
|
||||
try {
|
||||
harness = await mountHook(
|
||||
{
|
||||
...DEFAULT_PROPS,
|
||||
pubkey: scope.pubkey,
|
||||
getTs: () => NOW_S,
|
||||
onPruned: () => {
|
||||
notifications += 1;
|
||||
},
|
||||
},
|
||||
makeRefs(),
|
||||
);
|
||||
await settle();
|
||||
rig.scope(scope).lastSequence = -1;
|
||||
|
||||
harness.api.syncMarkers(["channel-gap"]);
|
||||
await settle();
|
||||
await settle();
|
||||
|
||||
assert.equal(
|
||||
rig.requests("observed_unread_open_scope").length,
|
||||
2,
|
||||
"a sequence gap must reopen the scope even when it carries no projection rows",
|
||||
);
|
||||
assert.equal(
|
||||
notifications,
|
||||
2,
|
||||
"the replacement snapshot must still notify the renderer",
|
||||
);
|
||||
} finally {
|
||||
await harness?.unmount();
|
||||
rig.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Entry: the boundary that makes every other test meaningful ────────────────
|
||||
|
||||
test("native mode is ENTERED: the hook opens the scope over the bridge", async () => {
|
||||
|
||||
@@ -170,7 +170,9 @@ export function useObservedUnreadPersistence(
|
||||
revision: response.revision,
|
||||
sequence: response.ackedSequence,
|
||||
};
|
||||
optionsRef.current.onPruned?.();
|
||||
if (response.removed.length > 0 || response.upserts.length > 0) {
|
||||
optionsRef.current.onPruned?.();
|
||||
}
|
||||
},
|
||||
[reopen],
|
||||
);
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
handleDeleteCustomHarness,
|
||||
} from "./e2eBridgeCustomHarnesses.ts";
|
||||
|
||||
import type {
|
||||
ObservedUnreadProjection,
|
||||
ObservedUnreadResponse,
|
||||
} from "@/shared/api/tauriObservedUnread";
|
||||
import type { UnreadCatchUpChannelResult } from "@/shared/api/tauriUnreadCatchUp";
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import { activateRateLimit } from "@/shared/api/relayRateLimitGate";
|
||||
@@ -3078,6 +3082,111 @@ type MockSaveSubscriptionRow = {
|
||||
};
|
||||
let mockSaveSubscriptions: MockSaveSubscriptionRow[] = [];
|
||||
|
||||
type MockObservedUnreadScope = {
|
||||
generation: string;
|
||||
revision: number;
|
||||
lastSequence: number;
|
||||
migrationComplete: boolean;
|
||||
events: Map<
|
||||
string,
|
||||
{
|
||||
channelId: string;
|
||||
id: string;
|
||||
createdAt: number;
|
||||
rootId: string | null;
|
||||
highPriority: boolean;
|
||||
countsTowardBadge: boolean;
|
||||
countsTowardAppBadge: boolean;
|
||||
}
|
||||
>;
|
||||
channelLatest: Map<string, number>;
|
||||
markers: Map<string, number>;
|
||||
};
|
||||
|
||||
const mockObservedUnreadScopes = new Map<string, MockObservedUnreadScope>();
|
||||
|
||||
function mockObservedUnreadScopeKey(scope: {
|
||||
pubkey: string;
|
||||
relayUrl: string;
|
||||
}) {
|
||||
return `${scope.pubkey.trim().toLowerCase()}:${scope.relayUrl
|
||||
.trim()
|
||||
.replace(/\/+$/, "")}`;
|
||||
}
|
||||
|
||||
function getMockObservedUnreadScope(scope: {
|
||||
pubkey: string;
|
||||
relayUrl: string;
|
||||
}) {
|
||||
const key = mockObservedUnreadScopeKey(scope);
|
||||
const existing = mockObservedUnreadScopes.get(key);
|
||||
if (existing) return existing;
|
||||
const created: MockObservedUnreadScope = {
|
||||
generation: "e2e",
|
||||
revision: 0,
|
||||
lastSequence: 0,
|
||||
migrationComplete: false,
|
||||
events: new Map(),
|
||||
channelLatest: new Map(),
|
||||
markers: new Map(),
|
||||
};
|
||||
mockObservedUnreadScopes.set(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
function mockObservedUnreadProjections(
|
||||
scope: MockObservedUnreadScope,
|
||||
): ObservedUnreadProjection[] {
|
||||
const channels = new Map<string, ObservedUnreadProjection>();
|
||||
for (const [channelId, latest] of scope.channelLatest) {
|
||||
channels.set(channelId, {
|
||||
channelId,
|
||||
latest,
|
||||
count: 0,
|
||||
badgeCount: 0,
|
||||
appBadgeCount: 0,
|
||||
topLevelUnread: false,
|
||||
highPriorityUnread: false,
|
||||
});
|
||||
}
|
||||
for (const event of scope.events.values()) {
|
||||
let readAt = Math.max(
|
||||
scope.markers.get(event.channelId) ?? 0,
|
||||
scope.markers.get(`msg:${event.id}`) ?? 0,
|
||||
);
|
||||
if (event.rootId) {
|
||||
readAt = Math.max(
|
||||
readAt,
|
||||
scope.markers.get(`thread:${event.rootId}`) ?? 0,
|
||||
);
|
||||
}
|
||||
if (event.createdAt <= readAt) continue;
|
||||
const projection = channels.get(event.channelId) ?? {
|
||||
channelId: event.channelId,
|
||||
latest: 0,
|
||||
count: 0,
|
||||
badgeCount: 0,
|
||||
appBadgeCount: 0,
|
||||
topLevelUnread: false,
|
||||
highPriorityUnread: false,
|
||||
};
|
||||
projection.latest = Math.max(projection.latest, event.createdAt);
|
||||
projection.count += 1;
|
||||
projection.badgeCount += event.countsTowardBadge ? 1 : 0;
|
||||
projection.appBadgeCount += event.countsTowardAppBadge ? 1 : 0;
|
||||
projection.topLevelUnread ||= event.rootId === null;
|
||||
projection.highPriorityUnread ||= event.highPriority;
|
||||
channels.set(event.channelId, projection);
|
||||
}
|
||||
return [...channels.values()].sort((left, right) =>
|
||||
left.channelId.localeCompare(right.channelId),
|
||||
);
|
||||
}
|
||||
|
||||
function resetMockObservedUnread() {
|
||||
mockObservedUnreadScopes.clear();
|
||||
}
|
||||
|
||||
function resetMockSaveSubscriptions(config: E2eConfig | undefined) {
|
||||
mockSaveSubscriptions = (config?.mock?.saveSubscriptions ?? []).map((s) => ({
|
||||
...s,
|
||||
@@ -3094,6 +3203,123 @@ function resetMockPersonaCatalogEvents(config: E2eConfig | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
function mockPersonaCatalogPublications() {
|
||||
const publications = [];
|
||||
const claimed = new Set<string>();
|
||||
for (const event of [...mockPersonaEvents].sort(
|
||||
(a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id),
|
||||
)) {
|
||||
const dTags = event.tags.filter((tag) => tag[0] === "d");
|
||||
if (dTags.length !== 1 || !dTags[0]?.[1]) continue;
|
||||
const sourcePersonaId = dTags[0][1];
|
||||
const ownerPubkey = event.pubkey.toLowerCase();
|
||||
const coordinate = `${ownerPubkey}:${sourcePersonaId}`;
|
||||
if (claimed.has(coordinate)) continue;
|
||||
claimed.add(coordinate);
|
||||
if (!personaHasExactSharedTag(event)) continue;
|
||||
let content: Record<string, unknown>;
|
||||
try {
|
||||
content = JSON.parse(event.content) as Record<string, unknown>;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const displayName = content.display_name;
|
||||
const systemPrompt = content.system_prompt ?? "";
|
||||
const optionalString = (value: unknown) =>
|
||||
typeof value === "string" && value.trim() ? value : null;
|
||||
const isExtendedPictographic = (value: string) =>
|
||||
/^\p{Extended_Pictographic}$/u.test(value);
|
||||
const hasValidVisibleText = (value: string, allowLayout: boolean) => {
|
||||
const characters = [...value];
|
||||
return characters.every((character, index) => {
|
||||
if (allowLayout && (character === "\n" || character === "\t"))
|
||||
return true;
|
||||
if (/\p{Control}/u.test(character)) return false;
|
||||
const codepoint = character.codePointAt(0) ?? 0;
|
||||
const defaultIgnorable =
|
||||
codepoint === 0x00ad ||
|
||||
codepoint === 0x034f ||
|
||||
codepoint === 0x061c ||
|
||||
(codepoint >= 0x115f && codepoint <= 0x1160) ||
|
||||
(codepoint >= 0x17b4 && codepoint <= 0x17b5) ||
|
||||
(codepoint >= 0x180b && codepoint <= 0x180f) ||
|
||||
(codepoint >= 0x200b && codepoint <= 0x200f) ||
|
||||
(codepoint >= 0x202a && codepoint <= 0x202e) ||
|
||||
(codepoint >= 0x2060 && codepoint <= 0x206f) ||
|
||||
codepoint === 0x3164 ||
|
||||
(codepoint >= 0xfe00 && codepoint <= 0xfe0f) ||
|
||||
codepoint === 0xfeff ||
|
||||
codepoint === 0xffa0 ||
|
||||
(codepoint >= 0xfff0 && codepoint <= 0xfff8) ||
|
||||
(codepoint >= 0x1bca0 && codepoint <= 0x1bca3) ||
|
||||
(codepoint >= 0x1d173 && codepoint <= 0x1d17a) ||
|
||||
(codepoint >= 0xe0000 && codepoint <= 0xe0fff);
|
||||
if (!defaultIgnorable) return true;
|
||||
if (character === "\ufe0f") {
|
||||
const previous = characters[index - 1];
|
||||
return (
|
||||
previous !== undefined &&
|
||||
(/^[#*0-9]$/u.test(previous) || isExtendedPictographic(previous))
|
||||
);
|
||||
}
|
||||
if (character !== "\u200d") return false;
|
||||
let previousIndex = index - 1;
|
||||
while (
|
||||
previousIndex >= 0 &&
|
||||
(characters[previousIndex] === "\ufe0f" ||
|
||||
/[\u{1f3fb}-\u{1f3ff}]/u.test(characters[previousIndex] ?? ""))
|
||||
) {
|
||||
previousIndex -= 1;
|
||||
}
|
||||
return (
|
||||
previousIndex >= 0 &&
|
||||
isExtendedPictographic(characters[previousIndex] ?? "") &&
|
||||
isExtendedPictographic(characters[index + 1] ?? "")
|
||||
);
|
||||
});
|
||||
};
|
||||
if (
|
||||
typeof displayName !== "string" ||
|
||||
!displayName.trim() ||
|
||||
[...displayName].length > 128 ||
|
||||
typeof systemPrompt !== "string" ||
|
||||
new TextEncoder().encode(systemPrompt).length > 64 * 1024 ||
|
||||
!hasValidVisibleText(displayName, false) ||
|
||||
!hasValidVisibleText(systemPrompt, true)
|
||||
)
|
||||
continue;
|
||||
publications.push({
|
||||
eventId: event.id,
|
||||
ownerPubkey,
|
||||
sourcePersonaId,
|
||||
createdAt: event.created_at,
|
||||
agent: {
|
||||
displayName,
|
||||
avatarUrl: optionalString(content.avatar_url),
|
||||
systemPrompt,
|
||||
runtime: optionalString(content.runtime),
|
||||
model: optionalString(content.model),
|
||||
provider: optionalString(content.provider),
|
||||
namePool: Array.isArray(content.name_pool)
|
||||
? content.name_pool.filter(
|
||||
(value): value is string => typeof value === "string",
|
||||
)
|
||||
: [],
|
||||
respondTo:
|
||||
content.respond_to === "allowlist"
|
||||
? "owner-only"
|
||||
: content.respond_to === "owner-only" ||
|
||||
content.respond_to === "anyone"
|
||||
? content.respond_to
|
||||
: null,
|
||||
parallelism:
|
||||
typeof content.parallelism === "number" ? content.parallelism : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
return publications;
|
||||
}
|
||||
|
||||
// Mesh-compute mock state — TEST-ONLY.
|
||||
//
|
||||
// This entire module (e2eBridge.ts) is loaded only when `window.__BUZZ_E2E__`
|
||||
@@ -10267,6 +10493,7 @@ export function maybeInstallE2eTauriMocks() {
|
||||
resetMockMesh();
|
||||
resetMockUserStatuses();
|
||||
resetMockPersonaCatalogEvents(config);
|
||||
resetMockObservedUnread();
|
||||
resetMockSaveSubscriptions(config);
|
||||
resetMockPendingCommunityDeepLinks(config);
|
||||
resetMockPendingNavigationDeepLinks(config);
|
||||
@@ -13377,43 +13604,144 @@ export function maybeInstallE2eTauriMocks() {
|
||||
case "start_archive_sync":
|
||||
case "stop_archive_sync":
|
||||
return null;
|
||||
case "fetch_persona_catalog":
|
||||
return mockPersonaCatalogPublications();
|
||||
case "observed_unread_open_scope": {
|
||||
const request = payload as {
|
||||
request: { scope: { pubkey: string; relayUrl: string } };
|
||||
request: {
|
||||
scope: { pubkey: string; relayUrl: string };
|
||||
legacyPayload?: {
|
||||
eventsByChannel?: Record<
|
||||
string,
|
||||
Array<{
|
||||
id: string;
|
||||
createdAt: number;
|
||||
rootId?: string | null;
|
||||
highPriority: boolean;
|
||||
countsTowardBadge: boolean;
|
||||
countsTowardAppBadge: boolean;
|
||||
}>
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
const scope = getMockObservedUnreadScope(request.request.scope);
|
||||
if (!scope.migrationComplete) {
|
||||
for (const [channelId, events] of Object.entries(
|
||||
request.request.legacyPayload?.eventsByChannel ?? {},
|
||||
)) {
|
||||
for (const event of events) {
|
||||
if (!scope.events.has(event.id)) {
|
||||
scope.events.set(event.id, {
|
||||
channelId,
|
||||
...event,
|
||||
rootId: event.rootId ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.migrationComplete = true;
|
||||
}
|
||||
return {
|
||||
kind: "snapshot",
|
||||
scope: request.request.scope,
|
||||
generation: "e2e",
|
||||
revision: 0,
|
||||
lastAckedSequence: 0,
|
||||
migrationComplete: true,
|
||||
generation: scope.generation,
|
||||
revision: scope.revision,
|
||||
lastAckedSequence: scope.lastSequence,
|
||||
migrationComplete: scope.migrationComplete,
|
||||
membershipSeeded: true,
|
||||
channels: [],
|
||||
};
|
||||
channels: mockObservedUnreadProjections(scope),
|
||||
} satisfies ObservedUnreadResponse;
|
||||
}
|
||||
case "observed_unread_ingest": {
|
||||
const request = payload as {
|
||||
const { request } = payload as {
|
||||
request: {
|
||||
scope: { pubkey: string; relayUrl: string };
|
||||
sequence: number;
|
||||
baseRevision: number;
|
||||
events: Array<{
|
||||
channelId: string;
|
||||
id: string;
|
||||
createdAt: number;
|
||||
rootId: string | null;
|
||||
highPriority: boolean;
|
||||
countsTowardBadge: boolean;
|
||||
countsTowardAppBadge: boolean;
|
||||
}>;
|
||||
channelLatest: Array<{ channelId: string; createdAt: number }>;
|
||||
markers: Array<{ contextId: string; readAt: number | null }>;
|
||||
membership: Array<{
|
||||
kind: string;
|
||||
value: string;
|
||||
present: boolean;
|
||||
}>;
|
||||
clearChannels: string[];
|
||||
clearAll: boolean;
|
||||
};
|
||||
};
|
||||
const scope = getMockObservedUnreadScope(request.scope);
|
||||
const before = new Map(
|
||||
mockObservedUnreadProjections(scope).map((item) => [
|
||||
item.channelId,
|
||||
item,
|
||||
]),
|
||||
);
|
||||
if (request.clearAll) {
|
||||
scope.events.clear();
|
||||
scope.channelLatest.clear();
|
||||
}
|
||||
for (const channelId of request.clearChannels) {
|
||||
scope.channelLatest.delete(channelId);
|
||||
for (const [id, event] of scope.events) {
|
||||
if (event.channelId === channelId) scope.events.delete(id);
|
||||
}
|
||||
}
|
||||
for (const latest of request.channelLatest ?? []) {
|
||||
scope.channelLatest.set(
|
||||
latest.channelId,
|
||||
Math.max(
|
||||
scope.channelLatest.get(latest.channelId) ?? 0,
|
||||
latest.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const event of request.events ?? []) {
|
||||
if (!scope.events.has(event.id)) scope.events.set(event.id, event);
|
||||
}
|
||||
for (const marker of request.markers ?? []) {
|
||||
if (marker.readAt === null) scope.markers.delete(marker.contextId);
|
||||
else
|
||||
scope.markers.set(
|
||||
marker.contextId,
|
||||
Math.max(scope.markers.get(marker.contextId) ?? 0, marker.readAt),
|
||||
);
|
||||
}
|
||||
const after = mockObservedUnreadProjections(scope);
|
||||
const afterIds = new Set(after.map((item) => item.channelId));
|
||||
const baseRevision = scope.revision;
|
||||
scope.revision += 1;
|
||||
scope.lastSequence = request.sequence;
|
||||
return {
|
||||
kind: "delta",
|
||||
scope: request.request.scope,
|
||||
generation: "e2e",
|
||||
baseRevision: request.request.baseRevision,
|
||||
revision: request.request.baseRevision + 1,
|
||||
ackedSequence: request.request.sequence,
|
||||
upserts: [],
|
||||
removed: [],
|
||||
};
|
||||
scope: request.scope,
|
||||
generation: scope.generation,
|
||||
baseRevision,
|
||||
revision: scope.revision,
|
||||
ackedSequence: request.sequence,
|
||||
upserts: after.filter(
|
||||
(item) =>
|
||||
JSON.stringify(before.get(item.channelId)) !==
|
||||
JSON.stringify(item),
|
||||
),
|
||||
removed: [...before.keys()].filter((id) => !afterIds.has(id)),
|
||||
} satisfies ObservedUnreadResponse;
|
||||
}
|
||||
case "unread_catch_up": {
|
||||
const request = payload as {
|
||||
request: { channels: Array<{ id: string }> };
|
||||
request: {
|
||||
channels: Array<{ id: string }>;
|
||||
selfPubkey: string;
|
||||
};
|
||||
};
|
||||
const results: UnreadCatchUpChannelResult[] =
|
||||
request.request.channels.map((channel) => ({
|
||||
@@ -13422,7 +13750,18 @@ export function maybeInstallE2eTauriMocks() {
|
||||
observedEvents: [],
|
||||
maxTrigger: 0,
|
||||
activityRows: [],
|
||||
discovered: { participated: [], authored: [], mentioned: [] },
|
||||
discovered: {
|
||||
participated: [],
|
||||
authored: getMockMessageStore(channel.id)
|
||||
.filter(
|
||||
(event) =>
|
||||
event.pubkey === request.request.selfPubkey &&
|
||||
getThreadReferenceFromTags(event.tags).parentEventId ===
|
||||
null,
|
||||
)
|
||||
.map((event) => event.id),
|
||||
mentioned: [],
|
||||
},
|
||||
}));
|
||||
// Keep this mock aligned with the complete Rust serde shape pinned by
|
||||
// `serialized_response_matches_the_typescript_contract`.
|
||||
|
||||
Reference in New Issue
Block a user