1use kernel::event::record::HandoffWritten;
11use kernel::{AxCode, AxError, Locator, Payload, RunId};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Handoff {
24 must_read: Vec<Locator>,
25 overview: String,
26 progress: String,
27 context: String,
28 next_step: String,
29}
30
31impl Handoff {
32 pub fn new(
36 must_read: Vec<Locator>,
37 overview: String,
38 progress: String,
39 context: String,
40 next_step: String,
41 ) -> Result<Handoff, AxError> {
42 if must_read.is_empty() {
43 return Err(AxError::failure(
44 AxCode::InvalidArgs,
45 "construct handoff",
46 "empty must-read list",
47 )
48 .with_recovery(
49 "list the locators the successor must read before working; \
50 norms are machine-filled, add only this run's specifics",
51 ));
52 }
53 Ok(Handoff {
54 must_read,
55 overview,
56 progress,
57 context,
58 next_step,
59 })
60 }
61
62 pub fn must_read(&self) -> &[Locator] {
63 &self.must_read
64 }
65
66 pub fn overview(&self) -> &str {
67 &self.overview
68 }
69
70 pub fn progress(&self) -> &str {
71 &self.progress
72 }
73
74 pub fn context(&self) -> &str {
75 &self.context
76 }
77
78 pub fn next_step(&self) -> &str {
79 &self.next_step
80 }
81
82 pub fn payload(&self) -> Result<Payload, AxError> {
87 Payload::of(&HandoffWritten {
88 must_read: self.must_read.clone(),
89 overview: self.overview.clone(),
90 progress: self.progress.clone(),
91 context: self.context.clone(),
92 next_step: self.next_step.clone(),
93 })
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ResumeSeed {
102 pub run: RunId,
103 pub must_read: Vec<Locator>,
104}
105
106pub fn resume(handoff: &Handoff, new_run: RunId) -> ResumeSeed {
109 ResumeSeed {
110 run: new_run,
111 must_read: handoff.must_read().to_vec(),
112 }
113}
114
115#[cfg(test)]
116#[allow(
117 clippy::unwrap_used,
118 clippy::expect_used,
119 clippy::panic,
120 clippy::indexing_slicing,
121 reason = "test code"
122)]
123mod tests {
124 use super::*;
125
126 fn locator() -> Locator {
127 Locator::parse(&format!("cas:b3-{}", "ab".repeat(32))).unwrap()
128 }
129
130 #[test]
131 fn empty_must_read_cannot_be_constructed() {
132 let err = Handoff::new(vec![], "o".into(), "p".into(), "c".into(), "n".into()).unwrap_err();
133 assert_eq!(err.code(), &AxCode::InvalidArgs);
134 }
135
136 #[test]
137 fn payload_carries_five_sections() {
138 let handoff = Handoff::new(
139 vec![locator()],
140 "overview".into(),
141 "progress".into(),
142 "context".into(),
143 "next".into(),
144 )
145 .unwrap();
146 let json = serde_json::to_value(handoff.payload().unwrap()).unwrap();
147 for key in ["must_read", "overview", "progress", "context", "next_step"] {
148 assert!(json.get(key).is_some(), "missing section {key}");
149 }
150 assert_eq!(json["must_read"].as_array().unwrap().len(), 1);
151 }
152
153 #[test]
154 fn resume_mints_the_supplied_identity_and_carries_must_read() {
155 let handoff = Handoff::new(
156 vec![locator()],
157 String::new(),
158 String::new(),
159 String::new(),
160 "user-specified next action".into(),
161 )
162 .unwrap();
163 let new_run = RunId::parse("0198f6a2-7c4a-7bbb-9d1e-000000000002").unwrap();
164 let seed = resume(&handoff, new_run);
165 assert_eq!(seed.run, new_run);
166 assert_eq!(seed.must_read.len(), 1);
167 }
168}