agentmux_srv/server/app_api/
presence.rs1use super::*;
8
9pub(crate) const NO_STATUS: &str = "Cloud presence hasn't started on this install.";
12
13pub fn register(engine: &Arc<WshRpcEngine>, _state: &AppState) {
14 engine.register_typed(COMMAND_PRESENCE_STATUS, |_req: Option<NoArgsReq>, _ctx| async move {
15 crate::muxbus::wan_presence::status().ok_or_else(|| NO_STATUS.to_string())
16 });
17 engine.register_typed(COMMAND_PRESENCE_PUBLISH_NOW, |_req: Option<NoArgsReq>, _ctx| async move {
18 Ok(PresencePublishNowResult { started: crate::muxbus::wan_presence::publish_now() })
19 });
20}
21
22#[cfg(test)]
23mod tests {
24 use super::*;
25
26 async fn call(command: &str) -> (serde_json::Value, String) {
27 let state = crate::server::tests::test_state();
28 let (engine, mut rx) = WshRpcEngine::new();
29 register(&engine, &state);
30 engine.handle_message(RpcMessage {
31 command: command.to_string(),
32 reqid: "req-1".to_string(),
33 data: Some(json!({})),
34 ..Default::default()
35 });
36 let resp = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv()).await.unwrap().unwrap();
37 (resp.data.unwrap_or_default(), resp.error)
38 }
39
40 #[test]
41 fn the_status_is_snake_case_and_leaves_out_what_it_doesnt_know() {
42 let status = PresenceStatusResult {
43 state: PresenceState::SignedOut,
44 since_ms: 5,
45 last_ok_ms: None,
46 last_error: None,
47 next_try_ms: None,
48 relay_version: None,
49 offset_ms: None,
50 record_version: 2,
51 note: None,
52 off_reason: None,
53 };
54 assert_eq!(
55 serde_json::to_value(&status).unwrap(),
56 json!({ "state": "signed_out", "since_ms": 5, "record_version": 2 })
57 );
58 let full = PresenceStatusResult {
59 state: PresenceState::Retrying,
60 last_ok_ms: Some(1),
61 last_error: Some("cloud unreachable".into()),
62 next_try_ms: Some(9),
63 relay_version: Some("1.1.0".into()),
64 offset_ms: Some(-720_000),
65 note: Some("Your clock differs from the cloud's by 12 min".into()),
66 ..status
67 };
68 assert_eq!(
69 serde_json::to_value(&full).unwrap(),
70 json!({
71 "state": "retrying",
72 "since_ms": 5,
73 "last_ok_ms": 1,
74 "last_error": "cloud unreachable",
75 "next_try_ms": 9,
76 "relay_version": "1.1.0",
77 "offset_ms": -720_000,
78 "record_version": 2,
79 "note": "Your clock differs from the cloud's by 12 min",
80 })
81 );
82 for (state, wire) in [
83 (PresenceState::Publishing, "publishing"),
84 (PresenceState::Unsupported, "unsupported"),
85 (PresenceState::Rejected, "rejected"),
86 (PresenceState::Off, "off"),
87 (PresenceState::SignedOff, "signed_off"),
88 ] {
89 assert_eq!(serde_json::to_value(state).unwrap(), json!(wire));
90 }
91 let off = PresenceStatusResult {
92 state: PresenceState::Off,
93 off_reason: Some(PresenceOffReason::DevBuild),
94 ..status
95 };
96 assert_eq!(
97 serde_json::to_value(&off).unwrap(),
98 json!({ "state": "off", "since_ms": 5, "record_version": 2, "off_reason": "dev_build" })
99 );
100 for (reason, wire) in [
101 (PresenceOffReason::Setting, "setting"),
102 (PresenceOffReason::Headless, "headless"),
103 (PresenceOffReason::IsolatedHome, "isolated_home"),
104 (PresenceOffReason::TestHarness, "test_harness"),
105 ] {
106 assert_eq!(serde_json::to_value(reason).unwrap(), json!(wire));
107 }
108 }
109
110 #[tokio::test]
111 async fn without_a_publisher_the_status_says_why_and_publish_now_starts_nothing() {
112 let (_, error) = call(COMMAND_PRESENCE_STATUS).await;
113 assert_eq!(error, NO_STATUS);
114 let (data, error) = call(COMMAND_PRESENCE_PUBLISH_NOW).await;
115 assert!(error.is_empty(), "{error}");
116 assert_eq!(data, json!({ "started": false }));
117 }
118}