1use std::path::Path;
5
6use crate::entity::EntityId;
7use crate::ops::WarningHint;
8use crate::provenance::{Provenance, ProvenanceKind};
9use crate::vcs::{Actor, ClientId, CommitContext};
10use crate::workspace::MountCapability;
11
12use super::super::{DeleteEntityArgs, DeleteEntityOutcome, Engine, EngineError, ReferrerInfo};
13use super::{gc_orphan_stubs, make_stub};
14
15#[derive(Debug, Clone)]
23pub struct DeleteReferrers {
24 pub write_referrers: Vec<ReferrerInfo>,
25 pub readonly_referrers: Vec<EntityId>,
26}
27
28impl DeleteReferrers {
29 pub fn would_refuse(&self) -> bool {
31 !self.write_referrers.is_empty()
32 }
33}
34
35impl Engine {
36 pub fn classify_delete_referrers(&self, id: &EntityId) -> DeleteReferrers {
48 let mut write_referrers: Vec<ReferrerInfo> = Vec::new();
49 let mut readonly_referrers: Vec<EntityId> = Vec::new();
50 for edge in self.store.incoming(id) {
51 let from_mem = edge.from.mem().to_string();
52 let cap = self
53 .mounts
54 .iter()
55 .find(|m| m.mount.mem == from_mem)
56 .map(|m| m.mount.capability)
57 .unwrap_or(MountCapability::Write);
58 match cap {
59 MountCapability::Write => {
60 let from_id = edge.from.to_string();
61 if let Some(existing) =
62 write_referrers.iter_mut().find(|r| r.from_id == from_id)
63 {
64 if !existing.rel_types.contains(&edge.rel_type) {
65 existing.rel_types.push(edge.rel_type.clone());
66 }
67 } else {
68 write_referrers.push(ReferrerInfo {
69 from_id,
70 rel_types: vec![edge.rel_type.clone()],
71 mem: from_mem,
72 });
73 }
74 }
75 MountCapability::ReadOnly => readonly_referrers.push(edge.from.clone()),
76 }
77 }
78 DeleteReferrers {
79 write_referrers,
80 readonly_referrers,
81 }
82 }
83
84 pub fn delete_entity(
106 &mut self,
107 args: DeleteEntityArgs,
108 actor: Actor,
109 client: Option<&ClientId>,
110 note: Option<&str>,
111 ) -> Result<DeleteEntityOutcome, EngineError> {
112 let id = &args.id;
113 let mem = id.mem().to_string();
114
115 let mount_idx = self
116 .mounts
117 .iter()
118 .position(|m| m.mount.mem == mem)
119 .ok_or_else(|| self.unknown_mem_error(&mem))?;
120 if self.mounts[mount_idx].mount.capability != MountCapability::Write {
121 return Err(EngineError::ReadOnlyMount(mem));
122 }
123
124 self.ensure_mems_loaded(None);
132
133 let mut drift_warnings = self.reload_if_stale(Some(&mem));
138
139 let entity = self
140 .store
141 .get(id)
142 .ok_or_else(|| EngineError::NotFound { id: id.to_string() })?;
143
144 if let Some(expected) = args.expected_hash.as_deref()
145 && entity.content_hash != expected
146 {
147 return Err(EngineError::HashMismatch {
155 id: id.to_string(),
156 current: entity.content_hash.clone(),
157 is_stub: entity.stub,
158 });
159 }
160
161 let file_path = entity.file_path.clone();
162 let entity_is_stub = entity.stub;
163
164 let DeleteReferrers {
170 write_referrers,
171 readonly_referrers,
172 } = self.classify_delete_referrers(id);
173 if !write_referrers.is_empty() {
174 return Err(EngineError::HasIncomingRefs {
175 id: id.to_string(),
176 referrers: write_referrers,
177 });
178 }
179
180 let demote_to_stub = !readonly_referrers.is_empty();
181 let removed_incoming: Vec<String> =
182 readonly_referrers.iter().map(|id| id.to_string()).collect();
183
184 let relations_removed = if demote_to_stub {
190 self.store.outgoing(id).len()
191 } else {
192 self.store.outgoing(id).len() + self.store.incoming(id).len()
193 };
194
195 let backend = self.mounts[mount_idx].backend.as_ref();
204 let write_id = if entity_is_stub {
205 String::new()
206 } else {
207 backend.delete_entity(Path::new(&file_path))?;
208 super::stage_anchors_removal(backend, id)?;
212 let commit_subject = format!("memstead: delete {id}");
213 let ctx = CommitContext {
214 actor,
215 client: client.cloned(),
216 tool: Some("delete_entity"),
217 note: note.map(String::from),
218 role: self.current_role,
219 identity: self.current_identity.clone(),
220 logical_operation_id: None,
221 entity_ids: None,
222 };
223 backend.commit(&commit_subject, &ctx)?
224 };
225
226 backend.append_provenance(
227 &Provenance::new(
228 std::time::SystemTime::now(),
229 ProvenanceKind::Delete,
230 Some(id.to_string()),
231 actor,
232 client.cloned(),
233 note.map(String::from),
234 )
235 .with_role(self.current_role)
236 .with_identity(self.current_identity.clone()),
237 )?;
238
239 let mut stamp_warnings: Vec<WarningHint> = Vec::new();
240 if !write_id.is_empty() {
241 self.record_self_write(mount_idx, &write_id);
242 stamp_warnings = self.stamp_mutation_versions(mount_idx);
243 }
244
245 let mut warnings: Vec<WarningHint> = Vec::new();
246 warnings.extend(std::mem::take(&mut stamp_warnings));
247 warnings.append(&mut drift_warnings);
249 let orphan_stubs_removed = if demote_to_stub {
250 self.store.remove_edges_from(id);
257 self.store.upsert(
258 id.clone(),
259 make_stub(
260 id,
261 crate::entity::StubKind::Residual {
262 since_commit: write_id.clone(),
263 readonly_referrers: readonly_referrers.clone(),
264 },
265 ),
266 );
267 warnings.push(WarningHint::ResidualStubForReadOnlyReferrers {
268 id: id.clone(),
269 referrers: readonly_referrers,
270 });
271 Vec::new()
277 } else {
278 self.store.remove(id);
279 gc_orphan_stubs(&mut self.store)
283 };
284
285 self.invalidate_communities();
286 self.maintain_search_indexes(std::slice::from_ref(id));
291
292 if !write_id.is_empty()
297 && let Some(w) = self.note_missing_warning("delete_entity", note)
298 {
299 warnings.push(w);
300 }
301
302 Ok(DeleteEntityOutcome {
303 id: id.clone(),
304 file_path,
305 removed_incoming,
306 relations_removed,
307 write_id,
308 orphan_stubs_removed,
309 warnings,
310 })
311 }
312
313 pub fn delete_entity_with_ctx(
317 &mut self,
318 id: &EntityId,
319 expected_hash: &str,
320 ctx: &CommitContext<'_>,
321 ) -> Result<DeleteEntityOutcome, EngineError> {
322 let args = DeleteEntityArgs {
323 id: id.clone(),
324 expected_hash: Some(expected_hash.to_string()),
325 };
326 self.delete_entity(args, ctx.actor, ctx.client.as_ref(), ctx.note.as_deref())
327 }
328}
329
330#[cfg(test)]
331mod tests {
332
333 use tempfile::TempDir;
334
335 use crate::backend::MemBackend;
336 use crate::engine::test_helpers::*;
337 use crate::engine::{
338 CreateEntityArgs, DeleteEntityArgs, Engine, EngineError, RelateEntityArgs,
339 };
340 use crate::ops::WarningHint;
341 use crate::storage::FilesystemMemWriter;
342
343 fn engine_with_anchored(
346 tmp: &TempDir,
347 title: &str,
348 ) -> (Engine, crate::engine::CreateEntityOutcome) {
349 let mem_dir = tmp.path().to_path_buf();
350 let writer = FilesystemMemWriter::new(mem_dir.clone());
351 let mut engine = Engine::from_mounts(vec![(
352 folder_mount("specs", mem_dir),
353 Box::new(writer) as Box<dyn MemBackend>,
354 )])
355 .unwrap();
356 let (actor, client) = cli_actor();
357 let mut args: CreateEntityArgs = empty_create_args("specs", title);
358 args.anchors = vec![crate::anchor::AnchorInput {
359 artifact: Some("src/lib.rs".into()),
360 grain: Some("file".into()),
361 class: Some("anchored".into()),
362 hash: Some("h1".into()),
363 hash_stability: Some("stable".into()),
364 ..Default::default()
365 }];
366 let outcome = engine
367 .create_entity(args, actor, Some(&client), None)
368 .unwrap();
369 (engine, outcome)
370 }
371
372 #[test]
373 fn delete_removes_entity_anchors_no_orphan() {
374 let tmp = TempDir::new().unwrap();
375 let (mut engine, seeded) = engine_with_anchored(&tmp, "Anchored Doomed");
376 assert_eq!(engine.entity_anchors(&seeded.id).len(), 1);
377 let (actor, client) = cli_actor();
378 engine
379 .delete_entity(
380 DeleteEntityArgs {
381 id: seeded.id.clone(),
382 expected_hash: Some(seeded.content_hash.clone()),
383 },
384 actor,
385 Some(&client),
386 None,
387 )
388 .unwrap();
389 assert!(engine.entity_anchors(&seeded.id).is_empty());
392 assert!(engine.anchors_referencing_artifact("src/lib.rs").is_empty());
393 }
394
395 #[test]
400 fn delete_through_the_engine_leaves_no_dangling_row() {
401 let tmp = TempDir::new().unwrap();
402 let (mut engine, seeded) = engine_with_anchored(&tmp, "Anchored Doomed");
403 let (actor, client) = cli_actor();
404 engine
405 .delete_entity(
406 DeleteEntityArgs {
407 id: seeded.id.clone(),
408 expected_hash: Some(seeded.content_hash.clone()),
409 },
410 actor,
411 Some(&client),
412 None,
413 )
414 .unwrap();
415 let report = engine.verify_mem_anchors("specs").unwrap();
416 assert_eq!(report.unreconciled, None, "the entity end was examined");
417 assert_eq!(
418 report.dangling, 0,
419 "the delete path took the row with it, so there is nothing to report"
420 );
421 }
422
423 #[test]
427 fn a_row_whose_entity_vanished_reads_dangling_not_resolved() {
428 let tmp = TempDir::new().unwrap();
429 let (_engine, seeded) = engine_with_anchored(&tmp, "Anchored Vanishing");
430 std::fs::remove_file(tmp.path().join(format!(
433 "{}.md",
434 seeded.id.as_ref().split_once("--").unwrap().1
435 )))
436 .unwrap();
437 let engine = Engine::from_mounts(vec![(
438 folder_mount("specs", tmp.path().to_path_buf()),
439 Box::new(FilesystemMemWriter::new(tmp.path().to_path_buf())) as Box<dyn MemBackend>,
440 )])
441 .unwrap();
442 drop(seeded);
443 let report = engine.verify_mem_anchors("specs").unwrap();
444 assert_eq!(report.dangling, 1);
445 assert_eq!(report.resolved, 0, "never evidence of health");
446 assert_eq!(
447 report.unresolvable, 0,
448 "and never folded into the artifact-end bucket, whose repair is the opposite"
449 );
450 assert_eq!(report.anchors[0].state, "dangling");
451 }
452
453 #[test]
454 fn delete_entity_removes_file_and_store_entry() {
455 let tmp = TempDir::new().unwrap();
456 let (mut engine, seeded) = engine_with_seed(&tmp, "Doomed");
457 let (actor, client) = cli_actor();
458 let outcome = engine
459 .delete_entity(
460 DeleteEntityArgs {
461 id: seeded.id.clone(),
462 expected_hash: Some(seeded.content_hash.clone()),
463 },
464 actor,
465 Some(&client),
466 None,
467 )
468 .unwrap();
469 assert_eq!(outcome.id, seeded.id);
470 assert_eq!(outcome.removed_incoming, Vec::<String>::new());
471 assert!(engine.get_entity(&seeded.id).is_none());
473 assert!(!tmp.path().join(&seeded.file_path).exists());
475 let log = std::fs::read_to_string(tmp.path().join(".memstead/changes.jsonl")).unwrap();
477 assert!(log.contains("\"kind\":\"delete\""));
478 }
479
480 #[test]
481 fn delete_entity_rejects_hash_mismatch() {
482 let tmp = TempDir::new().unwrap();
483 let (mut engine, seeded) = engine_with_seed(&tmp, "Locked");
484 let (actor, client) = cli_actor();
485 let err = engine
486 .delete_entity(
487 DeleteEntityArgs {
488 id: seeded.id.clone(),
489 expected_hash: Some("nope".to_string()),
490 },
491 actor,
492 Some(&client),
493 None,
494 )
495 .unwrap_err();
496 match err {
497 EngineError::HashMismatch {
498 id,
499 current,
500 is_stub,
501 } => {
502 assert_eq!(id, seeded.id.to_string());
503 assert_eq!(current, seeded.content_hash);
504 assert!(!is_stub, "real entity must not flag as stub");
505 }
506 other => panic!("expected HashMismatch, got {other:?}"),
507 }
508 }
509
510 #[test]
517 fn delete_entity_on_stub_with_non_empty_hash_surfaces_is_stub_flag() {
518 let tmp = TempDir::new().unwrap();
519 let (mut engine, _seed) = engine_with_seed(&tmp, "Anchor");
520 let (actor, client) = cli_actor();
521 let stub_id = crate::EntityId::new("specs", "stub-target");
530 let source = engine
531 .create_entity(
532 empty_create_args("specs", "Source With Link"),
533 actor,
534 Some(&client),
535 None,
536 )
537 .unwrap();
538 engine
539 .relate_entity(
540 RelateEntityArgs {
541 source: source.id.clone(),
542 expected_hash: Some(source.content_hash.clone()),
543 rel_type: "USES".to_string(),
544 target: stub_id.clone(),
545 remove: false,
546 description: None,
547 dry_run: false,
548 },
549 actor,
550 Some(&client),
551 None,
552 )
553 .unwrap();
554 assert!(
555 engine.store().contains(&stub_id),
556 "forward-reference relate must materialise stub"
557 );
558
559 let err = engine
563 .delete_entity(
564 DeleteEntityArgs {
565 id: stub_id.clone(),
566 expected_hash: Some("definitely-non-empty".to_string()),
567 },
568 actor,
569 Some(&client),
570 None,
571 )
572 .unwrap_err();
573 match err {
574 EngineError::HashMismatch {
575 id,
576 current,
577 is_stub,
578 } => {
579 assert_eq!(id, stub_id.to_string());
580 assert!(current.is_empty(), "stub has no content hash");
581 assert!(is_stub, "is_stub must be true on a stub mismatch");
582 let msg = format!(
583 "{}",
584 EngineError::HashMismatch {
585 id,
586 current,
587 is_stub
588 }
589 );
590 assert!(
591 msg.contains("stub") && msg.contains("expected_hash: \"\""),
592 "stub message must name the corrective action; got: {msg}",
593 );
594 }
595 other => panic!("expected HashMismatch, got {other:?}"),
596 }
597 }
598
599 #[test]
606 fn delete_entity_on_stub_with_empty_hash_succeeds_via_in_memory_route() {
607 let tmp = TempDir::new().unwrap();
608 let (mut engine, _anchor) = engine_with_seed(&tmp, "Anchor");
609 let (actor, client) = cli_actor();
610
611 let stub_id = crate::EntityId::new("specs", "ghost-target");
619 engine.store.upsert(
620 stub_id.clone(),
621 super::make_stub(&stub_id, crate::entity::StubKind::ForwardReference),
622 );
623 assert!(
624 engine.store().contains(&stub_id),
625 "injected orphan stub must be in store"
626 );
627
628 let outcome = engine
629 .delete_entity(
630 DeleteEntityArgs {
631 id: stub_id.clone(),
632 expected_hash: Some(String::new()),
633 },
634 actor,
635 Some(&client),
636 None,
637 )
638 .expect("stub delete with expected_hash=\"\" must succeed");
639
640 assert_eq!(outcome.id, stub_id);
641 assert!(
643 outcome.write_id.is_empty(),
644 "stub deletes skip the backend write; write_id is empty"
645 );
646 assert!(
648 !engine.store().contains(&stub_id),
649 "stub must be gone from the in-memory store"
650 );
651 assert!(engine.get_entity(&stub_id).is_none());
653 let log = std::fs::read_to_string(tmp.path().join(".memstead/changes.jsonl")).unwrap();
656 assert!(log.contains("\"kind\":\"delete\""));
657 }
658
659 #[test]
660 fn delete_entity_refuses_on_write_mem_referrers_with_typed_payload() {
661 let tmp = TempDir::new().unwrap();
662 let (mut engine, target) = engine_with_seed(&tmp, "Target");
663 let (actor, client) = cli_actor();
664 let source = engine
666 .create_entity(
667 empty_create_args("specs", "Source"),
668 actor,
669 Some(&client),
670 None,
671 )
672 .unwrap();
673 engine
674 .relate_entity(
675 RelateEntityArgs {
676 source: source.id.clone(),
677 expected_hash: Some(source.content_hash.clone()),
678 rel_type: "USES".to_string(),
679 target: target.id.clone(),
680 remove: false,
681 description: None,
682 dry_run: false,
683 },
684 actor,
685 Some(&client),
686 None,
687 )
688 .unwrap();
689
690 let err = engine
693 .delete_entity(
694 DeleteEntityArgs {
695 id: target.id.clone(),
696 expected_hash: None,
697 },
698 actor,
699 Some(&client),
700 None,
701 )
702 .unwrap_err();
703 match err {
704 EngineError::HasIncomingRefs { id, referrers } => {
705 assert_eq!(id, target.id.to_string());
706 assert_eq!(referrers.len(), 1);
707 let r = &referrers[0];
708 assert_eq!(r.from_id, source.id.to_string());
709 assert_eq!(r.rel_types, vec!["USES".to_string()]);
710 assert_eq!(r.mem, "specs");
711 }
712 other => panic!("expected HasIncomingRefs, got {other:?}"),
713 }
714
715 assert!(engine.get_entity(&target.id).is_some());
718 assert!(tmp.path().join(&target.file_path).exists());
719 }
720
721 #[test]
722 fn delete_entity_returns_write_id_and_relations_removed() {
723 let tmp = TempDir::new().unwrap();
724 let (mut engine, target) = engine_with_seed(&tmp, "Target");
725 let (actor, client) = cli_actor();
726
727 let source = engine
731 .create_entity(
732 empty_create_args("specs", "Source"),
733 actor,
734 Some(&client),
735 None,
736 )
737 .unwrap();
738 let related = engine
739 .relate_entity(
740 RelateEntityArgs {
741 source: source.id.clone(),
742 expected_hash: Some(source.content_hash.clone()),
743 rel_type: "USES".to_string(),
744 target: target.id.clone(),
745 remove: false,
746 description: None,
747 dry_run: false,
748 },
749 actor,
750 Some(&client),
751 None,
752 )
753 .unwrap();
754
755 let outcome = engine
756 .delete_entity(
757 DeleteEntityArgs {
758 id: source.id.clone(),
759 expected_hash: Some(related.content_hash.clone()),
760 },
761 actor,
762 Some(&client),
763 None,
764 )
765 .unwrap();
766
767 assert!(
769 !outcome.write_id.is_empty(),
770 "write_id must be populated on a real delete"
771 );
772 assert_eq!(outcome.relations_removed, 1);
774 assert!(outcome.orphan_stubs_removed.is_empty());
776 assert!(outcome.warnings.is_empty());
778 }
779
780 #[test]
781 fn delete_entity_garbage_collects_orphaned_stubs() {
782 let tmp = TempDir::new().unwrap();
783 let (mut engine, source) = engine_with_seed(&tmp, "Source");
784 let (actor, client) = cli_actor();
785 let stub_id = crate::EntityId::new("specs", "ghost-stub");
786
787 let related = engine
790 .relate_entity(
791 RelateEntityArgs {
792 source: source.id.clone(),
793 expected_hash: Some(source.content_hash.clone()),
794 rel_type: "USES".to_string(),
795 target: stub_id.clone(),
796 remove: false,
797 description: None,
798 dry_run: false,
799 },
800 actor,
801 Some(&client),
802 None,
803 )
804 .unwrap();
805 assert!(engine.store().contains(&stub_id), "stub must be in store");
806
807 let outcome = engine
810 .delete_entity(
811 DeleteEntityArgs {
812 id: source.id.clone(),
813 expected_hash: Some(related.content_hash.clone()),
814 },
815 actor,
816 Some(&client),
817 None,
818 )
819 .unwrap();
820
821 assert_eq!(outcome.orphan_stubs_removed, vec![stub_id.clone()]);
822 assert!(
823 !engine.store().contains(&stub_id),
824 "GC must drop the orphaned stub"
825 );
826 }
827
828 #[test]
837 fn delete_entity_demotes_to_stub_when_only_readonly_referrers_remain() {
838 use crate::engine::test_helpers::{archive_mount, build_archive};
839 use crate::workspace::{Mount, MountCapability, MountLifecycle, MountStorage};
840
841 let tmp = TempDir::new().unwrap();
842 let writable_dir = tmp.path().join("writable");
843 std::fs::create_dir_all(&writable_dir).unwrap();
844 let writer = FilesystemMemWriter::new(writable_dir.clone());
845
846 let archive_md = "---\ntype: spec\ncreated_date: 2026-01-15\nlast_modified: 2026-04-12\nlevel: M0\n---\n# Archived Source\n\n## Identity\n\nLinks to [[specs:target]].\n\n## Purpose\n\nFixture for residual-stub demotion.\n\n## Relationships\n\n- **REFERENCES**: [[specs:target]]\n";
851 let archive_path = build_archive(
852 tmp.path(),
853 "archive",
854 &[("archived-source.md", archive_md.as_bytes())],
855 );
856
857 let folder_mount = Mount {
858 mem: "specs".to_string(),
859 schema: Some(crate::engine::test_helpers::pin("default")),
860 storage: MountStorage::Folder {
861 path: writable_dir.clone(),
862 },
863 capability: MountCapability::Write,
864 lifecycle: MountLifecycle::Eager,
865 cross_linkable: true,
866 migration_target: None,
867 };
868 let archive_reader = crate::storage::ArchiveBackend::new(archive_path.clone());
869 let mut engine = Engine::from_mounts(vec![
870 (folder_mount, Box::new(writer) as Box<dyn MemBackend>),
871 (
872 archive_mount("archive", archive_path.clone()),
873 Box::new(archive_reader) as Box<dyn MemBackend>,
874 ),
875 ])
876 .unwrap();
877
878 let (actor, client) = cli_actor();
879 let target = engine
880 .create_entity(
881 empty_create_args("specs", "Target"),
882 actor,
883 Some(&client),
884 None,
885 )
886 .unwrap();
887
888 let archived_source_id = crate::EntityId::new("archive", "archived-source");
891 assert!(
892 engine.store().contains(&archived_source_id),
893 "archive entity must load into the store"
894 );
895 let incoming_pre: Vec<_> = engine
896 .store()
897 .incoming(&target.id)
898 .iter()
899 .map(|e| e.from.clone())
900 .collect();
901 assert!(
902 incoming_pre.contains(&archived_source_id),
903 "archive wiki-link must produce an incoming edge on target; got {incoming_pre:?}"
904 );
905
906 let outcome = engine
909 .delete_entity(
910 DeleteEntityArgs {
911 id: target.id.clone(),
912 expected_hash: Some(target.content_hash.clone()),
913 },
914 actor,
915 Some(&client),
916 None,
917 )
918 .unwrap();
919
920 assert!(!writable_dir.join(&target.file_path).exists());
922 let demoted = engine
924 .get_entity(&target.id)
925 .expect("residual stub must remain in store");
926 assert!(demoted.stub, "demoted entity must be flagged as stub");
927 assert!(demoted.entity_type.is_empty());
928 match &demoted.stub_kind {
932 Some(crate::entity::StubKind::Residual {
933 since_commit: _,
934 readonly_referrers,
935 }) => {
936 assert_eq!(
937 readonly_referrers,
938 &vec![archived_source_id.clone()],
939 "Residual.readonly_referrers must snapshot the surviving referrers at mutation time"
940 );
941 }
942 other => panic!("demoted stub must be tagged Residual; got {other:?}"),
943 }
944 let incoming_post: Vec<_> = engine
946 .store()
947 .incoming(&target.id)
948 .iter()
949 .map(|e| e.from.clone())
950 .collect();
951 assert!(
952 incoming_post.contains(&archived_source_id),
953 "archive incoming edge must survive demotion"
954 );
955 let referrers = outcome
957 .warnings
958 .iter()
959 .find_map(|w| match w {
960 WarningHint::ResidualStubForReadOnlyReferrers { referrers, .. } => {
961 Some(referrers.clone())
962 }
963 _ => None,
964 })
965 .expect("ResidualStubForReadOnlyReferrers warning must surface");
966 assert_eq!(referrers, vec![archived_source_id]);
967 assert_eq!(outcome.removed_incoming.len(), 1);
968 }
969
970 }