1use serde::Serialize;
18
19#[cfg(test)]
20use crate::backend::rpc::schema::{ts_type, TsType};
21
22pub trait Event {
24 const NAME: &'static str;
26 type Payload: Serialize + ts_rs::TS + 'static;
27}
28
29pub trait PerIdEvent {
31 const PREFIX: &'static str;
33 type Payload: Serialize + ts_rs::TS + 'static;
34}
35
36macro_rules! per_id_events {
37 ($($(#[$doc:meta])* $marker:ident: $prefix:expr => $payload:ty,)*) => {
38 $(
39 $(#[$doc])*
40 pub struct $marker;
41 impl PerIdEvent for $marker {
42 const PREFIX: &'static str = $prefix;
43 type Payload = $payload;
44 }
45 )*
46 #[cfg(test)]
48 pub fn per_id_schema() -> Vec<(&'static str, TsType)> {
49 vec![$(($prefix, ts_type::<$payload>())),*]
50 }
51 pub fn is_per_id_event(name: &str) -> bool {
53 $(name.starts_with(<$marker as PerIdEvent>::PREFIX) ||)* false
54 }
55 };
56}
57
58macro_rules! events {
59 ($($(#[$doc:meta])* $marker:ident: $name:expr => $payload:ty,)*) => {
60 $(
61 $(#[$doc])*
62 pub struct $marker;
63 impl Event for $marker {
64 const NAME: &'static str = $name;
65 type Payload = $payload;
66 }
67 )*
68 #[cfg(test)]
70 pub fn schema() -> Vec<(&'static str, TsType)> {
71 vec![$(($name, ts_type::<$payload>())),*]
72 }
73 pub fn is_typed_event(name: &str) -> bool {
75 $(name == <$marker as Event>::NAME ||)* false
76 }
77 };
78}
79
80events! {
81 ControllerStatus: super::mps::EVENT_CONTROLLER_STATUS => crate::backend::blockcontroller::BlockControllerRuntimeStatus,
83 SysInfo: super::mps::EVENT_SYS_INFO => crate::backend::rpc_types::TimeSeriesData,
85 AgentTurn: super::mps::EVENT_AGENT_TURN => crate::backend::blockcontroller::health::AgentTurnEvent,
87 AgentFailed: super::mps::EVENT_AGENT_FAILURE => crate::agents::failure::AgentFailure,
89 AgentRuntime: super::mps::EVENT_AGENT_RUNTIME => crate::backend::agent_runtime::AgentRuntimeEvent,
91 AgentContextUsageChanged: super::mps::EVENT_AGENT_CONTEXT_USAGE => crate::backend::agent_context_usage::AgentContextUsageEvent,
93 AgentMessageAccepted: super::mps::EVENT_AGENT_MESSAGE_ACCEPTED => crate::backend::blockcontroller::MessageAcceptedEvent,
95 AgentResumeRetry: super::mps::EVENT_AGENT_RESUME_RETRY => ResumeRetryEvent,
97 AgentShutdown: crate::sagas::close_pane::EVENT_AGENT_SHUTDOWN => crate::sagas::close_pane::ShutdownLineEvent,
99 UserAttention: super::mps::EVENT_USER_ATTENTION => crate::backend::user_attention::UserAttentionEvent,
101 SubagentActivity: "subagent:activity" => crate::backend::subagent_watcher::events::SubagentActivityEvent,
103 DispatchActivity: "dispatch:activity" => crate::backend::subagent_watcher::events::DispatchActivityEvent,
105 SubagentSpawned: "subagent:spawned" => crate::backend::subagent_watcher::events::SubagentSpawnedEvent,
106 SubagentCompleted: "subagent:completed" => crate::backend::subagent_watcher::events::SubagentCompletedEvent,
107 SubagentNamed: "subagent:named" => crate::backend::subagent_watcher::events::SubagentNamedEvent,
108 SubagentUpdated: "subagent:updated" => crate::backend::subagent_watcher::events::SubagentUpdatedEvent,
109 DispatchUpdated: "dispatch:updated" => crate::backend::subagent_watcher::events::DispatchUpdatedEvent,
110 SubagentAbandoned: "subagent:abandoned" => crate::backend::subagent_watcher::events::SubagentAbandonedEvent,
111 SubagentBlockPruned: "subagent:block_pruned" => crate::backend::subagent_watcher::events::SubagentBlockPrunedEvent,
112 SubagentBackfillStatus: super::mps::EVENT_SUBAGENT_BACKFILL_STATUS => crate::backend::subagent_watcher::events::SubagentBackfillStatusEvent,
114 BlockFile: super::mps::EVENT_BLOCK_FILE => super::mps::WSFileEventData,
116 BlockStats: super::mps::EVENT_BLOCK_STATS => crate::backend::rpc_types::TimeSeriesData,
118 MuxObjUpdated: super::mps::EVENT_MUX_OBJ_UPDATE => crate::backend::obj::MuxObjUpdate,
120 EditorFileChanged: crate::backend::editor_file_watcher::EVENT_EDITOR_FILE_CHANGED => PathChangedEvent,
122 MediaFileChanged: crate::backend::media_file_watcher::EVENT_MEDIA_FILE_CHANGED => PathChangedEvent,
124 FilesChanged: crate::backend::files_watcher::EVENT_FILES_CHANGED => FilesChangedEvent,
126 FilesOp: crate::backend::fs_ops::jobs::EVENT_FILES_OP => crate::backend::rpc_types::FsOpEvent,
128 InstallChunk: "install_chunk" => crate::server::install_handlers::InstallChunkEvent,
130 ShellNodeCreate: super::mps::EVENT_SHELL_NODE_CREATE => crate::backend::shell_node::ShellNodeCreateEvent,
132 ShellChunk: super::mps::EVENT_SHELL_CHUNK => crate::backend::shell_node::ShellChunkEvent,
134 AttachmentProgress: super::mps::EVENT_ATTACHMENT_PROGRESS => crate::backend::rpc_types::AttachmentProgressEvent,
136 AttachmentReady: super::mps::EVENT_ATTACHMENT_READY => crate::backend::rpc_types::AttachmentReadyEvent,
137 AttachmentFailed: super::mps::EVENT_ATTACHMENT_FAILED => crate::backend::rpc_types::AttachmentFailedEvent,
138 AttachmentBatchDone: super::mps::EVENT_ATTACHMENT_BATCH_DONE => crate::backend::rpc_types::AttachmentBatchDoneEvent,
139 ConnChange: super::mps::EVENT_CONN_CHANGE => crate::backend::rpc_types::ConnStatus,
141 BackgroundTaskUpdated: "background-task-updated" => BlockRefEvent,
143 AmbientNarration: "ambient-narration" => AmbientNarrationEvent,
145 McpObserved: "mcp:observed" => BlockRefEvent,
147 NotificationState: crate::backend::notify::router::EVENT_NOTIFICATION_STATE => crate::backend::notify::policy::TrayState,
149 AgentProgress: crate::backend::reactive::progress_watcher::EVENT_AGENT_PROGRESS => AgentProgressEvent,
151 WidgetPackages: super::mps::EVENT_WIDGET_PACKAGES => WidgetPackagesEvent,
153 WidgetStorage: super::mps::EVENT_WIDGET_STORAGE => WidgetStorageEvent,
155 WidgetRequests: super::mps::EVENT_WIDGET_REQUESTS => WidgetRequestsEvent,
157 ProcessStatusChanged: crate::broker::process::EVENT_STATUS_CHANGED => crate::broker::process::ProcessStatus,
159 IdentityNoDirectLinks: "identity:no-direct-links" => NoDirectLinksEvent,
161 BtwAnswerChunk: super::mps::EVENT_BTW_ANSWER_CHUNK => BtwAnswerChunkEvent,
163 DockClear: super::mps::EVENT_DOCK_CLEAR => DockClearEvent,
165 AgentReactiveRegistered: "agent:reactive-registered" => BlockRefEvent,
167 AgentReactiveUnregistered: "agent:reactive-unregistered" => BlockRefEvent,
168 AppServerOutput: "output" => ProtocolFrame,
170 SrvInfo: "srvinfo" => crate::srv_info::SrvInfo,
172 Config: super::mps::EVENT_CONFIG => ConfigEvent,
174 AmbientSpent: crate::ambient::spend::EVENT_AMBIENT_SPENT => crate::ambient::spend::AmbientSpentEvent,
176 AgentProcessAdded: "agent:process-added" => ProcessAddedEvent,
178 AgentProcessExited: "agent:process-exited" => ProcessExitedEvent,
179 AgentShutdownPending: crate::sagas::pending_shutdown::EVENT_SHUTDOWN_PENDING => crate::sagas::pending_shutdown::PendingView,
181 AgentShutdownPendingCleared: crate::sagas::pending_shutdown::EVENT_SHUTDOWN_PENDING_CLEARED => crate::sagas::pending_shutdown::PendingClearedEvent,
182 Notification: crate::backend::notify::router::EVENT_NOTIFICATION => crate::backend::notify::policy::Notification,
184 NotificationRetract: crate::backend::notify::router::EVENT_NOTIFICATION_RETRACT => crate::backend::notify::router::NotificationRetractEvent,
185 NotificationActivate: crate::backend::notify::router::EVENT_NOTIFICATION_ACTIVATE => crate::backend::notify::router::NotificationActivateEvent,
186 OpenFloatingPane: "openfloatingpane" => OpenFloatingPaneEvent,
188 LanInstances: "laninstances" => crate::backend::lan_discovery::LanInstanceList,
190 LanInstancesError: "laninstances:error" => crate::backend::lan_discovery::LanDiscoveryError,
192 LanInstancesHealth: "laninstances:health" => crate::backend::lan_discovery::LanDiscoverability,
194 LanInstancesFirewall: "laninstances:firewall" => crate::backend::lan_firewall::LanFirewall,
196 MuxObjBatchedUpdates: super::eventbus::WS_EVENT_MUX_OBJ_BATCHED_UPDATES => crate::backend::obj::MuxObjBatch,
198 InstallProgress: super::mps::EVENT_INSTALL_PROGRESS => InstallProgressEvent,
200 BlockActivity: super::mps::EVENT_BLOCK_ACTIVITY => BlockActivityEvent,
202 LspMessage: "lsp:message" => LspMessageEvent,
204 MuxbusStatus: crate::muxbus::delivery_status::EVENT_MUXBUS_STATUS => crate::backend::rpc_types::MuxBusDeliveryStatus,
206 ViewerPaired: "viewer:paired" => ViewerPairedEvent,
208 BlockReveal: crate::backend::notify::router::EVENT_BLOCK_REVEAL => crate::backend::notify::router::BlockRevealEvent,
210
211 ToolChunk: "tool_chunk" => agentmux_common::client_events::ToolChunk,
214 CompactionStarted: "compaction_started" => agentmux_common::client_events::CompactionStarted,
216 SingletonClaim: "singleton:claim" => SingletonClaimEvent,
218
219 AgentsChanged: "agents:changed" => (),
222 AgentContentChanged: "agentcontent:changed" => (),
224 AgentHistoryChanged: "agenthistory:changed" => (),
226 NamedAgentsChanged: "namedagents:changed" => (),
228 AgentSkillsChanged: "agentskills:changed" => (),
230 MemoriesChanged: "memories:changed" => (),
232 SkillsChanged: "skills:changed" => (),
234 McpChanged: "mcp:changed" => (),
236 IdentityAccountsChanged: "identityaccounts:changed" => (),
238 CronChanged: super::mps::EVENT_CRON_CHANGED => (),
240 DronesChanged: "drones:changed" => (),
242 RemotesChanged: super::mps::EVENT_REMOTES_CHANGE => (),
244 TrackedBlocksChanged: crate::broker::process::EVENT_TRACKED_BLOCKS_CHANGED => (),
246 WorkQueueChanged: "workqueue:changed" => (),
248}
249
250per_id_events! {
251 AgentMemoryChanged: "agent:memory:changed:" => (),
253 AgentIdentitiesChanged: "agentidentities:changed:" => (),
255 AgentInstancesChanged: "agentinstances:changed:" => (),
257 AgentCredentialsRevoked: "agentcredentials:revoked:" => CredentialsRevokedEvent,
259 DroneRun: "dronerun:" => crate::drone::executor::engine::RunEvent,
261 AgentEventStream: "agent_event:" => crate::agents::types::AgentEvent,
263}
264
265macro_rules! client_events {
266 ($($marker:ident,)*) => {
267 pub fn publish_client_event(
270 broker: &super::mps::Broker,
271 name: &str,
272 data: &serde_json::Value,
273 scopes: &[String],
274 persist: usize,
275 ) -> Option<Result<(), String>> {
276 $(
277 if name == <$marker as Event>::NAME {
278 return Some(match <<$marker as Event>::Payload as serde::Deserialize>::deserialize(data) {
279 Ok(payload) => {
280 broker.publish_event::<$marker>(&payload, scopes.to_vec(), persist);
281 Ok(())
282 }
283 Err(e) => Err(format!("{name}: {e}")),
284 });
285 }
286 )*
287 None
288 }
289 pub fn is_client_event(name: &str) -> bool {
291 $(name == <$marker as Event>::NAME ||)* false
292 }
293 };
294}
295
296client_events! {
300 ToolChunk,
301 CompactionStarted,
302 SingletonClaim,
303}
304
305pub fn is_srv_event(name: &str) -> bool {
308 (is_typed_event(name) || is_per_id_event(name)) && !is_client_event(name)
309}
310
311pub fn payload_data<P: Serialize>(payload: &P) -> Option<serde_json::Value> {
314 serde_json::to_value(payload).ok().filter(|v| !v.is_null())
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize, ts_rs::TS)]
320#[ts(export, export_to = "../../../frontend/types/rpc/")]
321pub struct SingletonClaimEvent {
322 pub kind: String,
323 pub holder: Option<String>,
325 #[ts(type = "number")]
327 pub epoch: u64,
328}
329
330#[derive(Debug, Clone, Serialize, ts_rs::TS)]
332#[ts(export, export_to = "../../../frontend/types/rpc/")]
333pub struct PathChangedEvent {
334 pub path: String,
335}
336
337#[derive(Debug, Clone, Serialize, ts_rs::TS)]
339#[ts(export, export_to = "../../../frontend/types/rpc/")]
340pub struct BlockRefEvent {
341 pub block_id: String,
342}
343
344#[derive(Debug, Clone, Serialize, ts_rs::TS)]
346#[ts(export, export_to = "../../../frontend/types/rpc/")]
347pub struct AmbientNarrationEvent {
348 pub block_id: String,
349 pub kind: String,
350 pub text: String,
351}
352
353#[derive(Debug, Clone, Serialize, ts_rs::TS)]
355#[ts(export, export_to = "../../../frontend/types/rpc/")]
356#[serde(rename_all = "camelCase")]
357pub struct AgentProgressEvent {
358 pub agent_id: String,
359 pub block_id: String,
360 pub todos: Vec<crate::backend::reactive::progress_watcher::TodoItem>,
361 pub todos_truncated: usize,
363 pub todos_partial: bool,
364 pub current_tool: Option<String>,
365 #[ts(type = "number")]
366 pub ts: u64,
367}
368
369#[derive(Debug, Clone, Serialize, ts_rs::TS)]
371#[ts(export, export_to = "../../../frontend/types/rpc/")]
372pub struct WidgetPackagesEvent {
373 pub packages: Vec<crate::backend::widget_packages::WidgetPackageInfo>,
374}
375
376#[derive(Debug, Clone, Serialize, ts_rs::TS)]
378#[ts(export, export_to = "../../../frontend/types/rpc/")]
379pub struct WidgetStorageEvent {
380 pub id: String,
381 pub keys: Vec<String>,
382}
383
384#[derive(Debug, Clone, Serialize, ts_rs::TS)]
386#[ts(export, export_to = "../../../frontend/types/rpc/")]
387pub struct WidgetRequestsEvent {
388 pub requests: Vec<crate::backend::widget_requests::WidgetInstallRequest>,
389}
390
391#[derive(Debug, Clone, Serialize, ts_rs::TS)]
393#[ts(export, export_to = "../../../frontend/types/rpc/")]
394pub struct NoDirectLinksEvent {
395 pub definition_id: String,
396 pub identity_id: String,
397}
398
399#[derive(Debug, Clone, Serialize, ts_rs::TS)]
402#[ts(export, export_to = "../../../frontend/types/rpc/")]
403#[serde(rename_all = "camelCase")]
404pub struct BtwAnswerChunkEvent {
405 pub block_id: String,
406 pub request_id: String,
407 pub event: crate::agents::types::AgentEvent,
408 pub done: bool,
409}
410
411#[derive(Debug, Clone, Serialize, ts_rs::TS)]
413#[ts(export, export_to = "../../../frontend/types/rpc/")]
414pub struct DockClearEvent {
415 pub node_id: String,
416}
417
418#[derive(Debug, Clone, Serialize, ts_rs::TS)]
420#[ts(export, export_to = "../../../frontend/types/rpc/")]
421pub struct ConfigEvent {
422 #[ts(type = "FullConfigType")]
423 pub fullconfig: serde_json::Value,
424}
425
426#[derive(Debug, Clone, Serialize, ts_rs::TS)]
428#[ts(export, export_to = "../../../frontend/types/rpc/")]
429pub struct InstallProgressEvent {
430 pub message: String,
431}
432
433#[derive(Debug, Clone, Serialize, ts_rs::TS)]
435#[ts(export, export_to = "../../../frontend/types/rpc/")]
436#[serde(rename_all = "camelCase")]
437pub struct BlockActivityEvent {
438 pub block_id: String,
439 pub activity: String,
440}
441
442#[derive(Debug, Clone, Serialize, ts_rs::TS)]
444#[ts(export, export_to = "../../../frontend/types/rpc/")]
445pub struct LspMessageEvent {
446 pub server_id: String,
447 #[ts(type = "unknown")]
448 pub message: serde_json::Value,
449}
450
451#[derive(Debug, Clone, Serialize, ts_rs::TS)]
453#[ts(export, export_to = "../../../frontend/types/rpc/")]
454pub struct ViewerPairedEvent {
455 pub device_id: String,
456 pub device_name: String,
457}
458
459#[derive(Debug, Clone, Serialize, ts_rs::TS)]
461#[ts(export, export_to = "../../../frontend/types/rpc/")]
462pub struct OpenFloatingPaneEvent {
463 pub block_id: String,
464 pub workspace_id: String,
465}
466
467#[derive(Debug, Clone, Serialize, ts_rs::TS)]
469#[ts(export, export_to = "../../../frontend/types/rpc/")]
470pub struct ProcessAddedEvent {
471 pub block_id: String,
472 pub process: crate::backend::process_tracker::TrackedProcess,
473}
474
475#[derive(Debug, Clone, Serialize, ts_rs::TS)]
477#[ts(export, export_to = "../../../frontend/types/rpc/")]
478pub struct ProcessExitedEvent {
479 pub block_id: String,
480 pub pid: u32,
481}
482
483#[derive(Debug, Clone, Serialize, ts_rs::TS)]
485#[ts(export, export_to = "../../../frontend/types/rpc/")]
486pub struct ProtocolFrame(#[ts(type = "unknown")] pub serde_json::Value);
487
488#[derive(Debug, Clone, Serialize, ts_rs::TS)]
490#[ts(export, export_to = "../../../frontend/types/rpc/")]
491#[serde(rename_all = "camelCase")]
492pub struct CredentialsRevokedEvent {
493 #[ts(type = "true")]
495 pub credentials_revoked: bool,
496 pub provider: String,
497 #[serde(skip_serializing_if = "Option::is_none")]
499 #[ts(optional)]
500 pub account_id: Option<String>,
501}
502
503#[derive(Debug, Clone, Serialize, ts_rs::TS)]
505#[ts(export, export_to = "../../../frontend/types/rpc/")]
506pub struct FilesChangedEvent {
507 pub dir: String,
508}
509
510#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
512#[ts(export, export_to = "../../../frontend/types/rpc/")]
513pub struct ResumeRetryEvent {
514 #[ts(type = r#""retrying" | "resolved""#)]
515 pub status: String,
516 #[serde(rename = "startedAt", skip_serializing_if = "Option::is_none")]
518 #[ts(rename = "startedAt", optional)]
519 pub started_at: Option<String>,
520}
521
522#[cfg(test)]
525pub fn schema_json() -> serde_json::Value {
526 let mut rows: Vec<(&str, TsType, bool)> = schema().into_iter().map(|(n, t)| (n, t, false)).collect();
527 rows.extend(per_id_schema().into_iter().map(|(n, t)| (n, t, true)));
528 rows.sort_by(|a, b| a.0.cmp(b.0));
529 serde_json::Value::Array(
530 rows.into_iter()
531 .map(|(name, ts, per_id)| {
532 let mut row = serde_json::json!({ "event": name, "payloadTs": { "name": ts.name, "imports": ts.imports } });
533 if per_id {
534 row["perId"] = true.into();
535 }
536 row
537 })
538 .collect(),
539 )
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn each_event_is_listed_once() {
548 let names: Vec<&str> = schema().into_iter().chain(per_id_schema()).map(|(n, _)| n).collect();
549 let mut sorted = names.clone();
550 sorted.sort();
551 sorted.dedup();
552 assert_eq!(sorted.len(), names.len(), "an event listed twice: {names:?}");
553 }
554
555 #[test]
556 fn payloads_that_replaced_json_serialize_the_same() {
557 let progress = AgentProgressEvent {
558 agent_id: "a1".into(),
559 block_id: "b1".into(),
560 todos: vec![],
561 todos_truncated: 2,
562 todos_partial: false,
563 current_tool: None,
564 ts: 5,
565 };
566 assert_eq!(
567 serde_json::to_value(&progress).unwrap(),
568 serde_json::json!({ "agentId": "a1", "blockId": "b1", "todos": [], "todosTruncated": 2, "todosPartial": false, "currentTool": null, "ts": 5 })
569 );
570 let chunk = BtwAnswerChunkEvent {
571 block_id: "b1".into(),
572 request_id: "r1".into(),
573 event: crate::agents::types::AgentEvent::Error { message: "no".into() },
574 done: true,
575 };
576 assert_eq!(
577 serde_json::to_value(&chunk).unwrap(),
578 serde_json::json!({ "blockId": "b1", "requestId": "r1", "event": { "type": "error", "message": "no" }, "done": true })
579 );
580 let frame = ProtocolFrame(serde_json::json!({ "method": "turn/started" }));
581 assert_eq!(serde_json::to_value(&frame).unwrap(), serde_json::json!({ "method": "turn/started" }));
582 }
583
584 #[test]
585 fn lan_payloads_serialize_as_the_json_they_replace() {
586 use crate::backend::eventbus::EventBus;
587 use crate::backend::lan_discovery::{LanDiscoverability, LanDiscoveryError, LanInstanceList};
588 let empty = EventBus::typed_ws_event::<LanInstances>(&LanInstanceList(Vec::new()), String::new());
589 assert_eq!((empty.eventtype.as_str(), empty.data), ("laninstances", Some(serde_json::json!([]))));
590 let error = LanDiscoveryError { error: "bind failed".into() };
591 assert_eq!(serde_json::to_value(&error).unwrap(), serde_json::json!({ "error": "bind failed" }));
592 let health = LanDiscoverability { state: "degraded".into(), missing: vec!["192.0.2.7".into()], rebuilds: 1 };
593 assert_eq!(serde_json::to_value(&health).unwrap(), serde_json::json!({ "state": "degraded", "missing": ["192.0.2.7"], "rebuilds": 1 }));
594 }
595
596 #[test]
597 fn raw_event_payloads_serialize_as_the_json_they_replace() {
598 use crate::backend::eventbus::EventBus;
599 let progress = EventBus::typed_ws_event::<InstallProgress>(&InstallProgressEvent { message: "npm i".into() }, "block:b1");
600 assert_eq!((progress.eventtype.as_str(), progress.oref.as_str()), ("install_progress", "block:b1"));
601 assert_eq!(progress.data, Some(serde_json::json!({ "message": "npm i" })));
602 let activity = BlockActivityEvent { block_id: "b1".into(), activity: "auth refactor".into() };
603 assert_eq!(serde_json::to_value(&activity).unwrap(), serde_json::json!({ "blockId": "b1", "activity": "auth refactor" }));
604 let lsp = LspMessageEvent { server_id: "rust".into(), message: serde_json::json!({ "id": 1 }) };
605 assert_eq!(serde_json::to_value(&lsp).unwrap(), serde_json::json!({ "server_id": "rust", "message": { "id": 1 } }));
606 let paired = ViewerPairedEvent { device_id: "d1".into(), device_name: "Phone".into() };
607 assert_eq!(serde_json::to_value(&paired).unwrap(), serde_json::json!({ "device_id": "d1", "device_name": "Phone" }));
608 let update = crate::backend::obj::MuxObjUpdate { updatetype: "delete".into(), otype: "tab".into(), oid: "t1".into(), obj: None };
610 let batch = EventBus::typed_ws_event::<MuxObjBatchedUpdates>(&crate::backend::obj::MuxObjBatch(vec![update]), String::new());
611 assert_eq!(batch.eventtype, "waveobj:batchedupdates");
612 assert_eq!(batch.data, Some(serde_json::json!([{ "updatetype": "delete", "otype": "tab", "oid": "t1" }])));
613 }
614
615 #[test]
616 fn a_typed_frame_is_the_eventrecv_frame_srv_wrote_by_hand() {
617 let frame = crate::backend::eventbus::EventBus::typed_frame::<Config>(&ConfigEvent { fullconfig: serde_json::json!({ "settings": {} }) });
618 assert_eq!(
619 frame,
620 serde_json::json!({ "eventtype": "rpc", "data": { "command": "eventrecv", "data": { "event": "config", "data": { "fullconfig": { "settings": {} } } } } })
621 );
622 let activate = crate::backend::notify::router::NotificationActivateEvent {
623 id: "n1".into(),
624 block_id: "b1".into(),
625 tab_id: "t1".into(),
626 window_id: "w1".into(),
627 workspace_id: None,
628 at_ms: 7,
629 };
630 assert_eq!(
631 serde_json::to_value(&activate).unwrap(),
632 serde_json::json!({ "id": "n1", "block_id": "b1", "tab_id": "t1", "window_id": "w1", "workspace_id": null, "at_ms": 7 })
633 );
634 }
635
636 #[test]
637 fn a_client_event_is_published_only_with_its_payload() {
638 use super::super::mps::Broker;
639 use serde_json::json;
640 let broker = Broker::new();
641 let scopes = vec!["block:b1".to_string()];
642 let chunk = json!({ "op": "chunk", "tool_id": "toolu_1", "kind": "stdout", "content": "hi\n", "timestamp": 5 });
643 assert_eq!(publish_client_event(&broker, "tool_chunk", &chunk, &scopes, 8), Some(Ok(())));
644 let stored = broker.read_event_history("tool_chunk", "block:b1", 8);
645 assert_eq!(stored.len(), 1);
646 assert_eq!(stored[0].data, Some(chunk), "goes out as it came in");
647
648 let wrong = json!({ "op": "chunk", "tool_id": "toolu_1", "kind": "tty", "content": "", "timestamp": 0 });
649 let refused = publish_client_event(&broker, "tool_chunk", &wrong, &scopes, 8).expect("a client event");
650 assert!(refused.unwrap_err().starts_with("tool_chunk: unknown variant `tty`"));
651 let claim = json!({ "kind": "bundles", "holder": "main", "epoch": "1" });
652 assert!(publish_client_event(&broker, "singleton:claim", &claim, &["singleton:bundles".to_string()], 1).unwrap().is_err());
653 assert_eq!(broker.read_event_history("tool_chunk", "block:b1", 8).len(), 1, "a refused event isn't stored");
654
655 assert_eq!(publish_client_event(&broker, "someones:event", &json!({ "any": 1 }), &scopes, 1), None, "not a client event");
656 }
657
658 #[test]
659 fn srv_events_are_the_declared_ones_clients_dont_publish() {
660 assert!(is_srv_event("controllerstatus"));
661 assert!(is_srv_event("agents:changed"), "a signal with no payload too");
662 assert!(is_srv_event("agent_event:b1"), "a per-id event, by its prefix");
663 assert!(is_srv_event("dronerun:r1"));
664 for client in ["tool_chunk", "compaction_started", "singleton:claim"] {
665 assert!(is_typed_event(client) && !is_srv_event(client), "{client} is a client's");
666 }
667 assert!(!is_srv_event("someones:event"), "an undeclared event is relayed");
668 assert!(!is_srv_event("agent_event"), "a prefix needs its id");
669 }
670
671 #[test]
672 fn a_released_singleton_claim_names_no_holder() {
673 let release = SingletonClaimEvent { kind: "bundles".into(), holder: None, epoch: 3 };
674 assert_eq!(serde_json::to_value(&release).unwrap(), serde_json::json!({ "kind": "bundles", "holder": null, "epoch": 3 }));
675 }
676
677 #[test]
678 fn payloads_name_their_generated_types() {
679 let rows = schema();
680 let sys = rows.iter().find(|(n, _)| *n == "sysinfo").expect("sysinfo");
681 assert_eq!(sys.1.name, "TimeSeriesData");
682 assert_eq!(sys.1.imports, vec!["TimeSeriesData".to_string()]);
683 }
684}