1use crate::store::Store;
5use crate::tick::{Check, Ground};
6use crate::verdict::Verdict;
7use serde_json::{json, Map, Value};
8use time::format_description::well_known::Rfc3339;
9use time::OffsetDateTime;
10
11pub fn write_state(
15 store: &Store,
16 tick_id: &str,
17 rows: &[(&Ground, Verdict)],
18 staleness_policy: &str,
19 staleness_sha: Option<&str>,
20) -> std::io::Result<()> {
21 let computed_at = OffsetDateTime::now_utc()
22 .format(&Rfc3339)
23 .unwrap_or_default();
24 let grounds: Vec<Value> = rows
25 .iter()
26 .map(|(g, v)| {
27 let mut row = Map::new();
28 row.insert("claim".into(), Value::String(g.claim.clone()));
29 row.insert("supports".into(), Value::String(g.supports.clone()));
30 let check = match &g.check {
31 Some(Check::Test {
32 reference,
33 verified_at_sha,
34 ..
35 }) => {
36 row.insert("ref".into(), Value::String(reference.clone()));
37 row.insert(
38 "verified_at_sha".into(),
39 Value::String(verified_at_sha.clone()),
40 );
41 "test"
42 }
43 Some(Check::Person { .. }) => "person",
44 None => "none",
45 };
46 row.insert("check".into(), Value::String(check.into()));
47 row.insert("verdict".into(), Value::String(v.label().into()));
48 if let Verdict::NotRun { missing_platforms } = v {
49 row.insert("missing_platforms".into(), json!(missing_platforms));
50 }
51 Value::Object(row)
52 })
53 .collect();
54 let doc = json!({
55 "tick_id": tick_id,
56 "computed_at": computed_at,
57 "staleness_ref": { "policy": staleness_policy, "sha": staleness_sha },
58 "grounds": grounds,
59 });
60 let dir = store.root.join("results").join("state");
61 std::fs::create_dir_all(&dir)?;
62 std::fs::write(
63 dir.join(format!("{tick_id}.json")),
64 serde_json::to_string_pretty(&doc).expect("serializable"),
65 )
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::tick::{Ground, Liveness, Tick};
72
73 fn store() -> (std::path::PathBuf, Store) {
74 use std::sync::atomic::{AtomicU64, Ordering};
75 static N: AtomicU64 = AtomicU64::new(0);
76 let p = std::env::temp_dir().join(format!(
77 "ev-state-{}-{}",
78 std::process::id(),
79 N.fetch_add(1, Ordering::Relaxed)
80 ));
81 let _ = std::fs::remove_dir_all(&p);
82 std::fs::create_dir_all(&p).unwrap();
83 let s = Store::at(&p);
84 s.init().unwrap();
85 (p, s)
86 }
87
88 #[test]
89 fn write_state_should_record_each_ground_verdict_when_a_tick_is_evaluated() {
90 let (_p, s) = store();
92 let tick = Tick {
93 id: "abcabcabcabc".into(),
94 parent_id: "".into(),
95 observe: "o".into(),
96 decision: "d".into(),
97 grounds: vec![
98 Ground {
99 claim: "no Redis".into(),
100 supports: "chosen".into(),
101 check: Some(Check::Test {
102 reference: "pytest x".into(),
103 verified_at_sha: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
104 counter_test: Some("ct".into()),
105 liveness: Liveness {
106 platforms: vec!["linux-ci".into()],
107 triggered_by: vec!["f".into()],
108 surfaces: vec!["s".into()],
109 },
110 }),
111 },
112 Ground {
113 claim: "team ok".into(),
114 supports: "chosen".into(),
115 check: Some(Check::Person {
116 reference: "Q3".into(),
117 }),
118 },
119 ],
120 status: "live".into(),
121 held_since: "".into(),
122 blame: "Wang Yu".into(),
123 authority: None,
124 jurisdiction: None,
125 source_ref: None,
126 provenance: None,
127 };
128 let rows = vec![
129 (
130 &tick.grounds[0],
131 Verdict::NotRun {
132 missing_platforms: vec!["linux-ci".into()],
133 },
134 ),
135 (&tick.grounds[1], Verdict::NotApplicable),
136 ];
137
138 write_state(&s, &tick.id, &rows, "live-origin", None).unwrap();
140
141 let text = std::fs::read_to_string(
143 s.root
144 .join("results")
145 .join("state")
146 .join("abcabcabcabc.json"),
147 )
148 .unwrap();
149 let v: Value = serde_json::from_str(&text).unwrap();
150 assert_eq!(v["tick_id"], "abcabcabcabc");
151 assert_eq!(v["grounds"][0]["check"], "test");
152 assert_eq!(v["grounds"][0]["ref"], "pytest x");
153 assert_eq!(v["grounds"][0]["verdict"], "not-run");
154 assert_eq!(v["grounds"][0]["missing_platforms"][0], "linux-ci");
155 assert_eq!(v["grounds"][1]["check"], "person");
156 assert_eq!(v["grounds"][1]["verdict"], "n/a");
157 }
158}