Skip to main content

arcane_engine/agent/
mod.rs

1pub mod inspector;
2
3use std::sync::mpsc;
4
5/// Requests the inspector HTTP server can send to the game loop.
6#[derive(Debug)]
7pub enum InspectorRequest {
8    Health,
9    GetState { path: Option<String> },
10    Describe { verbosity: Option<String> },
11    ListActions,
12    ExecuteAction { name: String, payload: String },
13    Rewind { steps: u32 },
14    Simulate { action: String },
15    GetHistory,
16}
17
18/// Response from the game loop back to the inspector HTTP server.
19#[derive(Debug)]
20pub struct InspectorResponse {
21    pub status: u16,
22    pub content_type: String,
23    pub body: String,
24}
25
26impl InspectorResponse {
27    pub fn json(body: String) -> Self {
28        Self {
29            status: 200,
30            content_type: "application/json".into(),
31            body,
32        }
33    }
34
35    pub fn text(body: String) -> Self {
36        Self {
37            status: 200,
38            content_type: "text/plain".into(),
39            body,
40        }
41    }
42
43    pub fn error(status: u16, message: String) -> Self {
44        Self {
45            status,
46            content_type: "application/json".into(),
47            body: format!("{{\"error\":\"{message}\"}}"),
48        }
49    }
50}
51
52/// Sender half: used by the game loop to respond to inspector requests.
53pub type ResponseSender = mpsc::Sender<InspectorResponse>;
54
55/// A request bundled with a channel to send the response back.
56pub type InspectorMessage = (InspectorRequest, ResponseSender);
57
58/// Sender half: used by the inspector HTTP server to send requests to the game loop.
59pub type RequestSender = mpsc::Sender<InspectorMessage>;
60
61/// Receiver half: used by the game loop to receive requests from the inspector.
62pub type RequestReceiver = mpsc::Receiver<InspectorMessage>;
63
64/// Create a new inspector channel pair.
65pub fn inspector_channel() -> (RequestSender, RequestReceiver) {
66    mpsc::channel()
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn inspector_channel_round_trip() {
75        let (tx, rx) = inspector_channel();
76
77        // Simulate inspector sending a request
78        let (resp_tx, resp_rx) = mpsc::channel();
79        tx.send((InspectorRequest::Health, resp_tx)).unwrap();
80
81        // Simulate game loop receiving and responding
82        let (req, sender) = rx.recv().unwrap();
83        assert!(matches!(req, InspectorRequest::Health));
84        sender
85            .send(InspectorResponse::json("{\"status\":\"ok\"}".into()))
86            .unwrap();
87
88        // Verify inspector gets the response
89        let response = resp_rx.recv().unwrap();
90        assert_eq!(response.status, 200);
91        assert_eq!(response.content_type, "application/json");
92        assert!(response.body.contains("ok"));
93    }
94
95    #[test]
96    fn inspector_response_constructors() {
97        let json = InspectorResponse::json("{\"key\":1}".into());
98        assert_eq!(json.status, 200);
99        assert_eq!(json.content_type, "application/json");
100
101        let text = InspectorResponse::text("hello".into());
102        assert_eq!(text.status, 200);
103        assert_eq!(text.content_type, "text/plain");
104
105        let err = InspectorResponse::error(404, "not found".into());
106        assert_eq!(err.status, 404);
107        assert!(err.body.contains("not found"));
108    }
109
110    #[test]
111    fn inspector_request_variants() {
112        // Ensure all variants construct correctly
113        let requests = vec![
114            InspectorRequest::Health,
115            InspectorRequest::GetState { path: None },
116            InspectorRequest::GetState {
117                path: Some("player.hp".into()),
118            },
119            InspectorRequest::Describe {
120                verbosity: Some("full".into()),
121            },
122            InspectorRequest::ListActions,
123            InspectorRequest::ExecuteAction {
124                name: "move".into(),
125                payload: "{}".into(),
126            },
127            InspectorRequest::Rewind { steps: 3 },
128            InspectorRequest::Simulate {
129                action: "attack".into(),
130            },
131            InspectorRequest::GetHistory,
132        ];
133        assert_eq!(requests.len(), 9);
134    }
135}