//! Read/write helpers for the `test_jobs` LWW-map collection. //! //! Test-job entries are keyed by `story_id` and track the status, timing, //! and captured output for each test run. use bft_json_crdt::json_crdt::{JsonValue, *}; use bft_json_crdt::op::ROOT_ID; use serde_json::json; use super::super::state::{apply_and_persist, get_crdt, rebuild_test_job_index}; use super::super::types::{TestJobCrdt, TestJobView}; use super::list_id_at; /// Write or update a test-job entry keyed by `story_id`. pub fn write_test_job( story_id: &str, status: &str, started_at: f64, finished_at: Option, output: Option<&str>, ) { let Some(state_mutex) = get_crdt() else { return; }; let Ok(mut state) = state_mutex.lock() else { return; }; if let Some(&idx) = state.test_job_index.get(story_id) { apply_and_persist(&mut state, |s| { s.crdt.doc.test_jobs[idx].status.set(status.to_string()) }); apply_and_persist(&mut state, |s| { s.crdt.doc.test_jobs[idx].started_at.set(started_at) }); if let Some(fa) = finished_at { apply_and_persist(&mut state, |s| { s.crdt.doc.test_jobs[idx].finished_at.set(fa) }); } if let Some(o) = output { apply_and_persist(&mut state, |s| { s.crdt.doc.test_jobs[idx].output.set(o.to_string()) }); } } else { let entry: JsonValue = json!({ "story_id": story_id, "status": status, "started_at": started_at, "finished_at": finished_at.unwrap_or(0.0), "output": output.unwrap_or(""), }) .into(); apply_and_persist(&mut state, |s| s.crdt.doc.test_jobs.insert(ROOT_ID, entry)); state.test_job_index = rebuild_test_job_index(&state.crdt); } } /// Read all test-job entries. pub fn read_all_test_jobs() -> Option> { let state_mutex = get_crdt()?; let state = state_mutex.lock().ok()?; let mut out = Vec::new(); for entry in state.crdt.doc.test_jobs.iter() { if let Some(v) = extract_test_job_view(entry) { out.push(v); } } Some(out) } /// Read a single test-job entry by `story_id`. pub fn read_test_job(story_id: &str) -> Option { let state_mutex = get_crdt()?; let state = state_mutex.lock().ok()?; let &idx = state.test_job_index.get(story_id)?; extract_test_job_view(&state.crdt.doc.test_jobs[idx]) } /// Tombstone a test-job entry by `story_id`. pub fn delete_test_job(story_id: &str) -> bool { let Some(state_mutex) = get_crdt() else { return false; }; let Ok(mut state) = state_mutex.lock() else { return false; }; let Some(&idx) = state.test_job_index.get(story_id) else { return false; }; let Some(op_id) = list_id_at(&state.crdt.doc.test_jobs, idx) else { return false; }; apply_and_persist(&mut state, |s| s.crdt.doc.test_jobs.delete(op_id)); state.test_job_index = rebuild_test_job_index(&state.crdt); true } /// Convert a CRDT test-job entry into its read-only view representation. pub(super) fn extract_test_job_view(entry: &TestJobCrdt) -> Option { let story_id = match entry.story_id.view() { JsonValue::String(s) if !s.is_empty() => s, _ => return None, }; let status = match entry.status.view() { JsonValue::String(s) => s, _ => String::new(), }; let started_at = match entry.started_at.view() { JsonValue::Number(n) => n, _ => 0.0, }; let finished_at = match entry.finished_at.view() { JsonValue::Number(n) if n > 0.0 => Some(n), _ => None, }; let output = match entry.output.view() { JsonValue::String(s) if !s.is_empty() => Some(s), _ => None, }; Some(TestJobView { story_id, status, started_at, finished_at, output, }) }