use crate::engine::{Engine, EngineError, MemChangedEvent};
use crate::entity::id::file_path_to_id;
use crate::entity::loader::parse_entries;
use crate::entity::source::SourceEntry;
use crate::entity::store_builder::push_entities_into_store;
use crate::ops::{CommitEnvelope, EntityChange};
impl Engine {
pub fn apply_external_commit(&mut self, envelope: &CommitEnvelope) -> Result<(), EngineError> {
let mem = envelope.mem.as_str();
let mount_idx = self
.mounts
.iter()
.position(|m| m.mount.mem == mem)
.ok_or_else(|| self.unknown_mem_error(mem))?;
let schema = self
.schemas
.get(mem)
.cloned()
.ok_or_else(|| self.unknown_mem_error(mem))?;
let mut to_upsert: Vec<SourceEntry> = Vec::new();
let mut to_remove: Vec<String> = Vec::new();
for change in &envelope.changes {
match change {
EntityChange::Added { path, content }
| EntityChange::Modified { path, content } => {
to_upsert.push(SourceEntry {
relative_path: path.clone(),
source_path: std::path::PathBuf::from(path.clone()),
content: content.clone(),
});
}
EntityChange::Deleted { path } => {
to_remove.push(path.clone());
}
EntityChange::Renamed { from, to, content } => {
to_remove.push(from.clone());
to_upsert.push(SourceEntry {
relative_path: to.clone(),
source_path: std::path::PathBuf::from(to.clone()),
content: content.clone(),
});
}
}
}
let load_result = parse_entries(to_upsert, Vec::new(), mem, schema.as_ref());
if let Some((path, msg)) = load_result.errors.into_iter().next() {
return Err(EngineError::ParseAfterWrite(format!(
"apply_external_commit: failed to parse '{}' in mem '{}': {}",
path.display(),
mem,
msg
)));
}
for path in &to_remove {
let id = file_path_to_id(path, mem);
self.store.remove(&id);
}
let mut parsed = load_result.entities;
for result in parsed.iter_mut() {
result.entity.file_path = result.entity.id.0.clone();
}
let fallback = crate::engine_fallback_type();
push_entities_into_store(&mut self.store, parsed, fallback.as_ref(), None);
let previous = self
.mounts
.get(mount_idx)
.and_then(|m| m.last_known_head.clone())
.unwrap_or_default();
if let Some(state) = self.mounts.get_mut(mount_idx) {
state.last_known_head = Some(envelope.sha.clone());
}
self.invalidate_communities();
#[cfg(not(target_arch = "wasm32"))]
self.invalidate_search_indexes();
if previous != envelope.sha {
let event = MemChangedEvent {
mem: mem.to_string(),
head: envelope.sha.clone(),
previous,
n_commits: 1,
};
self.emit_mem_changed(&event);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::MemBackend;
use crate::engine::test_helpers::*;
use crate::entity::EntityId;
use crate::storage::FilesystemMemWriter;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
fn empty_folder_engine(tmp: &TempDir, mem: &str) -> Engine {
let mem_dir = tmp.path().to_path_buf();
let writer = FilesystemMemWriter::new(mem_dir.clone());
Engine::from_mounts(vec![(
folder_mount(mem, mem_dir),
Box::new(writer) as Box<dyn MemBackend>,
)])
.unwrap()
}
fn envelope(mem: &str, sha: &str, parent: &str, changes: Vec<EntityChange>) -> CommitEnvelope {
CommitEnvelope {
sha: sha.to_string(),
parent: parent.to_string(),
mem: mem.to_string(),
timestamp: "2026-05-19T10:00:00Z".to_string(),
trailers: BTreeMap::new(),
changes,
}
}
fn well_formed_body(title: &str) -> String {
format!(
"---\ntype: spec\ncreated_date: 2026-01-01\nlast_modified: 2026-01-01\nlevel: M0\n---\n# {title}\n\n## Identity\n\n{title}\n"
)
}
#[test]
fn apply_external_commit_unknown_mem_refuses() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
let env = envelope("missing", "deadbeef", "", vec![]);
let err = engine.apply_external_commit(&env).unwrap_err();
assert_eq!(err.code(), "UNKNOWN_MEM");
}
#[test]
fn apply_external_commit_added_lands_entity_in_store() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
let env = envelope(
"specs",
"abc123",
"",
vec![EntityChange::Added {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha"),
}],
);
engine.apply_external_commit(&env).unwrap();
let id = EntityId::new("specs", "alpha");
assert!(
engine.get_entity(&id).is_some(),
"expected 'specs--alpha' in store after Added apply"
);
}
#[test]
fn apply_external_commit_modified_replaces_existing_body() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
engine
.apply_external_commit(&envelope(
"specs",
"sha1",
"",
vec![EntityChange::Added {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha-v0"),
}],
))
.unwrap();
engine
.apply_external_commit(&envelope(
"specs",
"sha2",
"sha1",
vec![EntityChange::Modified {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha-v1"),
}],
))
.unwrap();
let id = EntityId::new("specs", "alpha");
let entity = engine.get_entity(&id).expect("alpha must still exist");
assert_eq!(entity.title, "Alpha-v1");
}
#[test]
fn apply_external_commit_deleted_removes_entity_from_store() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
engine
.apply_external_commit(&envelope(
"specs",
"sha1",
"",
vec![EntityChange::Added {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha"),
}],
))
.unwrap();
engine
.apply_external_commit(&envelope(
"specs",
"sha2",
"sha1",
vec![EntityChange::Deleted {
path: "alpha.md".to_string(),
}],
))
.unwrap();
let id = EntityId::new("specs", "alpha");
assert!(
engine.get_entity(&id).is_none(),
"expected 'specs--alpha' removed after Deleted apply"
);
}
#[test]
fn apply_external_commit_renamed_moves_entity_to_new_id() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
engine
.apply_external_commit(&envelope(
"specs",
"sha1",
"",
vec![EntityChange::Added {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha"),
}],
))
.unwrap();
engine
.apply_external_commit(&envelope(
"specs",
"sha2",
"sha1",
vec![EntityChange::Renamed {
from: "alpha.md".to_string(),
to: "alpha-renamed.md".to_string(),
content: well_formed_body("Alpha Renamed"),
}],
))
.unwrap();
let old_id = EntityId::new("specs", "alpha");
let new_id = EntityId::new("specs", "alpha-renamed");
assert!(engine.get_entity(&old_id).is_none(), "old id must be gone");
assert!(
engine.get_entity(&new_id).is_some(),
"new id must be present"
);
}
#[test]
fn apply_external_commit_mixed_changes_apply_atomically() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
engine
.apply_external_commit(&envelope(
"specs",
"sha1",
"",
vec![
EntityChange::Added {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha"),
},
EntityChange::Added {
path: "beta.md".to_string(),
content: well_formed_body("Beta"),
},
],
))
.unwrap();
engine
.apply_external_commit(&envelope(
"specs",
"sha2",
"sha1",
vec![
EntityChange::Deleted {
path: "alpha.md".to_string(),
},
EntityChange::Modified {
path: "beta.md".to_string(),
content: well_formed_body("Beta-v2"),
},
EntityChange::Added {
path: "gamma.md".to_string(),
content: well_formed_body("Gamma"),
},
],
))
.unwrap();
let alpha = EntityId::new("specs", "alpha");
let beta = EntityId::new("specs", "beta");
let gamma = EntityId::new("specs", "gamma");
assert!(engine.get_entity(&alpha).is_none());
assert_eq!(engine.get_entity(&beta).unwrap().title, "Beta-v2");
assert!(engine.get_entity(&gamma).is_some());
}
#[test]
fn apply_external_commit_permissive_parser_accepts_minimal_body() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
let bare_body = "# Bare\n\nNo frontmatter, no schema sections.\n";
engine
.apply_external_commit(&envelope(
"specs",
"sha1",
"",
vec![EntityChange::Added {
path: "bare.md".to_string(),
content: bare_body.to_string(),
}],
))
.unwrap();
let id = EntityId::new("specs", "bare");
let entity = engine
.get_entity(&id)
.expect("permissive parser must produce an entity even without frontmatter");
assert_eq!(entity.title, "Bare");
}
#[test]
fn apply_external_commit_emits_mem_changed_event_to_subscribers() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
let observed = Arc::new(Mutex::new(Vec::<MemChangedEvent>::new()));
let observed_clone = observed.clone();
let _handle = engine
.subscribe_mem_changes(
"specs",
Arc::new(move |event| {
observed_clone.lock().unwrap().push(event.clone());
}),
)
.unwrap();
engine
.apply_external_commit(&envelope(
"specs",
"sha-new",
"",
vec![EntityChange::Added {
path: "alpha.md".to_string(),
content: well_formed_body("Alpha"),
}],
))
.unwrap();
let events = observed.lock().unwrap();
assert_eq!(events.len(), 1, "expected one MemChangedEvent");
assert_eq!(events[0].head, "sha-new");
assert_eq!(events[0].mem, "specs");
assert_eq!(events[0].n_commits, 1);
}
#[test]
fn apply_external_commit_empty_changes_still_advances_head() {
let tmp = TempDir::new().unwrap();
let mut engine = empty_folder_engine(&tmp, "specs");
let observed = Arc::new(Mutex::new(Vec::<MemChangedEvent>::new()));
let observed_clone = observed.clone();
let _handle = engine
.subscribe_mem_changes(
"specs",
Arc::new(move |event| {
observed_clone.lock().unwrap().push(event.clone());
}),
)
.unwrap();
engine
.apply_external_commit(&envelope("specs", "empty-commit-sha", "", vec![]))
.unwrap();
let events = observed.lock().unwrap();
assert_eq!(events.len(), 1, "empty envelope must still emit one event");
assert_eq!(events[0].head, "empty-commit-sha");
}
}