Skip to content

Commit 072087b

Browse files
gaurav-karkiclaude
andcommitted
fix(casework): exit non-zero on failed submissions, tighten the review nits
CodeRabbit review on #444: - main() returned 0 even when every POST failed, which is the same hole the credential and write-guard aborts exist to close. Now returns 1 if any case errored. This diverges from the sibling enrichers, which always return 0; the divergence is documented in the README. - README said "Two failures" then "All three" of the same list. - submit_review built its headers by hand instead of using _headers's content_type parameter, as create_material already does. - Cover the bare-list branch of reviews_for_slug; pagination is a project-wide DRF setting, so the untested branch could go stale silently. - Build the render_report test row through _row -- the literal still carried the `title` key that was dropped. - Record why --force is load-bearing in the guard-wiring test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a5ee01d commit 072087b

6 files changed

Lines changed: 57 additions & 9 deletions

File tree

casework/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,15 +506,21 @@ the batch is the authority, and a PUBLISHED case is a legitimate review target.
506506

507507
Two failures abort the run instead of being counted per case: a credential rejection
508508
(**401** an expired or invalid token, **403** a valid token without the Caseworker
509-
role) and the write-guard refusal (a remote host without `--allow-remote-writes`). All
510-
three fail identically on every remaining case, so counting them would bury one
511-
configuration mistake under several hundred warnings and still exit 0.
509+
role) and the write-guard refusal (a remote host without `--allow-remote-writes`). Both
510+
fail identically on every remaining case, so counting them would bury one configuration
511+
mistake under several hundred warnings.
512512

513513
Everything else is per-case. A read or POST that fails on one slug costs that slug and
514514
the batch continues; in `--report` it becomes an `unreadable` row rather than losing the
515515
whole file. Recover a failed subset with `--slug <a> --slug <b> --force` — re-running
516516
the whole batch with `--force` re-grades every passing case too.
517517

518+
**Exit code, and how it differs from the other stages.** `submit_reviews` returns **1**
519+
when any case failed, 0 otherwise. Every other stage in this package always returns 0.
520+
The divergence is deliberate: this one is meant to be chained after the enrichers in a
521+
script, and a batch where every POST 500'd is otherwise indistinguishable from a clean
522+
run. Skips and dry-run `would_submit` counts are not errors.
523+
518524
---
519525

520526
## End-to-end production runbook

