mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user