Skip to content

[obvious-fleet] Backend: one repeatable no-cloud capture-flow runner with a JSON receipt - #258

Merged
SamGu-NRX merged 2 commits into
alphafrom
obv/burst5-20261015-bs-local-runner
Oct 16, 2026
Merged

SamGu-NRX merged 2 commits into
alphafrom
obv/burst5-20261015-bs-local-runner

Conversation

@obvious-autobuild

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

Copy link
Copy Markdown

fleet-packet: burst5-20261015-bs-local-runner

What changed

Reviewers who want to replay the capture journey by hand had two bad options: assemble the environment variables, uvicorn command and curl sequence from backend/server/api/README.md themselves, or read the journey out of a pytest file. This adds one checked-in command:

cd backend && uv run --extra api python scripts/capture_flow_e2e.py

It copies the committed data/fixtures/packets/mini-arkit packet into a fresh temporary directory, starts the real app entry point create_app() in-process, and sends the app's own sequence: create the capture, register files, PUT bytes to the signed upload URLs, commit, finalize packet.json, read status and events. When it finishes it writes a small JSON receipt (<workdir>/receipt.json) and exits 0 only if every stage matched what the app should do — including a crash before any stage, which now fails closed.

Shell-only recipe vs the checked-in runner. The README's shell recipe is the same journey, but a reviewer must set six environment variables, pick a volume root, mint two tokens, start a server, then retype roughly six requests with correct idempotency keys and the X-Packet-Sha256 header — every step hand-carried, and the evidence (response bodies) lives only in the reviewer's scrollback. The runner keeps the same app entry point and the same request sequence, but makes the environment deterministic (memory state, volume storage, static auth, test HMAC), copies the committed fixture rather than trusting whatever VOLUME_ROOT points at, and leaves a file on disk that a later session can diff against a rerun. The shell recipe still exists and still works; the runner replaces the hand-carried part, not the stack.

Why the receipt helps replay. A failed run today leaves nothing behind: a red CI line or a dead curl, with the capture ID only in logs. The receipt records captureId, the finalize runId, the idempotency key, per-stage HTTP statuses, file counts (registered / uploaded / committed / expected), the final capture status, and error codes (create_http_503, commit_sha_mismatch, finalize_missing_files, runner_exception, unexpected_final_status, …). Rerunning the command reproduces the journey byte-for-byte against the same committed fixture, and a failed receipt names the exact stage and code that diverged, so the next session starts from evidence instead of from a description. The tests pin the receipt's shape (backend/tests/test_capture_flow_e2e_runner.py), so later fields get added deliberately.

What it executes. The HTTP ingest API and the finalize handoff exactly as the iOS device walks them: POST /v1/captures, POST /v1/captures/{id}/files, PUT /v1/uploads/{token}, POST /v1/captures/{id}/files:commit, POST /v1/captures/{id}/finalize (with the finalize background work drained), then GET status and events. What it does not execute. The reconstruction/orchestrator pipeline after ingest (HSS_INGEST_HOOKS=null, so no runs are computed), no Modal or GCP deployment, no cloud storage, no network beyond the in-process ASGI transport, and no secrets — the bearer token and HMAC key are the same static test values the API suite uses. It also does not modify any route behavior; server/api code is untouched.

--skip-upload PATH registers a file and never uploads it, which drives the capture into awaiting_files on purpose — combine with --expect-status awaiting_files to check that path instead of asserting a failure.

Why

The capture-fixture integration test (backend/tests/test_capture_fixture_flow.py) proves the journey but only inside pytest. A reviewer reproducing it by hand rebuilds the setup from a README snippet and gets no comparable artifact back. The runner makes the same journey a single command whose output can be attached to a bug report and replayed later.

Review fixes

Addressing the independent review of head 911efd6d (verdict: changes needed):

Finding 1 (medium) — fixed. A crashed run reported success: _finish computed ok from errorCodes alone, and the catch-all handler finished the run with only an errorDetail and no code, so a crash before any stage wrote "ok": true beside the detail and main exited 0 — contradicting the exit contract above and making the receipt lie in exactly the situations it exists for. The reviewer's two reproducers were real: rerunning into the receipt's own recorded --workdir (the natural replay action) hits copytree FileExistsError, and a missing --packet hits FileNotFoundError — both exited 0 with ok: true.

