Skip to content

Give the batch commands one exit status and one JSON document - #39

Merged
ergofobe merged 5 commits into
mainfrom
issue-27-batch-exit-status
Sep 16, 2026
Merged

ergofobe merged 5 commits into
mainfrom
issue-27-batch-exit-status

Conversation

@ergofobe

@ergofobe ergofobe commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

Closes #27.

upload, download and edit each ran a batch and each decided for itself what a refusal meant:

  • edit exited 0 when every edit was refused. It printed Edited 0 photographs. and returned Ok(()). A script that submitted a hundred edits and had a hundred refused saw success — which is what undercut assets edit drops the fields a 400 named, and prints raw JSON at the person #23, since the rejection said which field was wrong and $? still said nothing was.
  • upload --json wrote two documents when a file failed. The summary, then main::report's {"error":"N files failed",…} after it, on the same stdout. jq and every ordinary parser choke on the second.
  • download --json exited 0 where the identical run in prose exited 1. Its JSON branch returned before the bail!.

The rule

One finish_batch in src/commands/mod.rs, called by all three.

Exit status. 0 when nothing was refused; 1 when nothing got through; 3 when some items succeeded and some were refused.

The partial case is the decision the three drifted apart over, so it is worth saying why 3. A script that fires a hundred uploads wants to know which failure it has: "the server is down / the token is wrong / nothing landed" calls for stopping and shouting, and "ninety-seven landed, three were refused" calls for reading failures and retrying those three. Collapsing both into 1 throws that away and costs a document read to recover. 2 is not used, because clap already exits with it for a command line it could not parse, and a script cannot tell a usage error from a batch outcome if they share a code. A file the server already had, and a download skipped because it was already on disk, count as got through: the caller asked for the file to be there and it is.

One document. In --json a failing batch writes nothing to stdout itself. The summary travels out with BatchFailure and main::report writes it once, with the sentence added under error so a reader looking only for that key is not misled by a document full of items. Success still writes the summary from the command. Whichever way the run went, stdout parses as exactly one document.

Prose. A run that got nothing through no longer closes with a green summary line saying so — Edited 0 photographs. immediately above error: 2 photographs failed was the success line contradicting the exit status. All three leave it out when nothing succeeded.

This is a behaviour change for scripts

Someone may be relying on today's zero. Plainly:

Run Today Now
edit with every edit refused 0 1
edit partly refused 0 3
download --json with a failure 0 1 or 3
upload/download prose, all refused 1 1
upload/download partly refused 1 3
anything wholly successful 0 0

The prose sentence for a wholly failed run is unchanged (error: 3 files failed); a partial one now reads error: 1 of 5 files failed.

Tests

Written first and watched fail — all three failed against main for exactly the reason #27 gives, and the upload one printed the two concatenated documents verbatim.

They run the real program against a stub HTTP server (std TcpListener, no new dependency), because $? and the bytes on stdout only exist once there is a process. The single-document property is asserted by parsing the whole of stdout as a JSON stream and counting what comes out — a second document is invisible to a grep and obvious to a parser.

cargo test --locked && cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo build --release --locked is green: 137 unit, 4 integration.

Found, not fixed

🤖 Generated with Claude Code

ergofobe and others added 5 commits September 15, 2026 22:39
`upload`, `download` and `edit` each ran a batch and each decided for
itself what a refusal meant. An edit the server refused entirely printed
"Edited 0 photographs." and exited 0. `download --json` returned before
the failure was raised and exited 0 where the same run in prose exited 1.
`upload --json` wrote its summary and then let the failure write a second
document to the same stdout, which no ordinary parser will read.

One rule now, in `finish_batch`:

- Nothing refused exits 0; nothing got through exits 1; some of each
  exits 3. "Nothing worked" and "most of it worked" ask different things
  of the script that called, and 2 is left to clap's usage error.
- `--json` puts exactly one document on stdout whatever happened. On a
  failure the command writes nothing there: the summary travels out with
  `BatchFailure` and `main` writes it once, with the sentence added under
  `error` so a reader looking only for that key is not misled.

This is a behaviour change for scripts. A run that today exits 0 because
every edit was refused, or because a download failed under `--json`, now
exits non-zero, and a partly refused run exits 3 rather than 1.

The tests run the real program against a stub server, because `$?` and
the bytes on stdout only exist once there is a process; stdout is parsed
as a stream and the documents are counted, since a second one is
invisible to a grep.

Closes #27

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bullet above it promises `{"error", "causes"}` for a `--json` error,
and a batch failure's document is the run's summary with `error` added —
no `causes`, because the run did not fail, some of its items did. A
reader keying off `causes` had no way to know that from the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`edit` printed "Edited 0 photographs." in green and then, now that the
run is non-zero, `error: 2 photographs failed` under it — the success
line contradicting the exit status. `upload` and `download` had the same
shape. All three now leave the closing line out when nothing got
through: the warnings and the sentence `main` prints are the whole of
what happened.

Also: the README claimed one document reaches stdout whatever happened,
which an empty batch does not do — it says so on stderr and writes
nothing. And the partly-refused upload test picked its victim by request
ordinal, so a preflight the program learns to make one day would have
become the refused request; it keys on the endpoint instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two `report`s in one function, the file handle and the JSON answer. The
JSONL writes all happen before the shadow, so nothing was wrong today —
it is the next edit that would find out. The answer is `summary`, which
is what `finish_batch` calls its parameter, and the prose line is the
`sentence`.

The README's guarantee also covered `--dry-run`, which lists a plan and
has no `failed` to give.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One promised `causes` on every JSON error; the other said a batch has
none. The first now says which kind of failure it describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ergofobe
ergofobe merged commit 455d52c into main Sep 16, 2026
2 checks passed
@ergofobe
ergofobe deleted the issue-27-batch-exit-status branch September 16, 2026 02:51
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.

The batch commands disagree about exit status, and upload --json emits two documents

1 participant