diff --git a/src/DiffEngineViewer.Tests/HeldDeleteTests.cs b/src/DiffEngineViewer.Tests/HeldDeleteTests.cs
new file mode 100644
index 00000000..01e467e6
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/HeldDeleteTests.cs
@@ -0,0 +1,69 @@
+///
+/// When a sweep holds its deletes. A snapshot moving inline arrives as two unrelated entries - the
+/// patch and a delete of the verified file it replaces - so a patch the applier will not take must
+/// stop the delete, or the snapshot loses both copies at once.
+///
+public class HeldDeleteTests
+{
+ ///
+ /// A refusal recorded on a conflicted entry belongs to the targeted accept that made it, not
+ /// to the batch reading it. A bulk accept hands conflicted entries back untouched, so that
+ /// status sat there and held every pending delete on every later accept-all, citing a refusal
+ /// that had not happened.
+ ///
+ [Test]
+ public async Task A_refusal_left_on_a_conflicted_entry_does_not_hold_a_later_sweep()
+ {
+ var state = Conflicted();
+
+ // One variant accepted on its own, and refused - the file was locked, say. The entry
+ // stays, conflicted still, and carries what the applier said
+ var refused = ViewerSession.Apply(
+ state,
+ CommandKind.Accept,
+ Sweeping(InlineApplyResult.Failed("Failed to write: BTests.cs")));
+ await Assert.That(refused.Queue.Single(_ => _.Kind == QueueEntryKind.Inline).Status).IsNotNull();
+
+ var swept = ViewerSession.Apply(refused, CommandKind.AcceptAll, Sweeping(InlineApplyResult.Applied));
+ await Assert.That(swept.Queue.Where(_ => _.Kind == QueueEntryKind.Delete)).IsEmpty();
+ }
+
+ ///
+ /// The reason the read exists at all: a patch this batch could not write still holds the
+ /// deletes.
+ ///
+ [Test]
+ public async Task A_refusal_in_this_sweep_holds_the_deletes()
+ {
+ var state = ViewerSession.EnqueueTracked(
+ Fixtures.Inline(Fixtures.Patch("A.cs", 1)),
+ Fixtures.Delete());
+
+ var swept = ViewerSession.Apply(
+ state,
+ CommandKind.AcceptAll,
+ Sweeping(InlineApplyResult.NotFound("Could not locate the VerifyInline call")));
+
+ var delete = swept.Queue.Single(_ => _.Kind == QueueEntryKind.Delete);
+ await Assert.That(delete.Status).StartsWith("Held:");
+ }
+
+ ///
+ /// A delete that is actually deletable, so that a held one and a failed one cannot be confused
+ /// for each other: the fixture's default actions throw for anything touching a file.
+ ///
+ static ViewerActions Sweeping(InlineApplyResult result) =>
+ Fixtures.Applying(result) with
+ {
+ DeleteFile = static _ =>
+ {
+ }
+ };
+
+ static SessionState Conflicted() =>
+ ViewerSession.EnqueueTracked(
+ Fixtures.Inline(
+ Fixtures.Patch(content: "eight", framework: "net8.0"),
+ Fixtures.Patch(content: "nine", framework: "net9.0")),
+ Fixtures.Delete());
+}
diff --git a/src/DiffEngineViewer/ViewerSession.cs b/src/DiffEngineViewer/ViewerSession.cs
index 245a246d..f9f7abac 100644
--- a/src/DiffEngineViewer/ViewerSession.cs
+++ b/src/DiffEngineViewer/ViewerSession.cs
@@ -630,8 +630,12 @@ static SessionState SweepTracked(
/// not the last copy of it leaving.
///
///
- /// A status on an inline entry is the outcome of an attempt, so this reads failures only:
- /// conflicted entries that a bulk accept skips are handed back untouched and carry none.
+ /// A status on an inline entry is the outcome of an attempt, and this has to read only the
+ /// attempts the sweep it is answering for made. A bulk accept skips conflicted entries and
+ /// hands them back exactly as they were, so a status on one of those was left by a targeted
+ /// accept of a single variant, at some earlier point - and reading it held every pending
+ /// delete on every accept-all after it, citing a refusal in a batch that never touched the
+ /// entry. Everything else left in the queue with a status was tried and refused just now.
/// Discarding asks nothing of the patches, so it sweeps as it always did.
///
///
@@ -644,7 +648,7 @@ static bool InlineRefused(IReadOnlyList queue, bool discarding)
foreach (var entry in queue)
{
- if (entry is {Kind: QueueEntryKind.Inline, Status: not null})
+ if (entry is {Kind: QueueEntryKind.Inline, Conflicted: false, Status: not null})
{
return true;
}