Two-layer fix in scripts/capture_flow_e2e.py:

  • the except handler records a runner_exception code before finishing, so a crash is visible in errorCodes, not just prose in errorDetail;
  • _finish computes ok = not errors and not detail, so any path that records a detail without a code fails closed.

Both reproducers now exit 1 with ok: false, errorCodes: ["runner_exception"], and the detail preserved; the happy path is unchanged. Two new tests pin the contract — test_rerun_into_a_used_workdir_fails_closed and test_missing_packet_dir_fails_closed — and each failed before the fix: on head 911efd6d without the change, pytest tests/test_capture_flow_e2e_runner.py -q returned 2 failed, 3 passed (both new tests asserting exit 1 against an exit 0); with the fix, 7 passed.

Finding 2 (low, duplicated journey helpers) — no code change, on purpose. The reviewer marks it non-blocking and prescribes sharing a module only if a third copy appears; I agree with that trade and am not restructuring a reviewer tool it calls defensible. Sharing now would also have a concrete cost in either direction: importing the helpers from tests/test_capture_fixture_flow.py would pull pytest and server.api.tests.conftest into the CLI's runtime (both are imported there, tests/test_capture_fixture_flow.py:21-27), and having that test import from the script couples the base PR's committed test file to this stacked branch until both merge, breaking the base suite in the window between. The copies are identical today — inspect.getsource equality holds for _content_type and _sealed_files.

How it works

The runner is a linear script over one in-process app instance; each stage appends to the receipt before any check runs, so a failure mid-journey still leaves every earlier stage's evidence behind:

flowchart LR
    A[copy committed packet] --> B[create_app\(\) in-process]
    B --> C[create 201] --> D[register 200] --> E[PUT signed URLs] --> F[commit 200] --> G[finalize 202] --> H[status + events]
    H --> I[receipt.json + exit 0/1]
Loading

Verification

All commands run from backend/ with uv run --frozen --extra api, at head da0b6380:

  • pytest tests/test_capture_flow_e2e_runner.py tests/test_capture_fixture_flow.py -q — 7 passed (runner happy-path receipt, nonzero failure exit via injected missing upload, CLI subprocess from a clean directory, the two new crash-contract tests, plus the two pre-existing fixture-journey tests).
  • Both review reproducers, live: rerun into the used --workdir → exit 1, receipt ok: false, errorCodes: ["runner_exception"], errorDetail: "exception: FileExistsError: …"; --packet /nonexistent/pkt → exit 1, same shape.
  • Full suite (pytest -q, the make test command): 1210 passed / 5 failed / 17 skipped in 4:54. The 5 failures are the same pre-existing set as the base branch — test_ssr_overlap_check.py::test_repo_has_no_ssr_shingle_overlap (needs a git-ignored hygiene file) and 4 server/eval/tests/test_gates.py cases — unchanged by this PR; head adds exactly its 5 new tests as passes over the base's 1205/5/17.
  • Fail-before evidence: the two new tests failed against head 911efd6d before the fix (2 failed / 3 passed).
  • ruff check . — all checks passed. ruff format --check . — all 462 files formatted. lint-imports — 5 contracts kept, 0 broken.

Not verified

  • Not run against a deployed stack, by design — this runner is the no-cloud path; scripts/e2e_dev.py remains the live-stack gate.
  • The GitHub Actions run of this branch: the checks on this PR are Label PR size (pass), Aikido (skipping) and CodeRabbit (skipped, bot not eligible) — no test or lint job runs on backend/, so the local runs above are the only test evidence. Not caused by this PR; the branch changes no path CI watches.
  • On machines where /tmp is a small tmpfs the default temp workdir can fail; pass --workdir (observed here — see Assumptions).

Risk

Low: two new files, no production code, no route changes, nothing under .github/. Open PRs #256 and #257 add separate test files under server/api/tests/ and change no production code, so there is no overlap to reconcile with them.

