Skip to main content

eggup_core/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3// Crate-local doc-accuracy check. `clippy::missing_errors_doc` lives in
4// `clippy::pedantic`, which this workspace does not enable, so without this
5// attribute the gate never notices a public fallible item that documents no
6// failure mode. Enabling it here keeps the check inside the existing gate and
7// affects no other crate.
8#![warn(clippy::missing_errors_doc)]
9#![doc = "Policy-neutral local mechanics for verified multi-artifact updates."]
10#![doc = ""]
11#![doc = "The core crate deliberately does not fetch bytes, manage services, or choose release policy."]
12#![doc = "Callers acquire artifacts and prove destination ownership; the core"]
13#![doc = "provides validated preparation, SHA-256 integrity verification, bounded"]
14#![doc = "candidate validation, locked ownership revalidation, staged-digest"]
15#![doc = "revalidation, and atomic-feeling multi-artifact commit with rollback."]
16#![doc = ""]
17#![doc = "Integrity here is checksum evidence only. No authenticity or signature"]
18#![doc = "claim is made: there is no authenticity verifier in this crate."]
19#![doc = ""]
20#![doc = "Runnable flows live in `examples/`: one-member, multi-member, custom"]
21#![doc = "validator, ownership verifier, and receipt interpretation."]
22
23mod candidate;
24mod current_exe;
25mod domain;
26mod error;
27mod integrity;
28mod lock;
29mod stage;
30mod transaction;
31
32pub use candidate::{
33    run_bounded, AllValidators, CandidateValidator, CommandOutput, CommandSpec,
34    CrossMemberAgreementValidator, ExactIdentityValidator, ValidatedTransaction, MAX_STDIN_BYTES,
35};
36pub use current_exe::CurrentExecutable;
37pub use domain::{
38    AbsentOnlyVerifier, AbsentPolicy, ArtifactMember, ArtifactSet, BoundSources, CommitOwnership,
39    ExactDigestVerifier, ExistingAsOwnedVerifier, FileKind, InstallPlan, IntegrityRequirement,
40    MemberId, Ownership, OwnershipVerifier, PermissionsIntent, ProductId, ReleaseId,
41    StagePlacement,
42};
43pub use error::{Error, RecoveryError, RecoveryResult, Result};
44pub use integrity::{
45    hash_file, parse_sha256_sidecar, verify_file, IntegrityResult, IntegrityStatus, Sha256Manifest,
46    VerifiedTransaction,
47};
48pub use lock::{LockObservation, LockStatus, MutationLock, StaleLockDecision, StaleLockVerifier};
49pub use stage::PreparedTransaction;
50pub use transaction::{
51    CleanupDisposition, FailureCategory, FailurePhase, FailureReport, PostCommitFailurePolicy,
52    TransactionDisposition, TransactionReceipt,
53};
54
55#[cfg(test)]
56mod test_support;
57
58#[cfg(test)]
59mod failure_taxonomy_tests;
60
61#[cfg(test)]
62mod tests {
63    use super::test_support::{FailureInjector, FailurePoint, InstallationRoot};
64    use super::transaction::CommitFault;
65    use super::{
66        AbsentOnlyVerifier, AbsentPolicy, AllValidators, ArtifactMember, ArtifactSet, BoundSources,
67        CleanupDisposition, CommitOwnership, Error, ExactDigestVerifier, ExactIdentityValidator,
68        ExistingAsOwnedVerifier, InstallPlan, MemberId, MutationLock, Ownership, OwnershipVerifier,
69        PostCommitFailurePolicy, ProductId, ReleaseId, TransactionDisposition,
70    };
71    use std::collections::HashMap;
72    use std::fs;
73    use std::path::Path;
74    #[cfg(unix)]
75    use std::time::Duration;
76
77    struct FixedVerifier(Ownership);
78    impl std::fmt::Debug for FixedVerifier {
79        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80            f.debug_tuple("FixedVerifier").field(&self.0).finish()
81        }
82    }
83    impl OwnershipVerifier for FixedVerifier {
84        fn verify(&self, _m: &MemberId, _d: &Path) -> Ownership {
85            self.0
86        }
87    }
88
89    fn allow_create() -> ExistingAsOwnedVerifier {
90        ExistingAsOwnedVerifier
91    }
92
93    fn commit_verified(
94        prepared: super::PreparedTransaction,
95        verifier: &dyn OwnershipVerifier,
96        absent: AbsentPolicy,
97    ) -> super::Result<super::TransactionReceipt> {
98        let verified = prepared.verify_integrity()?;
99        let validated = verified.validate(&AllValidators::new())?;
100        validated.commit(CommitOwnership::new(verifier, absent))
101    }
102
103    fn commit_verified_with_fault(
104        prepared: super::PreparedTransaction,
105        verifier: &dyn OwnershipVerifier,
106        absent: AbsentPolicy,
107        fault: CommitFault,
108    ) -> super::Result<super::TransactionReceipt> {
109        let verified = prepared.verify_integrity()?;
110        let validated = verified.validate(&AllValidators::new())?;
111        validated.commit_with_fault(CommitOwnership::new(verifier, absent), fault)
112    }
113
114    #[test]
115    fn fixture_writes_and_reads_exact_bytes() {
116        let fixture = InstallationRoot::new().expect("fixture");
117        fixture
118            .write_file("bin/eggup", b"exact bytes")
119            .expect("write");
120
121        assert_eq!(
122            fixture.read_file("bin/eggup").expect("read"),
123            b"exact bytes"
124        );
125        assert!(fixture.path().join("bin/eggup").is_file());
126    }
127
128    #[test]
129    fn fixture_rejects_escape_paths() {
130        let fixture = InstallationRoot::new().expect("fixture");
131
132        let error = fixture.write_file("../outside", b"nope").unwrap_err();
133
134        assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
135        assert!(!fixture.path().parent().unwrap().join("outside").exists());
136    }
137
138    #[test]
139    fn failure_injector_defaults_to_no_failure() {
140        let mut injector = FailureInjector::default();
141
142        assert!(injector.check(FailurePoint::Prepare).is_ok());
143        assert!(injector.check(FailurePoint::Commit).is_ok());
144    }
145
146    #[test]
147    fn prepares_a_multi_member_bundle_without_touching_destinations() {
148        let inputs = InstallationRoot::new().expect("inputs");
149        let install = InstallationRoot::new().expect("install");
150        inputs.write_file("egress", b"egress").expect("write");
151        inputs.write_file("helper", b"helper").expect("write");
152        let product = ProductId::new("egress").expect("product");
153        let release = ReleaseId::new("2026.09.22").expect("release");
154        let members = ArtifactSet::new(vec![
155            ArtifactMember::new(
156                MemberId::new("main").expect("member"),
157                inputs.path().join("egress"),
158                "bin/egress",
159            )
160            .expect("artifact"),
161            ArtifactMember::new(
162                MemberId::new("helper").expect("member"),
163                inputs.path().join("helper"),
164                "bin/helper",
165            )
166            .expect("artifact"),
167        ])
168        .expect("members");
169
170        let prepared = InstallPlan::new(product, release, install.path(), members)
171            .expect("plan")
172            .prepare()
173            .expect("prepare");
174
175        assert!(!install.path().join("bin/egress").exists());
176        assert_eq!(
177            fs::read(
178                prepared
179                    .staged_path(&MemberId::new("main").unwrap())
180                    .unwrap()
181            )
182            .unwrap(),
183            b"egress"
184        );
185    }
186
187    #[test]
188    fn preparation_failure_cleans_private_stage() {
189        let inputs = InstallationRoot::new().expect("inputs");
190        let install = InstallationRoot::new().expect("install");
191        inputs.write_file("eggup", b"bytes").expect("write");
192        let plan = InstallPlan::new(
193            ProductId::new("eggup").unwrap(),
194            ReleaseId::new("r1").unwrap(),
195            install.path(),
196            ArtifactSet::single(
197                ArtifactMember::new(
198                    MemberId::new("main").unwrap(),
199                    inputs.path().join("eggup"),
200                    "eggup",
201                )
202                .unwrap(),
203            )
204            .unwrap(),
205        )
206        .unwrap();
207
208        assert!(plan
209            .clone()
210            .prepare_with_failure(FailurePoint::StageCreate)
211            .is_err());
212        let result = plan.prepare_with_failure(FailurePoint::StageCopy);
213
214        assert!(result.is_err());
215        assert!(!install.path().join("eggup").exists());
216        let stage_prefix = format!(
217            ".eggup-stage-{}-",
218            install.path().file_name().unwrap().to_string_lossy()
219        );
220        assert!(install
221            .path()
222            .parent()
223            .unwrap()
224            .read_dir()
225            .unwrap()
226            .filter_map(std::result::Result::ok)
227            .all(|entry| !entry
228                .file_name()
229                .to_string_lossy()
230                .starts_with(&stage_prefix)));
231    }
232
233    #[test]
234    fn rejects_duplicate_normalized_destinations() {
235        let inputs = InstallationRoot::new().expect("inputs");
236        let install = InstallationRoot::new().expect("install");
237        inputs.write_file("one", b"one").expect("write");
238        inputs.write_file("two", b"two").expect("write");
239        let members = ArtifactSet::new(vec![
240            ArtifactMember::new(
241                MemberId::new("one").unwrap(),
242                inputs.path().join("one"),
243                "bin/./app",
244            )
245            .unwrap(),
246            ArtifactMember::new(
247                MemberId::new("two").unwrap(),
248                inputs.path().join("two"),
249                "bin/app",
250            )
251            .unwrap(),
252        ])
253        .unwrap();
254
255        let result = InstallPlan::new(
256            ProductId::new("eggup").unwrap(),
257            ReleaseId::new("r1").unwrap(),
258            install.path(),
259            members,
260        );
261
262        assert!(result.is_err());
263    }
264
265    #[test]
266    fn rejects_empty_sets_and_escaping_destinations() {
267        assert!(ArtifactSet::new(Vec::new()).is_err());
268        assert!(ArtifactMember::new(
269            MemberId::new("main").unwrap(),
270            "/tmp/acquired",
271            "../outside",
272        )
273        .is_err());
274        assert!(ArtifactMember::new(
275            MemberId::new("main").unwrap(),
276            "/tmp/acquired",
277            "/absolute/path",
278        )
279        .is_err());
280    }
281
282    #[test]
283    fn bound_source_stages_open_object_despite_foreign_replacement() {
284        use std::fs::File;
285
286        let inputs = InstallationRoot::new().expect("inputs");
287        let install = InstallationRoot::new().expect("install");
288        let source = inputs.write_file("owned", b"owned-bytes").expect("write");
289        let handle = File::open(&source).expect("open");
290        let member =
291            ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
292                .unwrap();
293        let plan = InstallPlan::new(
294            ProductId::new("eggup").unwrap(),
295            ReleaseId::new("r1").unwrap(),
296            install.path(),
297            ArtifactSet::single(member).unwrap(),
298        )
299        .unwrap();
300        // Post-handoff replacement: the recorded name now resolves to foreign
301        // bytes while the open object still proves the owned bytes.
302        fs::remove_file(&source).expect("remove");
303        fs::write(&source, b"foreign-bytes").expect("foreign");
304        let mut bound = BoundSources::new();
305        bound.insert(MemberId::new("main").unwrap(), handle);
306        let prepared = plan.prepare_with_bound_sources(bound).unwrap();
307        assert_eq!(
308            fs::read(
309                prepared
310                    .staged_path(&MemberId::new("main").unwrap())
311                    .unwrap()
312            )
313            .unwrap(),
314            b"owned-bytes"
315        );
316        assert_eq!(fs::read(&source).unwrap(), b"foreign-bytes");
317    }
318
319    #[test]
320    fn bound_source_stages_open_object_after_root_rename() {
321        use std::fs::File;
322
323        let inputs = InstallationRoot::new().expect("inputs");
324        let install = InstallationRoot::new().expect("install");
325        let subdir = inputs.path().join("owned-dir");
326        fs::create_dir(&subdir).expect("mkdir");
327        let source = subdir.join("member");
328        fs::write(&source, b"owned-bytes").expect("write");
329        let handle = File::open(&source).expect("open");
330        let member =
331            ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
332                .unwrap();
333        let plan = InstallPlan::new(
334            ProductId::new("eggup").unwrap(),
335            ReleaseId::new("r1").unwrap(),
336            install.path(),
337            ArtifactSet::single(member).unwrap(),
338        )
339        .unwrap();
340        // Make the recorded advisory path stale from here on, while keeping the
341        // bound handle open. Staging must then resolve the owned object through
342        // the handle and never through the name.
343        //
344        // Unix renames the whole *directory*, the stronger case: the recorded
345        // prefix is stale too. Windows refuses to rename a directory that still
346        // contains an open handle, so there the file itself is renamed, which
347        // leaves the recorded path stale all the same. Both are possible only
348        // because `File::open` requests `FILE_SHARE_DELETE` on Windows.
349        #[cfg(unix)]
350        fs::rename(&subdir, inputs.path().join("renamed-dir")).expect("rename");
351        #[cfg(windows)]
352        fs::rename(&source, subdir.join("renamed-member")).expect("rename");
353        let mut bound = BoundSources::new();
354        bound.insert(MemberId::new("main").unwrap(), handle);
355        let prepared = plan.prepare_with_bound_sources(bound).unwrap();
356        assert_eq!(
357            fs::read(
358                prepared
359                    .staged_path(&MemberId::new("main").unwrap())
360                    .unwrap()
361            )
362            .unwrap(),
363            b"owned-bytes"
364        );
365    }
366
367    #[test]
368    fn bound_source_stages_full_bytes_from_nonzero_cursor() {
369        use std::fs::File;
370        use std::io::{Read, Seek, SeekFrom};
371
372        let inputs = InstallationRoot::new().expect("inputs");
373        let install = InstallationRoot::new().expect("install");
374        let source = inputs.write_file("owned", b"owned-bytes").expect("write");
375        let mut handle = File::open(&source).expect("open");
376        let mut prefix = [0u8; 5];
377        handle.read_exact(&mut prefix).expect("read");
378        assert_eq!(&prefix, b"owned");
379        handle.seek(SeekFrom::Start(2)).expect("seek");
380        let member =
381            ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
382                .unwrap();
383        let plan = InstallPlan::new(
384            ProductId::new("eggup").unwrap(),
385            ReleaseId::new("r1").unwrap(),
386            install.path(),
387            ArtifactSet::single(member).unwrap(),
388        )
389        .unwrap();
390        let mut bound = BoundSources::new();
391        bound.insert(MemberId::new("main").unwrap(), handle);
392        let prepared = plan.prepare_with_bound_sources(bound).unwrap();
393        assert_eq!(
394            fs::read(
395                prepared
396                    .staged_path(&MemberId::new("main").unwrap())
397                    .unwrap()
398            )
399            .unwrap(),
400            b"owned-bytes"
401        );
402    }
403
404    #[test]
405    fn bound_source_for_unknown_member_fails_closed() {
406        use std::fs::File;
407
408        let inputs = InstallationRoot::new().expect("inputs");
409        let install = InstallationRoot::new().expect("install");
410        let source = inputs.write_file("owned", b"owned-bytes").expect("write");
411        let handle = File::open(&source).expect("open");
412        let member =
413            ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
414                .unwrap();
415        let plan = InstallPlan::new(
416            ProductId::new("eggup").unwrap(),
417            ReleaseId::new("r1").unwrap(),
418            install.path(),
419            ArtifactSet::single(member).unwrap(),
420        )
421        .unwrap();
422        let mut bound = BoundSources::new();
423        bound.insert(MemberId::new("ghost").unwrap(), handle);
424        assert!(plan.prepare_with_bound_sources(bound).is_err());
425        assert!(!install.path().join("bin/main").exists());
426    }
427
428    #[cfg(unix)]
429    #[test]
430    fn rejects_symlink_sources_and_preserves_executable_intent_in_stage() {
431        use std::os::unix::fs::symlink;
432
433        let inputs = InstallationRoot::new().expect("inputs");
434        let install = InstallationRoot::new().expect("install");
435        inputs.write_file("real", b"real").expect("write");
436        symlink(inputs.path().join("real"), inputs.path().join("link")).expect("symlink");
437        let symlink_member = ArtifactMember::new(
438            MemberId::new("link").unwrap(),
439            inputs.path().join("link"),
440            "bin/link",
441        )
442        .unwrap();
443        assert!(InstallPlan::new(
444            ProductId::new("eggup").unwrap(),
445            ReleaseId::new("r1").unwrap(),
446            install.path(),
447            ArtifactSet::single(symlink_member).unwrap(),
448        )
449        .is_err());
450
451        let member = ArtifactMember::new(
452            MemberId::new("real").unwrap(),
453            inputs.path().join("real"),
454            "bin/real",
455        )
456        .unwrap()
457        .with_permissions(super::PermissionsIntent::Executable);
458        let prepared = InstallPlan::new(
459            ProductId::new("eggup").unwrap(),
460            ReleaseId::new("r1").unwrap(),
461            install.path(),
462            ArtifactSet::single(member).unwrap(),
463        )
464        .unwrap()
465        .prepare()
466        .unwrap();
467        #[cfg(unix)]
468        {
469            use std::os::unix::fs::PermissionsExt;
470            let mode = fs::metadata(
471                prepared
472                    .staged_path(&MemberId::new("real").unwrap())
473                    .unwrap(),
474            )
475            .unwrap()
476            .permissions()
477            .mode();
478            assert_ne!(mode & 0o111, 0);
479            assert_eq!(mode & 0o777, 0o700);
480        }
481    }
482
483    #[cfg(unix)]
484    #[test]
485    fn bound_source_preserves_executable_intent_that_the_fresh_stage_masks() {
486        use std::fs::File;
487        use std::os::unix::fs::PermissionsExt;
488
489        let inputs = InstallationRoot::new().expect("inputs");
490        let install = InstallationRoot::new().expect("install");
491        let source = inputs.write_file("owned", b"owned-bytes").expect("write");
492        fs::set_permissions(&source, fs::Permissions::from_mode(0o755)).expect("chmod source");
493        let handle = File::open(&source).expect("open");
494        let digest = super::hash_file(&source).unwrap();
495        fs::create_dir_all(install.path().join("bin")).expect("bin");
496        // `PermissionsIntent::Preserve` is the default set by `ArtifactMember::new`.
497        let member =
498            ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
499                .unwrap()
500                .with_integrity(super::IntegrityRequirement::Sha256(digest));
501        let plan = InstallPlan::new(
502            ProductId::new("eggup").unwrap(),
503            ReleaseId::new("r1").unwrap(),
504            install.path(),
505            ArtifactSet::single(member).unwrap(),
506        )
507        .unwrap();
508        let mut bound = BoundSources::new();
509        bound.insert(MemberId::new("main").unwrap(), handle);
510        let prepared = plan.prepare_with_bound_sources(bound).unwrap();
511
512        let staged = prepared
513            .staged_path(&MemberId::new("main").unwrap())
514            .unwrap();
515        let staged_mode = fs::metadata(&staged).unwrap().permissions().mode();
516        assert_ne!(
517            staged_mode & 0o111,
518            0,
519            "bound staging must not read the hardcoded 0600 back as source intent"
520        );
521        assert_eq!(staged_mode & 0o777, 0o700);
522
523        // Commit installs by rename, so the staged mode becomes the live mode.
524        let receipt =
525            commit_verified(prepared, &allow_create(), AbsentPolicy::AllowCreate).unwrap();
526        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
527        let live_mode = fs::metadata(install.path().join("bin/main"))
528            .unwrap()
529            .permissions()
530            .mode();
531        assert_ne!(
532            live_mode & 0o111,
533            0,
534            "installed executable must land executable"
535        );
536        assert_eq!(live_mode & 0o777, 0o700);
537    }
538
539    #[cfg(unix)]
540    #[test]
541    fn bound_source_does_not_widen_a_non_executable_source() {
542        use std::fs::File;
543        use std::os::unix::fs::PermissionsExt;
544
545        let inputs = InstallationRoot::new().expect("inputs");
546        let install = InstallationRoot::new().expect("install");
547        let source = inputs.write_file("owned", b"owned-bytes").expect("write");
548        fs::set_permissions(&source, fs::Permissions::from_mode(0o644)).expect("chmod source");
549        let handle = File::open(&source).expect("open");
550        let member =
551            ArtifactMember::new(MemberId::new("main").unwrap(), source.clone(), "bin/main")
552                .unwrap();
553        let plan = InstallPlan::new(
554            ProductId::new("eggup").unwrap(),
555            ReleaseId::new("r1").unwrap(),
556            install.path(),
557            ArtifactSet::single(member).unwrap(),
558        )
559        .unwrap();
560        let mut bound = BoundSources::new();
561        bound.insert(MemberId::new("main").unwrap(), handle);
562        let prepared = plan.prepare_with_bound_sources(bound).unwrap();
563        let staged_mode = fs::metadata(
564            prepared
565                .staged_path(&MemberId::new("main").unwrap())
566                .unwrap(),
567        )
568        .unwrap()
569        .permissions()
570        .mode();
571        assert_eq!(staged_mode & 0o777, 0o600);
572    }
573
574    #[test]
575    fn fault_category_is_structural_not_message_substring() {
576        // A legitimate caller-supplied invalid input that merely mentions the
577        // word must not be reported as the test-harness category.
578        let report = super::transaction::report_for_error(
579            super::FailurePhase::Ownership,
580            None,
581            &Error::invalid("member name 'injected' is reserved"),
582        );
583        assert_eq!(report.category(), super::FailureCategory::InvalidInput);
584
585        let injected = super::transaction::report_for_error(
586            super::FailurePhase::Ownership,
587            None,
588            &Error::injected("injected preparation failure at Copy"),
589        );
590        assert_eq!(injected.category(), super::FailureCategory::Injected);
591    }
592
593    fn prepared_bundle(
594        existing: bool,
595    ) -> (
596        InstallationRoot,
597        InstallationRoot,
598        super::PreparedTransaction,
599    ) {
600        let inputs = InstallationRoot::new().expect("inputs");
601        let install = InstallationRoot::new().expect("install");
602        let main_src = inputs.write_file("main", b"new-main").expect("write");
603        let helper_src = inputs.write_file("helper", b"new-helper").expect("write");
604        let main_digest = super::hash_file(&main_src).unwrap();
605        let helper_digest = super::hash_file(&helper_src).unwrap();
606        if existing {
607            install
608                .write_file("bin/main", b"old-main")
609                .expect("old main");
610            install
611                .write_file("bin/helper", b"old-helper")
612                .expect("old helper");
613        } else {
614            fs::create_dir_all(install.path().join("bin")).unwrap();
615        }
616        let members = ArtifactSet::new(vec![
617            ArtifactMember::new(
618                MemberId::new("main").unwrap(),
619                inputs.path().join("main"),
620                "bin/main",
621            )
622            .unwrap()
623            .with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
624            ArtifactMember::new(
625                MemberId::new("helper").unwrap(),
626                inputs.path().join("helper"),
627                "bin/helper",
628            )
629            .unwrap()
630            .with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
631        ])
632        .unwrap();
633        let prepared = InstallPlan::new(
634            ProductId::new("bundle").unwrap(),
635            ReleaseId::new("r1").unwrap(),
636            install.path(),
637            members,
638        )
639        .unwrap()
640        .prepare()
641        .unwrap();
642        (inputs, install, prepared)
643    }
644
645    #[test]
646    fn commits_a_complete_multi_member_generation_and_releases_lock() {
647        let (_inputs, install, prepared) = prepared_bundle(true);
648        let verifier = allow_create();
649
650        let receipt =
651            commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).expect("commit");
652
653        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
654        assert_eq!(receipt.cleanup(), CleanupDisposition::Cleaned);
655        assert!(receipt.failure().is_none());
656        assert_eq!(
657            fs::read(install.path().join("bin/main")).unwrap(),
658            b"new-main"
659        );
660        assert_eq!(
661            fs::read(install.path().join("bin/helper")).unwrap(),
662            b"new-helper"
663        );
664        assert!(!install.path().join(".eggup-mutation.lock").exists());
665        assert!(install
666            .path()
667            .read_dir()
668            .unwrap()
669            .filter_map(std::result::Result::ok)
670            .all(|entry| !entry
671                .file_name()
672                .to_string_lossy()
673                .starts_with(".eggup-backup-")));
674    }
675
676    #[test]
677    fn post_commit_check_sees_complete_generation_and_holds_mutation_lock() {
678        let (_inputs, install, prepared) = prepared_bundle(true);
679        let verifier = allow_create();
680        let validated = prepared
681            .verify_integrity()
682            .unwrap()
683            .validate(&AllValidators::new())
684            .unwrap();
685        let receipt = validated
686            .commit_with_post_commit(
687                CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
688                PostCommitFailurePolicy::RollBack,
689                || {
690                    assert_eq!(
691                        fs::read(install.path().join("bin/main")).unwrap(),
692                        b"new-main"
693                    );
694                    assert_eq!(
695                        fs::read(install.path().join("bin/helper")).unwrap(),
696                        b"new-helper"
697                    );
698                    assert!(MutationLock::acquire(
699                        install.path(),
700                        &ProductId::new("bundle").unwrap(),
701                        &ReleaseId::new("r1").unwrap(),
702                    )
703                    .is_err());
704                    Ok::<_, &str>(())
705                },
706            )
707            .unwrap();
708        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
709        assert!(receipt.post_commit_failure().is_none());
710    }
711
712    #[test]
713    fn post_commit_keep_installed_records_bounded_failure() {
714        let (_inputs, install, prepared) = prepared_bundle(true);
715        let verifier = allow_create();
716        let validated = prepared
717            .verify_integrity()
718            .unwrap()
719            .validate(&AllValidators::new())
720            .unwrap();
721        let receipt = validated
722            .commit_with_post_commit(
723                CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
724                PostCommitFailurePolicy::KeepInstalled,
725                || Err("é".repeat(400)),
726            )
727            .unwrap();
728        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
729        let failure = receipt.post_commit_failure().unwrap();
730        assert_eq!(failure.phase(), super::FailurePhase::PostCommit);
731        assert!(failure.detail().len() <= 512);
732        assert_eq!(
733            fs::read(install.path().join("bin/main")).unwrap(),
734            b"new-main"
735        );
736        assert_eq!(
737            fs::read(install.path().join("bin/helper")).unwrap(),
738            b"new-helper"
739        );
740    }
741
742    #[test]
743    fn post_commit_rollback_restores_old_generation_and_absent_members() {
744        let inputs = InstallationRoot::new().unwrap();
745        let install = InstallationRoot::new().unwrap();
746        let main = inputs.write_file("main", b"new-main").unwrap();
747        let helper = inputs.write_file("helper", b"new-helper").unwrap();
748        install.write_file("bin/main", b"old-main").unwrap();
749        let artifacts = ArtifactSet::new(vec![
750            ArtifactMember::new(MemberId::new("main").unwrap(), main, "bin/main")
751                .unwrap()
752                .with_integrity(super::IntegrityRequirement::Sha256(
753                    super::hash_file(&inputs.path().join("main")).unwrap(),
754                )),
755            ArtifactMember::new(MemberId::new("helper").unwrap(), helper, "bin/helper")
756                .unwrap()
757                .with_integrity(super::IntegrityRequirement::Sha256(
758                    super::hash_file(&inputs.path().join("helper")).unwrap(),
759                )),
760        ])
761        .unwrap();
762        let prepared = InstallPlan::new(
763            ProductId::new("bundle").unwrap(),
764            ReleaseId::new("r1").unwrap(),
765            install.path(),
766            artifacts,
767        )
768        .unwrap()
769        .prepare()
770        .unwrap();
771        let verifier = allow_create();
772        let receipt = prepared
773            .verify_integrity()
774            .unwrap()
775            .validate(&AllValidators::new())
776            .unwrap()
777            .commit_with_post_commit(
778                CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
779                PostCommitFailurePolicy::RollBack,
780                || Err("health check failed"),
781            )
782            .unwrap();
783        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
784        assert_eq!(
785            receipt.failure().unwrap().phase(),
786            super::FailurePhase::PostCommit
787        );
788        assert_eq!(
789            fs::read(install.path().join("bin/main")).unwrap(),
790            b"old-main"
791        );
792        assert!(!install.path().join("bin/helper").exists());
793    }
794
795    #[test]
796    fn post_commit_rollback_failure_retains_both_reports_and_evidence() {
797        let (_inputs, install, prepared) = prepared_bundle(true);
798        let verifier = allow_create();
799        let receipt = prepared
800            .verify_integrity()
801            .unwrap()
802            .validate(&AllValidators::new())
803            .unwrap()
804            .commit_with_post_commit_fault(
805                CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
806                PostCommitFailurePolicy::RollBack,
807                CommitFault::PostCommitRollback,
808            )
809            .unwrap();
810        assert_eq!(
811            receipt.disposition(),
812            TransactionDisposition::RecoveryRequired
813        );
814        assert_eq!(
815            receipt.post_commit_failure().unwrap().phase(),
816            super::FailurePhase::PostCommit
817        );
818        assert_eq!(
819            receipt.rollback_failure().unwrap().phase(),
820            super::FailurePhase::Rollback
821        );
822        assert!(receipt.recovery_path().unwrap().exists());
823        let recovery = receipt.recovery_path().unwrap().to_path_buf();
824        let _ = fs::remove_dir_all(recovery);
825        let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
826    }
827
828    #[test]
829    fn keep_installed_finalize_failure_preserves_both_failure_facts() {
830        let (_inputs, install, prepared) = prepared_bundle(true);
831        let verifier = allow_create();
832        let receipt = prepared
833            .verify_integrity()
834            .unwrap()
835            .validate(&AllValidators::new())
836            .unwrap()
837            .commit_with_post_commit_fault(
838                CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
839                PostCommitFailurePolicy::KeepInstalled,
840                CommitFault::Finalize,
841            )
842            .unwrap();
843        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
844        assert_eq!(
845            receipt.failure().unwrap().phase(),
846            super::FailurePhase::Finalize
847        );
848        assert_eq!(
849            receipt.post_commit_failure().unwrap().phase(),
850            super::FailurePhase::PostCommit
851        );
852        let recovery = receipt.recovery_path().unwrap().to_path_buf();
853        assert!(recovery.exists());
854        assert_eq!(
855            fs::read(install.path().join("bin/main")).unwrap(),
856            b"new-main"
857        );
858        let _ = fs::remove_dir_all(recovery);
859        let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
860    }
861
862    #[test]
863    fn post_commit_panic_is_reported_and_rolls_back() {
864        let (_inputs, install, prepared) = prepared_bundle(true);
865        let verifier = allow_create();
866        let receipt = prepared
867            .verify_integrity()
868            .unwrap()
869            .validate(&AllValidators::new())
870            .unwrap()
871            .commit_with_post_commit(
872                CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate),
873                PostCommitFailurePolicy::RollBack,
874                || -> Result<(), &str> { panic!("simulated check panic") },
875            )
876            .unwrap();
877        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
878        assert_eq!(
879            receipt.post_commit_failure().unwrap().detail(),
880            "post-commit check panicked"
881        );
882        assert_eq!(
883            fs::read(install.path().join("bin/main")).unwrap(),
884            b"old-main"
885        );
886    }
887
888    #[test]
889    fn partial_commit_failure_restores_every_old_member() {
890        let (_inputs, install, prepared) = prepared_bundle(true);
891        let verifier = allow_create();
892
893        let receipt = commit_verified_with_fault(
894            prepared,
895            &verifier,
896            AbsentPolicy::AllowCreate,
897            CommitFault::Commit(MemberId::new("helper").unwrap()),
898        )
899        .expect("receipt");
900
901        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
902        assert!(receipt.rollback_performed());
903        assert!(receipt.rollback_verified());
904        let failure = receipt.failure().expect("failure report");
905        assert_eq!(failure.phase(), super::FailurePhase::Commit);
906        assert_eq!(failure.member(), Some(&MemberId::new("helper").unwrap()));
907        assert_eq!(
908            fs::read(install.path().join("bin/main")).unwrap(),
909            b"old-main"
910        );
911        assert_eq!(
912            fs::read(install.path().join("bin/helper")).unwrap(),
913            b"old-helper"
914        );
915    }
916
917    #[test]
918    fn rollback_removes_new_members_that_were_absent_before_commit() {
919        let inputs = InstallationRoot::new().unwrap();
920        let install = InstallationRoot::new().unwrap();
921        let main_src = inputs.write_file("main", b"new-main").unwrap();
922        let helper_src = inputs.write_file("helper", b"new-helper").unwrap();
923        let main_digest = super::hash_file(&main_src).unwrap();
924        let helper_digest = super::hash_file(&helper_src).unwrap();
925        install.write_file("bin/main", b"old-main").unwrap();
926        let prepared = InstallPlan::new(
927            ProductId::new("bundle").unwrap(),
928            ReleaseId::new("r1").unwrap(),
929            install.path(),
930            ArtifactSet::new(vec![
931                ArtifactMember::new(
932                    MemberId::new("main").unwrap(),
933                    inputs.path().join("main"),
934                    "bin/main",
935                )
936                .unwrap()
937                .with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
938                ArtifactMember::new(
939                    MemberId::new("helper").unwrap(),
940                    inputs.path().join("helper"),
941                    "bin/helper",
942                )
943                .unwrap()
944                .with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
945            ])
946            .unwrap(),
947        )
948        .unwrap()
949        .prepare()
950        .unwrap();
951        let verifier = allow_create();
952
953        let receipt = commit_verified_with_fault(
954            prepared,
955            &verifier,
956            AbsentPolicy::AllowCreate,
957            CommitFault::Commit(MemberId::new("helper").unwrap()),
958        )
959        .unwrap();
960
961        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
962        assert_eq!(
963            fs::read(install.path().join("bin/main")).unwrap(),
964            b"old-main"
965        );
966        assert!(!install.path().join("bin/helper").exists());
967    }
968
969    #[test]
970    fn lock_contention_and_malformed_lock_fail_closed() {
971        let (_inputs, install, prepared) = prepared_bundle(false);
972        let product = ProductId::new("bundle").unwrap();
973        let release = ReleaseId::new("r1").unwrap();
974        let _lock = MutationLock::acquire(install.path(), &product, &release).unwrap();
975        let verifier = allow_create();
976
977        let error = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap_err();
978        assert!(matches!(error, Error::UpdateInProgress { .. }));
979        drop(_lock);
980
981        let (_inputs, install2, prepared) = prepared_bundle(false);
982        let lock = install2.path().join(".eggup-mutation.lock");
983        fs::write(&lock, b"malformed").unwrap();
984        let error = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap_err();
985        assert!(matches!(error, Error::UpdateInProgress { .. }));
986        assert!(lock.exists());
987    }
988
989    #[test]
990    fn rollback_failure_returns_recovery_required_and_retains_evidence() {
991        let (_inputs, install, prepared) = prepared_bundle(true);
992        let verifier = allow_create();
993
994        let receipt = commit_verified_with_fault(
995            prepared,
996            &verifier,
997            AbsentPolicy::AllowCreate,
998            CommitFault::CommitThenRollback(
999                MemberId::new("helper").unwrap(),
1000                MemberId::new("main").unwrap(),
1001            ),
1002        )
1003        .unwrap();
1004
1005        assert_eq!(
1006            receipt.disposition(),
1007            TransactionDisposition::RecoveryRequired
1008        );
1009        assert!(!receipt.rollback_verified());
1010        assert!(receipt.recovery_path().is_some());
1011        assert!(receipt.failure().is_some());
1012        assert!(receipt.rollback_failure().is_some());
1013        assert_eq!(
1014            receipt.failure().unwrap().phase(),
1015            super::FailurePhase::Commit
1016        );
1017        assert_eq!(
1018            receipt.rollback_failure().unwrap().phase(),
1019            super::FailurePhase::Rollback
1020        );
1021        assert!(install.path().join(".eggup-mutation.lock").exists());
1022        let recovery = receipt.recovery_path().unwrap();
1023        assert!(recovery.exists());
1024        let _ = fs::remove_dir_all(recovery);
1025        let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
1026    }
1027
1028    /// A self-update whose rollback cannot be completed reports
1029    /// `RecoveryRequired` and retains the real old-image path, rather than
1030    /// claiming a rollback it did not perform.
1031    #[test]
1032    fn self_update_rollback_failure_retains_the_real_old_image() {
1033        use super::{hash_file, CurrentExecutable, IntegrityRequirement, StagePlacement};
1034
1035        let inputs = InstallationRoot::new().expect("inputs");
1036        let install = InstallationRoot::new().expect("install");
1037        let installed = install.path().join("app");
1038        fs::write(&installed, b"old-generation").expect("install old");
1039        let candidate = inputs
1040            .write_file("candidate", b"new-generation")
1041            .expect("candidate");
1042
1043        let current = CurrentExecutable::bind(&installed).expect("bind current executable");
1044        let digest = hash_file(&candidate).expect("hash candidate");
1045        let plan = InstallPlan::for_current_executable(
1046            ProductId::new("app").unwrap(),
1047            ReleaseId::new("r1").unwrap(),
1048            &candidate,
1049            &current,
1050            IntegrityRequirement::Sha256(digest),
1051        )
1052        .expect("plan");
1053        assert_eq!(
1054            plan.stage_placement(),
1055            StagePlacement::InsideInstallationRoot
1056        );
1057
1058        let prepared = plan.prepare().expect("prepare");
1059        let receipt = prepared
1060            .verify_integrity()
1061            .expect("verify")
1062            .validate(&AllValidators::new())
1063            .expect("validate")
1064            .commit_with_post_commit_fault(
1065                CommitOwnership::new(&FixedVerifier(Ownership::Owned), AbsentPolicy::DenyCreate),
1066                PostCommitFailurePolicy::RollBack,
1067                CommitFault::PostCommitRollback,
1068            )
1069            .expect("receipt");
1070
1071        assert_eq!(
1072            receipt.disposition(),
1073            TransactionDisposition::RecoveryRequired,
1074            "an incomplete rollback is never reported as RolledBack"
1075        );
1076        assert!(!receipt.rollback_verified());
1077        assert!(receipt.rollback_failure().is_some());
1078        let recovery = receipt
1079            .recovery_path()
1080            .expect("the real retained path must be reported");
1081        assert!(
1082            recovery.exists(),
1083            "reported recovery path must really exist"
1084        );
1085        assert!(
1086            recovery.starts_with(fs::canonicalize(install.path()).expect("canonical install root")),
1087            "recovery evidence stays inside the executable's own directory: {}",
1088            recovery.display()
1089        );
1090        let _ = fs::remove_dir_all(recovery);
1091        let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
1092    }
1093
1094    #[test]
1095    fn precommit_and_backup_failures_restore_old_state() {
1096        let (_inputs, install, prepared) = prepared_bundle(true);
1097        let verifier = allow_create();
1098        let receipt = commit_verified_with_fault(
1099            prepared,
1100            &verifier,
1101            AbsentPolicy::AllowCreate,
1102            CommitFault::BeforeFirstCommit,
1103        )
1104        .unwrap();
1105        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1106        assert!(receipt.failure().is_some());
1107        assert_eq!(
1108            fs::read(install.path().join("bin/main")).unwrap(),
1109            b"old-main"
1110        );
1111
1112        let (_inputs, install, prepared) = prepared_bundle(true);
1113        let receipt = commit_verified_with_fault(
1114            prepared,
1115            &verifier,
1116            AbsentPolicy::AllowCreate,
1117            CommitFault::Backup(MemberId::new("helper").unwrap()),
1118        )
1119        .unwrap();
1120        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1121        assert_eq!(
1122            receipt.failure().unwrap().phase(),
1123            super::FailurePhase::Backup
1124        );
1125        assert_eq!(
1126            fs::read(install.path().join("bin/helper")).unwrap(),
1127            b"old-helper"
1128        );
1129    }
1130
1131    #[test]
1132    fn injected_lock_creation_failure_performs_no_mutation() {
1133        let (_inputs, install, prepared) = prepared_bundle(true);
1134        let verifier = allow_create();
1135
1136        let error = commit_verified_with_fault(
1137            prepared,
1138            &verifier,
1139            AbsentPolicy::AllowCreate,
1140            CommitFault::LockCreation,
1141        )
1142        .unwrap_err();
1143
1144        assert!(matches!(error, Error::Injected(_)));
1145        assert_eq!(
1146            fs::read(install.path().join("bin/main")).unwrap(),
1147            b"old-main"
1148        );
1149        assert!(!install.path().join(".eggup-mutation.lock").exists());
1150    }
1151
1152    #[cfg(unix)]
1153    #[test]
1154    fn destination_links_fail_immediately_before_backup() {
1155        use std::fs::hard_link;
1156        use std::os::unix::fs::symlink;
1157
1158        let inputs = InstallationRoot::new().unwrap();
1159        let install = InstallationRoot::new().unwrap();
1160        let src = inputs.write_file("main", b"new-main").unwrap();
1161        let digest = super::hash_file(&src).unwrap();
1162        install.write_file("bin/real", b"old-main").unwrap();
1163        symlink(install.path().join("real"), install.path().join("bin/main")).unwrap();
1164        let prepared = InstallPlan::new(
1165            ProductId::new("bundle").unwrap(),
1166            ReleaseId::new("r1").unwrap(),
1167            install.path(),
1168            ArtifactSet::single(
1169                ArtifactMember::new(
1170                    MemberId::new("main").unwrap(),
1171                    inputs.path().join("main"),
1172                    "bin/main",
1173                )
1174                .unwrap()
1175                .with_integrity(super::IntegrityRequirement::Sha256(digest)),
1176            )
1177            .unwrap(),
1178        )
1179        .unwrap()
1180        .prepare()
1181        .unwrap();
1182        let verifier = allow_create();
1183        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1184        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1185        assert_eq!(
1186            receipt.failure().unwrap().phase(),
1187            super::FailurePhase::Ownership
1188        );
1189        assert_eq!(
1190            fs::read(install.path().join("bin/real")).unwrap(),
1191            b"old-main"
1192        );
1193
1194        let (_inputs, install, prepared) = prepared_bundle(true);
1195        hard_link(
1196            install.path().join("bin/main"),
1197            install.path().join("bin/linked"),
1198        )
1199        .unwrap();
1200        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1201        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1202        assert_eq!(
1203            fs::read(install.path().join("bin/main")).unwrap(),
1204            b"old-main"
1205        );
1206    }
1207
1208    #[test]
1209    fn hashes_known_bytes_and_parses_strict_sidecars() {
1210        let fixture = InstallationRoot::new().unwrap();
1211        let path = fixture.write_file("abc", b"abc").unwrap();
1212        let manifest = super::parse_sha256_sidecar(
1213            "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD  abc\n",
1214        )
1215        .unwrap();
1216        assert_eq!(manifest.filename(), Some("abc"));
1217        assert_eq!(
1218            super::verify_file(&path, &manifest).unwrap(),
1219            *manifest.digest()
1220        );
1221        assert!(super::parse_sha256_sidecar("abc\ndef").is_err());
1222        assert!(super::parse_sha256_sidecar(
1223            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad  other"
1224        )
1225        .and_then(|manifest| super::verify_file(&path, &manifest))
1226        .is_err());
1227        assert!(super::parse_sha256_sidecar("not-a-digest  abc").is_err());
1228    }
1229
1230    #[test]
1231    fn integrity_verification_precedes_candidate_validation() {
1232        let inputs = InstallationRoot::new().unwrap();
1233        let install = InstallationRoot::new().unwrap();
1234        let source = inputs.write_file("main", b"verified bytes").unwrap();
1235        let digest = super::hash_file(&source).unwrap();
1236        let member = ArtifactMember::new(MemberId::new("main").unwrap(), source, "bin/main")
1237            .unwrap()
1238            .with_integrity(super::IntegrityRequirement::Sha256(digest));
1239        let verified = InstallPlan::new(
1240            ProductId::new("eggup").unwrap(),
1241            ReleaseId::new("r1").unwrap(),
1242            install.path(),
1243            ArtifactSet::single(member).unwrap(),
1244        )
1245        .unwrap()
1246        .prepare()
1247        .unwrap()
1248        .verify_integrity()
1249        .unwrap();
1250        assert_eq!(
1251            verified
1252                .integrity(&MemberId::new("main").unwrap())
1253                .unwrap()
1254                .status(),
1255            super::IntegrityStatus::Verified
1256        );
1257
1258        let bad_source = inputs.write_file("bad", b"bad bytes").unwrap();
1259        let bad_member = ArtifactMember::new(MemberId::new("bad").unwrap(), bad_source, "bin/bad")
1260            .unwrap()
1261            .with_integrity(super::IntegrityRequirement::Sha256(digest));
1262        assert!(matches!(
1263            InstallPlan::new(
1264                ProductId::new("eggup").unwrap(),
1265                ReleaseId::new("r1").unwrap(),
1266                install.path(),
1267                ArtifactSet::single(bad_member).unwrap(),
1268            )
1269            .unwrap()
1270            .prepare()
1271            .unwrap()
1272            .verify_integrity(),
1273            Err(Error::VerificationFailed(_))
1274        ));
1275
1276        let no_requirement = ArtifactMember::new(
1277            MemberId::new("none").unwrap(),
1278            inputs.write_file("none", b"no declaration").unwrap(),
1279            "bin/none",
1280        )
1281        .unwrap();
1282        let verified = InstallPlan::new(
1283            ProductId::new("eggup").unwrap(),
1284            ReleaseId::new("r1").unwrap(),
1285            install.path(),
1286            ArtifactSet::single(no_requirement).unwrap(),
1287        )
1288        .unwrap()
1289        .prepare()
1290        .unwrap()
1291        .verify_integrity()
1292        .unwrap();
1293        let validator = ExactIdentityValidator::new(MemberId::new("none").unwrap(), "anything");
1294        assert!(matches!(
1295            verified.validate(&validator),
1296            Err(Error::VerificationFailed(_))
1297        ));
1298    }
1299
1300    #[cfg(unix)]
1301    #[test]
1302    fn bounded_candidates_are_exact_and_environment_is_cleared() {
1303        use std::os::unix::fs::PermissionsExt;
1304
1305        let inputs = InstallationRoot::new().unwrap();
1306        let install = InstallationRoot::new().unwrap();
1307        let script = inputs
1308            .write_file("main", b"#!/bin/sh\nprintf 'eggup 1.2.3\\n'\n")
1309            .unwrap();
1310        fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
1311        let digest = super::hash_file(&script).unwrap();
1312        let member = ArtifactMember::new(MemberId::new("main").unwrap(), script, "bin/main")
1313            .unwrap()
1314            .with_integrity(super::IntegrityRequirement::Sha256(digest));
1315        fs::create_dir_all(install.path().join("bin")).unwrap();
1316        let verified = InstallPlan::new(
1317            ProductId::new("eggup").unwrap(),
1318            ReleaseId::new("r1").unwrap(),
1319            install.path(),
1320            ArtifactSet::single(member).unwrap(),
1321        )
1322        .unwrap()
1323        .prepare()
1324        .unwrap()
1325        .verify_integrity()
1326        .unwrap();
1327        let validator =
1328            ExactIdentityValidator::new(MemberId::new("main").unwrap(), "eggup 1.2.3\n");
1329        let validated = verified.validate(&validator).unwrap();
1330        let verifier = allow_create();
1331        assert_eq!(
1332            validated
1333                .commit(CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate))
1334                .unwrap()
1335                .disposition(),
1336            TransactionDisposition::Committed
1337        );
1338
1339        let secret_script = inputs
1340            .write_file(
1341                "secret",
1342                b"#!/bin/sh\nprintf '%s' \"${EGGUP_SECRET-unset}\"\n",
1343            )
1344            .unwrap();
1345        fs::set_permissions(&secret_script, fs::Permissions::from_mode(0o755)).unwrap();
1346        let output = super::run_bounded(&super::CommandSpec::new(secret_script)).unwrap();
1347        assert!(output.success(), "bounded candidate output: {output:?}");
1348        assert_eq!(output.stdout(), b"unset");
1349    }
1350
1351    #[cfg(unix)]
1352    #[test]
1353    fn bounded_runner_kills_timeouts_and_limits_output() {
1354        use std::os::unix::fs::PermissionsExt;
1355
1356        let fixture = InstallationRoot::new().unwrap();
1357        let timeout_script = fixture
1358            .write_file("timeout", b"#!/bin/sh\nsleep 2\n")
1359            .unwrap();
1360        let noisy_script = fixture
1361            .write_file("noisy", b"#!/bin/sh\nprintf '1234567890'\n")
1362            .unwrap();
1363        for path in [&timeout_script, &noisy_script] {
1364            fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
1365        }
1366        let timed_out = super::run_bounded(
1367            &super::CommandSpec::new(timeout_script).timeout(Duration::from_millis(20)),
1368        )
1369        .unwrap();
1370        assert!(timed_out.timed_out());
1371        assert!(!timed_out.success());
1372        let limited =
1373            super::run_bounded(&super::CommandSpec::new(noisy_script).max_output_bytes(4)).unwrap();
1374        assert!(limited.output_limited());
1375        assert!(!limited.success());
1376    }
1377
1378    #[cfg(unix)]
1379    #[test]
1380    fn cross_member_identity_requires_bundle_agreement() {
1381        use std::os::unix::fs::PermissionsExt;
1382
1383        let inputs = InstallationRoot::new().unwrap();
1384        let install = InstallationRoot::new().unwrap();
1385        let main = inputs
1386            .write_file("main", b"#!/bin/sh\nprintf 'bundle 9\\n'\n")
1387            .unwrap();
1388        let helper = inputs
1389            .write_file("helper", b"#!/bin/sh\nprintf 'bundle 9\\n'\n")
1390            .unwrap();
1391        for path in [&main, &helper] {
1392            fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
1393        }
1394        let main_digest = super::hash_file(&main).unwrap();
1395        let helper_digest = super::hash_file(&helper).unwrap();
1396        let verified = InstallPlan::new(
1397            ProductId::new("bundle").unwrap(),
1398            ReleaseId::new("r9").unwrap(),
1399            install.path(),
1400            ArtifactSet::new(vec![
1401                ArtifactMember::new(MemberId::new("main").unwrap(), main, "bin/main")
1402                    .unwrap()
1403                    .with_integrity(super::IntegrityRequirement::Sha256(main_digest)),
1404                ArtifactMember::new(MemberId::new("helper").unwrap(), helper, "bin/helper")
1405                    .unwrap()
1406                    .with_integrity(super::IntegrityRequirement::Sha256(helper_digest)),
1407            ])
1408            .unwrap(),
1409        )
1410        .unwrap()
1411        .prepare()
1412        .unwrap()
1413        .verify_integrity()
1414        .unwrap();
1415        let validator = super::CrossMemberAgreementValidator::new(
1416            [
1417                MemberId::new("main").unwrap(),
1418                MemberId::new("helper").unwrap(),
1419            ],
1420            "bundle 9\n",
1421        );
1422        assert!(verified.validate(&validator).is_ok());
1423    }
1424
1425    // ---- M005 corrective coverage ----
1426
1427    fn single_verified(
1428        new_bytes: &[u8],
1429        old_bytes: Option<&[u8]>,
1430        with_bin_dir: bool,
1431    ) -> (
1432        InstallationRoot,
1433        InstallationRoot,
1434        super::PreparedTransaction,
1435        [u8; 32],
1436    ) {
1437        let inputs = InstallationRoot::new().unwrap();
1438        let install = InstallationRoot::new().unwrap();
1439        let src = inputs.write_file("main", new_bytes).unwrap();
1440        let digest = super::hash_file(&src).unwrap();
1441        if let Some(old) = old_bytes {
1442            install.write_file("bin/main", old).unwrap();
1443        } else if with_bin_dir {
1444            fs::create_dir_all(install.path().join("bin")).unwrap();
1445        }
1446        let member = ArtifactMember::new(MemberId::new("main").unwrap(), src, "bin/main")
1447            .unwrap()
1448            .with_integrity(super::IntegrityRequirement::Sha256(digest));
1449        let prepared = InstallPlan::new(
1450            ProductId::new("p").unwrap(),
1451            ReleaseId::new("r1").unwrap(),
1452            install.path(),
1453            ArtifactSet::single(member).unwrap(),
1454        )
1455        .unwrap()
1456        .prepare()
1457        .unwrap();
1458        (inputs, install, prepared, digest)
1459    }
1460
1461    #[test]
1462    fn ownership_owned_allows_replacement_and_reports_no_failure() {
1463        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1464        let old_digest = super::hash_file(&install.path().join("bin/main")).unwrap();
1465        let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main").unwrap(), old_digest)]);
1466        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
1467        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1468        assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"new");
1469    }
1470
1471    #[test]
1472    fn ownership_foreign_and_unknown_fail_closed_with_zero_mutation() {
1473        for fixed in [Ownership::Foreign, Ownership::Unknown] {
1474            let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1475            let verifier = FixedVerifier(fixed);
1476            let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1477            assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1478            assert_eq!(
1479                receipt.failure().unwrap().phase(),
1480                super::FailurePhase::Ownership
1481            );
1482            assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
1483        }
1484    }
1485
1486    #[test]
1487    fn ownership_absent_create_policy_is_enforced() {
1488        let (_i, install, prepared, _) = single_verified(b"new", None, true);
1489        let verifier = AbsentOnlyVerifier;
1490        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1491        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1492        assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"new");
1493
1494        let (_i, install, prepared, _) = single_verified(b"new", None, true);
1495        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
1496        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1497        assert!(!install.path().join("bin/main").exists());
1498    }
1499
1500    #[test]
1501    fn ownership_flapping_between_preflight_and_lock_fails() {
1502        use std::cell::Cell;
1503        #[derive(Debug)]
1504        struct Flap {
1505            first: Ownership,
1506            second: Ownership,
1507            calls: Cell<usize>,
1508        }
1509        impl OwnershipVerifier for Flap {
1510            fn verify(&self, _m: &MemberId, _d: &Path) -> Ownership {
1511                let n = self.calls.get();
1512                self.calls.set(n + 1);
1513                if n == 0 {
1514                    self.first
1515                } else {
1516                    self.second
1517                }
1518            }
1519        }
1520        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1521        let verifier = Flap {
1522            first: Ownership::Owned,
1523            second: Ownership::Foreign,
1524            calls: Cell::new(0),
1525        };
1526        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1527        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1528        assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
1529    }
1530
1531    #[test]
1532    fn missing_parent_fails_without_creating_directories() {
1533        let (_i, install, prepared, _) = single_verified(b"new", None, false);
1534        assert!(!install.path().join("bin").exists());
1535        let verifier = allow_create();
1536        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1537        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1538        assert!(!install.path().join("bin").exists());
1539        assert!(!install.path().join("bin/main").exists());
1540    }
1541
1542    #[cfg(unix)]
1543    #[test]
1544    fn symlinked_parent_fails_closed() {
1545        use std::os::unix::fs::symlink;
1546        let (_i, install, prepared, _) = single_verified(b"new", None, false);
1547        fs::create_dir_all(install.path().join("real-bin")).unwrap();
1548        symlink(install.path().join("real-bin"), install.path().join("bin")).unwrap();
1549        // Re-prepare after the symlink exists would still stage fine; commit must fail.
1550        let verifier = allow_create();
1551        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1552        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1553    }
1554
1555    #[test]
1556    fn staged_mutation_after_validation_fails_before_live_mutation() {
1557        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1558        let verified = prepared.verify_integrity().unwrap();
1559        let validated = verified.validate(&AllValidators::new()).unwrap();
1560        // Mutate the staged copy after validation.
1561        let staged = validated
1562            .staged_path(&MemberId::new("main").unwrap())
1563            .unwrap();
1564        fs::write(&staged, b"tampered").unwrap();
1565        let verifier = allow_create();
1566        let receipt = validated
1567            .commit(CommitOwnership::new(&verifier, AbsentPolicy::AllowCreate))
1568            .unwrap();
1569        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1570        assert_eq!(
1571            receipt.failure().unwrap().phase(),
1572            super::FailurePhase::StageRevalidation
1573        );
1574        assert_eq!(fs::read(install.path().join("bin/main")).unwrap(), b"old");
1575    }
1576
1577    #[test]
1578    fn unverified_integrity_none_cannot_commit() {
1579        let inputs = InstallationRoot::new().unwrap();
1580        let install = InstallationRoot::new().unwrap();
1581        fs::create_dir_all(install.path().join("bin")).unwrap();
1582        let src = inputs.write_file("main", b"bytes").unwrap();
1583        let member = ArtifactMember::new(MemberId::new("main").unwrap(), src, "bin/main").unwrap();
1584        let prepared = InstallPlan::new(
1585            ProductId::new("p").unwrap(),
1586            ReleaseId::new("r1").unwrap(),
1587            install.path(),
1588            ArtifactSet::single(member).unwrap(),
1589        )
1590        .unwrap()
1591        .prepare()
1592        .unwrap();
1593        let verified = prepared.verify_integrity().unwrap();
1594        assert!(verified.validate(&AllValidators::new()).is_err());
1595    }
1596
1597    #[test]
1598    fn cleanup_failure_reports_real_backup_root() {
1599        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1600        let verifier = allow_create();
1601        // Finalize fault retains the real backup root and reports finalize phase.
1602        let receipt = commit_verified_with_fault(
1603            prepared,
1604            &verifier,
1605            AbsentPolicy::AllowCreate,
1606            CommitFault::Finalize,
1607        )
1608        .unwrap();
1609        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1610        assert_eq!(receipt.cleanup(), CleanupDisposition::RetainedForRecovery);
1611        let path = receipt.recovery_path().unwrap();
1612        assert!(path.exists());
1613        assert!(!path.to_string_lossy().contains("cleanup-failed"));
1614        assert_eq!(
1615            receipt.failure().unwrap().phase(),
1616            super::FailurePhase::Finalize
1617        );
1618        let _ = fs::remove_dir_all(path);
1619        let _ = fs::remove_file(install.path().join(".eggup-mutation.lock"));
1620    }
1621
1622    #[cfg(unix)]
1623    #[test]
1624    fn transaction_owned_state_is_owner_private() {
1625        use std::os::unix::fs::PermissionsExt;
1626        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1627        let stage_mode = fs::metadata(prepared.stage_root())
1628            .unwrap()
1629            .permissions()
1630            .mode()
1631            & 0o777;
1632        assert_eq!(stage_mode, 0o700);
1633        let staged = prepared
1634            .staged_path(&MemberId::new("main").unwrap())
1635            .unwrap();
1636        let file_mode = fs::metadata(&staged).unwrap().permissions().mode() & 0o777;
1637        assert_eq!(file_mode, 0o600);
1638        let verifier = allow_create();
1639        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1640        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1641        // Lock file, when preserved by a finalize fault, must be 0600.
1642        let (_i2, install2, prepared2, _) = single_verified(b"n2", Some(b"o2"), false);
1643        let receipt = commit_verified_with_fault(
1644            prepared2,
1645            &verifier,
1646            AbsentPolicy::AllowCreate,
1647            CommitFault::Finalize,
1648        )
1649        .unwrap();
1650        let lock_mode = fs::metadata(install2.path().join(".eggup-mutation.lock"))
1651            .unwrap()
1652            .permissions()
1653            .mode()
1654            & 0o777;
1655        assert_eq!(lock_mode, 0o600);
1656        let _ = fs::remove_dir_all(receipt.recovery_path().unwrap());
1657        let _ = fs::remove_file(install2.path().join(".eggup-mutation.lock"));
1658        let _ = install;
1659    }
1660
1661    #[test]
1662    fn lock_inspect_never_deletes_and_reports_status() {
1663        let install = InstallationRoot::new().unwrap();
1664        assert_eq!(
1665            MutationLock::inspect(install.path()).unwrap(),
1666            super::LockStatus::Available
1667        );
1668        let product = ProductId::new("p").unwrap();
1669        let release = ReleaseId::new("r").unwrap();
1670        let lock = MutationLock::acquire(install.path(), &product, &release).unwrap();
1671        match MutationLock::inspect(install.path()).unwrap() {
1672            super::LockStatus::Held { contents, .. } => assert!(contents.is_some()),
1673            other => panic!("expected held, got {other:?}"),
1674        }
1675        drop(lock);
1676        // After drop with cleanup, available again.
1677        assert_eq!(
1678            MutationLock::inspect(install.path()).unwrap(),
1679            super::LockStatus::Available
1680        );
1681        fs::write(install.path().join(".eggup-mutation.lock"), b"malformed").unwrap();
1682        match MutationLock::inspect(install.path()).unwrap() {
1683            super::LockStatus::Held { .. } | super::LockStatus::Malformed { .. } => {}
1684            other => panic!("expected held/malformed, got {other:?}"),
1685        }
1686        // Inspect must never delete.
1687        assert!(install.path().join(".eggup-mutation.lock").exists());
1688    }
1689
1690    #[test]
1691    fn oversized_lock_record_is_malformed_not_deleted() {
1692        let install = InstallationRoot::new().unwrap();
1693        let big = vec![b'x'; 8192];
1694        fs::write(install.path().join(".eggup-mutation.lock"), &big).unwrap();
1695        match MutationLock::inspect(install.path()).unwrap() {
1696            super::LockStatus::Malformed { .. } => {}
1697            other => panic!("expected malformed, got {other:?}"),
1698        }
1699        assert!(install.path().join(".eggup-mutation.lock").exists());
1700    }
1701
1702    #[test]
1703    fn exact_digest_verifier_proves_ownership() {
1704        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1705        let live_digest = super::hash_file(&install.path().join("bin/main")).unwrap();
1706        let verifier =
1707            ExactDigestVerifier::new(vec![(MemberId::new("main").unwrap(), live_digest)]);
1708        assert_eq!(
1709            verifier.verify(
1710                &MemberId::new("main").unwrap(),
1711                &install.path().join("bin/main")
1712            ),
1713            Ownership::Owned
1714        );
1715        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::DenyCreate).unwrap();
1716        assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
1717    }
1718
1719    #[test]
1720    fn absent_only_verifier_never_authorizes_replacement() {
1721        let (_i, install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1722        let verifier = AbsentOnlyVerifier;
1723        assert_eq!(
1724            verifier.verify(
1725                &MemberId::new("main").unwrap(),
1726                &install.path().join("bin/main")
1727            ),
1728            Ownership::Foreign
1729        );
1730        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1731        assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
1732    }
1733
1734    #[test]
1735    fn failure_reports_carry_phase_category_and_member() {
1736        let (_i, _install, prepared, _) = single_verified(b"new", Some(b"old"), false);
1737        let verifier = FixedVerifier(Ownership::Foreign);
1738        let receipt = commit_verified(prepared, &verifier, AbsentPolicy::AllowCreate).unwrap();
1739        let failure = receipt.failure().unwrap();
1740        assert_eq!(failure.phase(), super::FailurePhase::Ownership);
1741        assert!(!failure.detail().is_empty());
1742        assert!(failure.detail().len() <= 512);
1743    }
1744
1745    #[test]
1746    fn checksum_only_state_is_explicit_in_docs() {
1747        // Authenticity support does not exist: every commit requires a verified
1748        // SHA-256 digest, and there is no public authenticity type. This test
1749        // guards the API surface by asserting the commit path rejects missing
1750        // integrity evidence (see unverified_integrity_none_cannot_commit) and
1751        // documents the checksum-only contract here.
1752        let desc = env!("CARGO_PKG_DESCRIPTION");
1753        let _ = desc;
1754    }
1755
1756    #[allow(dead_code)]
1757    fn _unused() {
1758        let _m: HashMap<MemberId, [u8; 32]> = HashMap::new();
1759    }
1760
1761    /// A competing writer that takes the lock *after* our claim wins, and its
1762    /// record is never removed.
1763    ///
1764    /// The window between the claim and the create-new retry is microseconds
1765    /// wide, so this uses the `cfg(test)` hook in `lock` to inject the competing
1766    /// write deterministically. This is the branch the whole claim protocol
1767    /// exists to survive; without the hook it could only be reached by a real
1768    /// multi-process race, which would prove nothing about ordering.
1769    #[test]
1770    fn competing_writer_after_the_claim_is_preserved() {
1771        use crate::lock::POST_CLAIM_HOOK;
1772
1773        let root = InstallationRoot::new().expect("root");
1774        let lock_path = root.path().join(".eggup-mutation.lock");
1775        fs::write(&lock_path, "pid=1 nonce=1 product=a release=b\n").expect("plant");
1776        let competing = "pid=2 nonce=2 product=a release=b\n".to_string();
1777
1778        struct Proven;
1779        impl std::fmt::Debug for Proven {
1780            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1781                f.write_str("Proven")
1782            }
1783        }
1784        impl crate::lock::StaleLockVerifier for Proven {
1785            fn classify(
1786                &self,
1787                _observed: &crate::lock::LockObservation,
1788            ) -> crate::lock::StaleLockDecision {
1789                crate::lock::StaleLockDecision::ProvenStale
1790            }
1791        }
1792
1793        let hook_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1794        let hook_ran_in_hook = hook_ran.clone();
1795        *POST_CLAIM_HOOK.lock().expect("hook slot") = Some(Box::new(move || {
1796            fs::write(&lock_path, &competing).expect("competing writer");
1797            hook_ran_in_hook.store(true, std::sync::atomic::Ordering::SeqCst);
1798        }));
1799
1800        let error = MutationLock::acquire_with_recovery(
1801            root.path(),
1802            &ProductId::new("a").unwrap(),
1803            &ReleaseId::new("b").unwrap(),
1804            &Proven,
1805        )
1806        .expect_err("the competing writer owns the domain");
1807
1808        assert!(
1809            matches!(
1810                error,
1811                crate::error::RecoveryError::Core(Error::UpdateInProgress { .. })
1812            ),
1813            "expected typed contention, got {error:?}"
1814        );
1815        assert!(
1816            hook_ran.load(std::sync::atomic::Ordering::SeqCst),
1817            "the injected competing writer must actually have run, otherwise this \
1818             test is asserting the pre-claim rejection path and proves nothing \
1819             about the post-claim branch"
1820        );
1821        assert_eq!(
1822            fs::read_to_string(root.path().join(".eggup-mutation.lock")).expect("read"),
1823            "pid=2 nonce=2 product=a release=b\n",
1824            "the winning writer's record must survive byte-identical"
1825        );
1826        let leftovers: Vec<_> = fs::read_dir(root.path())
1827            .expect("read dir")
1828            .flatten()
1829            .map(|entry| entry.file_name().to_string_lossy().into_owned())
1830            .filter(|name| name.starts_with(".eggup-stale-claim-"))
1831            .collect();
1832        assert!(
1833            leftovers.is_empty(),
1834            "our own displaced record must be cleaned up: {leftovers:?}"
1835        );
1836    }
1837
1838    /// The restore path must never clobber a lock a competing writer took.
1839    ///
1840    /// POSIX `rename` atomically replaces its destination, so restoring by
1841    /// rename would be a TOCTOU. `restore_claim` re-materializes the record with
1842    /// `create_new` instead, which cannot replace anything.
1843    #[test]
1844    fn restore_never_clobbers_a_lock_taken_in_the_meantime() {
1845        let root = InstallationRoot::new().expect("root");
1846        let lock_path = root.path().join(".eggup-mutation.lock");
1847        let claim = root.path().join(".eggup-stale-claim-test");
1848        fs::write(&claim, "pid=1 nonce=1 product=a release=b\n").expect("write claim");
1849
1850        // Someone else owns the lock path.
1851        let winner = "pid=9 nonce=9 product=other release=z\n";
1852        fs::write(&lock_path, winner).expect("write winner");
1853
1854        let error =
1855            crate::lock::restore_claim(&claim, &lock_path, "pid=1 nonce=1 product=a release=b\n");
1856        assert!(
1857            matches!(error, crate::error::RecoveryError::RecoveryRequired { .. }),
1858            "a re-taken lock path must retain the claim, got {error:?}"
1859        );
1860        assert_eq!(
1861            fs::read_to_string(&lock_path).expect("read"),
1862            winner,
1863            "the competing writer's record must be untouched"
1864        );
1865        assert!(
1866            claim.exists(),
1867            "the displaced record is retained and reported, not deleted"
1868        );
1869    }
1870}