1use chrono::Utc;
21use serde::{Deserialize, Serialize};
22
23use crate::console::{BootEvent, KernelEventLog, LogLevel};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum SseEventType {
31 Init,
33 Ingest,
35 Delete,
37 Compact,
39 ChainAppend,
41 AgentSpawn,
43 AgentStop,
45 Heartbeat,
47}
48
49impl SseEventType {
50 pub fn as_str(&self) -> &'static str {
52 match self {
53 SseEventType::Init => "init",
54 SseEventType::Ingest => "ingest",
55 SseEventType::Delete => "delete",
56 SseEventType::Compact => "compact",
57 SseEventType::ChainAppend => "chain_append",
58 SseEventType::AgentSpawn => "agent_spawn",
59 SseEventType::AgentStop => "agent_stop",
60 SseEventType::Heartbeat => "heartbeat",
61 }
62 }
63}
64
65impl std::fmt::Display for SseEventType {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.write_str(self.as_str())
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SseMessage {
74 pub event_type: SseEventType,
76 pub data: serde_json::Value,
78 pub timestamp: String,
80}
81
82impl SseMessage {
83 pub fn new(event_type: SseEventType, data: serde_json::Value) -> Self {
85 Self {
86 event_type,
87 data,
88 timestamp: Utc::now().to_rfc3339(),
89 }
90 }
91
92 pub fn to_sse_frame(&self) -> String {
94 let json = serde_json::to_string(&SsePayload {
95 data: &self.data,
96 timestamp: &self.timestamp,
97 })
98 .unwrap_or_else(|_| "{}".to_string());
99 format!("event: {}\ndata: {}\n\n", self.event_type.as_str(), json)
100 }
101
102 pub fn heartbeat() -> Self {
104 Self::new(SseEventType::Heartbeat, serde_json::json!({}))
105 }
106}
107
108#[derive(Serialize)]
110struct SsePayload<'a> {
111 data: &'a serde_json::Value,
112 timestamp: &'a str,
113}
114
115pub const SSE_HEARTBEAT_COMMENT: &str = ":heartbeat\n\n";
117
118pub const SSE_HEARTBEAT_INTERVAL_SECS: u64 = 15;
120
121pub fn classify_boot_event(event: &BootEvent) -> Option<SseEventType> {
127 let msg = &event.message;
128 if msg.contains("[agent]") && msg.contains("spawned") {
129 Some(SseEventType::AgentSpawn)
130 } else if msg.contains("[agent]") && msg.contains("stopped") {
131 Some(SseEventType::AgentStop)
132 } else if msg.contains("ingest") || msg.contains("ingested") {
133 Some(SseEventType::Ingest)
134 } else if msg.contains("delete") || msg.contains("deleted") {
135 Some(SseEventType::Delete)
136 } else if msg.contains("compact") {
137 Some(SseEventType::Compact)
138 } else if msg.contains("chain") && msg.contains("append") {
139 Some(SseEventType::ChainAppend)
140 } else if msg.contains("booting") || msg.contains("ready") {
141 Some(SseEventType::Init)
142 } else {
143 None
146 }
147}
148
149pub fn poll_events(event_log: &KernelEventLog, cursor: usize) -> (Vec<SseMessage>, usize) {
156 let all = event_log.tail(0);
157 let total = all.len();
158 if cursor >= total {
159 return (Vec::new(), total);
160 }
161 let new_events = &all[cursor..];
162 let messages: Vec<SseMessage> = new_events
163 .iter()
164 .filter(|e| e.level != LogLevel::Debug)
165 .map(|e| {
166 let event_type = classify_boot_event(e).unwrap_or(SseEventType::Init);
167 SseMessage::new(
168 event_type,
169 serde_json::json!({
170 "phase": e.phase.tag(),
171 "level": format!("{:?}", e.level).to_lowercase(),
172 "message": e.message,
173 }),
174 )
175 })
176 .collect();
177 (messages, total)
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct WitnessRequest {
185 pub event_type: String,
187 pub payload: serde_json::Value,
189 pub signature: String,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct WitnessResponse {
196 pub accepted: bool,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub chain_hash: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub sequence: Option<u64>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub error: Option<String>,
207}
208
209impl WitnessResponse {
210 pub fn accepted(hash_hex: String, sequence: u64) -> Self {
212 Self {
213 accepted: true,
214 chain_hash: Some(hash_hex),
215 sequence: Some(sequence),
216 error: None,
217 }
218 }
219
220 pub fn rejected(reason: impl Into<String>) -> Self {
222 Self {
223 accepted: false,
224 chain_hash: None,
225 sequence: None,
226 error: Some(reason.into()),
227 }
228 }
229}
230
231#[cfg(feature = "exochain")]
238pub fn verify_witness_signature(
239 event_type: &str,
240 payload: &serde_json::Value,
241 signature_hex: &str,
242 verifying_key_bytes: &[u8; 32],
243) -> Result<(), String> {
244 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
245
246 let sig_bytes = hex_decode(signature_hex)
247 .map_err(|e| format!("invalid signature hex: {e}"))?;
248 if sig_bytes.len() != 64 {
249 return Err(format!(
250 "signature must be 64 bytes, got {}",
251 sig_bytes.len()
252 ));
253 }
254 let sig = Signature::from_bytes(
255 sig_bytes
256 .as_slice()
257 .try_into()
258 .map_err(|_| "signature length mismatch")?,
259 );
260 let vk = VerifyingKey::from_bytes(verifying_key_bytes)
261 .map_err(|e| format!("invalid verifying key: {e}"))?;
262
263 let payload_bytes = serde_json::to_vec(payload).unwrap_or_default();
265 let mut message = Vec::with_capacity(event_type.len() + payload_bytes.len());
266 message.extend_from_slice(event_type.as_bytes());
267 message.extend_from_slice(&payload_bytes);
268
269 vk.verify(&message, &sig)
270 .map_err(|e| format!("signature verification failed: {e}"))
271}
272
273#[cfg(any(feature = "exochain", test))]
275fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
276 if !hex.len().is_multiple_of(2) {
277 return Err("odd-length hex string".into());
278 }
279 (0..hex.len())
280 .step_by(2)
281 .map(|i| {
282 u8::from_str_radix(&hex[i..i + 2], 16)
283 .map_err(|e| format!("invalid hex at offset {i}: {e}"))
284 })
285 .collect()
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum HttpMethod {
293 Get,
294 Post,
295 Delete,
296}
297
298impl HttpMethod {
299 pub fn parse(s: &str) -> Option<Self> {
301 match s.to_ascii_uppercase().as_str() {
302 "GET" => Some(HttpMethod::Get),
303 "POST" => Some(HttpMethod::Post),
304 "DELETE" => Some(HttpMethod::Delete),
305 _ => None,
306 }
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
312pub enum FacadeRoute {
313 Events,
315 CustodyWitness,
317 Rpc {
319 method: &'static str,
320 params: Option<RpcParam>,
321 },
322 NotFound,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
328pub enum RpcParam {
329 Pid(u64),
331}
332
333pub fn match_facade_route(method: HttpMethod, path: &str) -> FacadeRoute {
354 let path = path.split('?').next().unwrap_or(path);
356 let path = path.trim_end_matches('/');
358
359 match (method, path) {
360 (HttpMethod::Get, "/events") => FacadeRoute::Events,
362
363 (HttpMethod::Post, "/custody/witness") => FacadeRoute::CustodyWitness,
365
366 (HttpMethod::Get, "/api/status") => FacadeRoute::Rpc {
368 method: "kernel.status",
369 params: None,
370 },
371 (HttpMethod::Get, "/api/processes") => FacadeRoute::Rpc {
372 method: "kernel.ps",
373 params: None,
374 },
375 (HttpMethod::Get, "/api/services") => FacadeRoute::Rpc {
376 method: "kernel.services",
377 params: None,
378 },
379 (HttpMethod::Get, "/api/chain/status") => FacadeRoute::Rpc {
380 method: "chain.status",
381 params: None,
382 },
383 (HttpMethod::Get, "/api/chain/events") => FacadeRoute::Rpc {
384 method: "kernel.logs",
385 params: None,
386 },
387 (HttpMethod::Get, "/api/vectors/status") => FacadeRoute::Rpc {
388 method: "ecc.status",
389 params: None,
390 },
391 (HttpMethod::Post, "/api/vectors/search") => FacadeRoute::Rpc {
392 method: "ecc.search",
393 params: None,
394 },
395 (HttpMethod::Get, "/api/ecc/calibration") => FacadeRoute::Rpc {
396 method: "ecc.calibrate",
397 params: None,
398 },
399 (HttpMethod::Get, "/api/ecc/coherence") => FacadeRoute::Rpc {
400 method: "ecc.coherence",
401 params: None,
402 },
403 (HttpMethod::Get, "/api/custody/attest") => FacadeRoute::Rpc {
404 method: "custody.attest",
405 params: None,
406 },
407 (HttpMethod::Get, "/api/fleet/snapshot") => FacadeRoute::Rpc {
410 method: "fleet.snapshot",
411 params: None,
412 },
413 (HttpMethod::Post, "/api/agents/spawn") => FacadeRoute::Rpc {
414 method: "agent.spawn",
415 params: None,
416 },
417 (HttpMethod::Delete, p) if p.starts_with("/api/agents/") => {
419 let pid_str = &p["/api/agents/".len()..];
420 match pid_str.parse::<u64>() {
421 Ok(pid) => FacadeRoute::Rpc {
422 method: "agent.stop",
423 params: Some(RpcParam::Pid(pid)),
424 },
425 Err(_) => FacadeRoute::NotFound,
426 }
427 }
428
429 _ => FacadeRoute::NotFound,
430 }
431}
432
433pub fn build_rpc_params(
439 route: &FacadeRoute,
440 body: &[u8],
441 query: &str,
442) -> serde_json::Value {
443 match route {
444 FacadeRoute::Rpc {
445 method: "kernel.logs",
446 ..
447 } => {
448 let count = parse_query_param(query, "count")
450 .and_then(|v| v.parse::<usize>().ok())
451 .unwrap_or(50);
452 serde_json::json!({ "count": count })
453 }
454 FacadeRoute::Rpc {
455 method: "agent.stop",
456 params: Some(RpcParam::Pid(pid)),
457 } => {
458 serde_json::json!({ "pid": pid, "graceful": true })
459 }
460 FacadeRoute::Rpc { .. } => {
461 serde_json::from_slice(body).unwrap_or(serde_json::json!({}))
463 }
464 _ => serde_json::json!({}),
465 }
466}
467
468fn parse_query_param<'a>(query: &'a str, key: &str) -> Option<&'a str> {
470 query
471 .split('&')
472 .find_map(|pair| {
473 let (k, v) = pair.split_once('=')?;
474 if k == key { Some(v) } else { None }
475 })
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct FacadeResponse {
483 pub status: u16,
485 pub body: serde_json::Value,
487}
488
489impl FacadeResponse {
490 pub fn ok(body: serde_json::Value) -> Self {
492 Self { status: 200, body }
493 }
494
495 pub fn bad_request(message: impl Into<String>) -> Self {
497 Self {
498 status: 400,
499 body: serde_json::json!({ "error": message.into() }),
500 }
501 }
502
503 pub fn not_found() -> Self {
505 Self {
506 status: 404,
507 body: serde_json::json!({ "error": "not found" }),
508 }
509 }
510
511 pub fn internal_error(message: impl Into<String>) -> Self {
513 Self {
514 status: 500,
515 body: serde_json::json!({ "error": message.into() }),
516 }
517 }
518
519 pub fn unauthorized(message: impl Into<String>) -> Self {
521 Self {
522 status: 401,
523 body: serde_json::json!({ "error": message.into() }),
524 }
525 }
526}
527
528pub const CONTENT_TYPE_SSE: &str = "text/event-stream";
532
533pub const CONTENT_TYPE_JSON: &str = "application/json";
535
536pub fn sse_response_headers() -> Vec<(&'static str, &'static str)> {
538 vec![
539 ("Content-Type", CONTENT_TYPE_SSE),
540 ("Cache-Control", "no-cache"),
541 ("Connection", "keep-alive"),
542 ("X-Accel-Buffering", "no"),
543 ]
544}
545
546#[cfg(test)]
549mod tests {
550 use super::*;
551 use crate::console::{BootEvent, BootPhase, KernelEventLog};
552
553 #[test]
556 fn sse_message_format() {
557 let msg = SseMessage::new(
558 SseEventType::AgentSpawn,
559 serde_json::json!({"pid": 42, "agent_id": "coder-1"}),
560 );
561 let frame = msg.to_sse_frame();
562 assert!(frame.starts_with("event: agent_spawn\n"));
563 assert!(frame.contains("data: "));
564 assert!(frame.ends_with("\n\n"));
565 let data_line = frame
567 .lines()
568 .find(|l| l.starts_with("data: "))
569 .unwrap();
570 let json_str = &data_line["data: ".len()..];
571 let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
572 assert!(parsed.get("data").is_some());
573 assert!(parsed.get("timestamp").is_some());
574 }
575
576 #[test]
577 fn sse_heartbeat_comment() {
578 assert_eq!(SSE_HEARTBEAT_COMMENT, ":heartbeat\n\n");
579 }
580
581 #[test]
582 fn sse_heartbeat_message() {
583 let msg = SseMessage::heartbeat();
584 assert_eq!(msg.event_type, SseEventType::Heartbeat);
585 let frame = msg.to_sse_frame();
586 assert!(frame.starts_with("event: heartbeat\n"));
587 }
588
589 #[test]
590 fn sse_event_type_display() {
591 assert_eq!(SseEventType::Init.as_str(), "init");
592 assert_eq!(SseEventType::ChainAppend.as_str(), "chain_append");
593 assert_eq!(SseEventType::AgentSpawn.as_str(), "agent_spawn");
594 assert_eq!(SseEventType::Heartbeat.as_str(), "heartbeat");
595 }
596
597 #[test]
600 fn classify_agent_spawn_event() {
601 let event = BootEvent::info(BootPhase::Ready, "[agent] spawned coder-1 (PID 5)");
602 assert_eq!(classify_boot_event(&event), Some(SseEventType::AgentSpawn));
603 }
604
605 #[test]
606 fn classify_agent_stop_event() {
607 let event = BootEvent::info(BootPhase::Ready, "[agent] stopped PID 5");
608 assert_eq!(classify_boot_event(&event), Some(SseEventType::AgentStop));
609 }
610
611 #[test]
612 fn classify_ingest_event() {
613 let event = BootEvent::info(BootPhase::Ready, "content ingested into store");
614 assert_eq!(classify_boot_event(&event), Some(SseEventType::Ingest));
615 }
616
617 #[test]
618 fn classify_compact_event() {
619 let event = BootEvent::info(BootPhase::Ready, "vector store compaction complete");
620 assert_eq!(classify_boot_event(&event), Some(SseEventType::Compact));
621 }
622
623 #[test]
624 fn classify_init_event() {
625 let event = BootEvent::info(BootPhase::Init, "WeftOS booting...");
626 assert_eq!(classify_boot_event(&event), Some(SseEventType::Init));
627 }
628
629 #[test]
630 fn classify_unknown_event() {
631 let event = BootEvent::info(BootPhase::Ready, "some random message");
632 assert_eq!(classify_boot_event(&event), None);
633 }
634
635 #[test]
638 fn poll_events_empty_log() {
639 let log = KernelEventLog::new();
640 let (messages, cursor) = poll_events(&log, 0);
641 assert!(messages.is_empty());
642 assert_eq!(cursor, 0);
643 }
644
645 #[test]
646 fn poll_events_with_cursor() {
647 let log = KernelEventLog::new();
648 log.info("agent", "spawned coder-1 (PID 5)");
649 log.info("test", "second event");
650
651 let (msgs, cursor) = poll_events(&log, 0);
653 assert_eq!(msgs.len(), 2);
654 assert_eq!(cursor, 2);
655
656 let (msgs, cursor2) = poll_events(&log, cursor);
658 assert!(msgs.is_empty());
659 assert_eq!(cursor2, 2);
660
661 log.info("agent", "stopped PID 5");
663 let (msgs, cursor3) = poll_events(&log, cursor2);
664 assert_eq!(msgs.len(), 1);
665 assert_eq!(cursor3, 3);
666 }
667
668 #[test]
671 fn witness_response_accepted() {
672 let resp = WitnessResponse::accepted("abcd1234".into(), 42);
673 assert!(resp.accepted);
674 assert_eq!(resp.chain_hash.as_deref(), Some("abcd1234"));
675 assert_eq!(resp.sequence, Some(42));
676 assert!(resp.error.is_none());
677 }
678
679 #[test]
680 fn witness_response_rejected() {
681 let resp = WitnessResponse::rejected("bad signature");
682 assert!(!resp.accepted);
683 assert!(resp.chain_hash.is_none());
684 assert_eq!(resp.error.as_deref(), Some("bad signature"));
685 }
686
687 #[test]
688 fn witness_request_serde() {
689 let req = WitnessRequest {
690 event_type: "audit.external".into(),
691 payload: serde_json::json!({"source": "client-A"}),
692 signature: "deadbeef".into(),
693 };
694 let json = serde_json::to_string(&req).unwrap();
695 let restored: WitnessRequest = serde_json::from_str(&json).unwrap();
696 assert_eq!(restored.event_type, "audit.external");
697 assert_eq!(restored.signature, "deadbeef");
698 }
699
700 #[test]
703 fn hex_decode_valid() {
704 let bytes = hex_decode("deadbeef").unwrap();
705 assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
706 }
707
708 #[test]
709 fn hex_decode_empty() {
710 let bytes = hex_decode("").unwrap();
711 assert!(bytes.is_empty());
712 }
713
714 #[test]
715 fn hex_decode_odd_length() {
716 assert!(hex_decode("abc").is_err());
717 }
718
719 #[test]
720 fn hex_decode_invalid_chars() {
721 assert!(hex_decode("zzzz").is_err());
722 }
723
724 #[test]
727 fn route_events() {
728 assert_eq!(
729 match_facade_route(HttpMethod::Get, "/events"),
730 FacadeRoute::Events
731 );
732 }
733
734 #[test]
735 fn route_events_with_trailing_slash() {
736 assert_eq!(
737 match_facade_route(HttpMethod::Get, "/events/"),
738 FacadeRoute::Events
739 );
740 }
741
742 #[test]
743 fn route_custody_witness() {
744 assert_eq!(
745 match_facade_route(HttpMethod::Post, "/custody/witness"),
746 FacadeRoute::CustodyWitness
747 );
748 }
749
750 #[test]
751 fn route_api_status() {
752 let route = match_facade_route(HttpMethod::Get, "/api/status");
753 assert_eq!(
754 route,
755 FacadeRoute::Rpc {
756 method: "kernel.status",
757 params: None
758 }
759 );
760 }
761
762 #[test]
763 fn route_api_processes() {
764 let route = match_facade_route(HttpMethod::Get, "/api/processes");
765 assert_eq!(
766 route,
767 FacadeRoute::Rpc {
768 method: "kernel.ps",
769 params: None
770 }
771 );
772 }
773
774 #[test]
775 fn route_api_fleet_snapshot_is_get_only() {
776 assert_eq!(
777 match_facade_route(HttpMethod::Get, "/api/fleet/snapshot"),
778 FacadeRoute::Rpc {
779 method: "fleet.snapshot",
780 params: None
781 }
782 );
783 assert_eq!(
785 match_facade_route(HttpMethod::Post, "/api/fleet/snapshot"),
786 FacadeRoute::NotFound
787 );
788 assert_eq!(
789 match_facade_route(HttpMethod::Post, "/api/fleet/location"),
790 FacadeRoute::NotFound
791 );
792 }
793
794 #[test]
795 fn route_api_chain_status() {
796 let route = match_facade_route(HttpMethod::Get, "/api/chain/status");
797 assert_eq!(
798 route,
799 FacadeRoute::Rpc {
800 method: "chain.status",
801 params: None
802 }
803 );
804 }
805
806 #[test]
807 fn route_api_chain_events() {
808 let route = match_facade_route(HttpMethod::Get, "/api/chain/events");
809 assert_eq!(
810 route,
811 FacadeRoute::Rpc {
812 method: "kernel.logs",
813 params: None
814 }
815 );
816 }
817
818 #[test]
819 fn route_api_chain_events_with_query() {
820 let route = match_facade_route(HttpMethod::Get, "/api/chain/events?count=20");
821 assert_eq!(
822 route,
823 FacadeRoute::Rpc {
824 method: "kernel.logs",
825 params: None
826 }
827 );
828 }
829
830 #[test]
831 fn route_api_vectors_status() {
832 let route = match_facade_route(HttpMethod::Get, "/api/vectors/status");
833 assert_eq!(
834 route,
835 FacadeRoute::Rpc {
836 method: "ecc.status",
837 params: None
838 }
839 );
840 }
841
842 #[test]
843 fn route_api_vectors_search() {
844 let route = match_facade_route(HttpMethod::Post, "/api/vectors/search");
845 assert_eq!(
846 route,
847 FacadeRoute::Rpc {
848 method: "ecc.search",
849 params: None
850 }
851 );
852 }
853
854 #[test]
855 fn route_api_agents_spawn() {
856 let route = match_facade_route(HttpMethod::Post, "/api/agents/spawn");
857 assert_eq!(
858 route,
859 FacadeRoute::Rpc {
860 method: "agent.spawn",
861 params: None
862 }
863 );
864 }
865
866 #[test]
867 fn route_api_agents_delete() {
868 let route = match_facade_route(HttpMethod::Delete, "/api/agents/42");
869 assert_eq!(
870 route,
871 FacadeRoute::Rpc {
872 method: "agent.stop",
873 params: Some(RpcParam::Pid(42))
874 }
875 );
876 }
877
878 #[test]
879 fn route_api_agents_delete_invalid_pid() {
880 let route = match_facade_route(HttpMethod::Delete, "/api/agents/notanumber");
881 assert_eq!(route, FacadeRoute::NotFound);
882 }
883
884 #[test]
885 fn route_not_found() {
886 assert_eq!(
887 match_facade_route(HttpMethod::Get, "/nonexistent"),
888 FacadeRoute::NotFound
889 );
890 }
891
892 #[test]
893 fn route_wrong_method() {
894 assert_eq!(
896 match_facade_route(HttpMethod::Post, "/api/status"),
897 FacadeRoute::NotFound
898 );
899 }
900
901 #[test]
902 fn route_ecc_calibration() {
903 let route = match_facade_route(HttpMethod::Get, "/api/ecc/calibration");
904 assert_eq!(
905 route,
906 FacadeRoute::Rpc {
907 method: "ecc.calibrate",
908 params: None
909 }
910 );
911 }
912
913 #[test]
914 fn route_ecc_coherence() {
915 let route = match_facade_route(HttpMethod::Get, "/api/ecc/coherence");
916 assert_eq!(
917 route,
918 FacadeRoute::Rpc {
919 method: "ecc.coherence",
920 params: None
921 }
922 );
923 }
924
925 #[test]
926 fn route_custody_attest() {
927 let route = match_facade_route(HttpMethod::Get, "/api/custody/attest");
928 assert_eq!(
929 route,
930 FacadeRoute::Rpc {
931 method: "custody.attest",
932 params: None
933 }
934 );
935 }
936
937 #[test]
940 fn build_params_kernel_logs_with_count() {
941 let route = FacadeRoute::Rpc {
942 method: "kernel.logs",
943 params: None,
944 };
945 let params = build_rpc_params(&route, b"", "count=25");
946 assert_eq!(params["count"], 25);
947 }
948
949 #[test]
950 fn build_params_kernel_logs_default_count() {
951 let route = FacadeRoute::Rpc {
952 method: "kernel.logs",
953 params: None,
954 };
955 let params = build_rpc_params(&route, b"", "");
956 assert_eq!(params["count"], 50);
957 }
958
959 #[test]
960 fn build_params_agent_stop() {
961 let route = FacadeRoute::Rpc {
962 method: "agent.stop",
963 params: Some(RpcParam::Pid(99)),
964 };
965 let params = build_rpc_params(&route, b"", "");
966 assert_eq!(params["pid"], 99);
967 assert_eq!(params["graceful"], true);
968 }
969
970 #[test]
971 fn build_params_from_body() {
972 let route = FacadeRoute::Rpc {
973 method: "agent.spawn",
974 params: None,
975 };
976 let body = br#"{"agent_id":"coder-1"}"#;
977 let params = build_rpc_params(&route, body, "");
978 assert_eq!(params["agent_id"], "coder-1");
979 }
980
981 #[test]
982 fn build_params_invalid_body_fallback() {
983 let route = FacadeRoute::Rpc {
984 method: "agent.spawn",
985 params: None,
986 };
987 let params = build_rpc_params(&route, b"not json", "");
988 assert_eq!(params, serde_json::json!({}));
989 }
990
991 #[test]
994 fn parse_query_param_found() {
995 assert_eq!(parse_query_param("count=20&level=info", "count"), Some("20"));
996 assert_eq!(parse_query_param("count=20&level=info", "level"), Some("info"));
997 }
998
999 #[test]
1000 fn parse_query_param_not_found() {
1001 assert_eq!(parse_query_param("count=20", "level"), None);
1002 }
1003
1004 #[test]
1005 fn parse_query_param_empty() {
1006 assert_eq!(parse_query_param("", "count"), None);
1007 }
1008
1009 #[test]
1012 fn http_method_parse() {
1013 assert_eq!(HttpMethod::parse("GET"), Some(HttpMethod::Get));
1014 assert_eq!(HttpMethod::parse("get"), Some(HttpMethod::Get));
1015 assert_eq!(HttpMethod::parse("POST"), Some(HttpMethod::Post));
1016 assert_eq!(HttpMethod::parse("DELETE"), Some(HttpMethod::Delete));
1017 assert_eq!(HttpMethod::parse("PATCH"), None);
1018 }
1019
1020 #[test]
1023 fn facade_response_ok() {
1024 let resp = FacadeResponse::ok(serde_json::json!({"status": "running"}));
1025 assert_eq!(resp.status, 200);
1026 }
1027
1028 #[test]
1029 fn facade_response_not_found() {
1030 let resp = FacadeResponse::not_found();
1031 assert_eq!(resp.status, 404);
1032 }
1033
1034 #[test]
1035 fn facade_response_bad_request() {
1036 let resp = FacadeResponse::bad_request("missing field");
1037 assert_eq!(resp.status, 400);
1038 assert_eq!(resp.body["error"], "missing field");
1039 }
1040
1041 #[test]
1042 fn facade_response_unauthorized() {
1043 let resp = FacadeResponse::unauthorized("invalid token");
1044 assert_eq!(resp.status, 401);
1045 }
1046
1047 #[test]
1050 fn sse_headers() {
1051 let headers = sse_response_headers();
1052 assert!(headers.iter().any(|(k, v)| *k == "Content-Type" && *v == "text/event-stream"));
1053 assert!(headers.iter().any(|(k, _)| *k == "Cache-Control"));
1054 }
1055
1056 #[test]
1059 fn content_types() {
1060 assert_eq!(CONTENT_TYPE_SSE, "text/event-stream");
1061 assert_eq!(CONTENT_TYPE_JSON, "application/json");
1062 }
1063}