Assumptions

  • The runner hard-codes the same static test credentials the API suite uses (server/api/tests/conftest.py), so reviewers need no secrets; real deployment credentials never apply here.
  • "Unexpected result" is defined as: any non-2xx stage response, a commit mismatch/notFound, finalize missing files, a final capture status other than the expected one (default processing), events that disagree with the capture status, a missing validate stage event, or any exception (recorded as runner_exception — a crash can never report success).
  • Default workdir is a fresh tempfile.mkdtemp under /tmp; on this sandbox /tmp is a small tmpfs, so verification ran with TMPDIR pointed at the larger root filesystem. No other thread's files were touched.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • N/A — no visible UI changes (backend runner only)
  • N/A — no motion or interaction to record

🔗 Obvious Project · 🧵 Obvious Thread

…N receipt

scripts/capture_flow_e2e.py drives create_app() in-process over the committed
mini-arkit packet: create, register, signed-URL PUTs, commit, finalize, events.
It writes a receipt (request IDs, per-stage HTTP statuses, file counts, final
capture status, error codes), exits nonzero on any unexpected result, and
supports --skip-upload to reach the awaiting_files path on purpose. A test
covers the happy-path receipt, the nonzero failure exit, and the CLI from a
clean directory.
@coderabbitai

coderabbitai Bot commented Oct 15, 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: 4d519011-9063-4a27-a3b8-fa694d9d4d81

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.

@obvious-autobuild

Copy link
Copy Markdown
Author

Independent review (Obvious, Autobuild + Max)

Verdict: changes needed — one real defect in the receipt/exit contract; everything else I checked verified clean. It is a draft, and the fix is a one-liner plus a test.

Findings

1. Medium — a crashed run exits 0 and the receipt says "ok": true

_finish computes success from errorCodes alone (backend/scripts/capture_flow_e2e.py:302, receipt["ok"] = not errors), and the catch-all handler (:293-294) calls it with whatever codes exist so far. Any exception before a stage records a code leaves errors empty, so a run that never executed reports success and main (:341) exits 0. That contradicts the body's own contract ("exits 0 only if every stage matched what the app should do") and makes the receipt, sold as the replay anchor for a later session, lie in exactly the situations it exists for.

Two reproducers from backend/ (uv 0.13.1, --frozen --extra api, head 911efd6d):

$ uv run --frozen --extra api python scripts/capture_flow_e2e.py --workdir <dir from an earlier run>
{"ok": true, "receipt": ".../receipt.json", "errorDetail": "exception: FileExistsError: [Errno 17] File exists: '.../happy/packet'"}
real exit: 0
$ uv run --frozen --extra api python scripts/capture_flow_e2e.py --workdir <fresh> --packet /nonexistent/pkt
{"ok": true, "receipt": ".../receipt.json", "errorDetail": "exception: FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent/pkt'"}
real exit: 0

Both receipts on disk read "ok": true, "errorCodes": [] with the errorDetail tucked beside them. The first trigger is the natural replay action: the receipt records workdir, and rerunning with it always hits copytree (:148, no dirs_exist_ok) on the previous run's packet/. So "rerunning the command reproduces the same journey" as shipped reports success while doing nothing.

Smallest correct fix: in _finish, receipt["ok"] = not errors and not detail (or append an error code like setup_failed in the except handler). Add one test asserting the crashed case exits 1 with ok false; the tests already pin the receipt's shape, so pin this too.

2. Low, non-blocking — journey helpers duplicated from the fixture test

_content_type, _sealed_files and the Settings block (backend/scripts/capture_flow_e2e.py:56-92) copy backend/tests/test_capture_fixture_flow.py. Self-contained script vs shared import is a defensible trade for a reviewer tool, and the two copies agree today. If a third copy appears, share a module instead.

