diff --git a/src-tauri/src/pyramid/stale_helpers_upper.rs b/src-tauri/src/pyramid/stale_helpers_upper.rs index 10b438c3..488878ce 100644 --- a/src-tauri/src/pyramid/stale_helpers_upper.rs +++ b/src-tauri/src/pyramid/stale_helpers_upper.rs @@ -3704,12 +3704,7 @@ async fn apply_supersession_manifest( } }; let conn = super::db::open_pyramid_connection(Path::new(&db))?; - conn.execute( - "UPDATE pyramid_file_hashes - SET hash = ?1, last_ingested_at = datetime('now') - WHERE slug = ?2 AND file_path = ?3", - rusqlite::params![hash, slug_owned, path_owned], - )?; + super::db::set_file_content_hash(&conn, &slug_owned, &path_owned, &hash)?; Ok(()) }) .await @@ -5279,6 +5274,8 @@ mod tests { ChangeManifest, ChildSwap, ContentUpdates, DecisionOp, ManifestValidationError, TermOp, TopicOp, }; + use crate::pyramid::understanding_rebuild::rebuild_pyramid_node_metadata_from_store; + use crate::pyramid::understanding_store::understanding_root_from_conn; use rusqlite::{params, Connection}; use std::sync::Arc; use tempfile::NamedTempFile; @@ -6637,6 +6634,25 @@ mod tests { ); let db_path_str = db_file.path().to_str().unwrap().to_string(); + { + let conn_seed_ids = open_pyramid_db(db_file.path()).expect("reopen db for ids seed"); + conn_seed_ids + .execute( + "UPDATE pyramid_file_hashes + SET node_ids = ?3 + WHERE slug = ?1 AND file_path = ?2", + params![ + slug, + file_path_str, + serde_json::to_string(&vec![ + "L0-targeted".to_string(), + node_id.to_string() + ]) + .unwrap(), + ], + ) + .expect("seed multi-node file hash references"); + } // Load the node context against the post-edit file. This verifies // load_supersession_node_context pulls the source file for L0 @@ -6741,6 +6757,55 @@ mod tests { stored_hash, post_edit_hash, "pyramid_file_hashes.hash should be rewritten to the post-edit hash so the watcher stops re-firing" ); + let stored_node_ids: String = conn_verify + .query_row( + "SELECT node_ids FROM pyramid_file_hashes WHERE slug = ?1 AND file_path = ?2", + params![slug, file_path_str], + |row| row.get(0), + ) + .expect("load post-apply node_ids"); + let parsed_node_ids: Vec = + serde_json::from_str(&stored_node_ids).expect("node_ids json"); + assert_eq!( + parsed_node_ids, + vec!["L0-targeted".to_string(), node_id.to_string()], + "change-manifest hash rewrite must preserve existing file_hash node_ids" + ); + + let root = understanding_root_from_conn(&conn_verify).expect("test DB has store root"); + let mirrored_manifest = crate::pyramid::understanding_store::read_all_node_metadata_records( + &root, + ) + .expect("read node-metadata log") + .into_iter() + .any(|rec| { + rec.kind == "change_manifest" + && rec.slug == slug + && rec.key == format!("{node_id}|2") + && rec.op == "upsert" + }); + assert!( + mirrored_manifest, + "apply path must mirror the change-manifest row before rebuild" + ); + crate::pyramid::db::backfill_node_metadata_to_store(&conn_verify) + .expect("backfill fixture metadata so rebuild gate can run"); + conn_verify + .execute("DELETE FROM pyramid_change_manifests", []) + .expect("clear change-manifest projection"); + let rebuilt = + rebuild_pyramid_node_metadata_from_store(&root, &conn_verify).expect("rebuild metadata"); + assert!( + rebuilt > 0, + "node-metadata rebuild should restore mirrored change-manifest rows" + ); + let manifests = get_change_manifests_for_node(&conn_verify, slug, node_id) + .expect("load rebuilt change manifests"); + assert_eq!(manifests.len(), 1, "change manifest should rebuild from store"); + assert_eq!( + manifests[0].build_version, 2, + "rebuilt manifest should preserve post-apply build_version" + ); } /// Fix pass test 2: manifest-generation failure must NOT fall back to