1use crate::error::Result;
6use crate::http::HttpClient;
7use crate::types::api::{CommandResponse, ShellResponse};
8use crate::types::message::{CommandRequest, Message, PromptRequest, ShellRequest};
9use crate::types::project::ModelRef;
10use reqwest::Method;
11
12#[derive(Clone)]
14pub struct MessagesApi {
15 http: HttpClient,
16}
17
18impl MessagesApi {
19 pub fn new(http: HttpClient) -> Self {
21 Self { http }
22 }
23
24 pub async fn prompt(&self, session_id: &str, req: &PromptRequest) -> Result<()> {
30 let body = serde_json::to_value(req)?;
31 self.http
32 .request_empty(
33 Method::POST,
34 &format!("/session/{}/message", session_id),
35 Some(body),
36 )
37 .await
38 }
39
40 pub async fn list(&self, session_id: &str) -> Result<Vec<Message>> {
46 self.http
47 .request_json(
48 Method::GET,
49 &format!("/session/{}/message", session_id),
50 None,
51 )
52 .await
53 }
54
55 pub async fn get(&self, session_id: &str, message_id: &str) -> Result<Message> {
61 self.http
62 .request_json(
63 Method::GET,
64 &format!("/session/{}/message/{}", session_id, message_id),
65 None,
66 )
67 .await
68 }
69
70 pub async fn prompt_async(&self, session_id: &str, req: &PromptRequest) -> Result<()> {
79 let body = serde_json::to_value(req)?;
80 self.http
81 .request_empty(
82 Method::POST,
83 &format!("/session/{}/prompt_async", session_id),
84 Some(body),
85 )
86 .await
87 }
88
89 pub async fn send_text_async(
98 &self,
99 session_id: &str,
100 text: impl Into<String>,
101 model: Option<ModelRef>,
102 ) -> Result<()> {
103 let mut req = PromptRequest::text(text);
104 req.model = model;
105 self.prompt_async(session_id, &req).await
106 }
107
108 pub async fn command(&self, session_id: &str, req: &CommandRequest) -> Result<CommandResponse> {
114 let body = serde_json::to_value(req)?;
115 self.http
116 .request_json(
117 Method::POST,
118 &format!("/session/{}/command", session_id),
119 Some(body),
120 )
121 .await
122 }
123
124 pub async fn shell(&self, session_id: &str, req: &ShellRequest) -> Result<ShellResponse> {
130 let body = serde_json::to_value(req)?;
131 self.http
132 .request_json(
133 Method::POST,
134 &format!("/session/{}/shell", session_id),
135 Some(body),
136 )
137 .await
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::http::HttpConfig;
145 use crate::types::message::{CommandRequest, PromptPart, ShellRequest};
146 use std::time::Duration;
147 use wiremock::matchers::{method, path};
148 use wiremock::{Mock, MockServer, ResponseTemplate};
149
150 #[tokio::test]
151 async fn test_prompt() {
152 let mock_server = MockServer::start().await;
153
154 Mock::given(method("POST"))
156 .and(path("/session/s1/message"))
157 .respond_with(ResponseTemplate::new(200))
158 .mount(&mock_server)
159 .await;
160
161 let http = HttpClient::new(HttpConfig {
162 base_url: mock_server.uri(),
163 directory: None,
164 timeout: Duration::from_secs(30),
165 })
166 .unwrap();
167
168 let messages = MessagesApi::new(http);
169 let result = messages
170 .prompt(
171 "s1",
172 &PromptRequest {
173 parts: vec![PromptPart::Text {
174 text: "Hello".to_string(),
175 synthetic: None,
176 ignored: None,
177 metadata: None,
178 }],
179 message_id: None,
180 model: None,
181 agent: None,
182 no_reply: None,
183 system: None,
184 variant: None,
185 },
186 )
187 .await;
188 assert!(result.is_ok());
189 }
190
191 #[tokio::test]
192 async fn test_list_messages() {
193 let mock_server = MockServer::start().await;
194
195 Mock::given(method("GET"))
196 .and(path("/session/s1/message"))
197 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
198 {
199 "info": {"id": "m1", "sessionId": "s1", "role": "user", "time": {"created": 1234567890}},
200 "parts": []
201 },
202 {
203 "info": {"id": "m2", "sessionId": "s1", "role": "assistant", "time": {"created": 1234567891}},
204 "parts": []
205 }
206 ])))
207 .mount(&mock_server)
208 .await;
209
210 let http = HttpClient::new(HttpConfig {
211 base_url: mock_server.uri(),
212 directory: None,
213 timeout: Duration::from_secs(30),
214 })
215 .unwrap();
216
217 let messages = MessagesApi::new(http);
218 let list = messages.list("s1").await.unwrap();
219 assert_eq!(list.len(), 2);
220 assert_eq!(list[0].role(), "user");
221 assert_eq!(list[1].role(), "assistant");
222 }
223
224 #[tokio::test]
225 async fn test_prompt_async() {
226 let mock_server = MockServer::start().await;
227
228 Mock::given(method("POST"))
230 .and(path("/session/s1/prompt_async"))
231 .respond_with(ResponseTemplate::new(200))
232 .mount(&mock_server)
233 .await;
234
235 let http = HttpClient::new(HttpConfig {
236 base_url: mock_server.uri(),
237 directory: None,
238 timeout: Duration::from_secs(30),
239 })
240 .unwrap();
241
242 let messages = MessagesApi::new(http);
243 let result = messages
244 .prompt_async(
245 "s1",
246 &PromptRequest {
247 parts: vec![PromptPart::Text {
248 text: "Hello async".to_string(),
249 synthetic: None,
250 ignored: None,
251 metadata: None,
252 }],
253 message_id: None,
254 model: None,
255 agent: None,
256 no_reply: None,
257 system: None,
258 variant: None,
259 },
260 )
261 .await;
262 assert!(result.is_ok());
263 }
264
265 #[tokio::test]
266 async fn test_send_text_async() {
267 let mock_server = MockServer::start().await;
268
269 Mock::given(method("POST"))
271 .and(path("/session/s1/prompt_async"))
272 .respond_with(ResponseTemplate::new(200))
273 .mount(&mock_server)
274 .await;
275
276 let http = HttpClient::new(HttpConfig {
277 base_url: mock_server.uri(),
278 directory: None,
279 timeout: Duration::from_secs(30),
280 })
281 .unwrap();
282
283 let messages = MessagesApi::new(http);
284 let result = messages
285 .send_text_async(
286 "s1",
287 "Hello from helper",
288 Some(ModelRef {
289 provider_id: "opencode".to_string(),
290 model_id: "kimi-k2.5-free".to_string(),
291 }),
292 )
293 .await;
294 assert!(result.is_ok());
295 }
296
297 #[tokio::test]
298 async fn test_command() {
299 let mock_server = MockServer::start().await;
300
301 Mock::given(method("POST"))
302 .and(path("/session/s1/command"))
303 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
304 "status": "executed"
305 })))
306 .mount(&mock_server)
307 .await;
308
309 let http = HttpClient::new(HttpConfig {
310 base_url: mock_server.uri(),
311 directory: None,
312 timeout: Duration::from_secs(30),
313 })
314 .unwrap();
315
316 let messages = MessagesApi::new(http);
317 let result = messages
318 .command(
319 "s1",
320 &CommandRequest {
321 command: "test_command".to_string(),
322 args: None,
323 },
324 )
325 .await
326 .unwrap();
327 assert_eq!(result.status, Some("executed".to_string()));
328 }
329
330 #[tokio::test]
331 async fn test_shell() {
332 let mock_server = MockServer::start().await;
333
334 Mock::given(method("POST"))
335 .and(path("/session/s1/shell"))
336 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
337 "status": "running"
338 })))
339 .mount(&mock_server)
340 .await;
341
342 let http = HttpClient::new(HttpConfig {
343 base_url: mock_server.uri(),
344 directory: None,
345 timeout: Duration::from_secs(30),
346 })
347 .unwrap();
348
349 let messages = MessagesApi::new(http);
350 let result = messages
351 .shell(
352 "s1",
353 &ShellRequest {
354 command: "echo hello".to_string(),
355 model: None,
356 },
357 )
358 .await
359 .unwrap();
360 assert_eq!(result.status, Some("running".to_string()));
361 }
362
363 #[tokio::test]
366 async fn test_prompt_session_not_found() {
367 let mock_server = MockServer::start().await;
368
369 Mock::given(method("POST"))
370 .and(path("/session/missing/message"))
371 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
372 "name": "NotFound",
373 "message": "Session not found"
374 })))
375 .mount(&mock_server)
376 .await;
377
378 let http = HttpClient::new(HttpConfig {
379 base_url: mock_server.uri(),
380 directory: None,
381 timeout: Duration::from_secs(30),
382 })
383 .unwrap();
384
385 let messages = MessagesApi::new(http);
386 let result = messages
387 .prompt(
388 "missing",
389 &PromptRequest {
390 parts: vec![PromptPart::Text {
391 text: "test".to_string(),
392 synthetic: None,
393 ignored: None,
394 metadata: None,
395 }],
396 message_id: None,
397 model: None,
398 agent: None,
399 no_reply: None,
400 system: None,
401 variant: None,
402 },
403 )
404 .await;
405 assert!(result.is_err());
406 assert!(result.unwrap_err().is_not_found());
407 }
408
409 #[tokio::test]
410 async fn test_prompt_validation_error() {
411 let mock_server = MockServer::start().await;
412
413 Mock::given(method("POST"))
414 .and(path("/session/s1/message"))
415 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
416 "name": "ValidationError",
417 "message": "Invalid prompt format"
418 })))
419 .mount(&mock_server)
420 .await;
421
422 let http = HttpClient::new(HttpConfig {
423 base_url: mock_server.uri(),
424 directory: None,
425 timeout: Duration::from_secs(30),
426 })
427 .unwrap();
428
429 let messages = MessagesApi::new(http);
430 let result = messages
431 .prompt(
432 "s1",
433 &PromptRequest {
434 parts: vec![],
435 message_id: None,
436 model: None,
437 agent: None,
438 no_reply: None,
439 system: None,
440 variant: None,
441 },
442 )
443 .await;
444 assert!(result.is_err());
445 assert!(result.unwrap_err().is_validation_error());
446 }
447
448 #[tokio::test]
449 async fn test_list_messages_not_found() {
450 let mock_server = MockServer::start().await;
451
452 Mock::given(method("GET"))
453 .and(path("/session/missing/message"))
454 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
455 "name": "NotFound",
456 "message": "Session not found"
457 })))
458 .mount(&mock_server)
459 .await;
460
461 let http = HttpClient::new(HttpConfig {
462 base_url: mock_server.uri(),
463 directory: None,
464 timeout: Duration::from_secs(30),
465 })
466 .unwrap();
467
468 let messages = MessagesApi::new(http);
469 let result = messages.list("missing").await;
470 assert!(result.is_err());
471 assert!(result.unwrap_err().is_not_found());
472 }
473
474 #[tokio::test]
475 async fn test_get_message_not_found() {
476 let mock_server = MockServer::start().await;
477
478 Mock::given(method("GET"))
479 .and(path("/session/s1/message/missing"))
480 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
481 "name": "NotFound",
482 "message": "Message not found"
483 })))
484 .mount(&mock_server)
485 .await;
486
487 let http = HttpClient::new(HttpConfig {
488 base_url: mock_server.uri(),
489 directory: None,
490 timeout: Duration::from_secs(30),
491 })
492 .unwrap();
493
494 let messages = MessagesApi::new(http);
495 let result = messages.get("s1", "missing").await;
496 assert!(result.is_err());
497 assert!(result.unwrap_err().is_not_found());
498 }
499
500 #[tokio::test]
501 async fn test_prompt_async_server_error() {
502 let mock_server = MockServer::start().await;
503
504 Mock::given(method("POST"))
505 .and(path("/session/s1/prompt_async"))
506 .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
507 "name": "InternalError",
508 "message": "Failed to queue prompt"
509 })))
510 .mount(&mock_server)
511 .await;
512
513 let http = HttpClient::new(HttpConfig {
514 base_url: mock_server.uri(),
515 directory: None,
516 timeout: Duration::from_secs(30),
517 })
518 .unwrap();
519
520 let messages = MessagesApi::new(http);
521 let result = messages
522 .prompt_async(
523 "s1",
524 &PromptRequest {
525 parts: vec![PromptPart::Text {
526 text: "test".to_string(),
527 synthetic: None,
528 ignored: None,
529 metadata: None,
530 }],
531 message_id: None,
532 model: None,
533 agent: None,
534 no_reply: None,
535 system: None,
536 variant: None,
537 },
538 )
539 .await;
540 assert!(result.is_err());
541 assert!(result.unwrap_err().is_server_error());
542 }
543
544 #[tokio::test]
545 async fn test_command_not_found() {
546 let mock_server = MockServer::start().await;
547
548 Mock::given(method("POST"))
549 .and(path("/session/missing/command"))
550 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
551 "name": "NotFound",
552 "message": "Session not found"
553 })))
554 .mount(&mock_server)
555 .await;
556
557 let http = HttpClient::new(HttpConfig {
558 base_url: mock_server.uri(),
559 directory: None,
560 timeout: Duration::from_secs(30),
561 })
562 .unwrap();
563
564 let messages = MessagesApi::new(http);
565 let result = messages
566 .command(
567 "missing",
568 &CommandRequest {
569 command: "test".to_string(),
570 args: None,
571 },
572 )
573 .await;
574 assert!(result.is_err());
575 assert!(result.unwrap_err().is_not_found());
576 }
577
578 #[tokio::test]
579 async fn test_shell_validation_error() {
580 let mock_server = MockServer::start().await;
581
582 Mock::given(method("POST"))
583 .and(path("/session/s1/shell"))
584 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
585 "name": "ValidationError",
586 "message": "Invalid shell command"
587 })))
588 .mount(&mock_server)
589 .await;
590
591 let http = HttpClient::new(HttpConfig {
592 base_url: mock_server.uri(),
593 directory: None,
594 timeout: Duration::from_secs(30),
595 })
596 .unwrap();
597
598 let messages = MessagesApi::new(http);
599 let result = messages
600 .shell(
601 "s1",
602 &ShellRequest {
603 command: "".to_string(),
604 model: None,
605 },
606 )
607 .await;
608 assert!(result.is_err());
609 assert!(result.unwrap_err().is_validation_error());
610 }
611}