diff --git a/crates/searchi-core/tests/targeted_reconciliation.rs b/crates/searchi-core/tests/targeted_reconciliation.rs index 7e08754..a5df7f6 100644 --- a/crates/searchi-core/tests/targeted_reconciliation.rs +++ b/crates/searchi-core/tests/targeted_reconciliation.rs @@ -104,7 +104,7 @@ fn directory_events_reconcile_only_subtree_and_prune_exclusions() -> Result<()> } #[test] -fn invalid_batches_and_failed_reads_roll_back_all_changes() -> Result<()> { +fn invalid_batches_and_unreadable_sources_preserve_records() -> Result<()> { let temp = tempfile::tempdir()?; let root = temp.path().join("root"); fs::create_dir(&root)?; @@ -126,10 +126,14 @@ fn invalid_batches_and_failed_reads_roll_back_all_changes() -> Result<()> { let permissions = { use std::os::unix::fs::PermissionsExt; let previous = fs::metadata(root.join("z.txt"))?.permissions(); - fs::set_permissions(root.join("z.txt"), fs::Permissions::from_mode(0))?; + fs::set_permissions(root.join("z.txt"), fs::Permissions::from_mode(0o000))?; previous }; - let failed = index.index_paths_with_options( + let denied = fs::File::open(root.join("z.txt")) + .expect_err("fixture must prevent reading the indexed source") + .kind() + == std::io::ErrorKind::PermissionDenied; + let result = index.index_paths_with_options( &root, &[root.join("a.txt"), root.join("z.txt")], &IndexOptions::default(), @@ -138,9 +142,19 @@ fn invalid_batches_and_failed_reads_roll_back_all_changes() -> Result<()> { drop(locked); #[cfg(unix)] fs::set_permissions(root.join("z.txt"), permissions)?; - assert!(failed.is_err()); - assert_eq!(index.search("beforetransaction", 10)?.len(), 1); - assert!(index.search("aftertransaction", 10)?.is_empty()); + if denied { + // Permission denial preserves that file while readable changes commit. + let report = result?; + assert_eq!(report.skipped, 1); + assert_eq!(index.search("aftertransaction", 10)?.len(), 1); + assert!(index.search("beforetransaction", 10)?.is_empty()); + } else { + // Other I/O errors, including platform-specific sharing failures, abort. + assert!(result.is_err()); + assert_eq!(index.search("beforetransaction", 10)?.len(), 1); + assert!(index.search("aftertransaction", 10)?.is_empty()); + } + assert_eq!(index.search("validlast", 10)?.len(), 1); assert!(index .index_paths_with_options( &root, diff --git a/crates/searchi-extract-worker/src/resources.rs b/crates/searchi-extract-worker/src/resources.rs index 018aa80..0fb47bb 100644 --- a/crates/searchi-extract-worker/src/resources.rs +++ b/crates/searchi-extract-worker/src/resources.rs @@ -40,11 +40,7 @@ pub fn apply() -> Result<()> { pub fn apply() -> Result<()> { // Darwin rejects finite AS/DATA limits. The parent enforces a sampled RSS // budget there; retain the CPU limit and bounded path-free transport here. - for (resource, maximum) in [ - #[cfg(not(target_os = "macos"))] - (libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t), - (libc::RLIMIT_CPU, 30 as libc::rlim_t), - ] { + let set_limit = |resource, maximum| -> Result<()> { let value = libc::rlimit { rlim_cur: maximum, rlim_max: maximum, @@ -55,8 +51,11 @@ pub fn apply() -> Result<()> { unsafe { libc::setrlimit(resource, &value) } == 0, "Cannot install extraction resource limit" ); - } - Ok(()) + Ok(()) + }; + #[cfg(not(target_os = "macos"))] + set_limit(libc::RLIMIT_AS, MEMORY_BYTES as libc::rlim_t)?; + set_limit(libc::RLIMIT_CPU, 30 as libc::rlim_t) } #[cfg(not(any(windows, unix)))] diff --git a/crates/searchi-service/src/store.rs b/crates/searchi-service/src/store.rs index 0825ecf..d7aed2c 100644 --- a/crates/searchi-service/src/store.rs +++ b/crates/searchi-service/src/store.rs @@ -787,10 +787,10 @@ impl Store { return Ok(Vec::new()); } trace.end(generation_span, Default::default()); - if !self + if self .ann_cache .as_ref() - .is_some_and(|(generation, _)| *generation == active) + .is_none_or(|(generation, _)| *generation != active) { let prepared = trace.timed("semantic.prepared_graph_lookup", parent, |_, _| { let checksum: String = snapshot.query_row( @@ -815,10 +815,10 @@ impl Store { .is_some_and(|(generation, _)| *generation == active), ), ); - if !self + if self .ann_cache .as_ref() - .is_some_and(|(generation, _)| *generation == active) + .is_none_or(|(generation, _)| *generation != active) { let blob_span = trace.begin("semantic.ann_blob_read", ann_load); let length: Option = snapshot.query_row( diff --git a/docs/MACOS-RUNTIME.md b/docs/MACOS-RUNTIME.md index bd489a7..acf19ec 100644 --- a/docs/MACOS-RUNTIME.md +++ b/docs/MACOS-RUNTIME.md @@ -97,10 +97,8 @@ measurements, not desktop UI timings or production-scale latency guarantees. Commit validation: all57native desktop tests pass, including real filesystem create/update/rename/delete events using canonical Mac paths;66frontend tests, format/build checks,3speech builder tests and installed Metal recognition pass. -The broader workspace run exposes an existing stale permission-denial rollback -expectation in `targeted_reconciliation` (current engine skips unreadable inputs). -Strict current-toolchain Clippy also flags unchanged permission-literal and -ANN-cache boolean expressions. These failures are not qualified as passing gates. +The initial broader checks exposed a stale permission-denial rollback test and +current-toolchain Clippy warnings; the correction below supersedes those failures. ### Native document extraction policy @@ -119,3 +117,20 @@ Resident-memory inspection also retries the short exit-to-waitable transition before treating a live child as uninspectable. Ten fresh installed CLI/helper DOCX runs returned the expected passage; the native roundtrip test repeats that short-lived worker path ten times to cover the race. + +## Workspace validation correction, 2026-10-23 + +The reconciliation fixture now verifies that permission-denied sources retain +prior records while readable changes commit. Other I/O errors still require +rollback; the separate cancellation-after-write test also checks atomic rollback. +The fixture explicitly verifies its read restriction instead of assuming an +identical sharing-error category on every OS. Production reconciliation behavior +is unchanged. Current Clippy syntax corrections cover octal permissions, +ANN-cache predicates and the Mac resource-limit single-element loop. + +Fresh native Mac checks: `cargo test --workspace --locked` passes192tests with +34existing opt-in tests ignored and none filtered; strict +`cargo clippy --workspace --all-targets --locked -- -D warnings` passes. +All57desktop tests and66frontend tests pass; Rust/desktop formatting, +frontend formatting/build and3speech-builder tests pass. These checks do not +claim execution of the ignored real-model/platform suites.