Skip to content

An empty record_id must be an error, not a collection request - #28

Merged
igor-ctrl merged 2 commits into
mainfrom
fix/empty-record-id
Aug 4, 2026
Merged

An empty record_id must be an error, not a collection request#28
igor-ctrl merged 2 commits into
mainfrom
fix/empty-record-id

Conversation

@igor-ctrl

Copy link
Copy Markdown
Owner

Overview

Follow-up to #27. The key validation added there guarded with if record_id:, so a
falsy key skipped validation and skipped being appended — silently retargeting a
single-record operation at the whole entity set. Found by a stop-time review gate after
#27 had already merged, so 0.7.0 on main currently has it.

None legitimately means "address the collection" — bcli get <entity> with no id is
a collection read, and that behaviour is unchanged. "" took the same path, and
delete/patch declare record_id as a required positional with no default:

bcli delete engineOverviews ""

built DELETE .../engineOverviews rather than DELETE .../engineOverviews(id).
Whether BC honours a collection DELETE is beside the point — the client should not
construct it.

Changes

Both the validation and the append now key on is not None, so anything actually
passed gets validated, including the empty string. Nothing in the tree passed a literal
record_id="", and get_cmd already uses Optional[str] = typer.Argument(None) for
collection reads, so no caller changes behaviour.

Test plan

Note on release

Worth landing this before 0.7.0 goes to PyPI, so the published wheel doesn't carry the
empty-id path.

…equest

The validation added in the previous commit guarded with `if record_id:`, so a
falsy key skipped validation *and* skipped being appended — silently retargeting
a single-record operation at the whole entity set.

`None` legitimately means "address the collection": `bcli get <entity>` with no
id is a collection read, and that is unchanged. But `""` took the same path, and
`delete`/`patch` declare `record_id` as a required positional with no default,
so `bcli delete <entity> ""` built a DELETE against the entity set instead of a
row. Whether BC would honour that is beside the point — the client should not
construct it.

Both the validation and the append now key on `is not None`, so anything
actually passed is validated, including the empty string.
`--dry-run` reported success for an empty or path-bearing record_id and exited
0, while the same command without the flag exited 1. A preview whose entire job
is to predict must not disagree with what it predicts, and the documented
consumers of the JSON envelope are agents deciding whether to proceed — so this
was a false green.

The cause was `try_resolve_url`'s bare `except Exception: return None`. Never
raising is right for the audit path (a broken URL build shouldn't retroactively
fail a command that already ran) and right for incidental resolution failures (a
registry miss records a null URL and lets the preview through, as documented).
It is wrong for invalid input, which the real request validates and rejects.

`strict=True`, used only by the dry-run renderer, re-raises ValueError while
still swallowing BCLIError subclasses. Those don't inherit ValueError, which is
what makes the split clean rather than a guess. The renderer catches it and
prints the same `Error:` line the real run prints, because each write command's
dry-run branch sits above its own try/except, so a raw raise surfaced as a
traceback.

Tests needed a fixture without the existing `_StubClient`, whose `_resolve_url`
ignores record_id and always returns a URL. That stub is why this survived: the
dry-run suite never built a real URL, so nothing here could have caught it.
@igor-ctrl
igor-ctrl merged commit f008cac into main Aug 4, 2026
3 checks passed
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.

1 participant