Skip to content

[obvious-fleet] Backend: served capture, events, and result bodies decode the way the Swift client decodes them - #285

Merged
SamGu-NRX merged 3 commits into
alphafrom
obv/burst11-bs-swift-contract-parity
Oct 16, 2026
Merged

SamGu-NRX merged 3 commits into
alphafrom
obv/burst11-bs-swift-contract-parity

Conversation

@obvious-autobuild

@obvious-autobuild obvious-autobuild Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

fleet-packet: burst11-bs-fix-contract-parity-285

What changed and why

The iOS client decodes this API with plain Codable — all-or-nothing. One wrong value anywhere in a body makes the whole body unreadable, and the uploader ends the scan answerUnreadable. PR #257 pinned the upload-side key names; nothing pinned the result envelope, the event data fields, the terminal/outcome-less status sets, or the error shape the app's CaptureAPI.errorCodes reads.

This PR adds backend/server/api/tests/test_capture_device_decoder_contract.py: a Python mirror of Swift Codable decoding for exactly the structs the app declares (CaptureAPI.swift, CaptureResult.swift), then runs every body the server actually serves through it — create, register, PUT, commit, finalize (both 202 shapes), events including an empty long-poll timeout, result for every reachable status, review/preview refusals, and every error body the walk produces.

Result: the served shapes already match Swift. One real gap surfaced — three answers the route handlers never produce used bodies the app cannot decode.

server/api/app.py now enforces INTEGRATION.md's "Errors are ApiError" for framework-level answers:

Answer Before Now
Route miss (404) plain {"detail": "Not Found"} not_found
Method mismatch (405) plain text method_not_allowed
Unhandled exception (500) plain text internal (Starlette still logs the traceback)

Review fixes (threads on head 15f0bed)

  • Framework headers survive the rewrite (Codex P2 + Greptile P2, app.py:239). http_error forwards exc.headers into the ApiFailure response, so a 405 keeps Starlette's required Allow list and any other framework headers (e.g. WWW-Authenticate). The 405 contract test now also asserts Allow names GET. Test (failing-first) in bc14fd2, fix in a7363eb.
  • The Int mirror accepts exactly integral numbers (Greptile P2, test_capture_device_decoder_contract.py:76). Swift's JSONDecoder round-trips through NSNumber: 1.0 and 1e0 decode into Int; 1.5, booleans, strings, and values outside Int64 still fail. as_int mirrors that, and the module docstring, mirror comment, and self-test now state the correct rule. Test (failing-first) in bc14fd2, fix in a7363eb.
  • Ready-result cases now serve the real producer's output (Greptile P2, :843). Three cases run the composer's own CLI (python -m server.result.compose, the make result-golden invocation, in a subprocess because the import contract keeps api tests out of the result package) over the committed synthetic fixtures — synthetic → complete, photo → needs_views, reject → manual_review — and serve exactly the result.json bytes the coordinator uploads as result/result.json, through the route and the decoder. A hand-written _outcome can no longer mask producer drift. Test in bc14fd2, hardened in a7363eb.

How I verified it

Test logs (local, uv run --frozen)
$ uv run --frozen pytest server/api/tests/test_capture_device_decoder_contract.py -q \
>   -k "int_mirrors or method_not_allowed or real_composer_output"        # before the fixes
FAILED ...::test_int_mirrors_swift_number_handling      # as_int(1.0) still raised DecodeFailure
FAILED ...::test_method_not_allowed_uses_the_documented_api_error_shape   # KeyError: 'Allow'
2 failed, 3 passed, 24 deselected in 0.55s              # the 3 real-composer cases passed pre-fix

$ uv run --frozen pytest server/api/tests/test_capture_device_decoder_contract.py -q   # after the fixes
29 passed, 1 warning in 1.92s

$ uv run --frozen pytest server/api/tests -q
208 passed, 1 warning in 13.04s

$ make lint
ruff check .            clean
ruff format --check .   clean
lint-imports            Contracts: 5 kept, 0 broken.

