test(chapters): an upload end to end, and the callback it found missing
Phase 9 of docs/chapter-uploads.md. ChapterUploadJvmTest drives the view model over a real database: five files become five sessions, one chapter leading its own chunks in each -- never two chapters sharing a batch -- numbered contiguously from zero, which the old `.size` numbering could not do because nothing is signed while the pass runs. Each chapter's chunks are the blocks of its own file, markers off. Four chapters in a three-dialect group ask for one scaffolding session of twelve rows, not four. It found one thing: propose() with nothing proposable returned without ever calling its callback. The FAB is disabled in that case so the screen never reached it, but a callback that sometimes never fires is a trap for the next caller, so it now fires onDone(0, 0). That is the whole reason for writing the end-to-end test rather than assuming the parts composed. One thing it deliberately does not test the way the plan asked. "A failure on file 4 leaves files 1-3 proposed" is unreachable by input: both caps are checked in the form against the same functions the manager enforces, and ChapterProposalSizeTest asserts the form's estimate is never under the manager's measurement, so no file passes the review and then fails the proposal. It is reachable by state, which is what the test does -- a room with no shared key fails all three, every row says so and stays retryable, and a second pass after the key arrives proposes all three from zero, the failed pass having spent no indices because it created nothing. 995 tests, no failures; m3Audit's budgets all met. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,10 +198,20 @@ class UploadChaptersViewModel(
|
||||
onDone: (proposed: Int, failed: Int) -> Unit,
|
||||
) {
|
||||
val loaded = _uiState.value as? UploadChaptersUIState.Loaded ?: return
|
||||
|
||||
// A pass already running will call [onDone] itself when it finishes;
|
||||
// firing it here as well would announce one pass twice.
|
||||
if (loaded.isProposing) return
|
||||
|
||||
val queue = loaded.proposable.map { it.id }
|
||||
if (queue.isEmpty()) return
|
||||
if (queue.isEmpty()) {
|
||||
// Nothing to do still has to be said. The FAB is disabled in this
|
||||
// case so the screen never reaches it, but a callback that
|
||||
// sometimes never fires is a trap for the next caller -- and it is
|
||||
// what a test of a second tap found.
|
||||
onDone(0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
updateLoaded { it.copy(isProposing = true) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user