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 tool_approval(
214 id: impl Into<String>,
215 tool_name: impl Into<String>,
216 arguments: serde_json::Value,
217 stage: impl Into<String>,
218 ) -> Self {
219 let tool = tool_name.into();
220 let prompt = match Self::approval_detail(&tool, &arguments) {
221 Some(detail) => format!("Allow tool call: `{tool}` - {detail}?"),
222 None => format!("Allow tool call: `{tool}`?"),
223 };
224 Self {
225 id: id.into(),
226 kind: InteractionKind::ToolApproval,
227 prompt,
228 options: vec![
229 "Allow once".to_string(),
230 "Allow for this session".to_string(),
231 "Deny".to_string(),
232 ],
233 tool_name: Some(tool),
234 tool_arguments: Some(arguments),
235 required: true,
236 stage_name: stage.into(),
237 body: None,
238 body_format: BodyFormat::Plain,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
247#[serde(rename_all = "snake_case")]
248pub enum ApprovalScope {
249 Once,
251 Session,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
257pub struct InteractionResponse {
258 pub request_id: String,
260 pub value: Option<String>,
262 pub choice_index: Option<usize>,
264 pub approved: Option<bool>,
266 pub scope: Option<ApprovalScope>,
268}
269
270impl InteractionResponse {
271 pub fn text(request_id: impl Into<String>, value: impl Into<String>) -> Self {
273 Self {
274 request_id: request_id.into(),
275 value: Some(value.into()),
276 choice_index: None,
277 approved: None,
278 scope: None,
279 }
280 }
281
282 pub fn choice(request_id: impl Into<String>, index: usize) -> Self {
284 Self {
285 request_id: request_id.into(),
286 value: None,
287 choice_index: Some(index),
288 approved: None,
289 scope: None,
290 }
291 }
292
293 pub fn approval(request_id: impl Into<String>, approved: bool, scope: ApprovalScope) -> Self {
295 Self {
296 request_id: request_id.into(),
297 value: None,
298 choice_index: None,
299 approved: Some(approved),
300 scope: Some(scope),
301 }
302 }
303}
304
305pub fn response_as_text(resp: &InteractionResponse) -> String {
309 resp.value.clone().unwrap_or_default()
310}
311
312pub fn response_as_choice<'a>(
314 resp: &InteractionResponse,
315 options: &'a [String],
316) -> Option<&'a String> {
317 resp.choice_index.and_then(|i| options.get(i))
318}
319
320pub fn response_approved(resp: &InteractionResponse) -> bool {
322 resp.approved.unwrap_or(false)
323}
324
325pub fn make_interaction_id(stage_idx: usize, iteration: usize) -> String {
327 format!("{}-{}", stage_idx, iteration)
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
338 fn a_tool_approval_says_what_it_is_asking_about() {
339 let req = InteractionRequest::tool_approval(
340 "id",
341 "bash",
342 serde_json::json!({"command": "rm -rf build && make"}),
343 "implement",
344 );
345 assert!(
346 req.prompt.contains("rm -rf build && make"),
347 "{}",
348 req.prompt
349 );
350
351 let req = InteractionRequest::tool_approval(
353 "id",
354 "write_file",
355 serde_json::json!({"path": "src/main.rs", "content": "..."}),
356 "implement",
357 );
358 assert!(req.prompt.contains("src/main.rs"), "{}", req.prompt);
359 }
360
361 #[test]
364 fn a_long_or_multiline_command_is_summarised() {
365 let long = "echo ".to_string() + &"x".repeat(400);
366 let req = InteractionRequest::tool_approval(
367 "id",
368 "bash",
369 serde_json::json!({ "command": long }),
370 "s",
371 );
372 assert!(req.prompt.chars().count() < 200, "{}", req.prompt);
373 assert!(req.prompt.contains('…'), "{}", req.prompt);
374
375 let req = InteractionRequest::tool_approval(
376 "id",
377 "bash",
378 serde_json::json!({"command": "cat <<'EOF' > f\nline two\nEOF"}),
379 "s",
380 );
381 assert!(req.prompt.contains("cat <<'EOF' > f"), "{}", req.prompt);
382 assert!(!req.prompt.contains("line two"), "{}", req.prompt);
383 assert!(req.tool_arguments.is_some());
385 }
386
387 #[test]
390 fn a_tool_without_a_telling_argument_reads_as_before() {
391 let req = InteractionRequest::tool_approval("id", "list_dir", serde_json::json!({}), "s");
392 assert_eq!(req.prompt, "Allow tool call: `list_dir`?");
393 let req = InteractionRequest::tool_approval(
394 "id",
395 "bash",
396 serde_json::json!({"command": " "}),
397 "s",
398 );
399 assert_eq!(req.prompt, "Allow tool call: `bash`?");
400 let req = InteractionRequest::tool_approval(
402 "id",
403 "bash",
404 serde_json::json!({"command": 42}),
405 "s",
406 );
407 assert_eq!(req.prompt, "Allow tool call: `bash`?");
408 }
409
410 #[test]
413 fn test_request_builders() {
414 let r = InteractionRequest::free_text("id1", "What now?", "plan", true);
415 assert_eq!(r.kind, InteractionKind::FreeText);
416 assert!(r.required);
417
418 let r = InteractionRequest::multiple_choice(
419 "id2",
420 "Pick one",
421 vec!["A".into(), "B".into()],
422 "plan",
423 );
424 assert_eq!(r.kind, InteractionKind::MultipleChoice);
425 assert_eq!(r.options.len(), 2);
426
427 let r = InteractionRequest::tool_approval(
428 "id3",
429 "bash",
430 serde_json::json!({"cmd": "ls"}),
431 "impl",
432 );
433 assert_eq!(r.kind, InteractionKind::ToolApproval);
434 assert_eq!(r.options.len(), 3);
435 }
436
437 #[test]
438 fn test_edit_text_request_builder_seeds_body() {
439 let r = InteractionRequest::edit_text("id4", "Edit this", "plan", "current text");
440 assert_eq!(r.kind, InteractionKind::EditText);
441 assert!(r.required);
442 assert_eq!(r.body.as_deref(), Some("current text"));
443 assert_eq!(r.prompt, "Edit this");
444 }
445
446 #[test]
447 fn test_edit_text_kind_serde_roundtrip_snake_case() {
448 let r = InteractionRequest::edit_text("id5", "p", "plan", "seed");
449 let json = serde_json::to_string(&r).unwrap();
450 assert!(json.contains("\"edit_text\""));
452 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
453 assert_eq!(back.kind, InteractionKind::EditText);
454 assert_eq!(back.body.as_deref(), Some("seed"));
455 }
456
457 #[test]
458 fn test_response_builders() {
459 let r = InteractionResponse::text("id1", "hello");
460 assert_eq!(r.value.as_deref(), Some("hello"));
461
462 let r = InteractionResponse::choice("id2", 1);
463 assert_eq!(r.choice_index, Some(1));
464
465 let r = InteractionResponse::approval("id3", true, ApprovalScope::Session);
466 assert_eq!(r.approved, Some(true));
467 assert_eq!(r.scope, Some(ApprovalScope::Session));
468 }
469
470 #[test]
471 fn test_response_as_text() {
472 let r = InteractionResponse::text("id", "answer");
473 assert_eq!(response_as_text(&r), "answer");
474 let empty = InteractionResponse {
475 request_id: "x".into(),
476 value: None,
477 choice_index: None,
478 approved: None,
479 scope: None,
480 };
481 assert_eq!(response_as_text(&empty), "");
482 }
483
484 #[test]
485 fn test_response_as_choice() {
486 let opts = vec!["Alpha".to_string(), "Beta".to_string()];
487 let r = InteractionResponse::choice("id", 0);
488 assert_eq!(response_as_choice(&r, &opts), Some(&"Alpha".to_string()));
489 let r = InteractionResponse::choice("id", 1);
490 assert_eq!(response_as_choice(&r, &opts), Some(&"Beta".to_string()));
491 let r = InteractionResponse::choice("id", 99);
492 assert!(response_as_choice(&r, &opts).is_none());
493 }
494
495 #[test]
496 fn test_make_interaction_id() {
497 let id = make_interaction_id(2, 5);
498 assert_eq!(id, "2-5");
499 }
500
501 #[test]
502 fn test_free_text_request_not_required() {
503 let r = InteractionRequest::free_text("ft1", "optional?", "stage1", false);
504 assert_eq!(r.kind, InteractionKind::FreeText);
505 assert!(!r.required);
506 assert_eq!(r.id, "ft1");
507 assert_eq!(r.prompt, "optional?");
508 assert_eq!(r.stage_name, "stage1");
509 assert!(r.options.is_empty());
510 assert!(r.tool_name.is_none());
511 assert!(r.tool_arguments.is_none());
512 assert!(r.body.is_none());
513 assert_eq!(r.body_format, BodyFormat::Plain);
514 }
515
516 #[test]
517 fn test_review_request() {
518 let r = InteractionRequest::review("rev1", "Review Title", "# Markdown body", "plan");
519 assert_eq!(r.kind, InteractionKind::FreeText);
520 assert!(r.required);
521 assert_eq!(r.prompt, "Review Title");
522 assert_eq!(r.body.as_deref(), Some("# Markdown body"));
523 assert_eq!(r.body_format, BodyFormat::Markdown);
524 assert_eq!(r.stage_name, "plan");
525 }
526
527 #[test]
528 fn test_confirm_request() {
529 let r = InteractionRequest::confirm("c1", "Proceed?", "deploy");
530 assert_eq!(r.kind, InteractionKind::Confirm);
531 assert_eq!(r.options, vec!["Yes", "No"]);
532 assert!(r.required);
533 assert_eq!(r.stage_name, "deploy");
534 }
535
536 #[test]
537 fn test_tool_approval_request() {
538 let args = serde_json::json!({"file": "test.txt"});
539 let r = InteractionRequest::tool_approval("ta1", "write_file", args, "code");
540 assert_eq!(r.kind, InteractionKind::ToolApproval);
541 assert_eq!(r.tool_name.as_deref(), Some("write_file"));
542 assert!(r.tool_arguments.is_some());
543 assert_eq!(r.options.len(), 3);
544 assert!(r.prompt.contains("write_file"));
545 }
546
547 #[test]
548 fn test_response_text_empty() {
549 let r = InteractionResponse::text("id", "");
550 assert_eq!(r.value.as_deref(), Some(""));
551 assert!(r.choice_index.is_none());
552 assert!(r.approved.is_none());
553 assert!(r.scope.is_none());
554 }
555
556 #[test]
557 fn test_response_approval_denied() {
558 let r = InteractionResponse::approval("id", false, ApprovalScope::Once);
559 assert_eq!(r.approved, Some(false));
560 assert_eq!(r.scope, Some(ApprovalScope::Once));
561 }
562
563 #[test]
564 fn test_response_approved_true() {
565 let r = InteractionResponse::approval("id", true, ApprovalScope::Session);
566 assert!(response_approved(&r));
567 }
568
569 #[test]
570 fn test_response_approved_false() {
571 let r = InteractionResponse::approval("id", false, ApprovalScope::Once);
572 assert!(!response_approved(&r));
573 }
574
575 #[test]
576 fn test_response_approved_none() {
577 let r = InteractionResponse::text("id", "hello");
578 assert!(!response_approved(&r));
579 }
580
581 #[test]
582 fn test_approval_scope_serde_roundtrip() {
583 for scope in [ApprovalScope::Once, ApprovalScope::Session] {
584 let json = serde_json::to_string(&scope).unwrap();
585 let back: ApprovalScope = serde_json::from_str(&json).unwrap();
586 assert_eq!(scope, back);
587 }
588 }
589
590 #[test]
591 fn test_approval_scope_snake_case() {
592 let json = serde_json::to_string(&ApprovalScope::Once).unwrap();
593 assert_eq!(json, "\"once\"");
594 let json = serde_json::to_string(&ApprovalScope::Session).unwrap();
595 assert_eq!(json, "\"session\"");
596 }
597
598 #[test]
599 fn test_interaction_kind_serde_roundtrip() {
600 for kind in [
601 InteractionKind::FreeText,
602 InteractionKind::MultipleChoice,
603 InteractionKind::Confirm,
604 InteractionKind::ToolApproval,
605 ] {
606 let json = serde_json::to_string(&kind).unwrap();
607 let back: InteractionKind = serde_json::from_str(&json).unwrap();
608 assert_eq!(kind, back);
609 }
610 }
611
612 #[test]
613 fn test_body_format_serde_roundtrip() {
614 for fmt in [BodyFormat::Plain, BodyFormat::Markdown] {
615 let json = serde_json::to_string(&fmt).unwrap();
616 let back: BodyFormat = serde_json::from_str(&json).unwrap();
617 assert_eq!(fmt, back);
618 }
619 }
620
621 #[test]
622 fn test_body_format_default_is_plain() {
623 let fmt = BodyFormat::default();
624 assert_eq!(fmt, BodyFormat::Plain);
625 }
626
627 #[test]
628 fn test_interaction_request_serde_roundtrip() {
629 let req = InteractionRequest::tool_approval(
630 "serde1",
631 "bash",
632 serde_json::json!({"cmd": "ls -la"}),
633 "code",
634 );
635 let json = serde_json::to_string(&req).unwrap();
636 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
637 assert_eq!(back.id, "serde1");
638 assert_eq!(back.kind, InteractionKind::ToolApproval);
639 assert_eq!(back.tool_name.as_deref(), Some("bash"));
640 }
641
642 #[test]
643 fn test_interaction_response_serde_roundtrip() {
644 let resp = InteractionResponse::approval("serde2", true, ApprovalScope::Session);
645 let json = serde_json::to_string(&resp).unwrap();
646 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
647 assert_eq!(back.request_id, "serde2");
648 assert_eq!(back.approved, Some(true));
649 assert_eq!(back.scope, Some(ApprovalScope::Session));
650 }
651
652 #[test]
653 fn test_make_interaction_id_zero() {
654 assert_eq!(make_interaction_id(0, 0), "0-0");
655 }
656
657 #[test]
658 fn test_make_interaction_id_large() {
659 assert_eq!(make_interaction_id(999, 1000), "999-1000");
660 }
661
662 #[test]
663 fn test_response_as_choice_no_choice_index() {
664 let opts = vec!["A".to_string(), "B".to_string()];
665 let r = InteractionResponse::text("id", "hello");
666 assert!(response_as_choice(&r, &opts).is_none());
667 }
668
669 #[test]
670 fn test_response_as_choice_empty_options() {
671 let opts: Vec<String> = vec![];
672 let r = InteractionResponse::choice("id", 0);
673 assert!(response_as_choice(&r, &opts).is_none());
674 }
675
676 #[test]
677 fn test_free_text_request_defaults() {
678 let r = InteractionRequest::free_text("ft", "prompt", "stage", true);
679 assert!(r.body.is_none());
680 assert_eq!(r.body_format, BodyFormat::Plain);
681 assert!(r.tool_name.is_none());
682 assert!(r.tool_arguments.is_none());
683 assert!(r.options.is_empty());
684 }
685
686 #[test]
687 fn test_multiple_choice_request_is_required() {
688 let r = InteractionRequest::multiple_choice("mc", "Pick", vec!["A".into()], "stage");
689 assert!(r.required);
690 }
691
692 #[test]
693 fn test_confirm_request_is_required() {
694 let r = InteractionRequest::confirm("c", "Sure?", "stage");
695 assert!(r.required);
696 }
697
698 #[test]
699 fn test_tool_approval_is_required() {
700 let r = InteractionRequest::tool_approval("ta", "bash", serde_json::json!({}), "stage");
701 assert!(r.required);
702 }
703
704 #[test]
705 fn test_interaction_kind_snake_case_values() {
706 assert_eq!(
707 serde_json::to_string(&InteractionKind::FreeText).unwrap(),
708 "\"free_text\""
709 );
710 assert_eq!(
711 serde_json::to_string(&InteractionKind::MultipleChoice).unwrap(),
712 "\"multiple_choice\""
713 );
714 assert_eq!(
715 serde_json::to_string(&InteractionKind::ToolApproval).unwrap(),
716 "\"tool_approval\""
717 );
718 assert_eq!(
719 serde_json::to_string(&InteractionKind::Confirm).unwrap(),
720 "\"confirm\""
721 );
722 }
723
724 #[test]
725 fn test_body_format_snake_case_values() {
726 assert_eq!(
727 serde_json::to_string(&BodyFormat::Plain).unwrap(),
728 "\"plain\""
729 );
730 assert_eq!(
731 serde_json::to_string(&BodyFormat::Markdown).unwrap(),
732 "\"markdown\""
733 );
734 }
735
736 #[test]
737 fn test_request_free_text_serde_roundtrip() {
738 let req = InteractionRequest::free_text("ft1", "What?", "main", false);
739 let json = serde_json::to_string(&req).unwrap();
740 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
741 assert_eq!(back.id, "ft1");
742 assert_eq!(back.kind, InteractionKind::FreeText);
743 assert!(!back.required);
744 assert_eq!(back.stage_name, "main");
745 }
746
747 #[test]
748 fn test_request_multiple_choice_serde_roundtrip() {
749 let req = InteractionRequest::multiple_choice(
750 "mc1",
751 "Choose",
752 vec!["A".into(), "B".into(), "C".into()],
753 "plan",
754 );
755 let json = serde_json::to_string(&req).unwrap();
756 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
757 assert_eq!(back.kind, InteractionKind::MultipleChoice);
758 assert_eq!(back.options.len(), 3);
759 assert_eq!(back.options[2], "C");
760 }
761
762 #[test]
763 fn test_request_confirm_serde_roundtrip() {
764 let req = InteractionRequest::confirm("c1", "Proceed?", "deploy");
765 let json = serde_json::to_string(&req).unwrap();
766 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
767 assert_eq!(back.kind, InteractionKind::Confirm);
768 assert_eq!(back.options, vec!["Yes", "No"]);
769 }
770
771 #[test]
772 fn test_request_review_serde_roundtrip() {
773 let req = InteractionRequest::review("rev1", "Title", "# Body\ntext", "review");
774 let json = serde_json::to_string(&req).unwrap();
775 let back: InteractionRequest = serde_json::from_str(&json).unwrap();
776 assert_eq!(back.body_format, BodyFormat::Markdown);
777 assert_eq!(back.body.as_deref(), Some("# Body\ntext"));
778 }
779
780 #[test]
781 fn test_response_text_serde_roundtrip() {
782 let resp = InteractionResponse::text("t1", "my answer");
783 let json = serde_json::to_string(&resp).unwrap();
784 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
785 assert_eq!(back.request_id, "t1");
786 assert_eq!(back.value.as_deref(), Some("my answer"));
787 assert!(back.choice_index.is_none());
788 assert!(back.approved.is_none());
789 assert!(back.scope.is_none());
790 }
791
792 #[test]
793 fn test_response_choice_serde_roundtrip() {
794 let resp = InteractionResponse::choice("c1", 2);
795 let json = serde_json::to_string(&resp).unwrap();
796 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
797 assert_eq!(back.choice_index, Some(2));
798 assert!(back.value.is_none());
799 }
800
801 #[test]
802 fn test_response_approval_serde_roundtrip() {
803 let resp = InteractionResponse::approval("a1", false, ApprovalScope::Session);
804 let json = serde_json::to_string(&resp).unwrap();
805 let back: InteractionResponse = serde_json::from_str(&json).unwrap();
806 assert_eq!(back.approved, Some(false));
807 assert_eq!(back.scope, Some(ApprovalScope::Session));
808 }
809
810 #[test]
811 fn test_response_as_text_with_value() {
812 let r = InteractionResponse::text("id", "some text value");
813 assert_eq!(response_as_text(&r), "some text value");
814 }
815
816 #[test]
817 fn test_response_approved_session_scope() {
818 let r = InteractionResponse::approval("id", true, ApprovalScope::Session);
819 assert!(response_approved(&r));
820 assert_eq!(r.scope, Some(ApprovalScope::Session));
821 }
822
823 #[test]
824 fn test_make_interaction_id_various() {
825 assert_eq!(make_interaction_id(1, 2), "1-2");
826 assert_eq!(make_interaction_id(10, 20), "10-20");
827 }
828
829 #[test]
830 fn test_default_true_via_serde_missing_required_field() {
831 let json = r#"{
833 "id": "dt1",
834 "kind": "free_text",
835 "prompt": "test prompt",
836 "stage_name": "stage"
837 }"#;
838 let req: InteractionRequest = serde_json::from_str(json).unwrap();
839 assert!(req.required);
840 }
841}