casework/common/api.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,8 @@ def submit_review(self, slug, timeout=60):
319319
"""
320320
url = self.base_url + "/casework/reviews/submit/"
321321
body = json.dumps({"slug": slug}).encode("utf-8")
322-
headers = dict(self._headers())
323-
headers["Content-Type"] = "application/json"
324-
with self._request("POST", url, data=body, headers=headers,
322+
with self._request("POST", url, data=body,
323+
headers=self._headers("application/json"),
325324
timeout=timeout) as r:
326325
return json.loads(r.read().decode())
327326

casework/submit_reviews.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,11 @@ def main(argv=None):
376376
duration_s=time.monotonic() - started)
377377
print(f"\n=== submit reviews ({'DRY RUN' if args.dry_run else 'APPLIED'}) ===")
378378
print(f" {format_counts(stats)}")
379-
return 0
379+
# Exit non-zero when any case failed. This DIVERGES from the sibling enrichers,
380+
# which always return 0: a wrapper cannot otherwise tell a clean batch from one
381+
# where every POST 500'd, and the abort paths above exist precisely to stop a
382+
# configuration mistake exiting 0. Skipped and would-submit cases are not errors.
383+
return 1 if stats["error"] else 0
380384

381385

382386
if __name__ == "__main__":

tests/casework/test_api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,15 @@ def fake_get(path, params=None, timeout=60):
10771077
assert rows == [{"id": 1841, "status": "done"}]
10781078

10791079

1080+
def test_reviews_for_slug_accepts_an_unpaginated_list(monkeypatch):
1081+
"""Pagination is a project-wide DRF setting, not this view's promise -- turning
1082+
it off must not make the skip check see zero reviews and re-grade the corpus."""
1083+
api = CaseworkApi("http://127.0.0.1:48010", basic=("u", "p"))
1084+
monkeypatch.setattr(api, "get", lambda *a, **kw: [{"id": 1841, "status": "done"}])
1085+
rows = api.reviews_for_slug("case-078-cr-0038-ciaa-special-court-case-078-cr-9a")
1086+
assert rows == [{"id": 1841, "status": "done"}]
1087+
1088+
10801089
def test_reviews_for_slug_on_a_never_reviewed_case_is_empty(monkeypatch):
10811090
api = CaseworkApi("http://127.0.0.1:48010", basic=("u", "p"))
10821091
monkeypatch.setattr(api, "get", lambda *a, **kw: {"count": 0, "results": []})

tests/casework/test_guard_wiring.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ def no_sockets(*a, **kw):
196196
batch.write_text(
197197
"slug\ncase-078-cr-0038-ciaa-special-court-case-078-cr-9a\n", encoding="utf-8")
198198

199+
# `--force` is load-bearing: it skips the pre-check GET. Reads are not
200+
# write-guarded, so an unforced run would reach `no_sockets` first and fail with
201+
# AssertionError before the POST guard ever fires.
199202
with pytest.raises(RuntimeError, match="refusing to write to non-loopback"):
200203
sr.main(["--batch-csv", str(batch), "--api-base-url", NON_LOOPBACK_BASE_URL,
201204
"--api-token", "t", "--apply", "--force"])

tests/casework/test_submit_reviews.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ def test_summary_counts_dispositions_and_scores():
180180

181181

182182
def test_the_rendered_report_names_every_case_and_the_totals():
183-
rows = [{"slug": SLUG_A, "review_id": 1841, "status": "done", "score": 84,
184-
"disposition": "PASS", "duration": 92.4, "title": "", "error": ""}]
183+
rows = [sr._row(SLUG_A, "done", review_id=1841, score=84,
184+
disposition="PASS", duration=92.4)]
185185
text = sr.render_report(rows, sr.summarize(rows),
186186
base_url="http://127.0.0.1:48010/api",
187187
run_id="testrun", batch="batch.csv")
@@ -303,3 +303,30 @@ def test_the_report_header_never_claims_apply_on_a_read_only_run(tmp_path, monke
303303

304304
log = next(p for p in tmp_path.iterdir() if p.suffix == ".log")
305305
assert "mode : DRY-RUN" in log.read_text(encoding="utf-8")
306+
307+
308+
# --- exit code --------------------------------------------------------------
309+
310+
311+
def _main_exit(api, batch_rows, tmp_path, monkeypatch, *args):
312+
monkeypatch.setenv("CASEWORK_RUN_LOG_DIR", str(tmp_path))
313+
monkeypatch.setattr(sr, "build_api", lambda a: api)
314+
batch = tmp_path / "batch.csv"
315+
batch.write_text("slug\n" + "".join(f"{s}\n" for s in batch_rows), encoding="utf-8")
316+
return sr.main(["--batch-csv", str(batch), "--api-base-url",
317+
"http://127.0.0.1:48010", "--api-token", "t", *args])
318+
319+
320+
def test_a_clean_run_exits_zero(tmp_path, monkeypatch):
321+
assert _main_exit(_StubApi(), [SLUG_A], tmp_path, monkeypatch, "--apply") == 0
322+
323+
324+
def test_a_run_with_any_failed_case_exits_non_zero(tmp_path, monkeypatch):
325+
"""A wrapper cannot otherwise tell a clean batch from one where every POST 500'd."""
326+
api = _StubApi(errors={SLUG_A: _http_error(500)})
327+
assert _main_exit(api, [SLUG_A, SLUG_B], tmp_path, monkeypatch, "--apply") == 1
328+
329+
330+
def test_skipped_cases_are_not_errors(tmp_path, monkeypatch):
331+
api = _StubApi(reviews={SLUG_A: [{"id": 1841, "status": "done"}]})
332+
assert _main_exit(api, [SLUG_A], tmp_path, monkeypatch, "--apply") == 0

0 commit comments

Comments
 (0)