1use super::{
3 LocalGitService,
4 observation::{ObservationInput, ObservationOwner, ObservationRecord},
5 observation_store::{Cursor, Plan, Spool},
6};
7use crate::{
8 files::{absent_structure, io_error, save_request_digest, target_path},
9 operations::ledger::SaveObservation,
10};
11use buntrail_core::contracts::*;
12use sha2::{Digest, Sha256};
13use std::{
14 io::Read,
15 sync::{
16 Arc,
17 atomic::{AtomicBool, Ordering},
18 },
19 time::Instant,
20};
21
22fn refused(message: &str) -> CoreError {
23 CoreError::new(ErrorCode::StaleRevision, message)
24}
25fn check_proof(cancel: &AtomicBool, started: Instant) -> Result<(), CoreError> {
26 if cancel.load(Ordering::SeqCst) || started.elapsed() >= super::process::DEADLINE {
27 Err(CoreError::new(
28 ErrorCode::Cancelled,
29 "proof verification cancelled or timed out",
30 ))
31 } else {
32 Ok(())
33 }
34}
35fn request_digest(original: &GitMutationRequest) -> Result<[u8; 32], CoreError> {
36 super::observation::serialized_digest(original)
37}
38#[derive(serde::Serialize, serde::Deserialize)]
39struct Evidence {
40 operation: OperationID,
41 file: FileRef,
42 before: Option<SaveObservation>,
43 after: SaveObservation,
44 captured_absence: bool,
45}
46fn read_evidence(reader: &mut impl Read) -> Result<Option<Evidence>, CoreError> {
47 let mut size = [0; 4];
48 if reader.read(&mut size[..1]).map_err(io_error)? == 0 {
49 return Ok(None);
50 }
51 reader.read_exact(&mut size[1..]).map_err(io_error)?;
52 let size = u32::from_be_bytes(size) as usize;
53 if size > 128 * 1024 {
54 return Err(refused(
55 "save evidence exceeds existing ledger record bound",
56 ));
57 }
58 let mut bytes = vec![0; size];
59 reader.read_exact(&mut bytes).map_err(io_error)?;
60 serde_json::from_slice(&bytes).map(Some).map_err(io_error)
61}
62fn read_approval(
63 reader: &mut impl Read,
64 owner: &ObservationOwner,
65) -> Result<Option<GitSaveApproval>, CoreError> {
66 let mut bytes = [0; 96];
67 if reader.read(&mut bytes[..1]).map_err(io_error)? == 0 {
68 return Ok(None);
69 }
70 reader.read_exact(&mut bytes[1..]).map_err(io_error)?;
71 Ok(Some(GitSaveApproval {
72 operation: OperationID(bytes[..16].try_into().unwrap()),
73 request_digest: bytes[16..48].try_into().unwrap(),
74 actor: WorkspaceLease {
75 client: ClientID(bytes[48..64].try_into().unwrap()),
76 workspace: WorkspaceID(bytes[64..80].try_into().unwrap()),
77 lease: WorkspaceLeaseID(bytes[80..96].try_into().unwrap()),
78 runtime: owner.registration().runtime.clone(),
79 },
80 }))
81}
82fn cursor(spool: &Arc<Spool>) -> Cursor {
83 Cursor {
84 spool: spool.clone(),
85 offset: 0,
86 }
87}
88fn approval_stream(plan: &Plan, additional: Option<&Arc<Spool>>) -> impl Read {
89 let tail: Box<dyn Read> = match additional {
90 Some(spool) => Box::new(cursor(spool)),
91 None => Box::new(std::io::empty()),
92 };
93 plan.approvals().chain(tail)
94}
95fn endpoint(record: &ObservationRecord, version: &FileVersion) -> bool {
96 record.regular.as_ref().is_some_and(|regular| {
97 regular.canonical.as_slice() == version.opaque_token.as_slice()
98 && regular.content_digest == version.content_digest
99 })
100}
101fn valid_endpoint(endpoint: &SaveObservation, target: &TargetIdentity) -> bool {
102 endpoint.requested.target == *target
103 && endpoint.canonical.target == *target
104 && endpoint.requested.opaque_token.len() == 32
105 && endpoint.canonical.opaque_token.len() == 32
106 && endpoint.requested.file_identity.len() == 16
107 && endpoint.requested.file_identity == endpoint.canonical.file_identity
108 && endpoint.requested.content_digest == endpoint.canonical.content_digest
109}
110
111#[derive(Debug)]
112pub enum WorktreePreparation {
113 Existing(Box<OperationReceipt>),
114 Prepared([u8; 16]),
115}
116
117impl LocalGitService {
118 pub fn save_approval_input(
119 &self,
120 owner: &ObservationOwner,
121 approvals: impl IntoIterator<Item = GitSaveApproval>,
122 ) -> Result<ObservationInput, CoreError> {
123 let mut writer = self.observation_input(owner)?;
124 for approval in approvals {
125 if approval.actor.client != owner.registration().client
126 || approval.actor.runtime != owner.registration().runtime
127 {
128 return Err(CoreError::new(
129 ErrorCode::TargetChanged,
130 "save approval client or runtime changed",
131 ));
132 }
133 for bytes in [
134 approval.operation.0.as_slice(),
135 approval.request_digest.as_slice(),
136 approval.actor.client.0.as_slice(),
137 approval.actor.workspace.0.as_slice(),
138 approval.actor.lease.0.as_slice(),
139 ] {
140 writer.append(bytes)?;
141 }
142 }
143 writer.seal()
144 }
145 pub fn observation_alias_digest(
146 &self,
147 owner: &ObservationOwner,
148 observation: [u8; 16],
149 ) -> Result<[u8; 32], CoreError> {
150 let mut reader = self.observation_reader(owner, observation)?;
151 let mut hash = Sha256::new();
152 hash.update(b"buntrail.original-aliases.v1\0");
153 while let Some(record) = reader.next_record()? {
154 if let Some(identity) = record.file_reference {
155 hash.update(identity);
156 }
157 }
158 Ok(hash.finalize().into())
159 }
160 pub fn prepare_worktree_sync(
161 &self,
162 owner: &ObservationOwner,
163 original: &GitMutationRequest,
164 observation: [u8; 16],
165 alias_digest: [u8; 32],
166 approvals: ObservationInput,
167 cancel: &AtomicBool,
168 ) -> Result<WorktreePreparation, CoreError> {
169 if let Some(receipt) = self.existing_receipt_sync(original)? {
170 return Ok(WorktreePreparation::Existing(Box::new(receipt)));
171 }
172 #[cfg(test)]
173 if let Some(checkpoint) = &self.after_proof_lookup {
174 checkpoint();
175 }
176 let mutex = super::mutations::repository_mutex(&original.worktree);
177 let _repository = mutex.lock().map_err(io_error)?;
178 if let Some(receipt) = self.existing_receipt_sync(original)? {
179 return Ok(WorktreePreparation::Existing(Box::new(receipt)));
180 }
181 if !original.command.changes_worktree() {
182 return Err(refused("only worktree changes use prepared execution"));
183 }
184 let store = self.observation_store()?;
185 let pin = self.pin_observation(owner, observation)?;
186 let (worktree, state) = pin.original_state()?;
187 if worktree != &original.worktree
188 || state != &original.expected
189 || self.observation_alias_digest(owner, observation)? != alias_digest
190 {
191 return Err(refused(
192 "request differs from its original owned observation or aliases",
193 ));
194 }
195 let _work = store.verification()?;
196 let plan = store.adopt_plan(owner, pin, original, alias_digest, approvals)?;
197 let result = (|| {
198 let evidence = self.freeze_evidence(owner, &plan, None, cancel)?;
199 self.verify_worktree(&plan, &evidence, cancel)?;
200 store.plan(owner, plan.id)?;
201 Ok(WorktreePreparation::Prepared(plan.id))
202 })();
203 if result.is_err() {
204 store.cancel_plan(owner, plan.id);
205 }
206 result
207 }
208 pub fn execute_worktree_sync(
209 &self,
210 owner: &ObservationOwner,
211 original: GitMutationRequest,
212 plan_id: [u8; 16],
213 alias_digest: [u8; 32],
214 approvals: ObservationInput,
215 cancel: &AtomicBool,
216 ) -> Result<OperationReceipt, CoreError> {
217 if let Some(receipt) = self.existing_receipt_sync(&original)? {
218 return Ok(receipt);
219 }
220 #[cfg(test)]
221 if let Some(checkpoint) = &self.after_proof_lookup {
222 checkpoint();
223 }
224 let digest = request_digest(&original)?;
225 let operation = original.operation;
226 let store = self.observation_store()?;
227 let mut input = Some(approvals);
228 let mut execution = None;
229 let mut verify = |accepted: bool| {
230 if !accepted {
231 let plan = store.plan(owner, plan_id)?;
232 if plan.request_digest != digest
233 || plan.operation != operation
234 || plan.alias_digest != alias_digest
235 {
236 return Err(refused(
237 "prepared original request or alias identity changed",
238 ));
239 }
240 let work = store.verification()?;
241 let additional = store.adopt_execution_input(
242 owner,
243 input
244 .take()
245 .ok_or_else(|| refused("execution input already consumed"))?,
246 )?;
247 let evidence = self.freeze_evidence(owner, &plan, Some(&additional), cancel)?;
248 let expected = self.verify_worktree(&plan, &evidence, cancel)?;
249 store.plan(owner, plan_id)?;
250 execution = Some((plan, evidence, work));
251 Ok(expected)
252 } else {
253 let (plan, evidence, _) = execution
254 .as_ref()
255 .ok_or_else(|| refused("missing accepted proof snapshot"))?;
256 self.verify_worktree(plan, evidence, cancel)
257 }
258 };
259 let expected = original.expected.clone();
260 let authorize = || store.plan(owner, plan_id).map(|_| ());
261 let receipt = self.mutate_guarded(
262 original,
263 expected,
264 cancel,
265 Some(super::mutations::WorktreeProofGuard {
266 owner,
267 verify: &mut verify,
268 authorize: &authorize,
269 }),
270 );
271 store.cancel_plan(owner, plan_id);
272 receipt
273 }
274 pub fn cancel_worktree_plan(&self, owner: &ObservationOwner, plan: [u8; 16]) {
275 if let Ok(store) = self.observation_store() {
276 store.cancel_plan(owner, plan);
277 }
278 }
279
280 fn freeze_evidence(
281 &self,
282 owner: &ObservationOwner,
283 plan: &Plan,
284 additional: Option<&Arc<Spool>>,
285 cancel: &AtomicBool,
286 ) -> Result<Arc<Spool>, CoreError> {
287 let started = Instant::now();
288 let mut writer = self.observation_input(owner)?;
289 let mut approvals = approval_stream(plan, additional);
290 let mut seen = 0u64;
291 while let Some(approval) = read_approval(&mut approvals, owner)? {
292 check_proof(cancel, started)?;
293 let mut prior = approval_stream(plan, additional);
294 for _ in 0..seen {
295 check_proof(cancel, started)?;
296 if read_approval(&mut prior, owner)?
297 .is_some_and(|old| old.operation == approval.operation)
298 {
299 return Err(refused("duplicate approved Save operation"));
300 }
301 }
302 seen += 1;
303 let record = {
304 let ledger = self.files.ledger.lock().map_err(io_error)?;
305 let _lock = ledger.lock()?;
306 let record = ledger
307 .read(approval.operation)?
308 .ok_or_else(|| refused("approved Save history is missing"))?;
309 if ledger.expired(&record) {
310 return Err(refused("approved Save history expired"));
311 }
312 record
313 };
314 let proof = record
315 .save_proof
316 .as_ref()
317 .ok_or_else(|| refused("Save has no authenticated history"))?;
318 let after = proof
319 .after
320 .as_ref()
321 .ok_or_else(|| refused("Save has no durable historical after"))?;
322 let ExpectedState::File { file, version } = &record.envelope.expected else {
323 return Err(refused("Save expected state is malformed"));
324 };
325 let OperationState::Confirmed(MutationResult::Saved(saved)) = &record.receipt.state
326 else {
327 return Err(refused("Save is not durably confirmed"));
328 };
329 let destination = record
330 .destination
331 .as_ref()
332 .ok_or_else(|| refused("Save has no original destination"))?;
333 let resolution_token: [u8; 32] =
334 Sha256::digest(serde_json::to_vec(destination).map_err(io_error)?).into();
335 if record.envelope.kind != MutationKind::Save
336 || record.envelope.target != owner.target
337 || file.target != owner.target
338 || record.envelope.request_digest != approval.request_digest
339 || proof.actor != approval.actor
340 || proof.actor.client != owner.registration().client
341 || proof.actor.runtime != owner.registration().runtime
342 || saved.saved_revision != record.revision
343 || saved.version != after.requested
344 || !valid_endpoint(after, &owner.target)
345 || after.requested.content_digest != record.requested_digest
346 || after.resolved != destination.resolved
347 || after.resolution_token != resolution_token
348 || save_request_digest(file, version, record.revision, record.requested_digest)?
349 != approval.request_digest
350 {
351 return Err(refused(
352 "approved Save identity or historical result differs",
353 ));
354 }
355 match (&proof.before, version) {
356 (Some(before), Some(expected))
357 if before.requested == *expected
358 && valid_endpoint(before, &owner.target)
359 && before.resolved == after.resolved
360 && before.resolution_token == after.resolution_token => {}
361 (None, None) => {}
362 _ => return Err(refused("Save before/after chain is malformed")),
363 }
364 let mut original = plan.original();
365 let mut covered = false;
366 let mut captured_absence = false;
367 while let Some(node) = ObservationRecord::read(&mut original)? {
368 check_proof(cancel, started)?;
369 if node.path != file.path
370 || node
371 .membership
372 .as_ref()
373 .is_some_and(|membership| !membership.affected)
374 {
375 continue;
376 }
377 if node.membership.is_some() {
378 if node.file_reference.is_none()
380 || (file.resolved_identity.is_some() && !node.matches_alias(file)?)
381 {
382 continue;
383 }
384 } else if file.resolved_identity.is_some() {
385 continue;
386 }
387 if node.resolved.as_ref() != Some(&after.resolved) {
388 continue;
389 }
390 if node.regular.is_none()
391 && node.structural_token != absent_structure(&Some(destination.clone()))?
392 {
393 continue;
394 }
395 if node
396 .regular
397 .as_ref()
398 .is_some_and(|regular| regular.resolution != resolution_token)
399 {
400 continue;
401 }
402 covered = true;
403 captured_absence = node.regular.is_none();
404 break;
405 }
406 if !covered {
407 return Err(refused(
408 "Save requested alias was not covered by the original observation",
409 ));
410 }
411 let evidence = Evidence {
412 operation: approval.operation,
413 file: file.clone(),
414 before: proof.before.clone(),
415 after: after.clone(),
416 captured_absence,
417 };
418 let bytes = serde_json::to_vec(&evidence).map_err(io_error)?;
419 if bytes.len() > 128 * 1024 {
420 return Err(refused("Save evidence exceeds its bounded durable record"));
421 }
422 writer.append(&(bytes.len() as u32).to_be_bytes())?;
423 writer.append(&bytes)?;
424 }
425 self.observation_store()?
426 .adopt_execution_input(owner, writer.seal()?)
427 }
428
429 fn compare_node(
430 &self,
431 original: &ObservationRecord,
432 current: &ObservationRecord,
433 current_absence: [u8; 32],
434 evidence: &Arc<Spool>,
435 cancel: &AtomicBool,
436 started: Instant,
437 ) -> Result<(), CoreError> {
438 let unaffected = original
439 .membership
440 .as_ref()
441 .is_some_and(|membership| !membership.affected);
442 if original.path != current.path
443 || original.resolved != current.resolved
444 || original.membership != current.membership
445 || original.file_reference != current.file_reference
446 {
447 return Err(refused("original path, alias or membership changed"));
448 }
449 if unaffected {
450 return if original.structural_token == current.structural_token {
451 Ok(())
452 } else {
453 Err(refused("unaffected alias structure changed"))
454 };
455 }
456 let mut reader = cursor(evidence);
458 let mut last: Option<Evidence> = None;
459 let mut matches = 0;
460 while let Some(save) = read_evidence(&mut reader)? {
461 check_proof(cancel, started)?;
462 if original.resolved.as_ref() != Some(&save.after.resolved) {
463 continue;
464 }
465 if let Some(previous) = &last {
466 if save
467 .before
468 .as_ref()
469 .is_none_or(|before| before.canonical != previous.after.canonical)
470 {
471 return Err(refused("approved canonical Save chain is not contiguous"));
472 }
473 } else if save
474 .before
475 .as_ref()
476 .is_some_and(|before| endpoint(original, &before.canonical))
477 || (save.before.is_none()
478 && original.regular.is_none()
479 && original.structural_token == current_absence)
480 {
481 matches += 1;
482 }
483 if endpoint(original, &save.after.canonical) {
484 matches += 1;
485 }
486 last = Some(save);
487 }
488 let Some(last) = last else {
489 return if original == current {
490 Ok(())
491 } else {
492 Err(refused(
493 "unapproved node changed since original observation",
494 ))
495 };
496 };
497 if matches != 1 || !endpoint(current, &last.after.canonical) {
498 return Err(refused(
499 "original or final node is not an exact Save-chain boundary",
500 ));
501 }
502 if let Some(regular) = &original.regular {
503 if original.structural_token != current.structural_token
504 || regular.resolution != current.regular.as_ref().unwrap().resolution
505 {
506 return Err(refused("saved node structure or resolution changed"));
507 }
508 } else if original.structural_token != current_absence {
509 return Err(refused("created node lost its original absence structure"));
510 }
511 if last.file.path == current.path
512 && current.regular.as_ref().unwrap().requested.as_slice()
513 != last.after.requested.opaque_token.as_slice()
514 {
515 return Err(refused(
516 "saved requested endpoint differs from historical after",
517 ));
518 }
519 Ok(())
520 }
521
522 fn verify_worktree(
523 &self,
524 plan: &Plan,
525 evidence: &Arc<Spool>,
526 cancel: &AtomicBool,
527 ) -> Result<GitExpectedState, CoreError> {
528 let worktree = &plan.pin.data.worktree;
529 let root = self.validate(worktree, cancel)?;
530 let started = Instant::now();
531 let mut reader = plan.original();
532 let mut next = ObservationRecord::read(&mut reader)?;
533 let (state, _) = self.state_records(&root, cancel, |name, node| {
534 use std::os::unix::ffi::OsStrExt;
535 let current = ObservationRecord::from_node(
536 target_path(&root.join(std::ffi::OsStr::from_bytes(name))),
537 node,
538 None,
539 )?;
540 if next.as_ref().is_some_and(|original| {
541 original.membership.is_none() && original.path == current.path
542 }) {
543 self.compare_node(
544 next.as_ref().unwrap(),
545 ¤t,
546 absent_structure(&node.destination)?,
547 evidence,
548 cancel,
549 started,
550 )?;
551 next = ObservationRecord::read(&mut reader)?;
552 } else {
553 let mut approved = cursor(evidence);
554 let mut created = false;
555 let mut final_matches = false;
556 while let Some(save) = read_evidence(&mut approved)? {
557 check_proof(cancel, started)?;
558 if save.after.resolved == current.path {
559 created |= save.before.is_none() && save.captured_absence;
560 final_matches = endpoint(¤t, &save.after.canonical);
561 }
562 }
563 if !created || !final_matches {
564 return Err(refused("unexpected newly enumerated Git path"));
565 }
566 }
567 Ok(())
568 })?;
569 if state.head != plan.pin.data.expected.head
570 || state.index_fingerprint != plan.pin.data.expected.index_fingerprint
571 {
572 return Err(refused("original HEAD or index changed"));
573 }
574 while let Some(original) = next {
575 if original.membership.is_none() {
576 return Err(refused("original Git path disappeared"));
577 }
578 let (mut current, destination) = self.alias_node_record(
579 worktree,
580 FileRef {
581 target: worktree.target.clone(),
582 path: original.path.clone(),
583 resolved_identity: None,
584 },
585 cancel,
586 started,
587 )?;
588 current.file_reference = original.file_reference;
589 self.compare_node(
590 &original,
591 ¤t,
592 absent_structure(&destination)?,
593 evidence,
594 cancel,
595 started,
596 )?;
597 next = ObservationRecord::read(&mut reader)?;
598 }
599 if self.state(&root, cancel)? != state {
600 return Err(refused("worktree changed during proof validation"));
601 }
602 check_proof(cancel, started)?;
603 Ok(state)
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::{connections::local::LocalConnection, files::target_path};
611 use std::{path::Path, sync::Arc};
612 #[test]
613 fn proof_scan_cannot_run_past_the_existing_git_deadline() {
614 assert_eq!(
615 check_proof(
616 &AtomicBool::new(false),
617 Instant::now() - std::time::Duration::from_secs(120)
618 )
619 .unwrap_err()
620 .code,
621 ErrorCode::Cancelled
622 );
623 assert!(check_proof(&AtomicBool::new(false), Instant::now()).is_ok());
624 }
625 fn git(root: &Path, args: &[&str]) {
626 let output = std::process::Command::new("git")
627 .arg("-C")
628 .arg(root)
629 .args(args)
630 .env("GIT_CONFIG_NOSYSTEM", "1")
631 .env("GIT_CONFIG_GLOBAL", "/dev/null")
632 .output()
633 .unwrap();
634 assert!(
635 output.status.success(),
636 "{}",
637 String::from_utf8_lossy(&output.stderr)
638 );
639 }
640 struct Fixture {
641 dir: tempfile::TempDir,
642 local: Arc<LocalConnection>,
643 owner: ObservationOwner,
644 worktree: WorktreeIdentity,
645 file: FileRef,
646 }
647 impl Fixture {
648 fn new() -> Self {
649 let dir = tempfile::tempdir().unwrap();
650 let root = dir.path().join("repo");
651 std::fs::create_dir(&root).unwrap();
652 git(&root, &["init", "-b", "main"]);
653 git(&root, &["config", "user.name", "Fixture"]);
654 git(&root, &["config", "user.email", "fixture@example.invalid"]);
655 std::fs::write(root.join("file"), "original").unwrap();
656 git(&root, &["add", "file"]);
657 git(&root, &["commit", "-m", "initial"]);
658 git(&root, &["branch", "other"]);
659 let local = LocalConnection::open(dir.path().join("state")).unwrap();
660 let lease = WorkspaceLeaseID([1; 16]);
661 local.register(WorkspaceID([2; 16]), lease).unwrap();
662 let owner = local.observation_owner(lease).unwrap();
663 let file = FileRef {
664 target: local.identity.clone(),
665 path: target_path(&root.join("file")),
666 resolved_identity: None,
667 };
668 let worktree = local
669 .git
670 .discover_sync(
671 FileRef {
672 path: target_path(&root),
673 ..file.clone()
674 },
675 &AtomicBool::new(false),
676 )
677 .unwrap()
678 .unwrap();
679 Self {
680 dir,
681 local: Arc::new(local),
682 owner,
683 worktree,
684 file,
685 }
686 }
687 fn save(&self, file: FileRef, id: u8, bytes: &[u8]) -> GitSaveApproval {
688 self.save_as(self.owner.registration().lease, file, id, bytes)
689 }
690 fn save_as(
691 &self,
692 lease: WorkspaceLeaseID,
693 file: FileRef,
694 id: u8,
695 bytes: &[u8],
696 ) -> GitSaveApproval {
697 let expected_version = self
698 .local
699 .files
700 .read_sync(file.clone())
701 .ok()
702 .map(|snapshot| snapshot.version);
703 let save = self
704 .local
705 .owned_files(lease)
706 .unwrap()
707 .prepare(SaveRequest {
708 operation: OperationID([id; 16]),
709 file,
710 expected_version,
711 submitted_revision: BufferRevision(id as u64),
712 source: ByteSource::Memory(Arc::from(bytes)),
713 })
714 .unwrap();
715 let approval = save.approval().clone();
716 assert!(matches!(save.execute().state, OperationState::Confirmed(_)));
717 approval
718 }
719 fn capture(&self, aliases: Vec<FileRef>) -> (GitMutationRequest, [u8; 16], [u8; 32]) {
720 let retained = self
721 .local
722 .git
723 .observe_owned_sync(
724 &self.owner,
725 self.worktree.clone(),
726 aliases.into_iter().map(Ok),
727 &AtomicBool::new(false),
728 )
729 .unwrap();
730 let digest = self
731 .local
732 .git
733 .observation_alias_digest(&self.owner, retained.id)
734 .unwrap();
735 (
736 GitMutationRequest {
737 operation: OperationID([90; 16]),
738 worktree: self.worktree.clone(),
739 expected: retained.snapshot.state,
740 command: GitMutation::Checkout {
741 reference: b"other".to_vec(),
742 },
743 },
744 retained.id,
745 digest,
746 )
747 }
748 fn execute(
749 &self,
750 original: GitMutationRequest,
751 observation: [u8; 16],
752 digest: [u8; 32],
753 approvals: Vec<GitSaveApproval>,
754 ) {
755 let input = self
756 .local
757 .git
758 .save_approval_input(&self.owner, approvals)
759 .unwrap();
760 let WorktreePreparation::Prepared(plan) = self
761 .local
762 .git
763 .prepare_worktree_sync(
764 &self.owner,
765 &original,
766 observation,
767 digest,
768 input,
769 &AtomicBool::new(false),
770 )
771 .expect("durable save chain must prepare")
772 else {
773 panic!("unexpected duplicate")
774 };
775 let additional = self
776 .local
777 .git
778 .save_approval_input(&self.owner, [])
779 .expect("adopted approvals must release only the staging slot");
780 let receipt = self
781 .local
782 .git
783 .execute_worktree_sync(
784 &self.owner,
785 original.clone(),
786 plan,
787 digest,
788 additional,
789 &AtomicBool::new(false),
790 )
791 .unwrap();
792 assert!(
793 matches!(receipt.state, OperationState::Confirmed(_)),
794 "{receipt:?}"
795 );
796 assert_eq!(
797 self.local.git.existing_receipt_sync(&original).unwrap(),
798 Some(receipt)
799 );
800 }
801 }
802 #[test]
803 fn original_pending_save_chain_can_prepare_and_execute() {
804 let f = Fixture::new();
805 let (original, observation, digest) = f.capture(vec![f.file.clone()]);
806 let approval = f.save(f.file.clone(), 20, b"saved");
807 f.execute(original, observation, digest, vec![approval]);
808 }
809 #[test]
810 fn already_reflected_save_is_not_applied_twice() {
811 let f = Fixture::new();
812 let first = f.save(f.file.clone(), 21, b"first");
813 let (original, observation, digest) = f.capture(vec![f.file.clone()]);
814 let second = f.save(f.file.clone(), 22, b"second");
815 f.execute(original, observation, digest, vec![first, second]);
816 }
817 #[test]
818 fn captured_absence_allows_only_the_actual_owned_creation() {
819 let f = Fixture::new();
820 let absent = FileRef {
821 path: target_path(&f.dir.path().join("repo/new")),
822 ..f.file.clone()
823 };
824 let (original, observation, digest) = f.capture(vec![absent.clone()]);
825 let approval = f.save(absent, 23, b"created");
826 f.execute(original, observation, digest, vec![approval]);
827 }
828 #[test]
829 fn captured_creation_then_later_save_forms_one_contiguous_chain() {
830 let f = Fixture::new();
831 let absent = FileRef {
832 path: target_path(&f.dir.path().join("repo/new")),
833 ..f.file.clone()
834 };
835 let (original, observation, digest) = f.capture(vec![absent.clone()]);
836 let first = f.save(absent.clone(), 24, b"created");
837 let second = f.save(absent, 25, b"updated");
838 f.execute(original, observation, digest, vec![first, second]);
839 }
840 #[test]
841 fn interleaved_cross_window_saves_through_outside_aliases_are_contiguous() {
842 let f = Fixture::new();
843 let root = f.dir.path().join("repo");
844 std::fs::write(root.join("second"), "second").unwrap();
845 git(&root, &["add", "second"]);
846 git(&root, &["commit", "-m", "second"]);
847 git(&root, &["branch", "-f", "other"]);
848 let second = FileRef {
849 path: target_path(&root.join("second")),
850 ..f.file.clone()
851 };
852 let alias_path = f.dir.path().join("outside");
853 std::os::unix::fs::symlink(root.join("file"), &alias_path).unwrap();
854 let alias = FileRef {
855 path: target_path(&alias_path),
856 ..f.file.clone()
857 };
858 let lease = WorkspaceLeaseID([3; 16]);
859 f.local.register(WorkspaceID([4; 16]), lease).unwrap();
860 let (original, observation, digest) =
861 f.capture(vec![f.file.clone(), alias.clone(), second.clone()]);
862 let first = f.save_as(lease, alias, 30, b"alias save");
863 let other = f.save(second, 31, b"second save");
864 let last = f.save(f.file.clone(), 32, b"last save");
865 f.local.release(lease);
866 f.execute(original, observation, digest, vec![first, other, last]);
867 }
868 #[test]
869 fn external_changes_never_become_approved_save_history() {
870 use std::os::unix::fs::PermissionsExt;
871 for case in [
872 "same-content",
873 "mode",
874 "head",
875 "index",
876 "new-path",
877 "missing-chain",
878 "duplicate",
879 "reverse",
880 "external-create",
881 ] {
882 let f = Fixture::new();
883 let root = f.dir.path().join("repo");
884 let file = if case == "external-create" {
885 FileRef {
886 path: target_path(&root.join("new")),
887 ..f.file.clone()
888 }
889 } else {
890 f.file.clone()
891 };
892 let (original, observation, digest) = f.capture(vec![file.clone()]);
893 if case == "external-create" {
894 std::fs::write(root.join("new"), "external").unwrap();
895 }
896 let first = f.save(file.clone(), 40, b"saved");
897 let mut approvals = vec![first.clone()];
898 match case {
899 "same-content" => {
900 std::fs::write(root.join("replacement"), b"saved").unwrap();
901 std::fs::rename(root.join("replacement"), root.join("file")).unwrap();
902 }
903 "mode" => std::fs::set_permissions(
904 root.join("file"),
905 std::fs::Permissions::from_mode(0o600),
906 )
907 .unwrap(),
908 "head" => git(&root, &["checkout", "other"]),
909 "index" => git(&root, &["add", "file"]),
910 "new-path" => std::fs::write(root.join("unapproved"), "external").unwrap(),
911 "missing-chain" => approvals = vec![f.save(file, 41, b"later")],
912 "duplicate" => approvals.push(first),
913 "reverse" => approvals.insert(0, f.save(file, 41, b"later")),
914 _ => {}
915 }
916 let input = f
917 .local
918 .git
919 .save_approval_input(&f.owner, approvals)
920 .unwrap();
921 assert!(
922 f.local
923 .git
924 .prepare_worktree_sync(
925 &f.owner,
926 &original,
927 observation,
928 digest,
929 input,
930 &AtomicBool::new(false)
931 )
932 .is_err(),
933 "{case}"
934 );
935 assert!(
936 f.local
937 .git
938 .existing_receipt_sync(&original)
939 .unwrap()
940 .is_none(),
941 "{case}"
942 );
943 }
944 }
945 #[test]
946 fn missing_expired_malformed_and_reconciled_records_cannot_prove_saves() {
947 for case in [
948 "missing", "expired", "legacy", "unknown", "no-after", "digest", "actor", "runtime",
949 "kind", "revision", "result", "before",
950 ] {
951 let f = Fixture::new();
952 let (original, observation, digest) = f.capture(vec![f.file.clone()]);
953 let mut approval = f.save(f.file.clone(), 45, b"saved");
954 {
955 let ledger = f.local.files.ledger.lock().unwrap();
956 let _lock = ledger.lock().unwrap();
957 let mut record = ledger.read(approval.operation).unwrap().unwrap();
958 match case {
959 "missing" => {
960 std::fs::remove_file(
961 ledger.directory.join(format!("{}.json", "2d".repeat(16))),
962 )
963 .unwrap();
964 }
965 "expired" => record.created = 0,
966 "legacy" => record.save_proof = None,
967 "unknown" => {
968 record.receipt = crate::operations::ledger::unknown(
969 record.envelope.id,
970 record.envelope.target.clone(),
971 "fixture uncertain",
972 )
973 }
974 "no-after" => record.save_proof.as_mut().unwrap().after = None,
975 "digest" => approval.request_digest[0] ^= 1,
976 "actor" => approval.actor.lease = WorkspaceLeaseID([9; 16]),
977 "runtime" => {
978 record
979 .save_proof
980 .as_mut()
981 .unwrap()
982 .actor
983 .runtime
984 .supervisor_boot[0] ^= 1
985 }
986 "kind" => record.envelope.kind = MutationKind::Git,
987 "revision" => record.revision = BufferRevision(99),
988 "result" => {
989 let OperationState::Confirmed(MutationResult::Saved(saved)) =
990 &mut record.receipt.state
991 else {
992 panic!()
993 };
994 saved.version.opaque_token[0] ^= 1;
995 }
996 "before" => {
997 record
998 .save_proof
999 .as_mut()
1000 .unwrap()
1001 .before
1002 .as_mut()
1003 .unwrap()
1004 .canonical
1005 .opaque_token[0] ^= 1
1006 }
1007 _ => unreachable!(),
1008 }
1009 if case != "missing" {
1010 ledger.write(&record).unwrap();
1011 }
1012 }
1013 let input = f
1014 .local
1015 .git
1016 .save_approval_input(&f.owner, [approval])
1017 .unwrap();
1018 assert!(
1019 f.local
1020 .git
1021 .prepare_worktree_sync(
1022 &f.owner,
1023 &original,
1024 observation,
1025 digest,
1026 input,
1027 &AtomicBool::new(false)
1028 )
1029 .is_err(),
1030 "{case}"
1031 );
1032 assert!(
1033 f.local
1034 .git
1035 .existing_receipt_sync(&original)
1036 .unwrap()
1037 .is_none()
1038 );
1039 }
1040 }
1041 #[test]
1042 fn exact_registration_survives_durable_admission_or_refuses_without_effect() {
1043 for phase in ["before", "during", "after"] {
1044 let f = Fixture::new();
1045 let (original, observation, digest) = f.capture(vec![f.file.clone()]);
1046 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1047 let WorktreePreparation::Prepared(plan) = f
1048 .local
1049 .git
1050 .prepare_worktree_sync(
1051 &f.owner,
1052 &original,
1053 observation,
1054 digest,
1055 input,
1056 &AtomicBool::new(false),
1057 )
1058 .unwrap()
1059 else {
1060 panic!()
1061 };
1062 let mut service = LocalGitService::new(f.local.files.clone());
1063 let local = f.local.clone();
1064 let lease = f.owner.registration().lease;
1065 let hook: Arc<dyn Fn() + Send + Sync> = Arc::new(move || local.release(lease));
1066 match phase {
1067 "before" => service.before_accept = Some(hook),
1068 "during" => service.after_accept = Some(hook),
1069 _ => service.after_authorization = Some(hook),
1070 }
1071 let input = service.save_approval_input(&f.owner, []).unwrap();
1072 let result = service.execute_worktree_sync(
1073 &f.owner,
1074 original.clone(),
1075 plan,
1076 digest,
1077 input,
1078 &AtomicBool::new(false),
1079 );
1080 if phase == "before" {
1081 assert!(result.is_err());
1082 assert!(service.existing_receipt_sync(&original).unwrap().is_none());
1083 } else {
1084 let receipt = result.unwrap();
1085 assert!(
1086 if phase == "after" {
1087 matches!(receipt.state, OperationState::Confirmed(_))
1088 } else {
1089 matches!(receipt.state, OperationState::Failed(_))
1090 },
1091 "{phase}: {receipt:?}"
1092 );
1093 assert_eq!(
1094 service.existing_receipt_sync(&original).unwrap(),
1095 Some(receipt)
1096 );
1097 }
1098 }
1099 }
1100 #[test]
1101 fn historical_canonical_identity_must_agree_with_the_same_saved_descriptor() {
1102 let f = Fixture::new();
1103 let (original, observation, digest) = f.capture(vec![f.file.clone()]);
1104 let approval = f.save(f.file.clone(), 46, b"saved");
1105 {
1106 let ledger = f.local.files.ledger.lock().unwrap();
1107 let _lock = ledger.lock().unwrap();
1108 let mut record = ledger.read(approval.operation).unwrap().unwrap();
1109 record
1110 .save_proof
1111 .as_mut()
1112 .unwrap()
1113 .after
1114 .as_mut()
1115 .unwrap()
1116 .canonical
1117 .file_identity[0] ^= 1;
1118 ledger.write(&record).unwrap();
1119 }
1120 let input = f
1121 .local
1122 .git
1123 .save_approval_input(&f.owner, [approval])
1124 .unwrap();
1125 assert!(
1126 f.local
1127 .git
1128 .prepare_worktree_sync(
1129 &f.owner,
1130 &original,
1131 observation,
1132 digest,
1133 input,
1134 &AtomicBool::new(false)
1135 )
1136 .is_err(),
1137 "malformed canonical endpoint was accepted"
1138 );
1139 }
1140 #[test]
1141 fn duplicate_receipt_precedes_invalid_plan_owner_cancellation_and_store() {
1142 let f = Fixture::new();
1143 let (original, _, _) = f.capture(vec![]);
1144 let first_input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1145 let other_lease = WorkspaceLeaseID([6; 16]);
1146 f.local.register(WorkspaceID([7; 16]), other_lease).unwrap();
1147 let other = f.local.observation_owner(other_lease).unwrap();
1148 let second_input = f.local.git.save_approval_input(&other, []).unwrap();
1149 let receipt = f
1150 .local
1151 .git
1152 .mutate_sync(original.clone(), &AtomicBool::new(false));
1153 assert!(matches!(receipt.state, OperationState::Confirmed(_)));
1154 f.local.release(f.owner.registration().lease);
1155 let mut service = LocalGitService::new(f.local.files.clone());
1156 service.observations = Err(io_error("fixture storage unavailable"));
1157 let WorktreePreparation::Existing(found) = service
1158 .prepare_worktree_sync(
1159 &f.owner,
1160 &original,
1161 [0; 16],
1162 [0; 32],
1163 first_input,
1164 &AtomicBool::new(true),
1165 )
1166 .unwrap()
1167 else {
1168 panic!()
1169 };
1170 assert_eq!(*found, receipt);
1171 assert_eq!(
1172 service
1173 .execute_worktree_sync(
1174 &f.owner,
1175 original,
1176 [0; 16],
1177 [0; 32],
1178 second_input,
1179 &AtomicBool::new(true)
1180 )
1181 .unwrap(),
1182 receipt
1183 );
1184 }
1185 #[test]
1186 fn execute_accepts_later_approvals_but_keeps_the_original_request() {
1187 let f = Fixture::new();
1188 let (original, observation, digest) = f.capture(vec![f.file.clone()]);
1189 let first = f.save(f.file.clone(), 47, b"first");
1190 let input = f.local.git.save_approval_input(&f.owner, [first]).unwrap();
1191 let WorktreePreparation::Prepared(plan) = f
1192 .local
1193 .git
1194 .prepare_worktree_sync(
1195 &f.owner,
1196 &original,
1197 observation,
1198 digest,
1199 input,
1200 &AtomicBool::new(false),
1201 )
1202 .unwrap()
1203 else {
1204 panic!()
1205 };
1206 let second = f.save(f.file.clone(), 48, b"second");
1207 let input = f.local.git.save_approval_input(&f.owner, [second]).unwrap();
1208 let receipt = f
1209 .local
1210 .git
1211 .execute_worktree_sync(
1212 &f.owner,
1213 original.clone(),
1214 plan,
1215 digest,
1216 input,
1217 &AtomicBool::new(false),
1218 )
1219 .unwrap();
1220 assert!(matches!(receipt.state, OperationState::Confirmed(_)));
1221 let ledger = f.local.files.ledger.lock().unwrap();
1222 let record = ledger.read(original.operation).unwrap().unwrap();
1223 assert_eq!(
1224 record.envelope.request_digest,
1225 request_digest(&original).unwrap()
1226 );
1227 }
1228 #[test]
1229 fn prepared_sibling_worktree_can_save_and_prepare_independently() {
1230 let f = Fixture::new();
1231 let root = f.dir.path().join("repo");
1232 let linked = f.dir.path().join("linked");
1233 git(
1234 &root,
1235 &["worktree", "add", "-b", "linked", linked.to_str().unwrap()],
1236 );
1237 let sibling_file = FileRef {
1238 path: target_path(&linked.join("file")),
1239 ..f.file.clone()
1240 };
1241 let lease = WorkspaceLeaseID([6; 16]);
1242 f.local.register(WorkspaceID([7; 16]), lease).unwrap();
1243 let owner = f.local.observation_owner(lease).unwrap();
1244 let sibling_worktree = f
1245 .local
1246 .git
1247 .discover_sync(
1248 FileRef {
1249 path: target_path(&linked),
1250 ..f.file.clone()
1251 },
1252 &AtomicBool::new(false),
1253 )
1254 .unwrap()
1255 .unwrap();
1256 let (original, observation, digest) = f.capture(vec![f.file.clone(), sibling_file.clone()]);
1257 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1258 let WorktreePreparation::Prepared(plan) = f
1259 .local
1260 .git
1261 .prepare_worktree_sync(
1262 &f.owner,
1263 &original,
1264 observation,
1265 digest,
1266 input,
1267 &AtomicBool::new(false),
1268 )
1269 .unwrap()
1270 else {
1271 panic!()
1272 };
1273 let sibling = f
1274 .local
1275 .git
1276 .observe_owned_sync(
1277 &owner,
1278 sibling_worktree.clone(),
1279 [Ok(sibling_file.clone())],
1280 &AtomicBool::new(false),
1281 )
1282 .unwrap();
1283 let sibling_digest = f
1284 .local
1285 .git
1286 .observation_alias_digest(&owner, sibling.id)
1287 .unwrap();
1288 let sibling_original = GitMutationRequest {
1289 operation: OperationID([91; 16]),
1290 worktree: sibling_worktree,
1291 expected: sibling.snapshot.state,
1292 command: GitMutation::Checkout {
1293 reference: b"linked".to_vec(),
1294 },
1295 };
1296 let input = f.local.git.save_approval_input(&owner, []).unwrap();
1297 let WorktreePreparation::Prepared(sibling_plan) = f
1298 .local
1299 .git
1300 .prepare_worktree_sync(
1301 &owner,
1302 &sibling_original,
1303 sibling.id,
1304 sibling_digest,
1305 input,
1306 &AtomicBool::new(false),
1307 )
1308 .unwrap()
1309 else {
1310 panic!()
1311 };
1312 f.save_as(lease, sibling_file, 49, b"sibling remains editable");
1313 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1314 assert!(matches!(
1315 f.local
1316 .git
1317 .execute_worktree_sync(
1318 &f.owner,
1319 original,
1320 plan,
1321 digest,
1322 input,
1323 &AtomicBool::new(false)
1324 )
1325 .unwrap()
1326 .state,
1327 OperationState::Confirmed(_)
1328 ));
1329 f.local.git.cancel_worktree_plan(&owner, sibling_plan);
1330 }
1331 #[test]
1332 fn ignored_alias_is_rechecked_after_durable_acceptance() {
1333 let f = Fixture::new();
1334 let root = f.dir.path().join("repo");
1335 std::fs::write(root.join(".git/info/exclude"), "ignored\n").unwrap();
1336 std::fs::write(root.join("ignored"), "original").unwrap();
1337 let file = FileRef {
1338 path: target_path(&root.join("ignored")),
1339 ..f.file.clone()
1340 };
1341 let (original, observation, digest) = f.capture(vec![file]);
1342 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1343 let WorktreePreparation::Prepared(plan) = f
1344 .local
1345 .git
1346 .prepare_worktree_sync(
1347 &f.owner,
1348 &original,
1349 observation,
1350 digest,
1351 input,
1352 &AtomicBool::new(false),
1353 )
1354 .unwrap()
1355 else {
1356 panic!()
1357 };
1358 let mut service = LocalGitService::new(f.local.files.clone());
1359 service.after_authorization = Some(Arc::new(move || {
1360 std::fs::write(root.join("ignored"), "external").unwrap()
1361 }));
1362 let input = service.save_approval_input(&f.owner, []).unwrap();
1363 let receipt = service
1364 .execute_worktree_sync(
1365 &f.owner,
1366 original.clone(),
1367 plan,
1368 digest,
1369 input,
1370 &AtomicBool::new(false),
1371 )
1372 .unwrap();
1373 assert!(matches!(
1374 receipt.state,
1375 OperationState::Failed(OperationFailure {
1376 effect: FailureEffect::ProvenNoEffect,
1377 ..
1378 })
1379 ));
1380 assert_eq!(
1381 service.existing_receipt_sync(&original).unwrap(),
1382 Some(receipt)
1383 );
1384 }
1385 #[test]
1386 fn concurrent_original_acceptance_wins_over_stale_cancelled_proof_after_preflight() {
1387 for preparing in [false, true] {
1388 let f = Fixture::new();
1389 let (original, _, _) = f.capture(vec![]);
1390 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1391 let entered = Arc::new(std::sync::Barrier::new(2));
1392 let resume = Arc::new(std::sync::Barrier::new(2));
1393 let mut service = LocalGitService::new(f.local.files.clone());
1394 let (at_lookup, continue_lookup) = (entered.clone(), resume.clone());
1395 service.after_proof_lookup = Some(Arc::new(move || {
1396 at_lookup.wait();
1397 continue_lookup.wait();
1398 }));
1399 let owner = f.owner.clone();
1400 let duplicate = original.clone();
1401 let worker = std::thread::spawn(move || {
1402 if preparing {
1403 let WorktreePreparation::Existing(receipt) = service
1404 .prepare_worktree_sync(
1405 &owner,
1406 &duplicate,
1407 [0; 16],
1408 [0; 32],
1409 input,
1410 &AtomicBool::new(true),
1411 )
1412 .unwrap()
1413 else {
1414 panic!()
1415 };
1416 *receipt
1417 } else {
1418 service
1419 .execute_worktree_sync(
1420 &owner,
1421 duplicate,
1422 [0; 16],
1423 [0; 32],
1424 input,
1425 &AtomicBool::new(true),
1426 )
1427 .unwrap()
1428 }
1429 });
1430 entered.wait();
1431 let expected = f
1432 .local
1433 .git
1434 .mutate_sync(original.clone(), &AtomicBool::new(false));
1435 resume.wait();
1436 assert!(matches!(expected.state, OperationState::Confirmed(_)));
1437 assert_eq!(worker.join().unwrap(), expected);
1438 assert_eq!(
1439 f.local.git.existing_receipt_sync(&original).unwrap(),
1440 Some(expected)
1441 );
1442 }
1443 }
1444 #[test]
1445 fn legacy_lookup_never_waits_for_repository_execution_and_existing_receipt_wins() {
1446 let f = Fixture::new();
1447 let (original, _, _) = f.capture(vec![]);
1448 let mutex = super::super::mutations::repository_mutex(&original.worktree);
1449 let held = mutex.lock().unwrap();
1450 assert_eq!(
1451 f.local
1452 .git
1453 .refuse_unprepared_worktree_sync(&original)
1454 .unwrap_err()
1455 .code,
1456 ErrorCode::Unavailable
1457 );
1458 drop(held);
1459 let expected = f
1460 .local
1461 .git
1462 .mutate_sync(original.clone(), &AtomicBool::new(false));
1463 assert!(matches!(expected.state, OperationState::Confirmed(_)));
1464 let _held = mutex.lock().unwrap();
1465 assert_eq!(
1466 f.local
1467 .git
1468 .refuse_unprepared_worktree_sync(&original)
1469 .unwrap(),
1470 expected
1471 );
1472 }
1473 #[test]
1474 fn legacy_refusal_rechecks_original_accepted_after_fast_preflight() {
1475 let f = Fixture::new();
1476 let (original, _, _) = f.capture(vec![]);
1477 let local = f.local.clone();
1478 let concurrent = original.clone();
1479 let mut service = LocalGitService::new(f.local.files.clone());
1480 service.after_proof_lookup = Some(Arc::new(move || {
1481 assert!(matches!(
1482 local
1483 .git
1484 .mutate_sync(concurrent.clone(), &AtomicBool::new(false))
1485 .state,
1486 OperationState::Confirmed(_)
1487 ));
1488 }));
1489 let receipt = service.refuse_unprepared_worktree_sync(&original).unwrap();
1490 assert_eq!(
1491 Some(receipt),
1492 f.local.git.existing_receipt_sync(&original).unwrap()
1493 );
1494 }
1495 #[test]
1496 fn legacy_existing_accepted_lookup_does_not_wait_for_the_executing_repository() {
1497 let f = Fixture::new();
1498 let (original, _, _) = f.capture(vec![]);
1499 let local = f.local.clone();
1500 let duplicate = original.clone();
1501 let mut service = LocalGitService::new(f.local.files.clone());
1502 service.after_authorization = Some(Arc::new(move || {
1503 let receipt = local
1504 .git
1505 .refuse_unprepared_worktree_sync(&duplicate)
1506 .unwrap();
1507 assert!(matches!(receipt.state, OperationState::Unknown(_)));
1508 }));
1509 let receipt = service.mutate_sync(original.clone(), &AtomicBool::new(false));
1510 assert!(matches!(receipt.state, OperationState::Confirmed(_)));
1511 assert_eq!(
1512 Some(receipt),
1513 f.local.git.existing_receipt_sync(&original).unwrap()
1514 );
1515 }
1516 #[test]
1517 fn hintless_owned_save_uses_captured_same_path_without_rewriting_alias_identity() {
1518 for changed_hint in [false, true] {
1519 let f = Fixture::new();
1520 let outside = f.dir.path().join("original-alias");
1521 std::os::unix::fs::symlink(f.dir.path().join("repo/file"), &outside).unwrap();
1522 let captured = FileRef {
1523 path: target_path(&outside),
1524 resolved_identity: Some(vec![7]),
1525 ..f.file.clone()
1526 };
1527 let (original, observation, digest) = f.capture(vec![captured.clone()]);
1528 let effective = FileRef {
1529 resolved_identity: if changed_hint { Some(vec![8]) } else { None },
1530 ..captured.clone()
1531 };
1532 let approval = f.save(effective, 48, b"owned save");
1533 let input = f
1534 .local
1535 .git
1536 .save_approval_input(&f.owner, [approval])
1537 .unwrap();
1538 let result = f.local.git.prepare_worktree_sync(
1539 &f.owner,
1540 &original,
1541 observation,
1542 digest,
1543 input,
1544 &AtomicBool::new(false),
1545 );
1546 if changed_hint {
1547 assert!(result.is_err());
1548 } else {
1549 let WorktreePreparation::Prepared(plan) = result.unwrap() else {
1550 panic!("expected prepared")
1551 };
1552 assert_eq!(
1553 f.local
1554 .git
1555 .observation_alias_digest(&f.owner, observation)
1556 .unwrap(),
1557 digest
1558 );
1559 let mut reader = f
1560 .local
1561 .git
1562 .observation_reader(&f.owner, observation)
1563 .unwrap();
1564 let mut found = false;
1565 while let Some(record) = reader.next_record().unwrap() {
1566 if record.membership.is_some() {
1567 assert!(record.matches_alias(&captured).unwrap());
1568 found = true;
1569 }
1570 }
1571 assert!(found);
1572 drop(reader);
1573 f.local.git.cancel_worktree_plan(&f.owner, plan);
1574 }
1575 }
1576 }
1577 #[test]
1578 fn original_alias_set_and_link_identity_cannot_be_reconstructed_later() {
1579 for case in [
1580 "uncovered",
1581 "same-target-link-replaced",
1582 "hint-changed",
1583 "membership-changed",
1584 ] {
1585 let f = Fixture::new();
1586 let root = f.dir.path().join("repo");
1587 let outside = f.dir.path().join("alias");
1588 let destination = if case == "membership-changed" {
1589 let path = f.dir.path().join("unrelated");
1590 std::fs::write(&path, "unrelated").unwrap();
1591 path
1592 } else {
1593 root.join("file")
1594 };
1595 std::os::unix::fs::symlink(&destination, &outside).unwrap();
1596 let mut alias = FileRef {
1597 path: target_path(&outside),
1598 ..f.file.clone()
1599 };
1600 let aliases = if case == "uncovered" {
1601 vec![f.file.clone()]
1602 } else {
1603 vec![f.file.clone(), alias.clone()]
1604 };
1605 let (original, observation, digest) = f.capture(aliases);
1606 let approvals = if case == "membership-changed" {
1607 vec![]
1608 } else {
1609 if case == "hint-changed" {
1610 alias.resolved_identity = Some(vec![7]);
1611 }
1612 vec![f.save(alias, 50, b"saved through alias")]
1613 };
1614 if matches!(case, "same-target-link-replaced" | "membership-changed") {
1615 let replacement = f.dir.path().join("new-link");
1616 std::os::unix::fs::symlink(root.join("file"), &replacement).unwrap();
1617 std::fs::rename(replacement, outside).unwrap();
1618 }
1619 let input = f
1620 .local
1621 .git
1622 .save_approval_input(&f.owner, approvals)
1623 .unwrap();
1624 assert!(
1625 f.local
1626 .git
1627 .prepare_worktree_sync(
1628 &f.owner,
1629 &original,
1630 observation,
1631 digest,
1632 input,
1633 &AtomicBool::new(false)
1634 )
1635 .is_err(),
1636 "{case}"
1637 );
1638 }
1639 }
1640 #[test]
1641 fn owned_creation_through_original_directory_alias_is_verified() {
1642 let f = Fixture::new();
1643 let root = f.dir.path().join("repo");
1644 let alias = f.dir.path().join("alias-dir");
1645 std::os::unix::fs::symlink(&root, &alias).unwrap();
1646 let file = FileRef {
1647 path: target_path(&alias.join("new")),
1648 ..f.file.clone()
1649 };
1650 let (original, observation, digest) = f.capture(vec![file.clone()]);
1651 let approval = f.save(file, 51, b"created through original parent link");
1652 f.execute(original, observation, digest, vec![approval]);
1653 }
1654 #[test]
1655 fn nested_repository_and_submodule_aliases_preserve_original_membership() {
1656 let f = Fixture::new();
1657 let root = f.dir.path().join("repo");
1658 let nested = root.join("nested");
1659 std::fs::create_dir(&nested).unwrap();
1660 git(&nested, &["init", "-b", "nested"]);
1661 std::fs::write(nested.join("file"), "nested").unwrap();
1662 std::fs::write(root.join(".git/info/exclude"), "nested/\n").unwrap();
1663 let source = f.dir.path().join("source");
1664 std::fs::create_dir(&source).unwrap();
1665 git(&source, &["init", "-b", "main"]);
1666 git(&source, &["config", "user.name", "Fixture"]);
1667 git(
1668 &source,
1669 &["config", "user.email", "fixture@example.invalid"],
1670 );
1671 std::fs::write(source.join("file"), "submodule").unwrap();
1672 git(&source, &["add", "file"]);
1673 git(&source, &["commit", "-m", "initial"]);
1674 git(
1675 &root,
1676 &[
1677 "-c",
1678 "protocol.file.allow=always",
1679 "submodule",
1680 "add",
1681 source.to_str().unwrap(),
1682 "sub",
1683 ],
1684 );
1685 git(&root, &["commit", "-am", "submodule"]);
1686 git(&root, &["branch", "-f", "other"]);
1687 let files = [nested.join("file"), root.join("sub/file")].map(|path| FileRef {
1688 path: target_path(&path),
1689 ..f.file.clone()
1690 });
1691 let (original, observation, digest) = f.capture(files.to_vec());
1692 let mut reader = f
1693 .local
1694 .git
1695 .observation_reader(&f.owner, observation)
1696 .unwrap();
1697 let mut unaffected = 0;
1698 while let Some(record) = reader.next_record().unwrap() {
1699 if let Some(membership) = record.membership {
1700 assert!(!membership.affected);
1701 assert!(membership.requested_repository.is_some());
1702 unaffected += 1;
1703 }
1704 }
1705 assert_eq!(unaffected, 2);
1706 drop(reader);
1707 f.execute(original, observation, digest, vec![]);
1708 }
1709 #[test]
1710 fn truncated_approval_input_cannot_publish_a_plan_or_leak_its_pin() {
1711 let f = Fixture::new();
1712 let (original, observation, digest) = f.capture(vec![]);
1713 let before = f.local.git.observation_storage_stats().unwrap().bytes;
1714 let mut input = f.local.git.observation_input(&f.owner).unwrap();
1715 input.append(&[0; 95]).unwrap();
1716 assert!(
1717 f.local
1718 .git
1719 .prepare_worktree_sync(
1720 &f.owner,
1721 &original,
1722 observation,
1723 digest,
1724 input.seal().unwrap(),
1725 &AtomicBool::new(false)
1726 )
1727 .is_err()
1728 );
1729 let stats = f.local.git.observation_storage_stats().unwrap();
1730 assert_eq!(stats.bytes, before);
1731 assert_eq!(stats.pins, 0);
1732 assert_eq!(stats.captures, 0);
1733 }
1734 #[test]
1735 fn plan_cancel_or_observation_release_must_survive_durable_admission() {
1736 for retirement in ["cancel", "release", "expire"] {
1737 for phase in ["before", "during", "after"] {
1738 let f = Fixture::new();
1739 let (original, observation, digest) = f.capture(vec![]);
1740 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1741 let WorktreePreparation::Prepared(plan) = f
1742 .local
1743 .git
1744 .prepare_worktree_sync(
1745 &f.owner,
1746 &original,
1747 observation,
1748 digest,
1749 input,
1750 &AtomicBool::new(false),
1751 )
1752 .unwrap()
1753 else {
1754 panic!()
1755 };
1756 let mut service = LocalGitService::new(f.local.files.clone());
1757 let local = f.local.clone();
1758 let owner = f.owner.clone();
1759 let hook: Arc<dyn Fn() + Send + Sync> = Arc::new(move || match retirement {
1760 "release" => local.git.release_observation(&owner, observation),
1761 "expire" => super::super::observation_store::testing::expire_owner(
1762 local.git.observation_store().unwrap(),
1763 &owner,
1764 ),
1765 _ => local.git.cancel_worktree_plan(&owner, plan),
1766 });
1767 match phase {
1768 "before" => service.before_accept = Some(hook),
1769 "during" => service.after_accept = Some(hook),
1770 _ => service.after_authorization = Some(hook),
1771 }
1772 let input = service.save_approval_input(&f.owner, []).unwrap();
1773 let result = service.execute_worktree_sync(
1774 &f.owner,
1775 original.clone(),
1776 plan,
1777 digest,
1778 input,
1779 &AtomicBool::new(false),
1780 );
1781 if phase == "before" {
1782 assert!(
1783 result.is_err(),
1784 "cancelled/released plan accepted before fsync"
1785 );
1786 assert!(service.existing_receipt_sync(&original).unwrap().is_none());
1787 } else {
1788 let receipt = result.unwrap();
1789 assert!(
1790 if phase == "after" {
1791 matches!(receipt.state, OperationState::Confirmed(_))
1792 } else {
1793 matches!(
1794 receipt.state,
1795 OperationState::Failed(OperationFailure {
1796 effect: FailureEffect::ProvenNoEffect,
1797 ..
1798 })
1799 )
1800 },
1801 "{phase}, {retirement}: {receipt:?}"
1802 );
1803 }
1804 }
1805 }
1806 }
1807 #[test]
1808 fn unrecordable_revocation_remains_unknown_and_the_accepted_id_never_replays() {
1809 let f = Fixture::new();
1810 let (original, observation, digest) = f.capture(vec![]);
1811 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1812 let WorktreePreparation::Prepared(plan) = f
1813 .local
1814 .git
1815 .prepare_worktree_sync(
1816 &f.owner,
1817 &original,
1818 observation,
1819 digest,
1820 input,
1821 &AtomicBool::new(false),
1822 )
1823 .unwrap()
1824 else {
1825 panic!()
1826 };
1827 let mut service = LocalGitService::new(f.local.files.clone());
1828 let directory = f.local.files.ledger.lock().unwrap().directory.clone();
1829 let relocated = directory.with_file_name("temporarily-unavailable-operations");
1830 let (original_directory, unavailable_directory) = (directory.clone(), relocated.clone());
1831 let local = f.local.clone();
1832 let lease = f.owner.registration().lease;
1833 service.after_accept = Some(Arc::new(move || {
1834 local.release(lease);
1835 std::fs::rename(&original_directory, &unavailable_directory).unwrap();
1836 }));
1837 let input = service.save_approval_input(&f.owner, []).unwrap();
1838 let receipt = service
1839 .execute_worktree_sync(
1840 &f.owner,
1841 original.clone(),
1842 plan,
1843 digest,
1844 input,
1845 &AtomicBool::new(false),
1846 )
1847 .unwrap();
1848 assert!(matches!(receipt.state, OperationState::Unknown(_)));
1849 std::fs::rename(relocated, directory).unwrap();
1850 assert!(matches!(
1851 service
1852 .mutate_sync(original.clone(), &AtomicBool::new(false))
1853 .state,
1854 OperationState::Unknown(_)
1855 ));
1856 assert_eq!(
1857 service
1858 .status_sync(original.worktree, &AtomicBool::new(false))
1859 .unwrap()
1860 .state,
1861 original.expected
1862 );
1863 }
1864
1865 #[test]
1866 fn immutable_original_request_bytes_are_retained_and_charged_with_the_plan() {
1867 let f = Fixture::new();
1868 let (original, observation, digest) = f.capture(vec![]);
1869 let baseline = f.local.git.observation_storage_stats().unwrap().bytes;
1870 let input = f.local.git.save_approval_input(&f.owner, []).unwrap();
1871 let WorktreePreparation::Prepared(plan) = f
1872 .local
1873 .git
1874 .prepare_worktree_sync(
1875 &f.owner,
1876 &original,
1877 observation,
1878 digest,
1879 input,
1880 &AtomicBool::new(false),
1881 )
1882 .unwrap()
1883 else {
1884 panic!()
1885 };
1886 assert_eq!(
1887 f.local.git.observation_storage_stats().unwrap().bytes,
1888 baseline + serde_json::to_vec(&original).unwrap().len() as u64,
1889 "full immutable request is missing from charged plan storage"
1890 );
1891 let store = f.local.git.observation_store().unwrap();
1892 let retained = store.plan(&f.owner, plan).unwrap();
1893 assert_eq!(retained.request_digest, request_digest(&original).unwrap());
1894 let mut bytes = Vec::new();
1895 cursor(&retained._original_request)
1896 .read_to_end(&mut bytes)
1897 .unwrap();
1898 assert_eq!(bytes, serde_json::to_vec(&original).unwrap());
1899 drop(retained);
1900 f.local.git.cancel_worktree_plan(&f.owner, plan);
1901 assert_eq!(
1902 f.local.git.observation_storage_stats().unwrap().bytes,
1903 baseline
1904 );
1905 }
1906 #[test]
1907 #[ignore = "explicit modest verifier geometry measurement"]
1908 fn actual_verifier_chain_and_alias_measurement() {
1909 fn rss() -> i64 {
1910 let mut usage = std::mem::MaybeUninit::uninit();
1911 assert_eq!(
1912 unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) },
1913 0
1914 );
1915 unsafe { usage.assume_init() }.ru_maxrss
1916 }
1917 let f = Fixture::new();
1918 let root = f.dir.path().join("repo");
1919 let mut files = vec![];
1920 for n in 0..64 {
1921 let path = root.join(format!("path-{n:026}"));
1922 std::fs::write(&path, b"original\n").unwrap();
1923 files.push(FileRef {
1924 path: target_path(&path),
1925 ..f.file.clone()
1926 });
1927 }
1928 git(&root, &["add", "."]);
1929 git(&root, &["commit", "-m", "geometry"]);
1930 git(&root, &["branch", "-f", "other"]);
1931 let mut destination = root.join("file");
1932 for n in 0..8 {
1933 let link = f.dir.path().join(format!("indirect-{n}"));
1934 std::os::unix::fs::symlink(destination, &link).unwrap();
1935 destination = link;
1936 }
1937 let mut aliases = vec![f.file.clone()];
1938 aliases.extend(files.iter().take(16).cloned());
1939 for n in 0..16 {
1940 let path = f.dir.path().join(format!("outside-{n}"));
1941 std::os::unix::fs::symlink(&destination, &path).unwrap();
1942 aliases.push(FileRef {
1943 path: target_path(&path),
1944 ..f.file.clone()
1945 });
1946 }
1947 let rss_before = rss();
1948 let capture_start = Instant::now();
1949 let (original, observation, digest) = f.capture(aliases);
1950 let capture_seconds = capture_start.elapsed().as_secs_f64();
1951 let original_bytes = f.local.git.observation_storage_stats().unwrap().bytes;
1952 let mut approvals = vec![f.save(f.file.clone(), 100, b"alias endpoint saved")];
1953 for round in 0..2 {
1954 for (n, file) in files.iter().take(16).enumerate() {
1955 approvals.push(f.save(
1956 file.clone(),
1957 (101 + round * 16 + n) as u8,
1958 if round == 0 { b"first" } else { b"final" },
1959 ));
1960 }
1961 }
1962 let input = f
1963 .local
1964 .git
1965 .save_approval_input(&f.owner, approvals)
1966 .unwrap();
1967 let approval_bytes = input.len();
1968 let prepare_start = Instant::now();
1969 let WorktreePreparation::Prepared(plan_id) = f
1970 .local
1971 .git
1972 .prepare_worktree_sync(
1973 &f.owner,
1974 &original,
1975 observation,
1976 digest,
1977 input,
1978 &AtomicBool::new(false),
1979 )
1980 .unwrap()
1981 else {
1982 panic!()
1983 };
1984 let prepare_seconds = prepare_start.elapsed().as_secs_f64();
1985 let retained_plan_bytes = f.local.git.observation_storage_stats().unwrap().bytes;
1986 let store = f.local.git.observation_store().unwrap();
1987 let plan = store.plan(&f.owner, plan_id).unwrap();
1988 let evidence = f
1989 .local
1990 .git
1991 .freeze_evidence(&f.owner, &plan, None, &AtomicBool::new(false))
1992 .unwrap();
1993 let sampled_bytes = store.stats().bytes;
1994 let mut reader = cursor(&evidence);
1995 let mut max_record_bytes = 0;
1996 let mut records = 0;
1997 while let Some(record) = read_evidence(&mut reader).unwrap() {
1998 max_record_bytes = max_record_bytes.max(serde_json::to_vec(&record).unwrap().len());
1999 records += 1;
2000 }
2001 drop(reader);
2002 drop(evidence);
2003 drop(plan);
2004 let additional = f.local.git.save_approval_input(&f.owner, []).unwrap();
2005 let execute_start = Instant::now();
2006 let receipt = f
2007 .local
2008 .git
2009 .execute_worktree_sync(
2010 &f.owner,
2011 original,
2012 plan_id,
2013 digest,
2014 additional,
2015 &AtomicBool::new(false),
2016 )
2017 .unwrap();
2018 let execute_seconds = execute_start.elapsed().as_secs_f64();
2019 assert!(matches!(receipt.state, OperationState::Confirmed(_)));
2020 println!(
2021 "{}",
2022 serde_json::json!({"git_nodes":65,"supplemental_aliases":33,"outside_aliases":16,"indirect_links":8,"save_records":records,
2023 "original_bytes":original_bytes,"approval_bytes":approval_bytes,"retained_plan_bytes":retained_plan_bytes,"sampled_with_frozen_evidence_bytes":sampled_bytes,
2024 "max_encoded_evidence_record_bytes":max_record_bytes,"capture_seconds":capture_seconds,"prepare_seconds":prepare_seconds,"execute_seconds":execute_seconds,
2025 "rss_before":rss_before,"rss_after":rss(),"hash_scratch_bytes_per_worker":65536,"ledger_record_parse_bound":131072})
2026 );
2027 f.local.git.release_observation(&f.owner, observation);
2028 assert_eq!(store.stats().bytes, 0);
2029 }
2030}