memstead_base/engine/
check_ops.rs1use crate::check::{CheckLedger, CheckRecord, CheckState, Verdict, derive_state};
18use crate::vcs::{Actor, ClientId};
19
20use super::{Engine, error::EngineError};
21
22impl Engine {
23 pub fn record_check(
31 &mut self,
32 mem_name: &str,
33 entity_id: &str,
34 verdict: Verdict,
35 method: Option<&str>,
36 actor: Actor,
37 client: Option<&ClientId>,
38 ) -> Result<CheckRecord, EngineError> {
39 let mount_idx = self
40 .mounts
41 .iter()
42 .position(|m| m.mount.mem == mem_name)
43 .ok_or_else(|| self.unknown_mem_error(mem_name))?;
44 if self.mounts[mount_idx].mount.capability != crate::workspace::MountCapability::Write {
45 return Err(EngineError::ReadOnlyMount(mem_name.to_string()));
46 }
47 let entity_hash = self
48 .store
49 .all_entities()
50 .find(|e| e.mem == mem_name && e.id.0 == entity_id)
51 .map(|e| e.content_hash.clone())
52 .ok_or_else(|| EngineError::NotFound {
53 id: entity_id.to_string(),
54 })?;
55 let Some(root) = self.workspace_root() else {
56 return Err(EngineError::CheckNotRecorded {
57 reason: "engine has no workspace root — no durable check store".to_string(),
58 });
59 };
60 let ledger = CheckLedger::for_workspace(root);
61 let record = CheckRecord {
62 ts: std::time::SystemTime::now()
63 .duration_since(std::time::UNIX_EPOCH)
64 .map(|d| d.as_secs())
65 .unwrap_or(0),
66 entity: entity_id.to_string(),
67 verdict: verdict.as_str().to_string(),
68 method: method
69 .map(str::trim)
70 .filter(|m| !m.is_empty())
71 .map(str::to_string),
72 entity_hash,
73 actor: actor.as_trailer().to_string(),
74 client: client.map(|c| format!("{}@{}", c.name, c.version)),
75 role: self
76 .current_role()
77 .as_trailer()
78 .unwrap_or("unspecified")
79 .to_string(),
80 };
81 ledger
82 .record(&record)
83 .map_err(|e| EngineError::CheckNotRecorded {
84 reason: format!("ledger append failed: {e}"),
85 })?;
86 Ok(record)
87 }
88
89 pub fn entity_check_state(
94 &self,
95 mem_name: &str,
96 entity_id: &str,
97 ) -> Result<(CheckState, Option<CheckRecord>), EngineError> {
98 self.find_mount(mem_name)?;
99 let current_hash = self
100 .store
101 .all_entities()
102 .find(|e| e.mem == mem_name && e.id.0 == entity_id)
103 .map(|e| e.content_hash.clone())
104 .ok_or_else(|| EngineError::NotFound {
105 id: entity_id.to_string(),
106 })?;
107 let latest = self
108 .workspace_root()
109 .map(CheckLedger::for_workspace)
110 .and_then(|l| l.latest_for(entity_id));
111 Ok((derive_state(latest.as_ref(), ¤t_hash), latest))
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use crate::check::Verdict;
118 use crate::vcs::Actor;
119 use crate::workspace::MountCapability;
120
121 #[test]
125 fn check_refuses_read_only_mounts_typed() {
126 let tmp = tempfile::TempDir::new().unwrap();
127 let mut mount = crate::engine::test_helpers::folder_mount("ro", tmp.path().to_path_buf());
128 mount.capability = MountCapability::ReadOnly;
129 let mut engine = crate::Engine::from_mounts(vec![(
130 mount,
131 Box::new(crate::storage::FilesystemMemWriter::new(
132 tmp.path().to_path_buf(),
133 )) as Box<dyn crate::backend::MemBackend>,
134 )])
135 .unwrap();
136 let err = engine
137 .record_check("ro", "ro--anything", Verdict::Ok, None, Actor::Cli, None)
138 .unwrap_err();
139 assert_eq!(err.code(), "READ_ONLY_MOUNT");
140 }
141}