1use std::path::Path;
7
8use serde_json::Value;
9
10use crate::codec::{FrameReader, Inbound, MAX_FRAME_BYTES, write_frame};
11use crate::protocol::{
12 AuditSummaryResult, BackendSpec, BlobFetchParams, BlobFetchResult, BlobGrantParams,
13 BlobPublishParams, BlobPublishResult, BlobScopeList, Hello, InviteParams, InviteResult,
14 OpenSessionParams, OrgJoinParams, OrgJoinResult, PairParams, PairResult, PeerRemoveParams,
15 PeerRenameParams, RegisterServiceParams, Request, RosterInstallParams, RosterInstallResult,
16 SetAppMetadataParams, SetNicknameParams, SetRosterUrlParams, StatusResult, StreamFrame,
17};
18use crate::transport::{connect_local, split_local};
19
20pub type ControlRead = Box<dyn tokio::io::AsyncRead + Send + Unpin>;
24pub type ControlWrite = Box<dyn tokio::io::AsyncWrite + Send + Unpin>;
26
27pub struct ControlClient {
29 hello: Hello,
30 reader: FrameReader<ControlRead>,
31 writer: ControlWrite,
32}
33
34impl std::fmt::Debug for ControlClient {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("ControlClient")
39 .field("hello", &self.hello)
40 .finish_non_exhaustive()
41 }
42}
43
44#[derive(Debug)]
51pub enum ClientError {
52 Io(std::io::Error),
53 Closed(&'static str),
54 Malformed(&'static str),
55 WrongApi { got: String, want: &'static str },
56 Api(Value),
57}
58
59impl std::fmt::Display for ClientError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 ClientError::Io(err) => write!(f, "io: {err}"),
63 ClientError::Closed(what) => write!(f, "connection closed before {what}"),
64 ClientError::Malformed(what) => write!(f, "malformed {what} frame"),
65 ClientError::WrongApi { got, want } => {
66 write!(f, "unexpected api: got {got:?}, want {want:?}")
67 }
68 ClientError::Api(err) => write!(f, "control API error: {err}"),
69 }
70 }
71}
72
73impl std::error::Error for ClientError {}
74
75impl From<std::io::Error> for ClientError {
76 fn from(err: std::io::Error) -> Self {
77 ClientError::Io(err)
78 }
79}
80
81impl ControlClient {
82 pub fn hello(&self) -> &Hello {
83 &self.hello
84 }
85
86 pub async fn request(&mut self, request: Request) -> Result<Value, ClientError> {
89 let frame = serde_json::to_value(&request).expect("Request serializes");
90 self.request_value(&frame).await
91 }
92
93 pub async fn request_value(&mut self, request: &Value) -> Result<Value, ClientError> {
98 write_frame(&mut self.writer, request).await?;
99 match self.reader.next().await? {
100 Some(Inbound::Frame(resp)) => {
101 if let Some(err) = resp.get("error") {
102 return Err(ClientError::Api(err.clone()));
103 }
104 Ok(resp.get("result").cloned().unwrap_or(Value::Null))
105 }
106 Some(Inbound::Violation(_)) => Err(ClientError::Malformed("response")),
107 None => Err(ClientError::Closed("response")),
108 }
109 }
110
111 pub async fn open_session(
118 mut self,
119 peer: String,
120 service: String,
121 ) -> Result<(FrameReader<ControlRead>, ControlWrite), ClientError> {
122 let frame = serde_json::to_value(Request::OpenSession(OpenSessionParams { peer, service }))
123 .expect("Request serializes");
124 write_frame(&mut self.writer, &frame).await?;
125 Ok((self.reader, self.writer))
126 }
127
128 pub async fn open_stream(
136 mut self,
137 method: &str,
138 ) -> Result<(FrameReader<ControlRead>, ControlWrite), ClientError> {
139 let frame = serde_json::json!({ "method": method });
140 write_frame(&mut self.writer, &frame).await?;
141 Ok((self.reader, self.writer))
142 }
143
144 async fn request_typed<T: serde::de::DeserializeOwned>(
149 &mut self,
150 request: Request,
151 what: &'static str,
152 ) -> Result<T, ClientError> {
153 let v = self.request(request).await?;
154 serde_json::from_value(v).map_err(|_| ClientError::Malformed(what))
155 }
156
157 async fn request_ack(&mut self, request: Request) -> Result<(), ClientError> {
160 self.request(request).await.map(|_| ())
161 }
162
163 pub async fn status(&mut self) -> Result<StatusResult, ClientError> {
166 self.request_typed(Request::Status, "status result").await
167 }
168
169 pub async fn register_service(
172 &mut self,
173 name: &str,
174 backend: BackendSpec,
175 allow: Vec<String>,
176 ) -> Result<(), ClientError> {
177 self.register_service_with(name, backend, allow, false)
178 .await
179 }
180
181 pub async fn register_service_with(
186 &mut self,
187 name: &str,
188 backend: BackendSpec,
189 allow: Vec<String>,
190 ephemeral: bool,
191 ) -> Result<(), ClientError> {
192 self.request_ack(Request::RegisterService(RegisterServiceParams {
193 name: name.to_string(),
194 backend,
195 allow,
196 ephemeral,
197 }))
198 .await
199 }
200
201 pub async fn invite(&mut self, services: Vec<String>) -> Result<InviteResult, ClientError> {
204 self.invite_with(services, None).await
205 }
206
207 pub async fn invite_with(
210 &mut self,
211 services: Vec<String>,
212 app_label: Option<String>,
213 ) -> Result<InviteResult, ClientError> {
214 self.request_typed(
215 Request::Invite(InviteParams {
216 services,
217 app_label,
218 }),
219 "invite result",
220 )
221 .await
222 }
223
224 pub async fn pair(&mut self, invite_line: &str) -> Result<PairResult, ClientError> {
227 self.request_typed(
228 Request::Pair(PairParams {
229 invite_line: invite_line.to_string(),
230 }),
231 "pair result",
232 )
233 .await
234 }
235
236 pub async fn peer_remove(&mut self, nickname: &str) -> Result<(), ClientError> {
239 self.request_ack(Request::PeerRemove(PeerRemoveParams {
240 nickname: nickname.to_string(),
241 }))
242 .await
243 }
244
245 pub async fn peer_rename(
250 &mut self,
251 user_id: Option<String>,
252 nickname: Option<String>,
253 to: &str,
254 ) -> Result<(), ClientError> {
255 self.request_ack(Request::PeerRename(PeerRenameParams {
256 user_id,
257 nickname,
258 to: to.to_string(),
259 }))
260 .await
261 }
262
263 pub async fn roster_install(
266 &mut self,
267 path: &str,
268 org_root_pk: Option<String>,
269 ) -> Result<RosterInstallResult, ClientError> {
270 self.request_typed(
271 Request::RosterInstall(RosterInstallParams {
272 path: path.to_string(),
273 org_root_pk,
274 }),
275 "roster_install result",
276 )
277 .await
278 }
279
280 pub async fn org_join(
283 &mut self,
284 org_id: &str,
285 org_root_pk: &str,
286 user_id: &str,
287 user_key: &str,
288 ) -> Result<OrgJoinResult, ClientError> {
289 self.request_typed(
290 Request::OrgJoin(OrgJoinParams {
291 org_id: org_id.to_string(),
292 org_root_pk: org_root_pk.to_string(),
293 user_id: user_id.to_string(),
294 user_key: user_key.to_string(),
295 }),
296 "org_join result",
297 )
298 .await
299 }
300
301 pub async fn set_roster_url(&mut self, url: &str) -> Result<(), ClientError> {
304 self.request_ack(Request::SetRosterUrl(SetRosterUrlParams {
305 url: url.to_string(),
306 }))
307 .await
308 }
309
310 pub async fn set_app_metadata(&mut self, metadata: &str) -> Result<(), ClientError> {
314 self.request_ack(Request::SetAppMetadata(SetAppMetadataParams {
315 metadata: metadata.to_string(),
316 }))
317 .await
318 }
319
320 pub async fn set_nickname(&mut self, nickname: &str) -> Result<(), ClientError> {
324 self.request_ack(Request::SetNickname(SetNicknameParams {
325 nickname: nickname.to_string(),
326 }))
327 .await
328 }
329
330 pub async fn audit_summary(&mut self) -> Result<AuditSummaryResult, ClientError> {
333 self.request_typed(Request::AuditSummary, "audit_summary result")
334 .await
335 }
336
337 pub async fn blob_publish(
339 &mut self,
340 scope: &str,
341 path: &str,
342 ) -> Result<BlobPublishResult, ClientError> {
343 self.request_typed(
344 Request::BlobPublish(BlobPublishParams {
345 scope: scope.to_string(),
346 path: path.to_string(),
347 }),
348 "blob_publish result",
349 )
350 .await
351 }
352
353 pub async fn blob_list(&mut self) -> Result<BlobScopeList, ClientError> {
355 self.request_typed(Request::BlobList, "blob_list result")
356 .await
357 }
358
359 pub async fn blob_fetch(
362 &mut self,
363 ticket: &str,
364 dest_path: &str,
365 ) -> Result<BlobFetchResult, ClientError> {
366 self.request_typed(
367 Request::BlobFetch(BlobFetchParams {
368 ticket: ticket.to_string(),
369 dest_path: dest_path.to_string(),
370 }),
371 "blob_fetch result",
372 )
373 .await
374 }
375
376 pub async fn blob_grant(&mut self, scope: &str, principal: &str) -> Result<(), ClientError> {
382 self.request_ack(Request::BlobGrant(BlobGrantParams {
383 scope: scope.to_string(),
384 principal: principal.to_string(),
385 }))
386 .await
387 }
388
389 pub async fn subscribe(self) -> Result<StreamSubscription, ClientError> {
394 let (reader, writer) = self.open_stream("subscribe").await?;
395 Ok(StreamSubscription {
396 reader,
397 _writer: writer,
398 })
399 }
400}
401
402pub struct StreamSubscription {
407 reader: FrameReader<ControlRead>,
408 _writer: ControlWrite,
409}
410
411impl std::fmt::Debug for StreamSubscription {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 f.debug_struct("StreamSubscription").finish_non_exhaustive()
415 }
416}
417
418impl StreamSubscription {
419 pub async fn next(&mut self) -> Result<Option<StreamFrame>, ClientError> {
424 match self.reader.next().await? {
425 Some(Inbound::Frame(v)) => serde_json::from_value(v)
426 .map(Some)
427 .map_err(|_| ClientError::Malformed("stream frame")),
428 Some(Inbound::Violation(_)) => Err(ClientError::Malformed("stream frame")),
429 None => Ok(None),
430 }
431 }
432}
433
434pub async fn connect_control_io(
438 reader: impl tokio::io::AsyncRead + Send + Unpin + 'static,
439 writer: impl tokio::io::AsyncWrite + Send + Unpin + 'static,
440) -> Result<ControlClient, ClientError> {
441 let mut reader = FrameReader::new(Box::new(reader) as ControlRead, MAX_FRAME_BYTES);
442 let hello: Hello = match reader.next().await? {
443 Some(Inbound::Frame(v)) => {
444 serde_json::from_value(v).map_err(|_| ClientError::Malformed("hello"))?
445 }
446 Some(Inbound::Violation(_)) => return Err(ClientError::Malformed("hello")),
447 None => return Err(ClientError::Closed("hello")),
448 };
449 if hello.api != crate::protocol::API_NAME {
450 return Err(ClientError::WrongApi {
451 got: hello.api,
452 want: crate::protocol::API_NAME,
453 });
454 }
455 Ok(ControlClient {
456 hello,
457 reader,
458 writer: Box::new(writer) as ControlWrite,
459 })
460}
461
462pub async fn connect_control(path: &Path) -> Result<ControlClient, ClientError> {
464 let stream = connect_local(path).await?;
465 let (read_half, write_half) = split_local(stream);
466 connect_control_io(read_half, write_half).await
467}
468
469pub async fn connect_control_default() -> Result<ControlClient, ClientError> {
474 connect_control(&crate::paths::default_endpoint()?).await
475}
476
477#[cfg(all(test, feature = "service"))]
485mod tests {
486 use super::*;
487 use crate::protocol::{API_NAME, API_VERSION, BackendKind, ServiceInfo, StatusResult};
488 use crate::transport::{LocalListener, bind_local, split_local};
489 use tokio::io::AsyncWriteExt;
490
491 #[cfg(unix)]
496 fn test_endpoint(tag: &str) -> (std::path::PathBuf, tempfile::TempDir) {
497 let dir = tempfile::tempdir().unwrap();
498 let path = dir.path().join(format!("{tag}.sock"));
499 (path, dir)
500 }
501 #[cfg(windows)]
502 fn test_endpoint(tag: &str) -> (std::path::PathBuf, ()) {
503 use std::sync::atomic::{AtomicU64, Ordering};
504 static SEQ: AtomicU64 = AtomicU64::new(0);
505 let n = SEQ.fetch_add(1, Ordering::Relaxed);
506 let path = std::path::PathBuf::from(format!(
507 r"\\.\pipe\mcpmesh-client-test-{}-{tag}-{n}",
508 std::process::id()
509 ));
510 (path, ())
511 }
512
513 async fn stub_daemon(mut listener: LocalListener) {
515 let stream = listener.accept().await.unwrap();
516 let (read_half, mut writer) = split_local(stream);
517 write_frame(
518 &mut writer,
519 &serde_json::to_value(Hello {
520 api: API_NAME.into(),
521 api_version: API_VERSION.into(),
522 api_minor: 0,
523 stack_version: "0.1.0".into(),
524 })
525 .unwrap(),
526 )
527 .await
528 .unwrap();
529 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
530 let req = match reader.next().await.unwrap().unwrap() {
531 Inbound::Frame(v) => v,
532 Inbound::Violation(_) => panic!("violation"),
533 };
534 assert_eq!(req["method"], "status");
535 let result = StatusResult {
536 stack_version: "0.1.0".into(),
537 services: vec![ServiceInfo {
538 name: "kb".into(),
539 allow: vec![],
540 allow_display: vec![],
541 backend: BackendKind::Socket,
542 ephemeral: false,
543 }],
544 peers: vec![],
545 roster: None,
546 presence: vec![],
547 self_user_id: None,
548 recent_pairings: vec![],
549 reachability: vec![],
550 self_nickname: String::new(),
551 };
552 write_frame(
553 &mut writer,
554 &serde_json::json!({ "jsonrpc": "2.0", "id": 1, "result": result }),
555 )
556 .await
557 .unwrap();
558 writer.flush().await.unwrap();
559 }
560
561 #[tokio::test]
564 async fn connect_control_io_handshakes_over_a_duplex() {
565 let (client_io, mut server_io) = tokio::io::duplex(4096);
566 tokio::spawn(async move {
567 write_frame(
568 &mut server_io,
569 &serde_json::to_value(Hello {
570 api: API_NAME.into(),
571 api_version: API_VERSION.into(),
572 api_minor: 0,
573 stack_version: "in-proc".into(),
574 })
575 .unwrap(),
576 )
577 .await
578 .unwrap();
579 });
580 let (r, w) = tokio::io::split(client_io);
581 let client = connect_control_io(r, w).await.expect("handshake");
582 assert_eq!(client.hello().stack_version, "in-proc");
583 }
584
585 #[tokio::test]
586 async fn connect_reads_hello_asserts_api_and_requests() {
587 let (sock, _guard) = test_endpoint("status");
588 let listener = bind_local(&sock).unwrap();
589 let server = tokio::spawn(stub_daemon(listener));
590
591 let mut client = connect_control(&sock).await.unwrap();
592 assert_eq!(client.hello().api, API_NAME);
593 let result = client.request(Request::Status).await.unwrap();
594 assert_eq!(result["services"][0]["name"], "kb");
595 assert_eq!(result["services"][0]["backend"], "socket");
596 server.await.unwrap();
597 }
598
599 #[tokio::test]
600 async fn wrong_api_hello_is_rejected() {
601 let (sock, _guard) = test_endpoint("wrongapi");
602 let listener = bind_local(&sock).unwrap();
603 tokio::spawn(async move {
604 let mut listener = listener;
605 let stream = listener.accept().await.unwrap();
606 let (_r, mut w) = split_local(stream);
607 write_frame(
608 &mut w,
609 &serde_json::json!({"api":"other/1","api_version":"1.0","stack_version":"0"}),
610 )
611 .await
612 .unwrap();
613 w.flush().await.unwrap();
614 });
615 match connect_control(&sock).await {
616 Err(ClientError::WrongApi { got, want }) => {
617 assert_eq!(got, "other/1");
618 assert_eq!(want, API_NAME);
619 }
620 other => panic!("expected WrongApi, got {other:?}"),
621 }
622 }
623
624 #[tokio::test]
625 async fn blob_fetch_and_publish_deserialize_typed_results() {
626 use crate::protocol::{BlobFetchResult, BlobPublishResult};
627 let (sock, _guard) = test_endpoint("blob");
628 let listener = bind_local(&sock).unwrap();
629 let server = tokio::spawn(async move {
630 let mut listener = listener;
631 let stream = listener.accept().await.unwrap();
632 let (read_half, mut writer) = split_local(stream);
633 write_frame(
634 &mut writer,
635 &serde_json::to_value(Hello {
636 api: API_NAME.into(),
637 api_version: API_VERSION.into(),
638 api_minor: 0,
639 stack_version: "0.1.0".into(),
640 })
641 .unwrap(),
642 )
643 .await
644 .unwrap();
645 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
646 let req = match reader.next().await.unwrap().unwrap() {
648 Inbound::Frame(v) => v,
649 Inbound::Violation(_) => panic!("violation"),
650 };
651 assert_eq!(req["method"], "blob_publish");
652 assert_eq!(req["params"]["scope"], "eng");
653 write_frame(
654 &mut writer,
655 &serde_json::json!({"jsonrpc":"2.0","id":1,"result":{"ticket":"blobT","hash":"ab"}}),
656 )
657 .await
658 .unwrap();
659 let req = match reader.next().await.unwrap().unwrap() {
661 Inbound::Frame(v) => v,
662 Inbound::Violation(_) => panic!("violation"),
663 };
664 assert_eq!(req["method"], "blob_fetch");
665 assert_eq!(req["params"]["ticket"], "blobT");
666 assert_eq!(req["params"]["dest_path"], "/tmp/out.bin");
667 write_frame(
668 &mut writer,
669 &serde_json::json!({"jsonrpc":"2.0","id":2,"result":{"hash":"cd","bytes_len":7}}),
670 )
671 .await
672 .unwrap();
673 let _ = (
674 BlobFetchResult {
675 hash: "cd".into(),
676 bytes_len: 7,
677 },
678 BlobPublishResult {
679 ticket: "blobT".into(),
680 hash: "ab".into(),
681 },
682 );
683 });
684
685 let mut client = connect_control(&sock).await.unwrap();
686 let pub_res = client.blob_publish("eng", "/tmp/a.bin").await.unwrap();
687 assert_eq!(pub_res.ticket, "blobT");
688 assert_eq!(pub_res.hash, "ab");
689 let fetch_res = client.blob_fetch("blobT", "/tmp/out.bin").await.unwrap();
690 assert_eq!(fetch_res.hash, "cd");
691 assert_eq!(fetch_res.bytes_len, 7);
692 server.await.unwrap();
693 }
694
695 #[tokio::test]
702 async fn frame_pipelined_behind_hello_survives_open_session_rebox() {
703 use tokio::io::AsyncRead;
704
705 let (sock, _guard) = test_endpoint("pipelined");
706 let listener = bind_local(&sock).unwrap();
707 let server = tokio::spawn(async move {
708 let mut listener = listener;
709 let stream = listener.accept().await.unwrap();
710 let (read_half, mut writer) = split_local(stream);
711 let mut bytes = serde_json::to_vec(
714 &serde_json::to_value(Hello {
715 api: API_NAME.into(),
716 api_version: API_VERSION.into(),
717 api_minor: 0,
718 stack_version: "0.1.0".into(),
719 })
720 .unwrap(),
721 )
722 .unwrap();
723 bytes.push(b'\n');
724 bytes.extend_from_slice(b"{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":{}}\n");
725 writer.write_all(&bytes).await.unwrap();
726 writer.flush().await.unwrap();
727 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
729 let req = match reader.next().await.unwrap().unwrap() {
730 Inbound::Frame(v) => v,
731 Inbound::Violation(_) => panic!("violation"),
732 };
733 assert_eq!(req["method"], "open_session");
734 });
735
736 let client = connect_control(&sock).await.unwrap();
737 let (reader, _writer) = client
738 .open_session("peer".into(), "kb".into())
739 .await
740 .unwrap();
741 let boxed: Box<dyn AsyncRead + Unpin + Send> = Box::new(reader.into_inner());
743 let mut reframed = FrameReader::new(boxed, MAX_FRAME_BYTES);
744 match reframed.next().await.unwrap() {
745 Some(Inbound::Frame(v)) => assert_eq!(v["id"], 42),
746 other => panic!("pipelined frame was lost across the rebox: {other:?}"),
747 }
748 server.await.unwrap();
749 }
750
751 #[tokio::test]
752 async fn blob_grant_issues_request_and_acks() {
753 let (sock, _guard) = test_endpoint("grant");
754 let listener = bind_local(&sock).unwrap();
755 let server = tokio::spawn(async move {
756 let mut listener = listener;
757 let stream = listener.accept().await.unwrap();
758 let (read_half, mut writer) = split_local(stream);
759 write_frame(
760 &mut writer,
761 &serde_json::to_value(Hello {
762 api: API_NAME.into(),
763 api_version: API_VERSION.into(),
764 api_minor: 0,
765 stack_version: "0.1.0".into(),
766 })
767 .unwrap(),
768 )
769 .await
770 .unwrap();
771 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
772 let req = match reader.next().await.unwrap().unwrap() {
773 Inbound::Frame(v) => v,
774 Inbound::Violation(_) => panic!("violation"),
775 };
776 assert_eq!(req["method"], "blob_grant");
777 assert_eq!(req["params"]["scope"], "kb-sync");
778 assert_eq!(req["params"]["principal"], "alice");
779 write_frame(
780 &mut writer,
781 &serde_json::json!({"jsonrpc":"2.0","id":1,"result":{"ok":true}}),
782 )
783 .await
784 .unwrap();
785 });
786 let mut client = connect_control(&sock).await.unwrap();
787 client.blob_grant("kb-sync", "alice").await.unwrap();
788 server.await.unwrap();
789 }
790
791 #[tokio::test]
795 async fn typed_status_helper_deserializes_the_result() {
796 let (sock, _guard) = test_endpoint("typedstatus");
797 let listener = bind_local(&sock).unwrap();
798 let server = tokio::spawn(stub_daemon(listener));
799
800 let mut client = connect_control(&sock).await.unwrap();
801 let status = client.status().await.unwrap();
802 assert_eq!(status.stack_version, "0.1.0");
803 assert_eq!(status.services[0].name, "kb");
804 assert_eq!(status.services[0].backend, BackendKind::Socket);
805 assert!(status.peers.is_empty());
806 server.await.unwrap();
807 }
808
809 #[tokio::test]
812 async fn typed_ack_helpers_issue_requests_and_surface_api_errors() {
813 let (sock, _guard) = test_endpoint("typedack");
814 let listener = bind_local(&sock).unwrap();
815 let server = tokio::spawn(async move {
816 let mut listener = listener;
817 let stream = listener.accept().await.unwrap();
818 let (read_half, mut writer) = split_local(stream);
819 write_frame(
820 &mut writer,
821 &serde_json::to_value(Hello {
822 api: API_NAME.into(),
823 api_version: API_VERSION.into(),
824 api_minor: 0,
825 stack_version: "0.1.0".into(),
826 })
827 .unwrap(),
828 )
829 .await
830 .unwrap();
831 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
832 let req = match reader.next().await.unwrap().unwrap() {
834 Inbound::Frame(v) => v,
835 Inbound::Violation(_) => panic!("violation"),
836 };
837 assert_eq!(req["method"], "peer_remove");
838 assert_eq!(req["params"]["nickname"], "bob");
839 write_frame(
840 &mut writer,
841 &serde_json::json!({"jsonrpc":"2.0","id":1,"result":{}}),
842 )
843 .await
844 .unwrap();
845 let req = match reader.next().await.unwrap().unwrap() {
847 Inbound::Frame(v) => v,
848 Inbound::Violation(_) => panic!("violation"),
849 };
850 assert_eq!(req["method"], "peer_rename");
851 assert_eq!(req["params"]["to"], "Bobby");
852 write_frame(
853 &mut writer,
854 &serde_json::json!({"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"taken"}}),
855 )
856 .await
857 .unwrap();
858 });
859
860 let mut client = connect_control(&sock).await.unwrap();
861 client.peer_remove("bob").await.unwrap();
862 match client.peer_rename(None, Some("bob".into()), "Bobby").await {
863 Err(ClientError::Api(e)) => assert_eq!(e["message"], "taken"),
864 other => panic!("expected Api error, got {other:?}"),
865 }
866 server.await.unwrap();
867 }
868
869 #[tokio::test]
872 async fn typed_subscribe_yields_frames_then_end() {
873 use crate::protocol::{ActiveSession, AuditRecord, PeerReachability};
874
875 let (sock, _guard) = test_endpoint("subscribe");
876 let listener = bind_local(&sock).unwrap();
877 let server = tokio::spawn(async move {
878 let mut listener = listener;
879 let stream = listener.accept().await.unwrap();
880 let (read_half, mut writer) = split_local(stream);
881 write_frame(
882 &mut writer,
883 &serde_json::to_value(Hello {
884 api: API_NAME.into(),
885 api_version: API_VERSION.into(),
886 api_minor: 0,
887 stack_version: "0.1.0".into(),
888 })
889 .unwrap(),
890 )
891 .await
892 .unwrap();
893 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
894 let req = match reader.next().await.unwrap().unwrap() {
895 Inbound::Frame(v) => v,
896 Inbound::Violation(_) => panic!("violation"),
897 };
898 assert_eq!(req["method"], "subscribe");
899 for frame in [
900 StreamFrame::Snapshot {
901 active_sessions: vec![ActiveSession {
902 peer: "bob".into(),
903 service: "notes".into(),
904 opened_at: 7,
905 }],
906 reachability: vec![PeerReachability {
907 name: "bob".into(),
908 reachable: true,
909 rtt_ms: Some(42),
910 age_secs: Some(3),
911 meta: String::new(),
912 principal: None,
913 }],
914 },
915 StreamFrame::Event {
916 record: Box::new(AuditRecord::session_open(
917 "2026-07-03T14:02:11.480Z".into(),
918 Some("bob".into()),
919 "notes".into(),
920 )),
921 },
922 StreamFrame::Lagged { dropped: 12 },
923 ] {
924 write_frame(&mut writer, &serde_json::to_value(&frame).unwrap())
925 .await
926 .unwrap();
927 }
928 writer.flush().await.unwrap();
929 });
931
932 let client = connect_control(&sock).await.unwrap();
933 let mut sub = client.subscribe().await.unwrap();
934 match sub.next().await.unwrap().unwrap() {
935 StreamFrame::Snapshot {
936 active_sessions,
937 reachability,
938 } => {
939 assert_eq!(active_sessions[0].peer, "bob");
940 assert_eq!(reachability[0].rtt_ms, Some(42));
941 }
942 other => panic!("expected the snapshot first, got {other:?}"),
943 }
944 match sub.next().await.unwrap().unwrap() {
945 StreamFrame::Event { record } => {
946 assert_eq!(record.peer.as_deref(), Some("bob"));
947 assert_eq!(record.service.as_deref(), Some("notes"));
948 }
949 other => panic!("expected the event, got {other:?}"),
950 }
951 assert_eq!(
952 sub.next().await.unwrap(),
953 Some(StreamFrame::Lagged { dropped: 12 })
954 );
955 assert_eq!(sub.next().await.unwrap(), None, "clean end of stream");
956 server.await.unwrap();
957 }
958}