fix(desktop): pin native catch-up wire contract

Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
This commit is contained in:
Perci
2026-08-16 03:05:19 -04:00
parent 15f51f0412
commit 66cf8d50e0
4 changed files with 154 additions and 14 deletions
@@ -190,3 +190,46 @@ fn exact_tags_reject_duplicates_and_extra_fields() {
assert_eq!(coordinate_tag(&extended, "d").as_deref(), Some("reviewer"));
assert_eq!(exact_tag(&extended, "shared"), None);
}
/// Pins the serialized DTO output against the renderer's catalog contract.
/// The Tauri generic is only a TypeScript assertion; serde's bytes are the
/// actual boundary, so populate every optional field and compare the value.
#[test]
fn serialized_catalog_matches_the_typescript_contract() {
let publication = PersonaCatalogPublication {
event_id: "ev1".into(),
owner_pubkey: "owner".into(),
source_persona_id: "persona-1".into(),
created_at: 42,
agent: CatalogAgentProjection {
display_name: "Ada".into(),
avatar_url: Some("https://example.com/a.png".into()),
system_prompt: "be kind".into(),
runtime: Some("acp".into()),
model: Some("m1".into()),
provider: Some("p1".into()),
name_pool: vec!["Ada".into(), "Lin".into()],
respond_to: Some("mentions".into()),
parallelism: Some(2),
},
};
let actual = serde_json::to_value(vec![publication]).unwrap();
let expected = serde_json::json!([{
"eventId": "ev1",
"ownerPubkey": "owner",
"sourcePersonaId": "persona-1",
"createdAt": 42,
"agent": {
"displayName": "Ada",
"avatarUrl": "https://example.com/a.png",
"systemPrompt": "be kind",
"runtime": "acp",
"model": "m1",
"provider": "p1",
"namePool": ["Ada", "Lin"],
"respondTo": "mentions",
"parallelism": 2,
},
}]);
assert_eq!(actual, expected);
}
+98 -10
View File
@@ -53,7 +53,11 @@ pub(crate) struct UnreadCatchUpResponse {
}
#[derive(Serialize)]
#[serde(tag = "status", rename_all = "camelCase")]
#[serde(
tag = "status",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
enum ChannelResult {
Success {
channel_id: String,
@@ -271,14 +275,7 @@ fn classify_batch(
.channel
.read_at
.is_some_and(|read_at| event.created_at <= read_at)
|| !should_notify(
&event,
&item.channel.id,
&self_pubkey,
request,
&participated,
&authored,
)
|| !should_notify(&event, &self_pubkey, request, &participated, &authored)
{
continue;
}
@@ -384,7 +381,6 @@ fn thread_reference(tags: &[Vec<String>]) -> ThreadReference {
fn should_notify(
event: &EventView,
_channel_id: &str,
self_pubkey: &str,
request: &UnreadCatchUpRequest,
participated: &HashSet<String>,
@@ -556,4 +552,96 @@ mod tests {
);
assert_eq!(*max_trigger, 12);
}
/// Pins the SERIALIZED wire contract against `tauriUnreadCatchUp.ts`.
///
/// Asserts on serde's OUTPUT, not on `ChannelResult`: the renderer never
/// sees the Rust type, it sees bytes, through an `invokeTauri<T>` cast
/// that validates nothing. Every other test here inspects the enum before
/// serialization and the e2e bridge hand-writes the intended shape, so
/// without this nothing compares what Rust emits to what TypeScript
/// declares.
///
/// Whole-value rather than a key list, deliberately: a key-set assertion
/// passes a mutant that drops the variant rename and emits `"Success"`,
/// which the renderer's `status === "error"` branch silently misreads.
/// Failure here means the merge loop throws on the first success row and
/// catch-up yields nothing, silently.
#[test]
fn serialized_response_matches_the_typescript_contract() {
let channels = vec![
ChannelResult::Success {
channel_id: "ch".into(),
observed_events: vec![ObservedUnreadEvent {
id: "evt".into(),
created_at: 11,
root_id: Some("root".into()),
high_priority: true,
counts_toward_badge: true,
counts_toward_app_badge: false,
}],
max_trigger: 11,
activity_rows: vec![ActivityRow {
id: "evt".into(),
kind: 9,
pubkey: "other".into(),
content: "hi".into(),
created_at: 11,
channel_id: "ch".into(),
channel_name: "Ch".into(),
tags: vec![vec!["h".into(), "ch".into()]],
}],
discovered: DiscoveredRoots {
participated: vec!["root".into()],
authored: Vec::new(),
mentioned: Vec::new(),
},
},
ChannelResult::Error {
channel_id: "ch-2".into(),
error: "relay request timed out".into(),
},
];
let actual = serde_json::to_value(UnreadCatchUpResponse { channels }).unwrap();
let expected = serde_json::json!({
"channels": [
{
"status": "success",
"channelId": "ch",
"observedEvents": [{
"id": "evt",
"createdAt": 11,
"rootId": "root",
"highPriority": true,
"countsTowardBadge": true,
"countsTowardAppBadge": false,
}],
"maxTrigger": 11,
"activityRows": [{
"id": "evt",
"kind": 9,
"pubkey": "other",
"content": "hi",
"createdAt": 11,
"channelId": "ch",
"channelName": "Ch",
"tags": [["h", "ch"]],
}],
"discovered": {
"participated": ["root"],
"authored": [],
"mentioned": [],
},
},
{
"status": "error",
"channelId": "ch-2",
"error": "relay request timed out",
},
]
});
assert_eq!(actual, expected);
}
}
@@ -590,6 +590,10 @@ export function useUnreadChannels(
const authoredSizeBefore = authoredRootIdsRef.current.size;
const mentionedSizeBefore = mentionedRootIdsRef.current.size;
// Membership remains renderer-owned until E's native observed-unread store,
// so unchanged sets cross IPC on every catch-up. The five 1,000-entry
// stores bound that interim cost at roughly 332 KiB per request. Command
// arguments use a fetch body, so this cost is linear with no size cliff.
void unreadCatchUp({
channels: toFetch.map((channelId) => {
const channel = channels.find(
@@ -621,6 +625,8 @@ export function useUnreadChannels(
const allThreadReplies: ThreadActivityItem[] = [];
for (const result of results) {
if (result.status === "error") {
// The error arm carries only this identity; releasing its claim is
// what lets a failed channel retry on the next effect run.
caughtUpChannelsRef.current.delete(result.channelId);
continue;
}
+7 -4
View File
@@ -10,6 +10,7 @@ import {
handleDeleteCustomHarness,
} from "./e2eBridgeCustomHarnesses.ts";
import type { UnreadCatchUpChannelResult } from "@/shared/api/tauriUnreadCatchUp";
import { relayClient } from "@/shared/api/relayClient";
import { activateRateLimit } from "@/shared/api/relayRateLimitGate";
import { resolveAgentParallelism } from "@/features/agents/lib/agentParallelism";
@@ -13380,16 +13381,18 @@ export function maybeInstallE2eTauriMocks() {
const request = payload as {
request: { channels: Array<{ id: string }> };
};
return {
channels: request.request.channels.map((channel) => ({
const results: UnreadCatchUpChannelResult[] =
request.request.channels.map((channel) => ({
status: "success",
channelId: channel.id,
observedEvents: [],
maxTrigger: 0,
activityRows: [],
discovered: { participated: [], authored: [], mentioned: [] },
})),
};
}));
// Keep this mock aligned with the complete Rust serde shape pinned by
// `serialized_response_matches_the_typescript_contract`.
return { channels: results };
}
case "agent_metric_archive_default_enabled":
return activeConfig?.mock?.agentMetricArchiveDefaultEnabled ?? true;