diff --git a/crates/locality-google-docs/src/connector.rs b/crates/locality-google-docs/src/connector.rs index 8b4d34cf..644a834f 100644 --- a/crates/locality-google-docs/src/connector.rs +++ b/crates/locality-google-docs/src/connector.rs @@ -347,22 +347,22 @@ fn docs_revision_matches(expected: &str, current: &str) -> bool { } } -fn remote_versions_match_except_drive_counter(expected: &str, current: &str) -> bool { +fn remote_versions_match_for_entity_archive(expected: &str, current: &str) -> bool { drive_modified_time_from_remote_version(expected) .zip(drive_modified_time_from_remote_version(current)) .is_some_and(|(expected_modified, current_modified)| { expected_modified == current_modified - && docs_revision_semantically_matches(expected, current) + && docs_revision_matches_archive_precondition(expected, current) }) } -fn docs_revision_semantically_matches(expected: &str, current: &str) -> bool { +fn docs_revision_matches_archive_precondition(expected: &str, current: &str) -> bool { match ( docs_revision_from_remote_version(expected), docs_revision_from_remote_version(current), ) { (Some(expected), Some(current)) => expected == current, - (None, None) => true, + (None, _) => true, _ => false, } } @@ -505,7 +505,7 @@ fn check_remote_preconditions( if expected == current.as_str() { continue; } - if remote_versions_match_except_drive_counter(expected, current.as_str()) + if remote_versions_match_for_entity_archive(expected, current.as_str()) && plan_archives_entity(request.plan, &precondition.remote_id) { continue; @@ -3459,6 +3459,42 @@ mod tests { .expect("Drive version-only drift should not block entity archive"); } + #[test] + fn concurrency_allows_drive_only_stub_precondition_for_entity_archive() { + let mut file = doc_file("doc-1", "Launch Brief", "workspace"); + file.version = Some("8".to_string()); + let drive = Arc::new(FakeDrive::default().with_file(file)); + let docs = Arc::new(FakeDocs::default().with_document(document( + "doc-1", + "Launch Brief", + "rev-1", + "Hello\n", + ))); + let connector = GoogleDocsConnector::with_apis(GoogleDocsConfig::new("token"), drive, docs); + let plan = PushPlan::new( + vec![RemoteId::new("doc-1")], + vec![PushOperation::ArchiveEntity { + entity_id: RemoteId::new("doc-1"), + }], + ); + let op_ids = vec![PushOperationId("push-1:0:archive_entity:doc-1".to_string())]; + let preconditions = vec![RemotePrecondition { + remote_id: RemoteId::new("doc-1"), + remote_edited_at: Some("drive:8:2026-06-25T10:00:00.000Z".to_string()), + }]; + + connector + .check_concurrency(ApplyPlanRequest { + push_id: &PushId("push-1".to_string()), + mount_id: &MountId::new("google-docs-main"), + plan: &plan, + operation_ids: &op_ids, + remote_preconditions: &preconditions, + local_root: None, + }) + .expect("drive-only stub precondition should not block entity archive"); + } + #[test] fn apply_rejects_stale_docs_revision_precondition_without_writing() { let drive = Arc::new(FakeDrive::default().with_file(doc_file( diff --git a/crates/localityd/src/push.rs b/crates/localityd/src/push.rs index 1b25b7e7..ebd03749 100644 --- a/crates/localityd/src/push.rs +++ b/crates/localityd/src/push.rs @@ -381,7 +381,7 @@ where .pipeline .plan .as_ref() - .is_some_and(|plan| plan.operations.iter().all(is_create_operation)) + .is_some_and(plan_can_apply_without_shadow_preimage) { return Err(LocalityError::InvalidState( "push pipeline approved apply without a shadow preimage".to_string(), @@ -1156,6 +1156,12 @@ fn is_create_operation(operation: &PushOperation) -> bool { ) } +fn plan_can_apply_without_shadow_preimage(plan: &PushPlan) -> bool { + plan.operations.iter().all(|operation| { + is_create_operation(operation) || matches!(operation, PushOperation::ArchiveEntity { .. }) + }) +} + fn create_operation_source(operation: &PushOperation) -> Option<(&RemoteId, &PathBuf)> { match operation { PushOperation::CreateEntity { diff --git a/crates/localityd/tests/push_execution.rs b/crates/localityd/tests/push_execution.rs index 4a3e62b0..84c78771 100644 --- a/crates/localityd/tests/push_execution.rs +++ b/crates/localityd/tests/push_execution.rs @@ -4022,6 +4022,75 @@ fn daemon_push_job_plans_pending_virtual_delete_from_file_path() { ); } +#[test] +fn reviewed_virtual_delete_without_shadow_applies_archive() { + let fixture = PushFixture::new(); + let state_root = fixture.root.join(".state"); + let mut store = InMemoryStateStore::new(); + store + .save_mount( + MountConfig::new( + fixture.mount_id.clone(), + "google-docs", + fixture.root.clone(), + ) + .projection(ProjectionMode::LinuxFuse), + ) + .expect("save mount"); + store + .save_entity( + EntityRecord::new( + fixture.mount_id.clone(), + fixture.remote_id.clone(), + EntityKind::Page, + "Roadmap", + "Roadmap.md", + ) + .with_hydration(HydrationState::Hydrated) + .with_remote_edited_at("drive:7:2026-06-25T10:00:00.000Z|docs:rev-1"), + ) + .expect("save page"); + store + .save_virtual_mutation(virtual_mutation( + &fixture.mount_id, + "delete:page-1", + VirtualMutationKind::Delete, + Some(fixture.remote_id.clone()), + None, + "Roadmap.md", + None, + )) + .expect("save mutation"); + let source = FakePushSource::default() + .with_apply_effects(vec![JournalApplyEffect::ArchivedEntity { + operation_id: PushOperationId("push-1:0:archive_entity:page-1".to_string()), + operation_index: 0, + entity_id: fixture.remote_id.clone(), + }]) + .with_changed_remote_ids(vec![fixture.remote_id.clone()]); + + let report = execute_push_job_with_content_root( + &mut store, + PushJob { + target_path: fixture.root.clone(), + assume_yes: true, + confirm_dangerous: false, + }, + &source, + Some(&state_root), + ) + .expect("execute reviewed delete"); + + assert_eq!(report.action, PushJobAction::Reconciled); + assert_eq!(source.applied_count(), 1); + assert_eq!( + report.pipeline.plan.expect("plan").operations, + vec![PushOperation::ArchiveEntity { + entity_id: fixture.remote_id.clone() + }] + ); +} + #[test] fn auto_save_push_blocks_pending_virtual_delete_without_applying() { let fixture = PushFixture::new();