fix(cli): make batch validate like render and report what actually happened - #146
Merged
Merged
Conversation
…ppened
`batch` produces N videos in one shot, and it was the one path that told
the truth about none of them.
With `--jobs > 1` a worker panic was swallowed: `let _ = h.join()` was true
to its comment ("panics are surfaced as failures below") in name only —
nothing below inspected the join result, and the closure's bookkeeping only
runs on the return path a panic skips. A batch where every render panicked
printed "0/N succeeded" and exited 0. Panics are now recorded as failures,
with a final guard should a row ever go neither counted nor reported.
Preflight ran a parse-only dry run, so `batch` skipped the schema and
geometry pass `render` applies — the viewport-overflow gate CLAUDE.md makes
mandatory was bypassed by the very mode that renders in bulk. It now runs
`validation::run_checks` per row, with render's no-flag defaults, and fails
the batch before a single frame is drawn.
`--name-template` interpolates values straight from the data file, so a row
carrying `../escaped` or `/tmp/absolute` wrote outside `--output-dir`.
Names are now rejected in preflight when they contain a parent-dir, root or
prefix component. The check is lexical rather than canonicalising: a
canonicalize-and-compare fails with ENOENT on a subdirectory that does not
exist yet, which would have broken the legitimate `{lang}/{id}.mp4` form.
This was referenced Aug 8, 2026
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #142 — CLI workstream, second of three. Four confirmed findings, all in
commands/batch.rs.batchrenders N videos in one shot. It was the one command path that told the truth about none of them.Silent success on worker panic
let _ = h.join();carried the comment "panics in threads are surfaced as failures below". Nothing below ever inspected the join result, and the worker closure's bookkeeping runs only on theOk/Errreturn path — which a panic skips entirely. So a batch where every render panicked printedBatch complete: 0/2 succeeded., left an empty output directory, and exited 0.Panics are now pushed onto
failureswith a readable payload. A final guard fires ifsuccess_count + fail_count != total— defence in depth against a future row going neither counted nor reported.Geometry validation bypassed
Preflight ran a parse-only dry load, so
batchskipped the schema + geometry passrenderapplies. The same template thatrenderrejects with1 geometry violation(s)sailed throughbatchand produced an overflowing video, reported as success.validation::run_checksnow runs per row with render's no-flag defaults, failing the whole batch before a frame is drawn. This is the mode that renders in bulk — it must not be the one that skips the gateCLAUDE.mdmakes mandatory.Writes outside
--output-dir--name-templateinterpolates values straight from the data file, so a row carrying{"id":"../secret/escaped"}or{"id":"/tmp/absolute"}wrote outside the output directory. Names are rejected in preflight when they contain a parent-dir, root or prefix component.Lexical, not canonicalising — deliberately.
canonicalize()fails with ENOENT on a subdirectory that does not exist yet, which would have broken the legitimate{lang}/{id}.mp4form the existingfield_placeholder_resolvedtest covers. A regression test pins that nested-output case alongside the two escape cases.Notes for the reviewer
The two fixes interact, and the interaction is why one test looks different from the audit's reproduction. Wiring geometry validation into preflight made the audit's original panic repro (odd 321×241 dimensions) unreachable —
batchnow rejects those scenarios before spawning a thread. The panic-capture test therefore uses a different trigger (70000×70000, which overflowsu32arithmetic in the openh264 path) to exercise the same mechanism. The batch path gained a second closed panic vector as a side effect.No
--lenient/--no-validate/--strict-animparity withrender. The audit suggested it; adding it needs new fields onCommands::Batchinlib.rs. Deliberately not done: bulk rendering is exactly where an escape hatch from validation costs the most, sobatchis always strict. Additive later if a real need appears.Verification
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace— 752 passed, 0 failed, re-run by the orchestrator on the integrated tree. 9 new tests, each written red first against the audit's reproduction.