1use rmcp::schemars;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
16pub struct AskUserRequest {
17 #[schemars(
18 description = "List of questions to ask the user. Each question has a label, question text, and options."
19 )]
20 pub questions: Vec<AskUserQuestion>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
25pub struct AskUserQuestion {
26 #[schemars(description = "Short unique label for tab display (max ~15 chars recommended)")]
27 pub label: String,
28 #[schemars(description = "Full question text to display")]
29 pub question: String,
30 #[schemars(description = "Predefined answer options")]
31 pub options: Vec<AskUserOption>,
32 #[serde(default = "default_true")]
34 #[schemars(description = "Whether to allow custom text input (default: true)")]
35 pub allow_custom: bool,
36 #[serde(default)]
38 #[schemars(
39 description = "When true, user can select/deselect multiple options (checkbox list). Default: false (single-select radio behavior)."
40 )]
41 pub multi_select: bool,
42}
43
44#[derive(Debug, Clone, Serialize, PartialEq, schemars::JsonSchema)]
50pub struct AskUserOption {
51 #[schemars(description = "Value to return to LLM when selected")]
52 pub value: String,
53 #[schemars(description = "Display label for the option")]
54 pub label: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 #[schemars(description = "Optional description shown below the label")]
58 pub description: Option<String>,
59 #[serde(default)]
61 #[schemars(
62 description = "Default selection state when multi_select is true. Pre-marks this option as selected. Ignored for single-select questions."
63 )]
64 pub selected: bool,
65}
66
67impl<'de> Deserialize<'de> for AskUserOption {
68 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
69 where
70 D: serde::Deserializer<'de>,
71 {
72 #[derive(Deserialize)]
73 struct Raw {
74 value: Option<String>,
75 label: String,
76 #[serde(default)]
77 description: Option<String>,
78 #[serde(default)]
79 selected: bool,
80 }
81
82 let raw = Raw::deserialize(deserializer)?;
83 Ok(AskUserOption {
84 value: raw.value.unwrap_or_else(|| raw.label.clone()),
85 label: raw.label,
86 description: raw.description,
87 selected: raw.selected,
88 })
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
98pub struct AskUserAnswer {
99 pub question_label: String,
101 pub answer: String,
104 pub is_custom: bool,
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub selected_values: Vec<String>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
113pub struct AskUserResult {
114 pub answers: Vec<AskUserAnswer>,
116 pub completed: bool,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub reason: Option<String>,
121}
122
123fn default_true() -> bool {
128 true
129}
130
131#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_question_serialization() {
141 let question = AskUserQuestion {
142 label: "Environment".to_string(),
143 question: "Which environment should I deploy to?".to_string(),
144 options: vec![
145 AskUserOption {
146 value: "dev".to_string(),
147 label: "Development".to_string(),
148 description: Some("For testing changes".to_string()),
149 selected: false,
150 },
151 AskUserOption {
152 value: "prod".to_string(),
153 label: "Production".to_string(),
154 description: None,
155 selected: false,
156 },
157 ],
158 allow_custom: true,
159 multi_select: false,
160 };
161
162 let json = serde_json::to_string(&question).unwrap();
163 assert!(json.contains("\"label\":\"Environment\""));
164 assert!(json.contains("\"value\":\"dev\""));
165 assert!(json.contains("\"description\":\"For testing changes\""));
166 assert!(!json.contains("\"description\":null"));
168 }
169
170 #[test]
171 fn test_question_deserialization_with_defaults() {
172 let json = r#"{
173 "label": "Test",
174 "question": "Is this a test?",
175 "options": []
176 }"#;
177
178 let question: AskUserQuestion = serde_json::from_str(json).unwrap();
179 assert_eq!(question.label, "Test");
180 assert!(question.allow_custom, "allow_custom should default to true");
181 }
182
183 #[test]
184 fn test_question_deserialization_explicit_false() {
185 let json = r#"{
186 "label": "Test",
187 "question": "Is this a test?",
188 "options": [],
189 "allow_custom": false
190 }"#;
191
192 let question: AskUserQuestion = serde_json::from_str(json).unwrap();
193 assert!(!question.allow_custom);
194 }
195
196 #[test]
197 fn test_answer_serialization() {
198 let answer = AskUserAnswer {
199 question_label: "Environment".to_string(),
200 answer: "production".to_string(),
201 is_custom: false,
202 selected_values: vec![],
203 };
204
205 let json = serde_json::to_string(&answer).unwrap();
206 assert!(json.contains("\"question_label\":\"Environment\""));
207 assert!(json.contains("\"answer\":\"production\""));
208 assert!(json.contains("\"is_custom\":false"));
209 }
210
211 #[test]
212 fn test_answer_custom_input() {
213 let answer = AskUserAnswer {
214 question_label: "Feedback".to_string(),
215 answer: "User typed this custom response".to_string(),
216 is_custom: true,
217 selected_values: vec![],
218 };
219
220 let json = serde_json::to_string(&answer).unwrap();
221 assert!(json.contains("\"is_custom\":true"));
222 assert!(json.contains("User typed this custom response"));
223 }
224
225 #[test]
226 fn test_result_completed() {
227 let result = AskUserResult {
228 answers: vec![
229 AskUserAnswer {
230 question_label: "q1".to_string(),
231 answer: "a1".to_string(),
232 is_custom: false,
233 selected_values: vec![],
234 },
235 AskUserAnswer {
236 question_label: "q2".to_string(),
237 answer: "custom answer".to_string(),
238 is_custom: true,
239 selected_values: vec![],
240 },
241 ],
242 completed: true,
243 reason: None,
244 };
245
246 let json = serde_json::to_string(&result).unwrap();
247 assert!(json.contains("\"completed\":true"));
248 assert!(!json.contains("\"reason\""));
250 assert!(json.contains("\"question_label\":\"q1\""));
251 assert!(json.contains("\"question_label\":\"q2\""));
252 }
253
254 #[test]
255 fn test_result_cancelled() {
256 let result = AskUserResult {
257 answers: vec![],
258 completed: false,
259 reason: Some("User cancelled the question prompt.".to_string()),
260 };
261
262 let json = serde_json::to_string(&result).unwrap();
263 assert!(json.contains("\"completed\":false"));
264 assert!(json.contains("\"reason\":\"User cancelled the question prompt.\""));
265 assert!(json.contains("\"answers\":[]"));
266 }
267
268 #[test]
269 fn test_result_deserialization() {
270 let json = r#"{
271 "answers": [
272 {"question_label": "env", "answer": "dev", "is_custom": false}
273 ],
274 "completed": true
275 }"#;
276
277 let result: AskUserResult = serde_json::from_str(json).unwrap();
278 assert!(result.completed);
279 assert!(result.reason.is_none());
280 assert_eq!(result.answers.len(), 1);
281 assert_eq!(result.answers[0].question_label, "env");
282 assert_eq!(result.answers[0].answer, "dev");
283 assert!(!result.answers[0].is_custom);
284 }
285
286 #[test]
287 fn test_option_without_description() {
288 let option = AskUserOption {
289 value: "yes".to_string(),
290 label: "Yes".to_string(),
291 description: None,
292 selected: false,
293 };
294
295 let json = serde_json::to_string(&option).unwrap();
296 assert!(!json.contains("description"));
298 assert!(json.contains("\"value\":\"yes\""));
299 assert!(json.contains("\"label\":\"Yes\""));
300 }
301
302 #[test]
303 fn test_unicode_handling() {
304 let question = AskUserQuestion {
305 label: "言語".to_string(),
306 question: "どの言語を使用しますか?".to_string(),
307 options: vec![
308 AskUserOption {
309 value: "ja".to_string(),
310 label: "日本語".to_string(),
311 description: Some("Japanese language".to_string()),
312 selected: false,
313 },
314 AskUserOption {
315 value: "emoji".to_string(),
316 label: "🚀 Rocket".to_string(),
317 description: Some("With emoji 🎉".to_string()),
318 selected: false,
319 },
320 ],
321 allow_custom: true,
322 multi_select: false,
323 };
324
325 let json = serde_json::to_string(&question).unwrap();
326 let parsed: AskUserQuestion = serde_json::from_str(&json).unwrap();
327
328 assert_eq!(parsed.label, "言語");
329 assert_eq!(parsed.question, "どの言語を使用しますか?");
330 assert_eq!(parsed.options[0].label, "日本語");
331 assert_eq!(parsed.options[1].label, "🚀 Rocket");
332 }
333
334 #[test]
335 fn test_types_equality() {
336 let q1 = AskUserQuestion {
337 label: "Test".to_string(),
338 question: "Question?".to_string(),
339 options: vec![],
340 allow_custom: true,
341 multi_select: false,
342 };
343
344 let q2 = q1.clone();
345 assert_eq!(q1, q2);
346
347 let a1 = AskUserAnswer {
348 question_label: "Test".to_string(),
349 answer: "answer".to_string(),
350 is_custom: false,
351 selected_values: vec![],
352 };
353
354 let a2 = a1.clone();
355 assert_eq!(a1, a2);
356
357 let r1 = AskUserResult {
358 answers: vec![a1],
359 completed: true,
360 reason: None,
361 };
362
363 let r2 = r1.clone();
364 assert_eq!(r1, r2);
365 }
366
367 #[test]
368 fn test_request_round_trip() {
369 let request = AskUserRequest {
370 questions: vec![AskUserQuestion {
371 label: "Env".to_string(),
372 question: "Which env?".to_string(),
373 options: vec![AskUserOption {
374 value: "dev".to_string(),
375 label: "Dev".to_string(),
376 description: None,
377 selected: false,
378 }],
379 allow_custom: false,
380 multi_select: false,
381 }],
382 };
383
384 let json = serde_json::to_string(&request).unwrap();
385 let parsed: AskUserRequest = serde_json::from_str(&json).unwrap();
386 assert_eq!(request, parsed);
387 }
388
389 #[test]
390 fn test_multi_select_defaults() {
391 let json = r#"{
392 "label": "Scope",
393 "question": "Which repos?",
394 "options": [
395 {"value": "a", "label": "Repo A"},
396 {"value": "b", "label": "Repo B", "selected": true}
397 ]
398 }"#;
399
400 let question: AskUserQuestion = serde_json::from_str(json).unwrap();
401 assert!(
402 !question.multi_select,
403 "multi_select should default to false"
404 );
405 assert!(
406 !question.options[0].selected,
407 "selected should default to false"
408 );
409 assert!(
410 question.options[1].selected,
411 "selected should be true when set"
412 );
413 }
414
415 #[test]
416 fn test_multi_select_question_round_trip() {
417 let question = AskUserQuestion {
418 label: "Scope".to_string(),
419 question: "Which repos should I include?".to_string(),
420 options: vec![
421 AskUserOption {
422 value: "repo:api".to_string(),
423 label: "~/projects/api".to_string(),
424 description: None,
425 selected: true,
426 },
427 AskUserOption {
428 value: "repo:web".to_string(),
429 label: "~/projects/web".to_string(),
430 description: None,
431 selected: false,
432 },
433 ],
434 allow_custom: false,
435 multi_select: true,
436 };
437
438 let json = serde_json::to_string(&question).unwrap();
439 assert!(json.contains("\"multi_select\":true"));
440 assert!(json.contains("\"selected\":true"));
441
442 let parsed: AskUserQuestion = serde_json::from_str(&json).unwrap();
443 assert_eq!(question, parsed);
444 }
445
446 #[test]
447 fn test_multi_select_answer_with_selected_values() {
448 let answer = AskUserAnswer {
449 question_label: "Scope".to_string(),
450 answer: "[\"repo:api\",\"repo:web\"]".to_string(),
451 is_custom: false,
452 selected_values: vec!["repo:api".to_string(), "repo:web".to_string()],
453 };
454
455 let json = serde_json::to_string(&answer).unwrap();
456 assert!(json.contains("\"selected_values\""));
457 assert!(json.contains("repo:api"));
458 assert!(json.contains("repo:web"));
459
460 let parsed: AskUserAnswer = serde_json::from_str(&json).unwrap();
461 assert_eq!(parsed.selected_values.len(), 2);
462 }
463
464 #[test]
465 fn test_selected_values_omitted_when_empty() {
466 let answer = AskUserAnswer {
467 question_label: "Env".to_string(),
468 answer: "dev".to_string(),
469 is_custom: false,
470 selected_values: vec![],
471 };
472
473 let json = serde_json::to_string(&answer).unwrap();
474 assert!(
475 !json.contains("selected_values"),
476 "selected_values should be omitted when empty"
477 );
478 }
479
480 #[test]
481 fn test_answer_deserialization_without_selected_values() {
482 let json = r#"{"question_label": "env", "answer": "dev", "is_custom": false}"#;
484 let answer: AskUserAnswer = serde_json::from_str(json).unwrap();
485 assert!(answer.selected_values.is_empty());
486 }
487
488 #[test]
489 fn test_option_value_defaults_to_label_when_missing() {
490 let json = r#"{"label": "Already configured", "description": "AWS CLI is configured"}"#;
491 let option: AskUserOption = serde_json::from_str(json).unwrap();
492 assert_eq!(option.value, "Already configured");
493 assert_eq!(option.label, "Already configured");
494 assert_eq!(
495 option.description,
496 Some("AWS CLI is configured".to_string())
497 );
498 assert!(!option.selected);
499 }
500
501 #[test]
502 fn test_option_explicit_value_preserved() {
503 let json =
504 r#"{"value": "dev", "label": "Development", "description": "For testing changes"}"#;
505 let option: AskUserOption = serde_json::from_str(json).unwrap();
506 assert_eq!(option.value, "dev");
507 assert_eq!(option.label, "Development");
508 }
509
510 #[test]
511 fn test_option_value_null_defaults_to_label() {
512 let json = r#"{"value": null, "label": "Production"}"#;
513 let option: AskUserOption = serde_json::from_str(json).unwrap();
514 assert_eq!(option.value, "Production");
515 assert_eq!(option.label, "Production");
516 }
517
518 #[test]
519 fn test_real_llm_payload_without_value_fields() {
520 let json = r#"{"questions":[{"allow_custom": false, "label": "AWS Config", "options": [{"description": "AWS CLI is configured with credentials (aws configure already done)", "label": "Already configured"}, {"description": "I'll provide Access Key ID and Secret Access Key", "label": "Need to configure"}], "question": "Is your AWS CLI already configured with credentials?"}, {"allow_custom": false, "label": "SSH Key", "options": [{"description": "I have an EC2 key pair created in AWS", "label": "Have key pair"}, {"description": "Need to create a new key pair in AWS", "label": "Need to create"}], "question": "Do you have an SSH key pair in AWS EC2 for instance access?"}]}"#;
522
523 let request: AskUserRequest = serde_json::from_str(json).unwrap();
524 assert_eq!(request.questions.len(), 2);
525 assert_eq!(request.questions[0].label, "AWS Config");
526 assert_eq!(request.questions[0].options.len(), 2);
527 assert_eq!(request.questions[0].options[0].value, "Already configured");
529 assert_eq!(request.questions[0].options[1].value, "Need to configure");
530 assert_eq!(request.questions[1].options[0].value, "Have key pair");
531 }
532
533 #[test]
534 fn test_option_roundtrip_with_explicit_value() {
535 let option = AskUserOption {
536 value: "custom_value".to_string(),
537 label: "Display Label".to_string(),
538 description: None,
539 selected: false,
540 };
541 let json = serde_json::to_string(&option).unwrap();
542 let parsed: AskUserOption = serde_json::from_str(&json).unwrap();
543 assert_eq!(parsed.value, "custom_value");
544 assert_eq!(parsed.label, "Display Label");
545 }
546}