Files
LibreMediaConverter/app
JMR-devandClaude Opus 5 c5c4c5323b Test the edge that feeds reattachment, and stop it reporting ENOENT
Reattachment.choose has twenty tests and every mutation aimed at it bites. Everything
that computes its inputs had none, and five mutations there passed the whole 257-test
suite. Four are closed here, each verified by applying the mutation and watching the new
test go red.

jobSnapshots() is the half that has to touch WorkManager and the filesystem, so it is
where the untested values live. JobSnapshotsTest drives it against a real WorkManager and
a real cacheDir:

 - A zero-byte staged file is not an output. Relaxing the filter to `exists()` -- which
   is what a job killed before its engine wrote anything leaves behind -- made the
   snapshot claim a result, and the user would meet a Save button for a zero-byte
   "conversion". The same case pins that the path is still reported and that the mtime
   stays 0 for a file that is not a result.
 - Each result carries its own file's mtime. Hardcoding it to zero starves the
   newest-file tie-break of the only data it has, which is precisely the failure the
   tie-break exists to prevent: the query has no ORDER BY, so an arbitrary winner keeps
   winning every launch. Timestamps are set with setLastModified and compared against what
   the filesystem stored, because mtime granularity is not this test's claim to make.

ReattachGuardsTest covers the two decisions the ViewModel makes that the pure rule cannot:

 - A file picked while the query was still in flight is not reattached over. Deleting the
   guard turns the user's pick into yesterday's job -- with the Save button pointing at a
   file the card does not name. Made deterministic by holding WorkManager's task executor
   rather than by racing two IO hops: the query cannot finish until the pick has landed.
   The test also asserts the brake really gripped, so a reattachment that never arrived
   cannot pass for one that was refused.
 - An Ambiguous result is offered without being attributed. Two finished jobs naming one
   staged file is what the device produced before staging was keyed on the job id; taking
   the first job's tags labels the file with the other conversion's name, which is the
   confident lie the KDoc rejects. The neutral label and the absent size are both pinned.

ReattachmentTest's FAILED exclusion was only ever tested with pathless FAILED jobs, so a
narrow regression ranking a FAILED job that carries a file like a result passed all 257
tests. The live shape is the 2 MB orphan the device pass found: a job killed mid-write
leaves a partial, and under that regression the user is offered a truncated file with a
Save button. One fixture with outputPath and outputExists set closes it.

save() re-checks the staged file, in both ViewModels. The check reattachment made ran
inside a tag query that can be hours older than the tap, and cacheDir is what the OS
empties when it wants space and what the sweep collects after a day. The file's absence
used to arrive as staged.inputStream() throwing, and e.message put
"/data/user/0/.../4b4882....mp4: open failed: ENOENT" on screen -- a true statement about
a path the user has never seen and cannot act on. It now reads as a sentence with an
action in it. The message is one constant next to OutputPublisher because both ViewModels
need it and staging is what it is about.

Reattachment's KDoc claimed a defect that was fixed in the commit before it -- that "Start
over" keeps its staged file -- which would send a maintainer to re-fix D2. Rewritten to
say what is actually true: the delete happens, and the gap it leaves is the reset() whose
delete is cancelled with the Activity, which is the sweep's job and is named in the
sweep's own KDoc.

R1 / #10, R2 / #11, R24 / #33, R25 / #34

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 23:06:37 -05:00
..
2026-08-19 17:29:15 -05:00