# Mutation check: drop the required memberActions key from the served composer output
3 failed (Swift keyNotFound on every case)   ->   reverted   ->   3 passed
  • Contract file: 29 tests, all green (the 3 new real-composer cases included)
  • Focused suite: server/api/tests — 208 passed
  • make lint: ruff check, ruff format --check, lint-imports — 5 kept / 0 broken
  • Failing-first: the Int-mirror test and the 405/Allow assertion both failed on the pre-fix tree; the composer cases bite under a mutation and pass on real output
  • No contract or schema change; contracts untouched since the make schemas no-drift check reported at 15f0bed

What is not verified

  • No Xcode here: the Swift side of the mirror is read from CaptureAPI.swift/CaptureResult.swift, not compiled. An iOS build is what would fully confirm the mirror against the app.
  • CI runs local-only: backend/** has no GitHub Actions workflow (pre-existing repo-wide gap), so the GitHub checks on this PR stay empty.
  • The known full-suite failures (4 in server/eval/tests/test_gates.py, 14 errors in tests/test_capture_cold_restart.py) were not touched and not rerun this turn.

Assumptions

  • Each review thread is resolved with the fix commit named in its reply; a reviewer who disagrees can unresolve and the thread reopens.
  • The composer CLI in a subprocess counts as the real producer output — it is the same code path make result-golden uses; done to keep the api → result import boundary the linter enforces.
  • as_int follows Swift Foundation's NSNumber round-trip semantics: exactly integral numbers decode, fractional/bool/string/out-of-range fail.

Human author: Sam Gu (sgu07966@gmail.com)

…p reads

A route miss, a method mismatch, and an unhandled stage crash answered with
plain-text or {"detail": ...} bodies, so the device's CaptureAPI.errorCodes
read no code from them and the app could only show a generic transport
failure. INTEGRATION.md promises "Errors are ApiError"; those three answers
now serve it too (404 not_found, 405 method_not_allowed, 500 internal),
while Starlette still logs the traceback.

test_capture_device_decoder_contract.py pins the whole served surface the
way Swift Codable decodes it: a strict mirror of the app's structs runs
every upload-walk, events (including an empty long-poll timeout), result,
review/preview-refusal, and error body through required-vs-optional keys,
Int-vs-Double strictness, unknown-key tolerance, and unknown-enum decoding.
The three error-shape tests failed on the pre-fix app; all 26 pass on this
one. Decisions and open contract questions are recorded in
backend/docs/hunter-contract-questions.md.

Co-Authored-By: Sam Gu <sgu07966@gmail.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 447fcb7b-fff4-4831-b7f5-6ca137c30f59

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:M 30-99 effective changed lines. label Oct 16, 2026
@codemetrics-ai

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add BaseScanning to your team's repositories.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15f0bed975

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread backend/server/api/app.py Outdated
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T11:06:34.197206Z a7363eb Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; the remaining issue is a non-blocking documentation correction.

Summary

The PR adds tests that check served API bodies against the Swift client's expected fields and types. It also gives framework errors the documented error body.

Since the previous review:

  • Framework errors reach the client in the capture API’s JSON error shape.
  • The capture API’s served replies are checked against the iOS decoder.

Reviews (2) · Last reviewed commit: "fix(backend): the 405 keeps its Allow he..." · Reviewed by Greptile

Comment thread backend/server/api/app.py Outdated
Comment thread backend/server/api/tests/test_capture_device_decoder_contract.py Outdated
Comment thread backend/server/api/tests/test_capture_device_decoder_contract.py
…ts integral numbers

Starlette's exception headers (the 405 Allow list, WWW-Authenticate) now pass
through the ApiError rewrite, and the Codable mirror's Int accepts exactly
integral numbers (1.0, 1e0) the way JSONDecoder's NSNumber round-trip does
while still rejecting 1.5, bools, strings, and out-of-Int64 values. The
ready-result cases now serve the real composer CLI output for the committed
synthetic fixtures, so producer drift fails the decoder check instead of
escaping it.
@obvious-autobuild obvious-autobuild Bot changed the title Backend: served capture, events, and result bodies decode the way the Swift client decodes them [obvious-fleet] Backend: served capture, events, and result bodies decode the way the Swift client decodes them Oct 16, 2026
@obvious-autobuild

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: a7363eb6ed

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@SamGu-NRX
SamGu-NRX merged commit 6c6fac3 into alpha Oct 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants