If a flush that prunes block files fails before the block index write, the files are already marked pruned in memory, but setFilesToPrune is a local in FlushStateToDisk(). The next successful flush writes the pruned file info and never deletes the blk/rev files. Only ScanAndUnlinkAlreadyPrunedFiles() deletes them, at the next startup.
This is number 7 of ryanofsky's [post-merge review of #35714](/bitcoin-bitcoin/35714/#pullrequestreview-5697890316). The block file flush failure from #35714 and the blocks directory CheckDiskSpace() failure both return before that write.
I worked on this because it lets me test my kernel pruning refactor (for #35627 and kernel-node/kernel-node#111) against the issue he describes. The refactor moves the flush, block index write and file deletion that a pruning flush runs from FlushStateToDisk() into WriteBlockStorage(), and no unit test ran that sequence in prune mode. I rebased my refactor onto this branch and they pass.
The first three commits are test the current behavior for a flush after a failed one writes what was skipped, but a failed prune flush still leaves its files on disk after a retry.
The last commit keeps the selected files in BlockManager and deletes them after the next block index write. If that write fails they are dropped with the dirty entries, which means a file is only deleted once it is marked pruned on disk. Pending files don't force a write. The next flush that writes deletes them.
The disk-space failure and automatic pruning take the same path but have no test. Neither does the FindNextBlockPos() line that drops a file from the pending set when a block is written to it, which can happen after an assumeutxo snapshot validates.
I will be opening follow-ups as drafts based on top of this shortly.