agentplane/testkit/
older_shape.rs1use serde_json::{Value, json};
18
19use crate::core::{Digest, StoreError};
20use crate::journal::Upcaster;
21
22pub const OLDER: u16 = 0;
24
25const MOVED: &str = "StepStarted";
27
28#[must_use]
36pub fn older_shape(export: &str) -> String {
37 let mut moved = 0usize;
38 let older = relinked(export, |wire| {
39 if wire["kind"] == json!(MOVED) {
40 let object = wire.as_object_mut().expect("a record is an object");
41 let skill = object
42 .remove("skill")
43 .expect("a StepStarted names its skill");
44 object.insert("name".into(), skill);
45 object.insert("v".into(), json!(OLDER));
46 moved += 1;
47 }
48 });
49 assert!(moved > 0, "the export carries no {MOVED} record to move");
50 older
51}
52
53pub fn relinked(export: &str, mut edit: impl FnMut(&mut Value)) -> String {
64 let mut lines: Vec<Value> = export
65 .lines()
66 .map(|line| serde_json::from_str::<Value>(line).expect("every export line is JSON"))
67 .collect();
68 let mut prev = Digest::ZERO;
69 let mut block: Option<usize> = None;
70 for at in 0..lines.len() {
71 if lines[at]["kind"] == json!("agentplane.export.run") {
72 prev = Digest::ZERO;
73 block = Some(at);
74 continue;
75 }
76 if lines[at].get("kind").is_some() {
77 continue;
78 }
79 let line = &mut lines[at];
80 let mut wire: Value =
81 serde_json::from_str(line["raw"].as_str().expect("wire bytes")).expect("json");
82 edit(&mut wire);
83 if wire["kind"] == json!("RunConcluded") {
84 wire["chain_head"] = json!(prev);
85 }
86 let raw = String::from_utf8(crate::core::canon::value_bytes(&wire)).expect("utf-8");
87 let hash = Digest::chain(prev, raw.as_bytes());
88 line["body"] = wire;
89 line["prev_hash"] = json!(prev);
90 line["hash"] = json!(hash);
91 line["raw"] = json!(raw);
92 prev = hash;
93 if let Some(b) = block
94 && lines[b].get("seal").is_some()
95 {
96 lines[b]["seal"] = json!(hash);
97 }
98 }
99 let mut seals: Vec<(u64, Digest)> = lines
100 .iter()
101 .filter(|v| v["kind"] == json!("agentplane.export.run"))
102 .filter_map(|v| {
103 Some((
104 v.get("index")?.as_u64()?,
105 serde_json::from_value(v.get("seal")?.clone()).ok()?,
106 ))
107 })
108 .collect();
109 seals.sort_by_key(|(i, _)| *i);
110 let root = crate::core::merkle::root(
111 &seals
112 .iter()
113 .map(|(_, s)| crate::core::merkle::leaf_hash(s))
114 .collect::<Vec<_>>(),
115 );
116 if let Some(header) = lines
117 .iter_mut()
118 .find(|l| l["kind"] == json!("agentplane.export"))
119 {
120 header["checkpoint"]["root"] = json!(root);
121 }
122 let mut out = lines
123 .iter()
124 .map(|l| serde_json::to_string(l).expect("serialises"))
125 .collect::<Vec<_>>()
126 .join("\n");
127 out.push('\n');
128 out
129}
130
131#[derive(Debug, Clone, Copy, Default)]
135pub struct LiftsTheOlderShape;
136
137impl Upcaster for LiftsTheOlderShape {
138 fn current_version(&self, _kind: &str) -> u16 {
139 1
140 }
141
142 fn upcast(&self, kind: &str, version: u16, mut payload: Value) -> Result<Value, StoreError> {
143 if kind != MOVED || version != OLDER {
144 return Err(StoreError::UnknownRecordVersion {
145 kind: kind.to_owned(),
146 version,
147 reads: 1,
148 });
149 }
150 let object = payload
151 .as_object_mut()
152 .ok_or_else(|| StoreError::Backend("a record is an object".into()))?;
153 let name = object.remove("name").unwrap_or(Value::Null);
154 object.insert("skill".into(), name);
155 object.insert("v".into(), json!(1));
156 Ok(payload)
157 }
158}
159
160#[derive(Debug, Clone, Copy, Default)]
164pub struct ReadsOnlyTheOlderShape;
165
166impl Upcaster for ReadsOnlyTheOlderShape {
167 fn current_version(&self, kind: &str) -> u16 {
168 if kind == MOVED { OLDER } else { 1 }
169 }
170
171 fn upcast(&self, kind: &str, version: u16, _payload: Value) -> Result<Value, StoreError> {
172 Err(StoreError::UnknownRecordVersion {
173 kind: kind.to_owned(),
174 version,
175 reads: self.current_version(kind),
176 })
177 }
178}