Skip to content

fix(filemover): log a blocked or failed mutation above Information - #853

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug16-blocked-mutation-visibility
Open

fix(filemover): log a blocked or failed mutation above Information#853
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug16-blocked-mutation-visibility

Conversation

@m4bard

@m4bard m4bard commented Aug 20, 2026

Copy link
Copy Markdown

Summary

LogMutation logs every outcome at Information, so a refusal to perform a file mutation sits at the same level as a successful one. Blocked and Failed both mean the action did not happen.

This changes the level for those two and nothing else. Full write-up in #852.

Changes

Fixed

  • FileMover.LogMutation selects Warning for Blocked and Failed, Information otherwise.

Why this and not more

The case that surfaced it is the Unix cross-volume Move refusal, which is deliberate and states its reason: the source retirement cannot be generation-fenced without a library-side namespace claim. That refusal is not touched here and I am not proposing it should be. What this changes is that an operator watching at the default level can now tell the difference between a move that was refused and a move that succeeded.

It applies to all outcomes rather than special-casing the cross-volume one because none of the seven Blocked sites in this class is routine: a non-publishable action, an empty operation ID, unavailable durable markerless state, linked aliases of the same file. Every one is a refusal to do what was asked.

Worth stating plainly: this does not make cross-volume moves work. It makes their refusal audible. Whether to implement the namespace claim described in that reason is a separate and much larger question.

Testing

FileMoverMarkerlessMoveTests gains a case that drives the real refusal through the existing ForceCrossVolumeForTest hook and asserts the level rather than the message text. CreateMover gains an optional ILogger<FileMover>; it previously passed NullLogger unconditionally, which is why no existing test in that file could observe a level. There is already a test asserting this move is rejected, so the gap was never whether it refuses, only how loudly.

Verified as a real guard: with the level flattened back to Information the new test fails with Expected: Warning, Actual: Information, and passes with the change.

Full suite: 3,030 passed, 0 failed, 125 skipped, against a 3,029 baseline on 03958c15.

Reproduced end to end against ghcr.io/listenarrs/listenarr:canary with a public check that runs the same import twice, once with the source and root folder on one mount and once across two. The same-mount case is the control and completes; the cross-mount case produces no destination and leaves the source untouched. The check is in the test-data repo linked from the issue, and it reports "nothing happened at all" as its own verdict rather than as an ordinary failure, since a stall and a failure are different things to operate.

LogMutation logged every outcome at Information, so a refusal to perform the
requested action sat at the same level as a successful one. Blocked and Failed
both mean the mutation did not happen.

That matters most for the Unix cross-volume Move refusal, which is deliberate and
says so in its reason: the source retirement cannot be generation-fenced without a
library-side namespace claim. The refusal is not the bug. Its visibility is. The
caller gets back a bare false, FileMutationResult is constructed and discarded with
no consumer anywhere outside this class, and the operator is left watching a file
that never arrives with nothing in the log that reads as a problem.

None of the seven Blocked reasons in this class is routine chatter. Each is a
refusal to do what was asked, so the level is wrong for all of them rather than
only for the cross-volume one.

The test drives the real path through the existing ForceCrossVolumeForTest hook and
asserts the level rather than the text, using the CapturingLogger shape already used
by the NZBGet and Usenet adapter tests. CreateMover gains an optional logger; it
defaulted to NullLogger, which is why no existing test could see this.
@m4bard
m4bard requested a review from a team August 20, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant