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