1use crate::error::MitosisError;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5pub const PROTOCOL_VERSION: u32 = 3;
6
7#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
8#[serde(tag = "kind", rename_all = "snake_case")]
9pub enum TemplateSource {
10 Url {
11 url: String,
12 },
13 Basebackup {
15 url: String,
16 },
17 Seed {
18 command: String,
19 #[serde(default)]
21 cwd: Option<PathBuf>,
22 #[serde(default)]
25 path: Option<String>,
26 },
27 Promoted {
29 clone_id: String,
30 },
31 Running {
33 dataset: String,
34 },
35}
36
37#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum GenKind {
41 Full,
42 Layered,
43 Promoted,
44 Upgraded,
45}
46
47#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
48#[serde(rename_all = "snake_case")]
49pub enum RefreshMode {
50 #[default]
51 Full,
52 Layered,
53}
54
55#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
56#[serde(rename_all = "snake_case")]
57pub enum OnStale {
58 #[default]
59 Warn,
60 Refresh,
61}
62
63#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
65pub struct MigrateCmd {
66 pub command: String,
67 pub cwd: PathBuf,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub path: Option<String>,
70}
71
72#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
73pub struct GenerationInfo {
74 pub gen: u32,
75 pub kind: GenKind,
76 pub built_at: i64,
77 pub pg_major: u32,
78 pub size_bytes: u64,
79 pub current: bool,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub from_clone: Option<String>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub from_gen: Option<u32>,
84}
85
86#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
89pub struct TemplateSettings {
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub keep: Option<u32>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub every_secs: Option<u64>,
94 #[serde(default)]
95 pub every_off: bool,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub every_mode: Option<RefreshMode>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub migrate: Option<MigrateCmd>,
100 #[serde(default)]
101 pub migrate_off: bool,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub auto_migrate: Option<bool>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub on_stale: Option<OnStale>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub mask: Option<PathBuf>,
109 #[serde(default)]
110 pub mask_off: bool,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub base_branch: Option<String>,
114 #[serde(default)]
116 pub base_branch_off: bool,
117}
118
119#[derive(Serialize, Deserialize, Clone, Debug)]
120pub struct TemplateInitArgs {
121 pub name: String,
122 pub source: TemplateSource,
123 pub database: Option<String>,
124 pub migrations: Option<PathBuf>,
125 pub max_age_secs: Option<u64>,
126 pub pg_major: Option<u32>,
127 #[serde(flatten)]
129 pub settings: TemplateSettings,
130}
131
132#[derive(Serialize, Deserialize, Clone, Debug)]
133pub struct TemplateSetArgs {
134 pub name: String,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub max_age_secs: Option<u64>,
137 #[serde(flatten)]
138 pub settings: TemplateSettings,
139 #[serde(default)]
141 pub resume: bool,
142}
143
144#[derive(Serialize, Deserialize, Clone, Debug)]
145pub struct CloneCreateArgs {
146 pub template: String,
147 pub name: Option<String>,
148 pub ttl_secs: u64,
149 pub durable: bool,
150 pub parent_pid: Option<i32>,
151 #[serde(default)]
153 pub fresh: bool,
154 #[serde(default)]
156 pub protect: bool,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub env: Option<EnvTarget>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub wait_secs: Option<u64>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub branch: Option<BranchArgs>,
167}
168
169#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
171pub struct BranchArgs {
172 pub name: String,
174 pub repo_root: PathBuf,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub path: Option<String>,
179}
180
181#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
183pub struct EnvTarget {
184 pub file: PathBuf,
186 pub var: String,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub previous: Option<String>,
190}
191
192#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
194pub struct PendingRestore {
195 pub id: i64,
196 pub file: PathBuf,
197 pub var: String,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub previous: Option<String>,
200 pub url: String,
202}
203
204#[derive(Serialize, Deserialize, Clone, Debug)]
205#[serde(tag = "type", rename_all = "snake_case")]
206pub enum Request {
207 TemplateInit(TemplateInitArgs),
208 TemplateRefresh {
209 name: String,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 path: Option<String>,
213 #[serde(default)]
215 layered: bool,
216 },
217 TemplateSet(TemplateSetArgs),
218 TemplateList,
219 TemplateHistory {
220 name: String,
221 },
222 TemplateRollback {
224 name: String,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 to: Option<u32>,
227 },
228 TemplateUpgrade {
231 name: String,
232 to: u32,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 as_name: Option<String>,
235 },
236 CloneCreate(CloneCreateArgs),
237 CloneList,
238 CloneDestroy {
239 id_or_name: String,
240 #[serde(default)]
242 force: bool,
243 #[serde(default)]
245 forget: bool,
246 },
247 ClonePromote {
249 id_or_name: String,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 template: Option<String>,
252 },
253 CloneWake {
255 id_or_name: String,
256 },
257 CloneDiff {
260 id_or_name: String,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 table: Option<String>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
266 limit: Option<u32>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 parent_pid: Option<i32>,
269 },
270 PendingRestores,
272 PendingRestoreDone {
273 id: i64,
274 },
275 Gc,
276 Status,
277 Usage,
279 ConfigReload,
281 Events {
284 #[serde(default)]
285 filter: EventFilter,
286 #[serde(default)]
287 follow: bool,
288 },
289 Shutdown,
290}
291
292#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
294pub struct EventRow {
295 pub id: i64,
296 pub ts: i64,
297 pub kind: String,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub clone: Option<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub template: Option<String>,
302 pub detail: serde_json::Value,
303}
304
305#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
307pub struct EventFilter {
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub clone: Option<String>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub template: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub kind: Option<String>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub since: Option<i64>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub limit: Option<u32>,
322}
323
324#[derive(Serialize, Deserialize, Clone, Debug)]
325pub struct TemplateInfo {
326 pub name: String,
327 pub pg_major: u32,
328 pub source: TemplateSource,
329 pub database: String,
330 pub size_bytes: u64,
331 pub built_at: i64,
332 pub age_secs: u64,
333 pub warnings: Vec<String>,
334 #[serde(default)]
335 pub current_gen: u32,
336 #[serde(default, rename = "gen_count")]
338 pub generations: u32,
339 #[serde(default)]
340 pub paused: bool,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub last_error: Option<String>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub every_secs: Option<u64>,
345 #[serde(default)]
346 pub every_mode: RefreshMode,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub migrate: Option<String>,
350 #[serde(default)]
351 pub auto_migrate: bool,
352 #[serde(default)]
353 pub on_stale: OnStale,
354 #[serde(default)]
355 pub keep: u32,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub mask: Option<PathBuf>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub base: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub branch: Option<String>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub base_branch: Option<String>,
368}
369
370#[derive(Serialize, Deserialize, Clone, Debug)]
371pub struct CloneInfo {
372 pub id: String,
373 pub name: Option<String>,
374 pub template: String,
375 pub url: String,
376 pub port: u16,
377 pub socket_dir: PathBuf,
378 pub template_age_secs: u64,
379 pub created_at: i64,
380 pub expires_at: i64,
381 pub durable: bool,
382 pub parent_pid: Option<i32>,
383 pub rss_bytes: Option<u64>,
384 pub own_disk_bytes: Option<u64>,
385 #[serde(default, rename = "gen")]
387 pub generation: u32,
388 #[serde(default)]
390 pub stale: bool,
391 #[serde(default)]
393 pub refreshing: bool,
394 #[serde(default)]
396 pub existing: bool,
397 #[serde(default)]
399 pub protected: bool,
400 #[serde(default)]
401 pub warnings: Vec<String>,
402 #[serde(default)]
404 pub duration_ms: u64,
405 #[serde(default)]
407 pub waited_ms: u64,
408 #[serde(default)]
410 pub disk_warn: bool,
411 #[serde(default)]
413 pub suspect: bool,
414 #[serde(default)]
416 pub direct_url: String,
417 #[serde(default)]
419 pub status: String,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub branch: Option<String>,
423 #[serde(default)]
425 pub rebuilt: bool,
426}
427
428#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
430pub struct DiffReport {
431 pub clone: String,
432 pub template: String,
433 #[serde(rename = "gen")]
434 pub generation: u32,
435 pub schema: Vec<SchemaChange>,
436 pub tables: Vec<TableCount>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub rows: Option<RowDiff>,
441}
442
443#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
444pub struct SchemaChange {
445 pub kind: String,
447 pub name: String,
448 pub change: String,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub before: Option<String>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub after: Option<String>,
454}
455
456#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
457pub struct TableCount {
458 pub table: String,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub before: Option<Count>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub after: Option<Count>,
464}
465
466#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
467pub struct Count {
468 pub n: u64,
469 #[serde(default)]
471 pub estimate: bool,
472}
473
474#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
476pub struct RowDiff {
477 pub table: String,
478 pub inserted: Vec<serde_json::Value>,
479 pub deleted: Vec<serde_json::Value>,
480 pub updated: Vec<RowChange>,
481 pub inserted_total: u64,
482 pub deleted_total: u64,
483 pub updated_total: u64,
484}
485
486#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
487pub struct RowChange {
488 pub pk: serde_json::Value,
489 pub columns: Vec<String>,
491}
492
493#[derive(Serialize, Deserialize, Clone, Debug, Default)]
494pub struct GcReport {
495 pub adopted: Vec<String>,
496 pub destroyed: Vec<String>,
497 pub orphan_folders_removed: Vec<PathBuf>,
498 pub records_dropped: Vec<String>,
499 #[serde(default)]
501 pub failed: Vec<GcFailure>,
502 #[serde(default)]
504 pub warnings: Vec<String>,
505}
506
507#[derive(Serialize, Deserialize, Clone, Debug)]
508pub struct GcFailure {
509 pub item: String,
511 pub error: String,
512}
513
514#[derive(Serialize, Deserialize, Clone, Debug)]
515pub struct DaemonStatus {
516 pub pid: u32,
517 pub home: PathBuf,
518 pub clones: usize,
519 pub templates: usize,
520 pub uptime_secs: u64,
521 #[serde(default)]
523 pub version: String,
524 #[serde(default)]
526 pub service: bool,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub router: Option<RouterStatus>,
530}
531
532#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
533pub struct RouterStatus {
534 pub listening: bool,
535 #[serde(default)]
537 pub listen: String,
538 pub port: u16,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub error: Option<String>,
542}
543
544#[derive(Serialize, Deserialize, Clone, Debug)]
545#[serde(tag = "type", rename_all = "snake_case")]
546pub enum Response {
547 TemplateBuilt(TemplateInfo),
548 Templates {
549 templates: Vec<TemplateInfo>,
550 },
551 Generations {
552 template: String,
553 generations: Vec<GenerationInfo>,
554 #[serde(default)]
556 paused: bool,
557 },
558 CloneCreated(CloneInfo),
559 Clone(CloneInfo),
561 Clones {
562 clones: Vec<CloneInfo>,
563 },
564 Destroyed {
565 id: String,
566 },
567 PendingRestores {
568 items: Vec<PendingRestore>,
569 },
570 Gc(GcReport),
571 Status(DaemonStatus),
572 Usage(crate::budget::Usage),
573 Events {
574 events: Vec<EventRow>,
575 },
576 Event(EventRow),
578 Diff(DiffReport),
579 Ok,
580}
581
582#[derive(Serialize, Deserialize, Clone, Debug)]
583pub struct RequestEnvelope {
584 pub v: u32,
585 pub request: Request,
586}
587
588#[derive(Serialize, Deserialize, Clone, Debug)]
589pub struct ResponseEnvelope {
590 pub v: u32,
591 pub result: std::result::Result<Response, MitosisError>,
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::error::ErrorCode;
598
599 #[test]
600 fn gc_is_tagged_only() {
601 assert_eq!(
602 serde_json::to_string(&Request::Gc).unwrap(),
603 r#"{"type":"gc"}"#
604 );
605 }
606
607 #[test]
608 fn request_envelope_round_trip() {
609 let env = RequestEnvelope {
610 v: PROTOCOL_VERSION,
611 request: Request::CloneCreate(CloneCreateArgs {
612 template: "main".into(),
613 name: Some("feat".into()),
614 ttl_secs: 7200,
615 durable: false,
616 parent_pid: Some(42),
617 fresh: false,
618 protect: false,
619 env: None,
620 wait_secs: None,
621 branch: None,
622 }),
623 };
624 let line = serde_json::to_string(&env).unwrap();
625 assert!(!line.contains('\n'));
626 let back: RequestEnvelope = serde_json::from_str(&line).unwrap();
627 assert_eq!(back.v, PROTOCOL_VERSION);
628 assert_eq!(PROTOCOL_VERSION, 3);
629 match back.request {
630 Request::CloneCreate(a) => {
631 assert_eq!(a.template, "main");
632 assert_eq!(a.name.as_deref(), Some("feat"));
633 assert_eq!(a.ttl_secs, 7200);
634 assert_eq!(a.parent_pid, Some(42));
635 }
636 other => panic!("wrong variant: {other:?}"),
637 }
638 }
639
640 #[test]
641 fn err_response_round_trip() {
642 let env = ResponseEnvelope {
643 v: PROTOCOL_VERSION,
644 result: Err(
645 MitosisError::new(ErrorCode::CapReached, "too many").with_hint("destroy one")
646 ),
647 };
648 let line = serde_json::to_string(&env).unwrap();
649 let back: ResponseEnvelope = serde_json::from_str(&line).unwrap();
650 let e = back.result.unwrap_err();
651 assert_eq!(e.code, ErrorCode::CapReached);
652 assert_eq!(e.hint.as_deref(), Some("destroy one"));
653 }
654
655 fn template_info() -> TemplateInfo {
656 TemplateInfo {
657 name: "main".into(),
658 pg_major: 16,
659 source: TemplateSource::Url {
660 url: "postgres://x".into(),
661 },
662 database: "app".into(),
663 size_bytes: 1024,
664 built_at: 1_700_000_000,
665 age_secs: 5,
666 warnings: vec!["w".into()],
667 current_gen: 2,
668 generations: 2,
669 paused: true,
670 last_error: Some("boom".into()),
671 every_secs: Some(86_400),
672 every_mode: RefreshMode::Layered,
673 migrate: Some("make migrate".into()),
674 auto_migrate: true,
675 on_stale: OnStale::Refresh,
676 keep: 3,
677 mask: None,
678 base: None,
679 branch: None,
680 base_branch: None,
681 }
682 }
683
684 fn clone_info() -> CloneInfo {
685 CloneInfo {
686 id: "abc123".into(),
687 name: Some("feat".into()),
688 template: "main".into(),
689 url: "postgres://localhost:5500/app".into(),
690 port: 5500,
691 socket_dir: "/tmp/s".into(),
692 template_age_secs: 5,
693 created_at: 1,
694 expires_at: 2,
695 durable: false,
696 parent_pid: Some(7),
697 rss_bytes: None,
698 own_disk_bytes: Some(9),
699 generation: 2,
700 stale: true,
701 refreshing: true,
702 existing: false,
703 protected: false,
704 warnings: Vec::new(),
705 duration_ms: 0,
706 waited_ms: 0,
707 disk_warn: false,
708 suspect: false,
709 direct_url: "postgres://localhost:5500/app".into(),
710 status: "ready".into(),
711 branch: None,
712 rebuilt: false,
713 }
714 }
715
716 fn round_trip(r: Response) {
717 let env = ResponseEnvelope {
718 v: PROTOCOL_VERSION,
719 result: Ok(r),
720 };
721 let line = serde_json::to_string(&env).unwrap();
722 let back: ResponseEnvelope = serde_json::from_str(&line).unwrap();
723 assert_eq!(
724 serde_json::to_value(&env).unwrap(),
725 serde_json::to_value(&back).unwrap()
726 );
727 }
728
729 #[test]
730 fn list_and_newtype_responses_round_trip() {
731 round_trip(Response::Templates {
732 templates: vec![template_info()],
733 });
734 round_trip(Response::Clones {
735 clones: vec![clone_info()],
736 });
737 round_trip(Response::TemplateBuilt(template_info()));
738 round_trip(Response::CloneCreated(clone_info()));
739 round_trip(Response::Generations {
740 template: "main".into(),
741 generations: vec![GenerationInfo {
742 gen: 1,
743 kind: GenKind::Promoted,
744 built_at: 1,
745 pg_major: 16,
746 size_bytes: 2,
747 current: true,
748 from_clone: Some("feat".into()),
749 from_gen: Some(3),
750 }],
751 paused: true,
752 });
753 }
754
755 #[test]
756 fn v01_shapes_still_parse() {
757 let t: TemplateInfo = serde_json::from_str(
758 r#"{"name":"main","pg_major":16,"source":{"kind":"url","url":"postgres://h/db"},
759 "database":"db","size_bytes":1,"built_at":1,"age_secs":1,"warnings":[]}"#,
760 )
761 .unwrap();
762 assert_eq!((t.current_gen, t.paused, t.last_error), (0, false, None));
763 let r: Request =
764 serde_json::from_str(r#"{"type":"template_rollback","name":"main"}"#).unwrap();
765 assert!(matches!(r, Request::TemplateRollback { to: None, .. }));
766 assert_eq!(
767 serde_json::to_string(&GenKind::Upgraded).unwrap(),
768 r#""upgraded""#
769 );
770 }
771
772 #[test]
773 fn seed_cwd_is_optional() {
774 let s: TemplateSource =
775 serde_json::from_str(r#"{"kind":"seed","command":"make seed"}"#).unwrap();
776 assert_eq!(
777 s,
778 TemplateSource::Seed {
779 command: "make seed".into(),
780 cwd: None,
781 path: None
782 }
783 );
784 let s = TemplateSource::Seed {
785 command: "x".into(),
786 cwd: Some("/w".into()),
787 path: Some("/usr/bin:/bin".into()),
788 };
789 assert_eq!(
790 serde_json::from_str::<TemplateSource>(&serde_json::to_string(&s).unwrap()).unwrap(),
791 s
792 );
793 }
794
795 #[test]
796 fn refresh_path_is_optional() {
797 let r: Request =
798 serde_json::from_str(r#"{"type":"template_refresh","name":"main"}"#).unwrap();
799 assert!(matches!(
800 r,
801 Request::TemplateRefresh {
802 path: None,
803 layered: false,
804 ..
805 }
806 ));
807 let r: GcReport = serde_json::from_str(
808 r#"{"adopted":[],"destroyed":[],"orphan_folders_removed":[],"records_dropped":[]}"#,
809 )
810 .unwrap();
811 assert!(r.failed.is_empty());
812 }
813
814 #[test]
815 fn template_settings_are_flat_and_optional() {
816 let r: Request = serde_json::from_str(
817 r#"{"type":"template_init","name":"main","source":{"kind":"seed","command":"x"},
818 "database":null,"migrations":null,"max_age_secs":null,"pg_major":null}"#,
819 )
820 .unwrap();
821 let Request::TemplateInit(a) = r else {
822 panic!("wrong variant")
823 };
824 assert_eq!(a.settings, TemplateSettings::default());
825
826 let set = Request::TemplateSet(TemplateSetArgs {
827 name: "main".into(),
828 max_age_secs: Some(60),
829 settings: TemplateSettings {
830 keep: Some(2),
831 every_secs: Some(3600),
832 every_mode: Some(RefreshMode::Layered),
833 migrate: Some(MigrateCmd {
834 command: "make migrate".into(),
835 cwd: "/w".into(),
836 path: None,
837 }),
838 auto_migrate: Some(true),
839 on_stale: Some(OnStale::Refresh),
840 ..Default::default()
841 },
842 resume: true,
843 });
844 let line = serde_json::to_string(&set).unwrap();
845 assert!(line.contains(r#""keep":2"#), "{line}");
846 let Request::TemplateSet(back) = serde_json::from_str(&line).unwrap() else {
847 panic!("wrong variant")
848 };
849 assert_eq!(back.settings.keep, Some(2));
850 assert_eq!(back.settings.every_mode, Some(RefreshMode::Layered));
851 assert_eq!(back.settings.migrate.unwrap().command, "make migrate");
852 assert!(back.resume && !back.settings.every_off);
853 let r: Response =
854 serde_json::from_str(r#"{"type":"generations","template":"main","generations":[]}"#)
855 .unwrap();
856 assert!(matches!(r, Response::Generations { paused: false, .. }));
857 }
858
859 #[test]
860 fn events_round_trip_and_defaults() {
861 let row = EventRow {
862 id: 7,
863 ts: 1_700_000_000,
864 kind: "clone.created".into(),
865 clone: Some("feat".into()),
866 template: None,
867 detail: serde_json::json!({ "id": "abc123" }),
868 };
869 round_trip(Response::Events {
870 events: vec![row.clone()],
871 });
872 round_trip(Response::Event(row));
873 let r: Request = serde_json::from_str(r#"{"type":"events"}"#).unwrap();
875 let Request::Events { filter, follow } = r else {
876 panic!("wrong variant")
877 };
878 assert_eq!((filter, follow), (EventFilter::default(), false));
879 assert_eq!(
880 serde_json::to_string(&EventFilter::default()).unwrap(),
881 "{}"
882 );
883 }
884
885 #[test]
886 fn ok_response_round_trip() {
887 let env = ResponseEnvelope {
888 v: PROTOCOL_VERSION,
889 result: Ok(Response::Ok),
890 };
891 let line = serde_json::to_string(&env).unwrap();
892 let back: ResponseEnvelope = serde_json::from_str(&line).unwrap();
893 assert!(matches!(back.result, Ok(Response::Ok)));
894 }
895}