No regression test covered the non-destructive source-edge delete semantics (D8) defined in the Phase 3 openspec. The delete path and executeSources filter behavior were correct but unverified: deleting a node whose identity appeared in another node’s sources list could theoretically cascade to subscribers, and a transient peer outage could in principle trigger destructive cleanup.
Phase 2 unified dispatch (executeSources) and Phase 3 affirmed the dangling-source design decision (D8: “absence is non-destructive; subscriber self-heals on reconnect”), but neither phase shipped assertion coverage. The structural parent→child cascade was pre-existing and correct; source-wiring edges were intentionally left intact on delete. There was no test to confirm the filter in executeSources excluded DELETING-state subscribers, nor that delete() respected the parent-field boundary and did not touch nodes reachable only via sources.
SourceDeleteSemanticsTest in server/src/jvmTest/kotlin/krill/zone/server/ with six deterministic tests:
delete(parent) marks both parent and child DELETING via recursion on the parent-field relationship.delete(sourceNode) does NOT mark subscribers DELETING and does NOT call nodePersistence.delete on them; the dangling NodeIdentity reference persists until the user clears it.nodeAvailable returns false for both null (offline peer, simulates “node never replicated locally”) and DELETING-state nodes — the two cases are visually identical to subscribers.executeSources skips DELETING-state subscribers without mutation; the proof is absence of KoinNotInitializedException (Koin is not started in unit tests; if the DELETING filter were absent, wakeFromSource → processor() → Koin inject would throw).delay(), no real prod paths, nodes carry host="peer-host" so isMine() returns false without touching ~/.krill/install_id.sources reference is in a valid, expected state; the UI shows “Missing” for unavailable sources and self-heals on reconnect. Do not add cleanup passes that remove stale sources references on absence — that would break the transient-tolerance invariant.targets field and its 8 processor consumers survive until Phase 4. New editors must not expose a targets picker; existing processor reads of meta.targets are intentionally preserved.SourceDeleteSemanticsTest as a regression gate.delete() behavior: mock NodePersistence, verify nodePersistence.save(match { it.state == NodeState.DELETING }) calls by node ID. Do not use isMine()-sensitive hosts in test fixtures; use host = "peer-host" (or any non-UUID string).