What I verified

  • Head 911efd6d8eba29ed79434bd70583cca7d80529b9 matches the PR; base 4e828aeb21ba55b132f38a2f52a1fe64c4f50afc is its direct parent; the diff is exactly the two claimed files, +440/-0, nothing under server/api/, .github/, or iOS.
  • uv run --frozen --extra api pytest tests/test_capture_flow_e2e_runner.py tests/test_capture_fixture_flow.py -q → 5 passed. Matches the body.
  • uv run --frozen --extra api pytest server/api/tests tests -q → 196 passed, 1 failed (tests/test_ssr_overlap_check.py::test_repo_has_no_ssr_shingle_overlap). Matches the body's numbers exactly.
  • That 1 failure is pre-existing: the same test fails identically on base 4e828aeb in a separate worktree with no PR files present.
  • The new tests need the change: with scripts/capture_flow_e2e.py moved aside, pytest tests/test_capture_flow_e2e_runner.py -q → 3 collection errors; restored, git status clean.
  • Runner end to end, happy path: exit 0; receipt shows create 201 → register 200 (11 files) → 11 PUTs → commit 200 (11/11, no mismatch) → finalize 202 → finalCaptureStatus: "processing", errorCodes: [], events files_committed plus the validate stage for the run. The body's receipt description is accurate.
  • Fault path: --skip-upload keyframes/k00001.jpg --expect-status awaiting_files → exit 1, capture ends awaiting_files, finalize reports missing: ["keyframes/k00001.jpg"], error codes commit_not_found + finalize_missing_files. With the default --expect-status processing the runner adds unexpected_final_status, which is what the failure test asserts.
  • Lint: ruff check . clean; ruff format --check . 462 files formatted; lint-imports 5 kept, 0 broken. Matches the body.
  • Credentials claim: TOKEN = "d" * 40 matches backend/server/api/tests/conftest.py:15; the runner's Settings block is identical to the fixture test's, including HSS_INGEST_HOOKS="null", so no real secrets are involved.
  • No-overlap claim: [obvious-fleet] test(backend): cover the device result boundary through the FastAPI app #256 (test_result_boundary.py) and [obvious-fleet] Backend: prove the capture API wire contract the iOS uploader reads #257 (test_capture_contract_swift.py) touch only files under backend/server/api/tests/.

What I couldn't verify

  • CI as test evidence: the checks on this branch are Label PR size (pass), Aikido (skipping) and CodeRabbit (skipped, bot not eligible). No test or lint job ran, so the local runs above are the only test evidence I have. Not caused by this PR; the branch changes no path CI watches.
  • Nothing here exercises a deployed stack, Modal/GCP, or real reconstruction. By design; scripts/e2e_dev.py remains that gate, and I verified the in-process path only.

… a stage

A crashed run reported ok:true and exited 0: _finish computed success from
errorCodes alone, so an exception with no stage code yet (rerunning into the
receipt's recorded workdir hits copytree FileExistsError; a missing --packet
hits FileNotFoundError) wrote ok:true beside the errorDetail and main exited
0. The except handler now records a runner_exception code and _finish treats
any recorded detail as failure (ok = not errors and not detail). Two new tests
cover the rerun-into-used-workdir and missing-packet crashes; both failed
against the previous behavior.
obvious-autobuild Bot added a commit that referenced this pull request Oct 16, 2026
…play can never report ok

Scoped imports of two completed upstream review fixes, with their regression
tests, onto the burst8 integration branch:

- #258 at da0b638 (capture_flow_e2e.py): a run that crashes before any stage
  code exists reported ok:true beside its errorDetail and exited 0. The except
  branch now records runner_exception and _finish treats any recorded detail as
  failure (ok = not errors and not detail). Tests: rerun into the receipt's
  recorded workdir and a missing --packet both exit 1 with ok:false.
- #262 at e2aa8a2 (replay_capture.py): run_replay's except branch stored
  errorDetail but appended no error code, and a pre-replay receipt without the
  replay block passed the fixture check vacuously and died on KeyError with a
  green report. The replay now validates the replay block up front with a named
  missing_replay_block error, appends exception:<Type> on any crash, flags
  receipt paths outside the committed manifest as fixture drift, and drops the
  dead _content_type. The later receipt metadata this branch already carries
  (idempotencyKeys/files/packetSha256 emission) is preserved.

