1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15#[serde(rename_all = "snake_case")]
16pub enum InteractionKind {
17 FreeText,
19 MultipleChoice,
21 Confirm,
23 ToolApproval,
25 EditText,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum BodyFormat {
34 #[default]
36 Plain,
37 Markdown,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct InteractionRequest {
44 pub id: String,
46 pub kind: InteractionKind,
48 pub prompt: String,
50 #[serde(default)]
52 pub options: Vec<String>,
53 pub tool_name: Option<String>,
55 pub tool_arguments: Option<serde_json::Value>,
57 #[serde(default = "default_true")]
59 pub required: bool,
60 pub stage_name: String,
62 #[serde(default)]
64 pub body: Option<String>,
65 #[serde(default)]
67 pub body_format: BodyFormat,
68}
69
70fn default_true() -> bool {
71 true
72}
73
74impl InteractionRequest {
75 pub fn free_text(
77 id: impl Into<String>,
78 prompt: impl Into<String>,
79 stage: impl Into<String>,
80 required: bool,
81 ) -> Self {
82 Self {
83 id: id.into(),
84 kind: InteractionKind::FreeText,
85 prompt: prompt.into(),
86 options: vec![],
87 tool_name: None,
88 tool_arguments: None,
89 required,
90 stage_name: stage.into(),
91 body: None,
92 body_format: BodyFormat::Plain,
93 }
94 }
95
96 pub fn review(
99 id: impl Into<String>,
100 title: impl Into<String>,
101 markdown: impl Into<String>,
102 stage: impl Into<String>,
103 ) -> Self {
104 Self {
105 id: id.into(),
106 kind: InteractionKind::FreeText,
107 prompt: title.into(),
108 options: vec![],
109 tool_name: None,
110 tool_arguments: None,
111 required: true,
112 stage_name: stage.into(),
113 body: Some(markdown.into()),
114 body_format: BodyFormat::Markdown,
115 }
116 }
117
118 pub fn edit_text(
122 id: impl Into<String>,
123 prompt: impl Into<String>,
124 stage: impl Into<String>,
125 initial_content: impl Into<String>,
126 ) -> Self {
127 Self {
128 id: id.into(),
129 kind: InteractionKind::EditText,
130 prompt: prompt.into(),
131 options: vec![],
132 tool_name: None,
133 tool_arguments: None,
134 required: true,
135 stage_name: stage.into(),
136 body: Some(initial_content.into()),
137 body_format: BodyFormat::Plain,
138 }
139 }
140
141 pub fn multiple_choice(
143 id: impl Into<String>,
144 prompt: impl Into<String>,
145 options: Vec<String>,
146 stage: impl Into<String>,
147 ) -> Self {
148 Self {
149 id: id.into(),
150 kind: InteractionKind::MultipleChoice,
151 prompt: prompt.into(),
152 options,
153 tool_name: None,
154 tool_arguments: None,
155 required: true,
156 stage_name: stage.into(),
157 body: None,
158 body_format: BodyFormat::Plain,
159 }
160 }
161
162 pub fn confirm(
164 id: impl Into<String>,
165 prompt: impl Into<String>,
166 stage: impl Into<String>,
167 ) -> Self {
168 Self {
169 id: id.into(),
170 kind: InteractionKind::Confirm,
171 prompt: prompt.into(),
172 options: vec!["Yes".to_string(), "No".to_string()],
173 tool_name: None,
174 tool_arguments: None,
175 required: true,
176 stage_name: stage.into(),
177 body: None,
178 body_format: BodyFormat::Plain,
179 }
180 }
181
182 fn approval_detail(tool: &str, arguments: &serde_json::Value) -> Option<String> {
194 let field = match tool {
195 "bash" | "shell" => "command",
196 "write_file" | "edit_file" | "read_file" => "path",
197 _ => return None,
198 };
199 let raw = arguments.get(field)?.as_str()?.trim();
200 if raw.is_empty() {
201 return None;
202 }
203 let first = raw.lines().next().unwrap_or(raw);
206 let mut shown: String = first.chars().take(120).collect();
207 if shown.chars().count() < first.chars().count() || first.len() < raw.len() {
208 shown.push('…');
209 }
210 Some(format!("`{shown}`"))
211 }
212
213 pub fn gate_approval(
220 id: impl Into<String>,
221 tool_name: impl Into<String>,
222 arguments: serde_json::Value,
223 stage: impl Into<String>,
224 ) -> Self {
225 let tool = tool_name.into();
226 Self {
227 id: id.into(),
228 kind: InteractionKind::ToolApproval,
229 prompt: format!("Allow tool call: `{tool}`?"),
230 options: vec![
231 "Allow once".to_string(),
232 "Allow for this run".to_string(),
233 "Deny".to_string(),
234 ],
235 tool_name: Some(tool),
236 tool_arguments: Some(arguments),
237 required: true,
238 stage_name: stage.into(),
239 body: None,
240 body_format: BodyFormat::Plain,
241 }
242 }
243
244 fn grant_summary(grant_keys: &[String]) -> Option<String> {
251 if grant_keys.is_empty() {
252 return None;
253 }
254 let named: Vec<&str> = grant_keys
255 .iter()
256 .take(3)
257 .map(|k| k.strip_prefix("shell:").unwrap_or(k))
258 .collect();
259 let mut summary = named.join(", ");
260 if grant_keys.len() > named.len() {
261 summary.push_str(&format!(" +{} more", grant_keys.len() - named.len()));
262 }
263 Some(summary)
264 }
265
266 pub fn tool_approval(
279 id: impl Into<String>,
280 tool_name: impl Into<String>,
281 arguments: serde_json::Value,
282 stage: impl Into<String>,
283 grant_keys: &[String],
284 ) -> Self {
285 let tool = tool_name.into();
286 let prompt = match Self::approval_detail(&tool, &arguments) {
287 Some(detail) => format!("Allow tool call: `{tool}` - {detail}?"),
288 None => format!("Allow tool call: `{tool}`?"),
289 };
290 let (stage_label, run_label) = match Self::grant_summary(grant_keys) {
291 Some(what) => (
292 format!("Allow {what} for this stage"),
293 format!("Allow {what} for this run"),
294 ),
295 None => (
296 "Allow for this stage (nothing reusable - it will ask again)".to_string(),
297 "Allow for this run (nothing reusable - it will ask again)".to_string(),
298 ),
299 };
300 Self {
301 id: id.into(),
302 kind: InteractionKind::ToolApproval,
303 prompt,
304 options: vec![
305 "Allow once".to_string(),
306 stage_label,
307 run_label,
308 "Deny".to_string(),
309 ],
310 tool_name: Some(tool),
311 tool_arguments: Some(arguments),
312 required: true,
313 stage_name: stage.into(),
314 body: None,
315 body_format: BodyFormat::Plain,
316 }
317 }
318}
319
320pub fn approval_choice(index: usize) -> Option<ApprovalScope> {
327 match index {
328 0 => Some(ApprovalScope::Once),
329 1 => Some(ApprovalScope::Stage),
330 2 => Some(ApprovalScope::Run),
331 _ => None,
332 }
333}
334
335#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
348#[serde(rename_all = "snake_case")]
349pub enum ApprovalScope {
350 Once,
352 Stage,
354 #[serde(rename = "session", alias = "run")]
360 Run,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
365pub struct InteractionResponse {
366 pub request_id: String,
368 pub value: Option<String>,
370 pub choice_index: Option<usize>,
372 pub approved: Option<bool>,
374 pub scope: Option<ApprovalScope>,
376}
377
378impl InteractionResponse {
379 pub fn text(request_id: impl Into<String>, value: impl Into<String>) -> Self {
381 Self {
382 request_id: request_id.into(),
383 value: Some(value.into()),
384 choice_index: None,
385 approved: None,
386 scope: None,
387 }
388 }
389
390 pub fn choice(request_id: impl Into<String>, index: usize) -> Self {
392 Self {
393 request_id: request_id.into(),
394 value: None,
395 choice_index: Some(index),
396 approved: None,
397 scope: None,
398 }
399 }
400
401 pub fn approval(request_id: impl Into<String>, approved: bool, scope: ApprovalScope) -> Self {
403 Self {
404 request_id: request_id.into(),
405 value: None,
406 choice_index: None,
407 approved: Some(approved),
408 scope: Some(scope),
409 }
410 }
411}
412
413pub fn response_as_text(resp: &InteractionResponse) -> String {
417 resp.value.clone().unwrap_or_default()
418}
419
420pub fn response_as_choice<'a>(
422 resp: &InteractionResponse,
423 options: &'a [String],
424) -> Option<&'a String> {
425 resp.choice_index.and_then(|i| options.get(i))
426}
427
428pub fn response_approved(resp: &InteractionResponse) -> bool {
430 resp.approved.unwrap_or(false)
431}
432
433pub fn make_interaction_id(stage_idx: usize, iteration: usize) -> String {
435 format!("{}-{}", stage_idx, iteration)
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
446 fn a_tool_approval_says_what_it_is_asking_about() {
447 let req = InteractionRequest::tool_approval(
448 "id",
449 "bash",
450 serde_json::json!({"command": "rm -rf build && make"}),
451 "implement",
452 &[],
453 );
454 assert!(
455 req.prompt.contains("rm -rf build && make"),
456 "{}",
457 req.prompt
458 );
459
460 let req = InteractionRequest::tool_approval(
462 "id",
463 "write_file",
464 serde_json::json!({"path": "src/main.rs", "content": "..."}),
465 "implement",
466 &[],
467 );
468 assert!(req.prompt.contains("src/main.rs"), "{}", req.prompt);
469 }
470
471 #[test]
474 fn a_long_or_multiline_command_is_summarised() {
475 let long = "echo ".to_string() + &"x".repeat(400);
476 let req = InteractionRequest::tool_approval(
477 "id",
478 "bash",
479 serde_json::json!({ "command": long }),
480 "s",
481 &[],
482 );
483 assert!(req.prompt.chars().count() < 200, "{}", req.prompt);
484 assert!(req.prompt.contains('…'), "{}", req.prompt);
485
486 let req = InteractionRequest::tool_approval(
487 "id",
488 "bash",
489 serde_json::json!({"command": "cat <<'EOF' > f\nline two\nEOF"}),
490 "s",
491 &[],
492 );
493 assert!(req.prompt.contains("cat <<'EOF' > f"), "{}", req.prompt);
494 assert!(!req.prompt.contains("line two"), "{}", req.prompt);
495 assert!(req.tool_arguments.is_some());
497 }
498
499 #[test]
502 fn a_tool_without_a_telling_argument_reads_as_before() {
503 let req =
504 InteractionRequest::tool_approval("id", "list_dir", serde_json::json!({}), "s", &[]);
505 assert_eq!(req.prompt, "Allow tool call: `list_dir`?");
506 let req = InteractionRequest::tool_approval(
507 "id",
508 "bash",
509 serde_json::json!({"command": " "}),
510 "s",
511 &[],
512 );
513 assert_eq!(req.prompt, "Allow tool call: `bash`?");
514 let req = InteractionRequest::tool_approval(
516 "id",
517 "bash",
518 serde_json::json!({"command": 42}),
519 "s",
520 &[],
521 );
522 assert_eq!(req.prompt, "Allow tool call: `bash`?");
523 }
524
525 #[test]
530 fn the_scope_options_name_what_they_grant() {
531 let keys = |names: &[&str]| -> Vec<String> {
532 names.iter().map(|n| format!("shell:{n}")).collect()
533 };
534 let req = InteractionRequest::tool_approval(
535 "id",
536 "shell",
537 serde_json::json!({"command": "ls && git status"}),
538 "s",
539 &keys(&["git status", "ls"]),
540 );
541 assert_eq!(req.options[1], "Allow git status, ls for this stage");
542 assert_eq!(req.options[2], "Allow git status, ls for this run");
543
544 let req = InteractionRequest::tool_approval(
546 "id",
547 "shell",
548 serde_json::json!({}),
549 "s",
550 &keys(&["a", "b", "c", "d", "e"]),
551 );
552 assert_eq!(req.options[2], "Allow a, b, c +2 more for this run");
553
554 let req = InteractionRequest::tool_approval(
556 "id",
557 "web_fetch",
558 serde_json::json!({}),
559 "s",
560 &["web_fetch".to_string()],
561 );
562 assert_eq!(req.options[2], "Allow web_fetch for this run");
563 }
564
565 #[test]
568 fn an_unkeyable_call_says_it_will_ask_again() {
569 let req = InteractionRequest::tool_approval(
570 "id",
571 "shell",
572 serde_json::json!({"command": "echo `whoami`"}),
573 "s",
574 &[],
575 );
576 assert!(
577 req.options[1].contains("it will ask again"),
578 "{:?}",
579 req.options
580 );
581 assert!(
582 req.options[2].contains("it will ask again"),
583 "{:?}",
584 req.options
585 );
586 }
587
588 #[test]
591 fn approval_choice_matches_the_option_order() {
592 let req = InteractionRequest::tool_approval("id", "shell", serde_json::json!({}), "s", &[]);
593 assert_eq!(approval_choice(0), Some(ApprovalScope::Once));
594 assert!(req.options[1].contains("stage"));
595 assert_eq!(approval_choice(1), Some(ApprovalScope::Stage));
596 assert!(req.options[2].contains("run"));
597 assert_eq!(approval_choice(2), Some(ApprovalScope::Run));
598 assert_eq!(req.options[3], "Deny");
599 assert_eq!(approval_choice(3), None);
600 assert_eq!(
601 approval_choice(99),
602 None,
603 "an unknown answer must not approve"
604 );
605 }
606
607 #[test]
610 fn a_gate_approval_offers_run_scope_and_no_stage_scope() {
611 let req =
612 InteractionRequest::gate_approval("g", "web_fetch", serde_json::json!({}), "research");
613 assert_eq!(req.kind, InteractionKind::ToolApproval);
614 assert_eq!(req.prompt, "Allow tool call: `web_fetch`?");
615 assert_eq!(req.options, ["Allow once", "Allow for this run", "Deny"]);
616 }
617
618 #[test]
621 fn run_scope_serialises_as_session() {
622 assert_eq!(
623 serde_json::to_string(&ApprovalScope::Run).unwrap(),
624 "\"session\""
625 );
626 for wire in ["\"session\"", "\"run\""] {
627 let back: ApprovalScope = serde_json::from_str(wire).unwrap();
628 assert_eq!(back, ApprovalScope::Run, "{wire}");
629 }
630 assert_eq!(
631 serde_json::to_string(&ApprovalScope::Stage).unwrap(),
632 "\"stage\""
633 );
634 }
635
636 #[test]
639 fn test_request_builders() {
640 let r = InteractionRequest::free_text("id1", "What now?", "plan", true);
641 assert_eq!(r.kind, InteractionKind::FreeText);
642 assert!(r.required);
643
644 let r = InteractionRequest::multiple_choice(
645 "id2",
646 "Pick one",
647 vec!["A".into(), "B".into()],
648 "plan",
649 );
650 assert_eq!(r.kind, InteractionKind::MultipleChoice);
651 assert_eq!(r.options.len(), 2);
652
653 let r = InteractionRequest::tool_approval(
654 "id3",
655 "bash",
656 serde_json::json!({"cmd": "ls"}),
657 "impl",
658 &[],
659 );
660 assert_eq!(r.kind, InteractionKind::ToolApproval);
661 assert_eq!(r.options.len(), 4);
662 }
663
664 #[test]
665 fn test_edit_text_request_builder_seeds_body() {
666 let r = InteractionRequest::edit_text("id4", "Edit this", "plan", "current text");
667 assert_eq!(r.kind, InteractionKind::EditText);
668 assert!(r.required);
669 assert_eq!(r.body.as_deref(), Some("current text"));
670 assert_eq!(r.prompt, "Edit this");
671 }
672
673 #[test]
674 fn test_edit_text_kind_serde_roundtrip_snake_case() {
675 let r = InteractionRequest::edit_text("id5", "p", "plan", "seed");
676 let json = serde_json::to_string(&r).unwrap();
677 assert!(json.contains("\"edit_text\""));
679 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
680 assert_eq!(back.kind, InteractionKind::EditText);
681 assert_eq!(back.body.as_deref(), Some("seed"));
682 }
683
684 #[test]
685 fn test_response_builders() {
686 let r = InteractionResponse::text("id1", "hello");
687 assert_eq!(r.value.as_deref(), Some("hello"));
688
689 let r = InteractionResponse::choice("id2", 1);
690 assert_eq!(r.choice_index, Some(1));
691
692 let r = InteractionResponse::approval("id3", true, ApprovalScope::Run);
693 assert_eq!(r.approved, Some(true));
694 assert_eq!(r.scope, Some(ApprovalScope::Run));
695 }
696
697 #[test]
698 fn test_response_as_text() {
699 let r = InteractionResponse::text("id", "answer");
700 assert_eq!(response_as_text(&r), "answer");
701 let empty = InteractionResponse {
702 request_id: "x".into(),
703 value: None,
704 choice_index: None,
705 approved: None,
706 scope: None,
707 };
708 assert_eq!(response_as_text(&empty), "");
709 }
710
711 #[test]
712 fn test_response_as_choice() {
713 let opts = vec!["Alpha".to_string(), "Beta".to_string()];
714 let r = InteractionResponse::choice("id", 0);
715 assert_eq!(response_as_choice(&r, &opts), Some(&"Alpha".to_string()));
716 let r = InteractionResponse::choice("id", 1);
717 assert_eq!(response_as_choice(&r, &opts), Some(&"Beta".to_string()));
718 let r = InteractionResponse::choice("id", 99);
719 assert!(response_as_choice(&r, &opts).is_none());
720 }
721
722 #[test]
723 fn test_make_interaction_id() {
724 let id = make_interaction_id(2, 5);
725 assert_eq!(id, "2-5");
726 }
727
728 #[test]
729 fn test_free_text_request_not_required() {
730 let r = InteractionRequest::free_text("ft1", "optional?", "stage1", false);
731 assert_eq!(r.kind, InteractionKind::FreeText);
732 assert!(!r.required);
733 assert_eq!(r.id, "ft1");
734 assert_eq!(r.prompt, "optional?");
735 assert_eq!(r.stage_name, "stage1");
736 assert!(r.options.is_empty());
737 assert!(r.tool_name.is_none());
738 assert!(r.tool_arguments.is_none());
739 assert!(r.body.is_none());
740 assert_eq!(r.body_format, BodyFormat::Plain);
741 }
742
743 #[test]
744 fn test_review_request() {
745 let r = InteractionRequest::review("rev1", "Review Title", "# Markdown body", "plan");
746 assert_eq!(r.kind, InteractionKind::FreeText);
747 assert!(r.required);
748 assert_eq!(r.prompt, "Review Title");
749 assert_eq!(r.body.as_deref(), Some("# Markdown body"));
750 assert_eq!(r.body_format, BodyFormat::Markdown);
751 assert_eq!(r.stage_name, "plan");
752 }
753
754 #[test]
755 fn test_confirm_request() {
756 let r = InteractionRequest::confirm("c1", "Proceed?", "deploy");
757 assert_eq!(r.kind, InteractionKind::Confirm);
758 assert_eq!(r.options, vec!["Yes", "No"]);
759 assert!(r.required);
760 assert_eq!(r.stage_name, "deploy");
761 }
762
763 #[test]
764 fn test_tool_approval_request() {
765 let args = serde_json::json!({"file": "test.txt"});
766 let r = InteractionRequest::tool_approval("ta1", "write_file", args, "code", &[]);
767 assert_eq!(r.kind, InteractionKind::ToolApproval);
768 assert_eq!(r.tool_name.as_deref(), Some("write_file"));
769 assert!(r.tool_arguments.is_some());
770 assert_eq!(r.options.len(), 4);
771 assert!(r.prompt.contains("write_file"));
772 }
773
774 #[test]
775 fn test_response_text_empty() {
776 let r = InteractionResponse::text("id", "");
777 assert_eq!(r.value.as_deref(), Some(""));
778 assert!(r.choice_index.is_none());
779 assert!(r.approved.is_none());
780 assert!(r.scope.is_none());
781 }
782
783 #[test]
784 fn test_response_approval_denied() {
785 let r = InteractionResponse::approval("id", false, ApprovalScope::Once);
786 assert_eq!(r.approved, Some(false));
787 assert_eq!(r.scope, Some(ApprovalScope::Once));
788 }
789
790 #[test]
791 fn test_response_approved_true() {
792 let r = InteractionResponse::approval("id", true, ApprovalScope::Run);
793 assert!(response_approved(&r));
794 }
795
796 #[test]
797 fn test_response_approved_false() {
798 let r = InteractionResponse::approval("id", false, ApprovalScope::Once);
799 assert!(!response_approved(&r));
800 }
801
802 #[test]
803 fn test_response_approved_none() {
804 let r = InteractionResponse::text("id", "hello");
805 assert!(!response_approved(&r));
806 }
807
808 #[test]
809 fn test_approval_scope_serde_roundtrip() {
810 for scope in [ApprovalScope::Once, ApprovalScope::Run] {
811 let json = serde_json::to_string(&scope).unwrap();
812 let back: ApprovalScope = serde_json::from_str(&json).unwrap();
813 assert_eq!(scope, back);
814 }
815 }
816
817 #[test]
818 fn test_approval_scope_snake_case() {
819 let json = serde_json::to_string(&ApprovalScope::Once).unwrap();
820 assert_eq!(json, "\"once\"");
821 let json = serde_json::to_string(&ApprovalScope::Run).unwrap();
822 assert_eq!(json, "\"session\"");
823 }
824
825 #[test]
826 fn test_interaction_kind_serde_roundtrip() {
827 for kind in [
828 InteractionKind::FreeText,
829 InteractionKind::MultipleChoice,
830 InteractionKind::Confirm,
831 InteractionKind::ToolApproval,
832 ] {
833 let json = serde_json::to_string(&kind).unwrap();
834 let back: InteractionKind = serde_json::from_str(&json).unwrap();
835 assert_eq!(kind, back);
836 }
837 }
838
839 #[test]
840 fn test_body_format_serde_roundtrip() {
841 for fmt in [BodyFormat::Plain, BodyFormat::Markdown] {
842 let json = serde_json::to_string(&fmt).unwrap();
843 let back: BodyFormat = serde_json::from_str(&json).unwrap();
844 assert_eq!(fmt, back);
845 }
846 }
847
848 #[test]
849 fn test_body_format_default_is_plain() {
850 let fmt = BodyFormat::default();
851 assert_eq!(fmt, BodyFormat::Plain);
852 }
853
854 #[test]
855 fn test_interaction_request_serde_roundtrip() {
856 let req = InteractionRequest::tool_approval(
857 "serde1",
858 "bash",
859 serde_json::json!({"cmd": "ls -la"}),
860 "code",
861 &[],
862 );
863 let json = serde_json::to_string(&req).unwrap();
864 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
865 assert_eq!(back.id, "serde1");
866 assert_eq!(back.kind, InteractionKind::ToolApproval);
867 assert_eq!(back.tool_name.as_deref(), Some("bash"));
868 }
869
870 #[test]
871 fn test_interaction_response_serde_roundtrip() {
872 let resp = InteractionResponse::approval("serde2", true, ApprovalScope::Run);
873 let json = serde_json::to_string(&resp).unwrap();
874 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
875 assert_eq!(back.request_id, "serde2");
876 assert_eq!(back.approved, Some(true));
877 assert_eq!(back.scope, Some(ApprovalScope::Run));
878 }
879
880 #[test]
881 fn test_make_interaction_id_zero() {
882 assert_eq!(make_interaction_id(0, 0), "0-0");
883 }
884
885 #[test]
886 fn test_make_interaction_id_large() {
887 assert_eq!(make_interaction_id(999, 1000), "999-1000");
888 }
889
890 #[test]
891 fn test_response_as_choice_no_choice_index() {
892 let opts = vec!["A".to_string(), "B".to_string()];
893 let r = InteractionResponse::text("id", "hello");
894 assert!(response_as_choice(&r, &opts).is_none());
895 }
896
897 #[test]
898 fn test_response_as_choice_empty_options() {
899 let opts: Vec<String> = vec![];
900 let r = InteractionResponse::choice("id", 0);
901 assert!(response_as_choice(&r, &opts).is_none());
902 }
903
904 #[test]
905 fn test_free_text_request_defaults() {
906 let r = InteractionRequest::free_text("ft", "prompt", "stage", true);
907 assert!(r.body.is_none());
908 assert_eq!(r.body_format, BodyFormat::Plain);
909 assert!(r.tool_name.is_none());
910 assert!(r.tool_arguments.is_none());
911 assert!(r.options.is_empty());
912 }
913
914 #[test]
915 fn test_multiple_choice_request_is_required() {
916 let r = InteractionRequest::multiple_choice("mc", "Pick", vec!["A".into()], "stage");
917 assert!(r.required);
918 }
919
920 #[test]
921 fn test_confirm_request_is_required() {
922 let r = InteractionRequest::confirm("c", "Sure?", "stage");
923 assert!(r.required);
924 }
925
926 #[test]
927 fn test_tool_approval_is_required() {
928 let r =
929 InteractionRequest::tool_approval("ta", "bash", serde_json::json!({}), "stage", &[]);
930 assert!(r.required);
931 }
932
933 #[test]
934 fn test_interaction_kind_snake_case_values() {
935 assert_eq!(
936 serde_json::to_string(&InteractionKind::FreeText).unwrap(),
937 "\"free_text\""
938 );
939 assert_eq!(
940 serde_json::to_string(&InteractionKind::MultipleChoice).unwrap(),
941 "\"multiple_choice\""
942 );
943 assert_eq!(
944 serde_json::to_string(&InteractionKind::ToolApproval).unwrap(),
945 "\"tool_approval\""
946 );
947 assert_eq!(
948 serde_json::to_string(&InteractionKind::Confirm).unwrap(),
949 "\"confirm\""
950 );
951 }
952
953 #[test]
954 fn test_body_format_snake_case_values() {
955 assert_eq!(
956 serde_json::to_string(&BodyFormat::Plain).unwrap(),
957 "\"plain\""
958 );
959 assert_eq!(
960 serde_json::to_string(&BodyFormat::Markdown).unwrap(),
961 "\"markdown\""
962 );
963 }
964
965 #[test]
966 fn test_request_free_text_serde_roundtrip() {
967 let req = InteractionRequest::free_text("ft1", "What?", "main", false);
968 let json = serde_json::to_string(&req).unwrap();
969 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
970 assert_eq!(back.id, "ft1");
971 assert_eq!(back.kind, InteractionKind::FreeText);
972 assert!(!back.required);
973 assert_eq!(back.stage_name, "main");
974 }
975
976 #[test]
977 fn test_request_multiple_choice_serde_roundtrip() {
978 let req = InteractionRequest::multiple_choice(
979 "mc1",
980 "Choose",
981 vec!["A".into(), "B".into(), "C".into()],
982 "plan",
983 );
984 let json = serde_json::to_string(&req).unwrap();
985 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
986 assert_eq!(back.kind, InteractionKind::MultipleChoice);
987 assert_eq!(back.options.len(), 3);
988 assert_eq!(back.options[2], "C");
989 }
990
991 #[test]
992 fn test_request_confirm_serde_roundtrip() {
993 let req = InteractionRequest::confirm("c1", "Proceed?", "deploy");
994 let json = serde_json::to_string(&req).unwrap();
995 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
996 assert_eq!(back.kind, InteractionKind::Confirm);
997 assert_eq!(back.options, vec!["Yes", "No"]);
998 }
999
1000 #[test]
1001 fn test_request_review_serde_roundtrip() {
1002 let req = InteractionRequest::review("rev1", "Title", "# Body\ntext", "review");
1003 let json = serde_json::to_string(&req).unwrap();
1004 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
1005 assert_eq!(back.body_format, BodyFormat::Markdown);
1006 assert_eq!(back.body.as_deref(), Some("# Body\ntext"));
1007 }
1008
1009 #[test]
1010 fn test_response_text_serde_roundtrip() {
1011 let resp = InteractionResponse::text("t1", "my answer");
1012 let json = serde_json::to_string(&resp).unwrap();
1013 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
1014 assert_eq!(back.request_id, "t1");
1015 assert_eq!(back.value.as_deref(), Some("my answer"));
1016 assert!(back.choice_index.is_none());
1017 assert!(back.approved.is_none());
1018 assert!(back.scope.is_none());
1019 }
1020
1021 #[test]
1022 fn test_response_choice_serde_roundtrip() {
1023 let resp = InteractionResponse::choice("c1", 2);
1024 let json = serde_json::to_string(&resp).unwrap();
1025 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
1026 assert_eq!(back.choice_index, Some(2));
1027 assert!(back.value.is_none());
1028 }
1029
1030 #[test]
1031 fn test_response_approval_serde_roundtrip() {
1032 let resp = InteractionResponse::approval("a1", false, ApprovalScope::Run);
1033 let json = serde_json::to_string(&resp).unwrap();
1034 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
1035 assert_eq!(back.approved, Some(false));
1036 assert_eq!(back.scope, Some(ApprovalScope::Run));
1037 }
1038
1039 #[test]
1040 fn test_response_as_text_with_value() {
1041 let r = InteractionResponse::text("id", "some text value");
1042 assert_eq!(response_as_text(&r), "some text value");
1043 }
1044
1045 #[test]
1046 fn test_response_approved_session_scope() {
1047 let r = InteractionResponse::approval("id", true, ApprovalScope::Run);
1048 assert!(response_approved(&r));
1049 assert_eq!(r.scope, Some(ApprovalScope::Run));
1050 }
1051
1052 #[test]
1053 fn test_make_interaction_id_various() {
1054 assert_eq!(make_interaction_id(1, 2), "1-2");
1055 assert_eq!(make_interaction_id(10, 20), "10-20");
1056 }
1057
1058 #[test]
1059 fn test_default_true_via_serde_missing_required_field() {
1060 let json = r#"{
1062 "id": "dt1",
1063 "kind": "free_text",
1064 "prompt": "test prompt",
1065 "stage_name": "stage"
1066 }"#;
1067 let req: InteractionRequest = serde_json::from_str(json).unwrap();
1068 assert!(req.required);
1069 }
1070}