Skip to main content

opencode_sdk/http/
questions.rs

1//! Questions API for OpenCode.
2//!
3//! Endpoints for listing and responding to interactive question requests.
4
5use crate::error::Result;
6use crate::http::HttpClient;
7use crate::types::question::{QuestionReplyRequest, QuestionRequest};
8use reqwest::Method;
9
10/// Questions API client.
11#[derive(Clone)]
12pub struct QuestionsApi {
13    http: HttpClient,
14}
15
16impl QuestionsApi {
17    /// Create a new Questions API client.
18    pub fn new(http: HttpClient) -> Self {
19        Self { http }
20    }
21
22    /// List pending question requests.
23    ///
24    /// # Errors
25    ///
26    /// Returns an error if the request fails.
27    pub async fn list(&self) -> Result<Vec<QuestionRequest>> {
28        self.http.request_json(Method::GET, "/question", None).await
29    }
30
31    /// Reply to a question request.
32    ///
33    /// Returns `true` on successful acceptance by the server.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the request fails.
38    pub async fn reply(&self, request_id: &str, req: &QuestionReplyRequest) -> Result<bool> {
39        let body = serde_json::to_value(req)?;
40        self.http
41            .request_json(
42                Method::POST,
43                &format!("/question/{}/reply", request_id),
44                Some(body),
45            )
46            .await
47    }
48
49    /// Reject a pending question request.
50    ///
51    /// Returns `true` on successful rejection by the server.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the request fails.
56    pub async fn reject(&self, request_id: &str) -> Result<bool> {
57        self.http
58            .request_json(
59                Method::POST,
60                &format!("/question/{}/reject", request_id),
61                None,
62            )
63            .await
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::http::HttpConfig;
71    use std::time::Duration;
72    use wiremock::matchers::{body_json, method, path};
73    use wiremock::{Mock, MockServer, ResponseTemplate};
74
75    #[tokio::test]
76    async fn test_list_questions() {
77        let mock_server = MockServer::start().await;
78
79        Mock::given(method("GET"))
80            .and(path("/question"))
81            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
82                {
83                    "id": "q-1",
84                    "sessionId": "s-1",
85                    "questions": [
86                        {
87                            "question": "Select one",
88                            "header": "Decision",
89                            "options": [
90                                {"label": "yes", "description": "Allow"},
91                                {"label": "no", "description": "Reject"}
92                            ]
93                        }
94                    ]
95                }
96            ])))
97            .mount(&mock_server)
98            .await;
99
100        let http = HttpClient::new(HttpConfig {
101            base_url: mock_server.uri(),
102            directory: None,
103            timeout: Duration::from_secs(30),
104        })
105        .unwrap();
106
107        let questions = QuestionsApi::new(http);
108        let list = questions.list().await.unwrap();
109        assert_eq!(list.len(), 1);
110        assert_eq!(list[0].id, "q-1");
111        assert_eq!(list[0].questions[0].header, "Decision");
112    }
113
114    #[tokio::test]
115    async fn test_reply_question() {
116        let mock_server = MockServer::start().await;
117
118        Mock::given(method("POST"))
119            .and(path("/question/q-1/reply"))
120            .and(body_json(serde_json::json!({
121                "answers": [["yes"]]
122            })))
123            .respond_with(ResponseTemplate::new(200).set_body_json(true))
124            .mount(&mock_server)
125            .await;
126
127        let http = HttpClient::new(HttpConfig {
128            base_url: mock_server.uri(),
129            directory: None,
130            timeout: Duration::from_secs(30),
131        })
132        .unwrap();
133
134        let questions = QuestionsApi::new(http);
135        let accepted = questions
136            .reply(
137                "q-1",
138                &QuestionReplyRequest {
139                    answers: vec![vec!["yes".to_string()]],
140                },
141            )
142            .await
143            .unwrap();
144        assert!(accepted);
145    }
146
147    #[tokio::test]
148    async fn test_reject_question() {
149        let mock_server = MockServer::start().await;
150
151        Mock::given(method("POST"))
152            .and(path("/question/q-2/reject"))
153            .respond_with(ResponseTemplate::new(200).set_body_json(true))
154            .mount(&mock_server)
155            .await;
156
157        let http = HttpClient::new(HttpConfig {
158            base_url: mock_server.uri(),
159            directory: None,
160            timeout: Duration::from_secs(30),
161        })
162        .unwrap();
163
164        let questions = QuestionsApi::new(http);
165        let rejected = questions.reject("q-2").await.unwrap();
166        assert!(rejected);
167    }
168
169    #[tokio::test]
170    async fn test_question_reply_not_found() {
171        let mock_server = MockServer::start().await;
172
173        Mock::given(method("POST"))
174            .and(path("/question/missing/reply"))
175            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
176                "name": "NotFound",
177                "message": "Question request not found"
178            })))
179            .mount(&mock_server)
180            .await;
181
182        let http = HttpClient::new(HttpConfig {
183            base_url: mock_server.uri(),
184            directory: None,
185            timeout: Duration::from_secs(30),
186        })
187        .unwrap();
188
189        let questions = QuestionsApi::new(http);
190        let result = questions
191            .reply(
192                "missing",
193                &QuestionReplyRequest {
194                    answers: vec![vec!["yes".to_string()]],
195                },
196            )
197            .await;
198        assert!(result.is_err());
199        assert!(result.unwrap_err().is_not_found());
200    }
201}