Skip to content

Name the file a download could not write, and the album an upload could not fill - #45

Open
ergofobe wants to merge 2 commits into
mainfrom
issue-34-error-seams
Open

ergofobe wants to merge 2 commits into
mainfrom
issue-34-error-seams

Conversation

@ergofobe

Copy link
Copy Markdown
Owner

Closes #34.

Two seams left by the error-reporting work of #21, #22 and #26.

1. A download IO failure named the asset and never the file

Failure::message() picked its subject as id.or(path). download.rs sets both, and
imogen_sdk::Error::Io wraps a bare std::io::Error that carries no path of its own, so a full
disk said:

warning: asset-7: io: No space left on device

Shape chosen: message() prefers the path for a failure that happened here — not download
dropping one of the two names. The record is what a script reads, and which asset and which
destination are both part of the answer; dropping either would lose information the run had.
Only the sentence has to choose one to lead with, and the honest rule is where the failure
happened: the server refused this asset, the filesystem refused this file. The marker is
#[serde(skip)], so the --json record is byte-for-byte what it was.

2. An album fill the server refused was a sentence on stderr

upload.rs rendered it as ctx.out.warn(format!("Could not fill an album: {error}")) — the last
place in a batch path formatting with {error} and discarding details. Under --json it was
absent from the document, addedToAlbums was silently short, and the exit status was 0.

It is now a Failure like every other, named by the album's id where a file's failure is named by
its path — which is also what tells the two apart in one failures list. It participates in the
rule #27 settled rather than inventing a second one: the file got through, so the run is partial
(exit 3), and --json still writes exactly one document.

That leaves a batch holding two kinds of item. Counting a refused fill among the files closed a
run that sent one file with 1 of 2 files failed, so the noun widens to item — the README's own
word for a batch's members — and only for a run that actually had one of the other kind.

Both audiences, against the release binary

$ imogen upload one.jpg --album Trip
warning: album-1: The request did not match what this endpoint expects (400 validation_failed)
  assetIds: That album will not take any more
Uploaded 1 file.
error: 1 of 2 items failed                      # exit 3

$ imogen --json upload one.jpg --album Trip     # exit 3, one document
{ "uploaded": 1, "addedToAlbums": 0, "failed": 1,
  "failures": [ { "id": "album-1", "error": "…", "details": { "assetIds": [ "…" ] } } ],
  "error": "1 of 2 items failed", … }

$ imogen download asset-1 -o <a path that cannot be written>
warning: /…/blocked/2024/06/harbour.jpg: io: Not a directory (os error 20)
error: 1 file failed                            # exit 1

A run with no album is unchanged: file stays the noun, exit 0 stays exit 0.

Tests

Written first, and both fail on main:

  • a_download_that_could_not_be_written_names_the_file — unit (the record keeps both names) and
    in tests/batch_exit_status.rs against the real binary. On main:
    warning: asset-1: io: Not a directory (os error 20).
  • an_album_that_could_not_be_filled_is_a_failure_like_any_other — the real binary, both
    audiences. On main: exit 0 where the rule wants 3.

The integration tests go in tests/batch_exit_status.rs, #27's harness, rather than a second stub
server inside upload.rs: $? and the bytes on stdout only exist once there is a process.

Scope

Nothing on the wire changed and the SDK pin stays at v0.5.0. #40 (what a --json batch emits
when there was nothing to do, and the --dry-run document shapes) is deliberately not taken —
it is a decision about empty batches and separable from this one. #25 (the TUI surface) is not
taken either.

Found, not fixed

Nothing.

Verify green locally: cargo test --locked && cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo build --release --locked.

🤖 Generated with Claude Code

ergofobe and others added 2 commits September 15, 2026 23:28
…ld not fill

Two seams the error-reporting work of #21, #22 and #26 created the conditions to
close and did not.

`Failure::message()` led with `id.or(path)`, but a download sets both and
`imogen_sdk::Error::Io` wraps a bare `std::io::Error` that carries no path of its
own. A full disk therefore said `warning: asset-7: io: No space left on device` —
which photograph, and never which file or directory to go and look at. The record
keeps both names, so the fix is which one the sentence leads with: a failure that
happened here is about the file, one the server sent is about the asset. The
marker is not serialized, because the record's shape is the API's own and both
names are in it either way.

`upload` still rendered a refused album fill with `{error}` onto stderr — the last
place in a batch path that dropped the `details` map. Under `--json` it was absent
from the document, `addedToAlbums` was silently short, and the run exited 0: a run
whose every fill was refused looked successful. Filing what was uploaded is part
of the run, so a refused fill is now a `Failure` like every other, named by the
album's id where a file's is named by its path, and it participates in the exit
rule #27 settled rather than inventing a second one.

That leaves a batch holding two kinds of item, and an album is not a file, so a
run that sent one file no longer closes with "1 of 2 files failed". The noun
widens to the README's own word for a batch's members, and only for a run that had
one of the other kind.

Both tests were written first and fail on main: the download names the asset, and
the upload exits 0.

Closes #34

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First review round.

The total the run closes with counted only the files that got through, while the
failures counted both kinds: a manifest filing into two albums where one fill is
refused did four things and three of them worked, and the sentence called it "1 of
3 items failed", dropping the fill that succeeded. The noun and the total have to
move together, because the noun names what is being counted — `download` has summed
the files it wrote with the ones it skipped under one noun since #27, and this is
the same rule with a second kind of item.

Two doc comments in `errors.rs` were left describing a world with one caller.
`with_id`'s said "an upload only has the file", which the album fill it now names
by id alone contradicts outright, and `for_id`'s named `assets edit` as though it
were the only way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ergofobe

Copy link
Copy Markdown
Owner Author

Code review

Round 1 (code-review at level medium, scoped to this PR). Found 2 issues, both significant, both fixed in 2998c61:

  1. The batch total counted only the files that got through, while the failure count included album fills. A manifest filing into two albums with one fill refused attempted four things and closed with 1 of 3 items failed, dropping the fill that worked. The noun and the total have to move together — download has summed the files it wrote with the ones it skipped under one noun since The batch commands disagree about exit status, and upload --json emits two documents #27.

};
// A file the server already had is not a file that failed, so it counts as got through.
crate::commands::finish_batch(ctx, summary, failures.len(), uploaded.len(), noun)
}

  1. Two doc comments left describing a world with one caller. with_id's said "an upload only has the file", which the album fill that is now named by id alone contradicts outright.

imogen-cli/src/errors.rs

Lines 80 to 87 in 5065516

/// `assets edit` works from ids and never touches a file.
pub fn for_id(id: impl Into<String>, error: &imogen_sdk::Error) -> Self {
Self::of(error).with_id(id)
}
/// A download knows which asset it was fetching; an upload only has the file.
pub fn with_id(mut self, id: impl Into<String>) -> Self {

Both carry a test that fails without the fix: a_fill_that_worked_is_counted_beside_the_one_that_did_not reports 1 of 3 on the old arithmetic.

🤖 Generated with Claude Code

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.

Two seams left by the error-reporting rework: a lost download path, and an un-migrated album failure

1 participant