Evidence: all five imported tests fail against the pre-fix scripts (5 failed)
and pass with them (14 passed across both suites).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@SamGu-NRX
SamGu-NRX changed the base branch from obv/burst5-20261015-bs-capture-fixture to alpha October 16, 2026 04:30
@SamGu-NRX
SamGu-NRX marked this pull request as ready for review October 16, 2026 04:30
@SamGu-NRX
SamGu-NRX merged commit c7d25cb into alpha Oct 16, 2026
3 checks passed
@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-16T04:32:53.920002Z da0b638 Draft marked ready
ℹ️ 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.

@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.

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Low impact] Adds a test script and test file for the capture flow.

Fix the documented expected-missing-upload command before merging; it currently fails even when the API behaves correctly.

Fix All in Claude CodeFindings

  1. P1 Expected missing upload still fails ▶
  2. P2 GET requests lose HTTP codes ▶
Fix with agent prompt
### Issue 1
backend/scripts/capture_flow_e2e.py:268-269
Running with `--skip-upload keyframes/k00001.jpg --expect-status awaiting_files` still exits 1. The deliberately skipped file adds both `commit_not_found` and `finalize_missing_files`, so matching the expected status cannot make the run pass.

Allow exactly the skipped path in those responses when `awaiting_files` is expected, keep rejecting other missing files, and add a test for this command.

### Issue 2
backend/scripts/capture_flow_e2e.py:273
The status and events requests discard their HTTP responses before adding them to the receipt. If either request fails, the receipt loses its HTTP code. A body that is not JSON also prevents that stage from being recorded at all, leaving reviewers without the evidence needed to diagnose the failure.

Keep each response, record its `status_code` before reading JSON, and add a stage-specific error for a failed request.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds a no-cloud capture runner and tests for its JSON receipt, CLI exit behavior, and early exceptions.

  • One local command replays the capture journey and saves its result.

Acknowledgments from obvious-autobuild[bot]: duplicated journey helpers are intentionally retained until a third copy appears, to avoid coupling the CLI to pytest or the existing test to this branch. Reusing a populated work directory is intentionally tested as a fail-closed error; no separate finding is raised for that behavior.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Copy packet fixture] --> B[Create capture]
    B --> C[Register and upload files]
    C --> D[Commit files]
    D --> E[Finalize packet]
    E --> F[Read status and events]
    F --> G[Write JSON receipt]
    G --> H[Exit according to receipt]
Loading

Reviews (1) · Last reviewed commit: "fix(backend): fail closed when the captu..." · Reviewed by Greptile

Comment on lines +268 to +269
if payload.get("missing"):
errors.append("finalize_missing_files")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Expected missing upload still fails

Running with --skip-upload keyframes/k00001.jpg --expect-status awaiting_files still exits 1. The deliberately skipped file adds both commit_not_found and finalize_missing_files, so matching the expected status cannot make the run pass.

Allow exactly the skipped path in those responses when awaiting_files is expected, keep rejecting other missing files, and add a test for this command.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/scripts/capture_flow_e2e.py
Line: 268-269

Comment:
**Expected missing upload still fails**

Running with `--skip-upload keyframes/k00001.jpg --expect-status awaiting_files` still exits 1. The deliberately skipped file adds both `commit_not_found` and `finalize_missing_files`, so matching the expected status cannot make the run pass.

Allow exactly the skipped path in those responses when `awaiting_files` is expected, keep rejecting other missing files, and add a test for this command.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

journey.drain()

# Final status and the event log the device observes.
status = journey.client.get(f"/v1/captures/{capture_id}").json()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 GET requests lose HTTP codes

The status and events requests discard their HTTP responses before adding them to the receipt. If either request fails, the receipt loses its HTTP code. A body that is not JSON also prevents that stage from being recorded at all, leaving reviewers without the evidence needed to diagnose the failure.

Keep each response, record its status_code before reading JSON, and add a stage-specific error for a failed request.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/scripts/capture_flow_e2e.py
Line: 273

Comment:
**GET requests lose HTTP codes**

The status and events requests discard their HTTP responses before adding them to the receipt. If either request fails, the receipt loses its HTTP code. A body that is not JSON also prevents that stage from being recorded at all, leaving reviewers without the evidence needed to diagnose the failure.

Keep each response, record its `status_code` before reading JSON, and add a stage-specific error for a failed request.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant