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