Skip to content

feat(casework): bulk-submit a batch into the review queue - #444

Open
gaurav-karki wants to merge 6 commits into
mainfrom
feat/casework-submit-reviews
Open

feat(casework): bulk-submit a batch into the review queue#444
gaurav-karki wants to merge 6 commits into
mainfrom
feat/casework-submit-reviews

Conversation

@gaurav-karki

@gaurav-karki gaurav-karki commented Aug 10, 2026

Copy link
Copy Markdown
Member

User description

Bulk-submits a batch of enriched cases into the casework review queue, and reads the
grades back in a separate read-only pass.

Today the only ways to submit a case for review are one click in the SPA, one MCP call,
or regrade-all — which only re-queues cases that already carry a review, so it can
never introduce a fresh batch. A 238-case batch means 238 clicks.

What it does

# submit (writes; remote needs all three, as every casework write does)
uv run python -m casework.submit_reviews --batch-csv batch.csv \
    --api-base-url https://api.jawafdehi.org --api-token "$JAWAFDEHI_API_TOKEN" \
    --apply --allow-remote-writes

# read the grades back, any time after. Read-only.
uv run python -m casework.submit_reviews --batch-csv batch.csv --report

Submitting is instant and tells you nothing — every case comes back pending. Grading
runs out of process on the jobs queue and takes hours, so the read-back is a separate
run. It writes work/reviews/<ts>-submit_reviews-<run>.md: status and disposition
counts, score spread, a per-case table, the failures with their error line, and the
batch slugs that carry no review at all.

Design notes for the reviewer

No case reads. A slug is all the POST needs, and CaseReviewListSerializer already
carries the status, score, disposition and duration the report shows. So submit mode is
2 requests per case and report mode is 1, with no 16-request corpus listing in front of
either. Only a failed row costs a third request, for the error the list row omits.

Skip on any existing review. The endpoint has no idempotency — every POST creates a
new row, and the job dedup key is the review id, which is new each time. Skipping makes
a run resumable: one that dies at case 120 picks up at 121 instead of re-grading the
first 120 at full LLM cost. --force overrides.

An explicit target is required. --batch-csv or --slug. A bare run is refused
rather than falling through to bulk selection, which would enqueue ~3,000 LLM grading
runs off a forgotten flag. The script deliberately does not call select_for_run: that
path hardcodes the DRAFT/IN_REVIEW gate, which is right for an enricher and wrong here,
because a PUBLISHED case is a legitimate review target.

No case state change. Cases stay DRAFT. Promotion to IN_REVIEW is a human decision
and the grade is its input, which is the wrong order to automate before anyone has read
a grade.

Which failures are fatal. A credential rejection (401 expired/invalid token, 403 no
Caseworker role) and the write-guard's refusal abort the run — all three fail identically
on every remaining case. Everything else costs one case and the batch continues. 401
matters specifically: OIDCAuthentication supplies authenticate_header, so an expired
token is a 401, and a 403-only guard would count the commonest credential failure 238
times and still exit 0.

One known hole, mitigated not closed. SubmitSerializer resolves retired slugs
through CaseSlugHistory; the list endpoint filters case__slug and sees live slugs
only. A stale batch row therefore submits fine but stays invisible to the skip check and
is re-graded every run. Closing it properly costs a case read per slug — the exact cost
this design avoids — so instead the submit compares the requested slug against the one in
the 201 body (no extra request) and warns, naming both.

Verification

1612 passed, 1 skipped in tests/casework/, ruff clean.

Smoke-tested end to end against a local DEV_AUTH harness on 127.0.0.1:48010 with three
sqlite DBs and two seeded cases — dry run wrote nothing, apply created two reviews and two
queued jobs, the re-run skipped both, the report rendered graded/failed/never-submitted,
an unknown slug logged HTTP 400 and the batch continued, --force created a new review
and dead-lettered the old job as superseded, and both refusal paths (no selector, remote
without --allow-remote-writes) refused before opening a connection.

No production writes were made at any point.

Design and plan: docs/superpowers/specs/2026-08-10-bulk-review-submit-design.md and
docs/superpowers/plans/2026-08-10-bulk-review-submit.md in the meta-repo.

🤖 Generated with Claude Code


PR Type

Enhancement, Tests, Documentation


Description

  • Batch review submission CLI

  • Review report generation

  • API review helpers

  • Guard/error tests


Diagram Walkthrough

flowchart LR
  A["Batch CSV or slugs"] --> B["submit_reviews CLI"]
  B -- "skip existing" --> C["review list API"]
  B -- "enqueue" --> D["review submit API"]
  B -- "read-only report" --> E["Markdown report"]
Loading

File Walkthrough

Relevant files
Enhancement
api.py
Add casework review API helpers                                                   

casework/common/api.py

  • Added reviews_for_slug
  • Added review_detail
  • Added guarded submit_review POST
+31/-0   
submit_reviews.py
Add batch review submit CLI                                                           

casework/submit_reviews.py

  • Added batch submit/report CLI
  • Skips existing reviews unless --force
  • Aborts on 401/403, write guard
  • Renders markdown grade reports
+387/-0 
Tests
test_api.py
Cover review API client helpers                                                   

tests/casework/test_api.py

  • Tests review list unwrapping
  • Tests detail endpoint path
  • Tests submit POST body
  • Tests remote write guard
+88/-0   
test_guard_wiring.py
Verify submit_reviews write guard wiring                                 

tests/casework/test_guard_wiring.py

  • Tests real guard wiring
  • Verifies no socket opens
  • Covers submit_reviews remote POST refusal
+22/-0   
test_submit_reviews.py
Cover submit_reviews CLI behavior                                               

tests/casework/test_submit_reviews.py

  • Tests submit skip/force/dry-run
  • Tests credential abort behavior
  • Tests report rows and summaries
  • Tests exit codes
+332/-0 
Documentation
README.md
Document batch review submission workflow                               

casework/README.md

  • Documents submit_reviews
  • Adds command examples
  • Explains failures and exit codes
+44/-0   


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
enable_ai_metadata: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_description]

publish_labels: False
add_original_user_description: True
generate_ai_title: False
use_bullet_points: True
extra_instructions: 
enable_pr_type: True
final_update_message: True
enable_help_text: False
enable_help_comment: False
enable_pr_diagram: True
publish_description_as_comment: False
publish_description_as_comment_persistent: True
enable_semantic_files_types: True
collapsible_file_list: adaptive
collapsible_file_list_threshold: 6
inline_file_summary: False
use_description_markers: False
enable_large_pr_handling: True
include_generated_by_header: True
max_ai_calls: 4
async_ai_calls: True

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@damo-da, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e86232c5-9237-4661-bff4-29af7c641bba

📥 Commits

Reviewing files that changed from the base of the PR and between a5ee01d and 595865e.

📒 Files selected for processing (6)
  • casework/README.md
  • casework/common/api.py
  • casework/submit_reviews.py
  • tests/casework/test_api.py
  • tests/casework/test_guard_wiring.py
  • tests/casework/test_submit_reviews.py
📝 Walkthrough

Walkthrough

This change adds review API methods and a submit_reviews CLI. The CLI supports scoped batch submission, dry runs, forced resubmission, credential and write guards, per-case failures, and Markdown reports with review summaries and remediation details.

Changes

Review workflow

Layer / File(s) Summary
Review API operations
casework/common/api.py, tests/casework/test_api.py
The API client lists reviews by slug, retrieves review details, and submits slug-only JSON payloads through the guarded POST path. Tests cover response formats and remote-write protection.
Batch selection and guarded submission
casework/submit_reviews.py, tests/casework/test_submit_reviews.py, tests/casework/test_guard_wiring.py, casework/README.md
The CLI selects scoped slugs, handles limits and duplicates, skips or forces existing reviews, supports dry runs, logs per-case results, warns about retired slugs, and stops on credential or write-guard failures.
Report generation and CLI execution
casework/submit_reviews.py, tests/casework/test_submit_reviews.py, casework/README.md
The CLI collects review rows, retrieves failed-review details, aggregates statuses and scores, renders Markdown reports, writes output files, and documents command usage and behavior.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

Suggested labels: Review effort 5/5

Suggested reviewers: jawafdehi-pr-agent

Poem

I’m a rabbit with reviews in a queue,
Slugs hop in batches, dry runs too.
Guards stop writes when rules say no,
Reports show scores in rows below.

 (\_/)
 (o.o)  
 (> <)  
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: bulk submission of a batch to the review queue.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/casework-submit-reviews

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.

@jawafdehi-pr-agent

jawafdehi-pr-agent Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 595865e)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Auth Handling

Failed-review detail reads swallow HTTPError, including 401/403. If the token expires between list and detail reads, report continues with a synthetic error instead of aborting like other credential failures. Call _raise_if_credential_failure in this handler before fallback.

try:
    detail = api.review_detail(review["id"]) or {}
except Exception as exc:  # noqa: BLE001 - the error line is a nicety
    detail = {"error": f"({type(exc).__name__} reading the detail)"}

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_reviewer]

require_ticket_analysis_review: False
require_score_review: False
require_tests_review: True
require_estimate_effort_to_review: True
require_can_be_split_review: False
require_security_review: True
require_estimate_contribution_time_cost: False
require_todo_scan: False
publish_output_no_suggestions: True
persistent_comment: True
extra_instructions: Focus on: logic errors and edge cases; security/authz regressions; missing error handling;
Django/DRF correctness (migrations, N+1 queries, transaction/atomicity, serializer & permission gaps).
Do NOT comment on formatting, import order, or naming — ruff handles those in CI.

num_max_findings: 3
final_update_message: True
enable_review_labels_security: True
enable_review_labels_effort: True
require_all_thresholds_for_incremental_review: False
minimal_commits_for_incremental_review: 0
minimal_minutes_for_incremental_review: 0
enable_intro_text: True
enable_help_text: False

@jawafdehi-pr-agent

jawafdehi-pr-agent Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to 072087b
Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve credential aborts

Failed-detail read swallows 401/403. Token expiry mid-report becomes a fake row
error, not the documented run-ending credential failure. Check
_raise_if_credential_failure for urllib.error.HTTPError here too.

casework/submit_reviews.py [214-220]

 if review.get("status") == "failed":
     try:
         detail = api.review_detail(review["id"]) or {}
+    except urllib.error.HTTPError as exc:
+        _raise_if_credential_failure(exc, slug)
+        detail = {"error": f"(HTTP {exc.code} reading the detail)"}
     except Exception as exc:  # noqa: BLE001 - the error line is a nicety
         detail = {"error": f"({type(exc).__name__} reading the detail)"}
     first_line = (detail.get("error") or "").strip().splitlines()
     error = first_line[0] if first_line else ""
Suggestion importance[1-10]: 7

__

Why: Valid bug. review_detail 401/403 currently swallowed by broad Exception, contradicts credential-abort behavior. Edge-case impact moderate.

Medium

🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

enable_ai_metadata: False
custom_model_max_tokens: 200000
git_provider: github
output_relevant_configurations: True
model: openai/cx/gpt-5.5
ENABLE_AUTO_APPROVAL: True
custom_reasoning_model: False
fallback_models: ['openai/cx/gpt-5.4-mini']
is_auto_command: True
publish_output: True
publish_output_progress: True
progress_gif_url: 
progress_gif_width: 48
verbosity_level: 0
use_extra_bad_extensions: False
log_level: DEBUG
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
extra_config_url: 
disable_auto_feedback: False
ai_timeout: 120
response_language: en-US
repo_context_files: ['AGENTS.md']
repo_context_from_default_branch: True
repo_context_max_lines: 500
max_description_tokens: 500
max_commits_tokens: 500
max_model_tokens: 32000
model_token_count_estimate_factor: 0.3
patch_extension_skip_types: ['.md', '.txt']
allow_dynamic_context: True
max_extra_lines_before_dynamic_context: 10
patch_extra_lines_before: 5
patch_extra_lines_after: 1
cli_mode: False
large_patch_policy: clip
duplicate_prompt_examples: False
seed: -1
temperature: 0.2
ignore_pr_title: ['^\\[Auto\\]', '^Auto', '^Bump ', '^chore\\(deps\\)']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
restricted_mode: False
reasoning_effort: medium
enable_claude_extended_thinking: False
extended_thinking_budget_tokens: 2048
extended_thinking_max_output_tokens: 4096
claude_extended_thinking_models_override: []
extract_issue_from_branch: True
branch_issue_regex: 
enable_custom_labels: False

[pr_code_suggestions]

commitable_code_suggestions: False
dual_publishing_score_threshold: -1
focus_only_on_problems: True
extra_instructions: Prefer a few high-impact, project-specific suggestions over many generic ones.
Skip style/formatting (ruff-enforced) and changes under cases/migrations/.

enable_help_text: False
enable_chat_text: False
persistent_comment: True
max_history_len: 4
publish_output_no_suggestions: True
suggestions_score_threshold: 0
new_score_mechanism: True
new_score_mechanism_th_high: 9
new_score_mechanism_th_medium: 7
auto_extended_mode: True
num_code_suggestions_per_chunk: 3
max_number_of_calls: 3
parallel_calls: True
final_clip_factor: 0.8
decouple_hunks: False
demand_code_suggestions_self_review: False
code_suggestions_self_review_text: **Author self-review**: I have reviewed the PR code suggestions, and addressed the relevant ones.
approve_pr_on_self_review: False
fold_suggestions_on_self_review: True
num_code_suggestions: 4

Previous suggestions

Suggestions up to commit a5ee01d
CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve credential aborts

Detail read hides 401/403. Credential failure then becomes a blank failed-row
nicety, not a run-ending auth error. Catch urllib.error.HTTPError first, call
_raise_if_credential_failure.

casework/submit_reviews.py [214-220]

 if review.get("status") == "failed":
     try:
         detail = api.review_detail(review["id"]) or {}
+    except urllib.error.HTTPError as exc:
+        _raise_if_credential_failure(exc, slug)
+        detail = {"error": f"(HTTP {exc.code} reading the detail)"}
     except Exception as exc:  # noqa: BLE001 - the error line is a nicety
         detail = {"error": f"({type(exc).__name__} reading the detail)"}
     first_line = (detail.get("error") or "").strip().splitlines()
     error = first_line[0] if first_line else ""
Suggestion importance[1-10]: 7

__

Why: Valid bug. api.review_detail can hide 401/403 under broad Exception, contradicting credential-abort policy.

Medium

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Agent Walkthrough 🤖

Welcome to the PR Agent, an AI-powered tool for automated pull request analysis, feedback, suggestions and more.

Here is a list of tools you can use to interact with the PR Agent:

ToolDescriptionTrigger Interactively 💎

DESCRIBE

Generates PR description - title, type, summary, code walkthrough and labels
  • Run

REVIEW

Adjustable feedback about the PR, possible issues, security concerns, review effort and more
  • Run

IMPROVE

Code suggestions for improving the PR
  • Run

UPDATE CHANGELOG

Automatically updates the changelog
  • Run

HELP DOCS

Answers a question regarding this repository, or a given one, based on given documentation path
  • Run

ADD DOCS

Generates documentation to methods/functions/classes that changed in the PR
  • Run

ASK

Answering free-text questions about the PR

[*]

GENERATE CUSTOM LABELS

Generates custom labels for the PR, based on specific guidelines defined by the user

[*]

(1) Note that each tool can be triggered automatically when a new PR is opened, or called manually by commenting on a PR.

(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the /ask tool, you need to comment on a PR: /ask "<question content>". See the relevant documentation for each tool for more details.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Auto-approved PR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
tests/casework/test_guard_wiring.py (1)

199-201: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Record why --force is required in this test.

--force skips the pre-check GET in submit_batch. Reads are not write-guarded, so without --force the pre-check would reach the patched urlopen and the test would fail with AssertionError instead of the expected RuntimeError. The flag is load-bearing, and that is not visible from the call.

♻️ Proposed comment
+    # `--force` is load-bearing: it skips the pre-check GET. Reads are not
+    # write-guarded, so an unforced run would hit `no_sockets` and fail with
+    # AssertionError before the POST guard ever fires.
     with pytest.raises(RuntimeError, match="refusing to write to non-loopback"):
         sr.main(["--batch-csv", str(batch), "--api-base-url", NON_LOOPBACK_BASE_URL,
                  "--api-token", "t", "--apply", "--force"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/casework/test_guard_wiring.py` around lines 199 - 201, Add an inline
comment beside the --force argument in the sr.main invocation explaining that it
skips submit_batch’s unguarded pre-check GET, allowing the test to reach the
expected non-loopback write RuntimeError instead of the patched urlopen
assertion.
tests/casework/test_api.py (1)

1061-1083: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for the bare-list response shape.

reviews_for_slug handles both a paginated envelope and a bare list (casework/common/api.py line 307). Only the envelope branch is covered. A pagination-setting change would silently break the untested branch.

💚 Proposed test
+def test_reviews_for_slug_accepts_an_unpaginated_list(monkeypatch):
+    api = CaseworkApi("http://127.0.0.1:48010", basic=("u", "p"))
+    monkeypatch.setattr(api, "get", lambda *a, **kw: [{"id": 1841, "status": "done"}])
+    assert api.reviews_for_slug(SLUG) == [{"id": 1841, "status": "done"}]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/casework/test_api.py` around lines 1061 - 1083, Add a test alongside
test_reviews_for_slug_unwraps_the_paginated_envelope that mocks CaseworkApi.get
to return a bare list, invokes reviews_for_slug with a slug, and asserts the
returned rows match that list. Keep the existing envelope and empty-result
coverage unchanged.
tests/casework/test_submit_reviews.py (1)

182-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Build the row with _row so the test tracks the real shape.

The literal at lines 183-184 includes a title key that _row never produces and render_report never reads. If _row gains a column, this literal diverges from the real row shape without failing.

♻️ Proposed change
-    rows = [{"slug": SLUG_A, "review_id": 1841, "status": "done", "score": 84,
-             "disposition": "PASS", "duration": 92.4, "title": "", "error": ""}]
+    rows = [sr._row(SLUG_A, "done", review_id=1841, score=84,
+                    disposition="PASS", duration=92.4)]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/casework/test_submit_reviews.py` around lines 182 - 190, Update
test_the_rendered_report_names_every_case_and_the_totals to construct its row
through the existing _row helper instead of an inline dictionary, preserving the
same values and assertions while keeping the test aligned with the real row
shape.
casework/common/api.py (1)

320-326: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the content_type parameter of _headers.

_headers accepts content_type and sets the header itself. create_material at line 539 already uses that form. The manual copy here repeats logic without benefit.

♻️ Proposed simplification
         url = self.base_url + "/casework/reviews/submit/"
         body = json.dumps({"slug": slug}).encode("utf-8")
-        headers = dict(self._headers())
-        headers["Content-Type"] = "application/json"
-        with self._request("POST", url, data=body, headers=headers,
+        with self._request("POST", url, data=body,
+                           headers=self._headers("application/json"),
                            timeout=timeout) as r:
             return json.loads(r.read().decode())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@casework/common/api.py` around lines 320 - 326, Update the review submission
request in the relevant API method to pass the JSON content type through
_headers(content_type=...) and remove the manual Content-Type assignment,
matching the existing create_material pattern while preserving the POST request
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@casework/README.md`:
- Around line 507-511: Update the paragraph in the README so the opening and
closing counts consistently describe the two abort classes: credential rejection
and write-guard refusal. Preserve the existing 401/403 credential details and
explanation.

In `@casework/submit_reviews.py`:
- Around line 373-379: Update main after submit_batch and its existing summary
output to return a non-zero exit code whenever stats["error"] is greater than
zero, while preserving zero for successful runs and intentional dry-run behavior
as defined by the existing contract. Use the error count from stats rather than
changing submit_batch’s per-case handling, and document the convention in the
project README only if partial failures are intentionally kept at exit 0.

---

Nitpick comments:
In `@casework/common/api.py`:
- Around line 320-326: Update the review submission request in the relevant API
method to pass the JSON content type through _headers(content_type=...) and
remove the manual Content-Type assignment, matching the existing create_material
pattern while preserving the POST request behavior.

In `@tests/casework/test_api.py`:
- Around line 1061-1083: Add a test alongside
test_reviews_for_slug_unwraps_the_paginated_envelope that mocks CaseworkApi.get
to return a bare list, invokes reviews_for_slug with a slug, and asserts the
returned rows match that list. Keep the existing envelope and empty-result
coverage unchanged.

In `@tests/casework/test_guard_wiring.py`:
- Around line 199-201: Add an inline comment beside the --force argument in the
sr.main invocation explaining that it skips submit_batch’s unguarded pre-check
GET, allowing the test to reach the expected non-loopback write RuntimeError
instead of the patched urlopen assertion.

In `@tests/casework/test_submit_reviews.py`:
- Around line 182-190: Update
test_the_rendered_report_names_every_case_and_the_totals to construct its row
through the existing _row helper instead of an inline dictionary, preserving the
same values and assertions while keeping the test aligned with the real row
shape.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e76e7310-d97e-4111-b78f-ca308a17b2f7

📥 Commits

Reviewing files that changed from the base of the PR and between b97fe18 and a5ee01d.

📒 Files selected for processing (6)
  • casework/README.md
  • casework/common/api.py
  • casework/submit_reviews.py
  • tests/casework/test_api.py
  • tests/casework/test_guard_wiring.py
  • tests/casework/test_submit_reviews.py

Comment thread casework/README.md Outdated
Comment thread casework/submit_reviews.py Outdated
gaurav-karki added a commit that referenced this pull request Aug 10, 2026
…w 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>
@jawafdehi-pr-agent

Copy link
Copy Markdown

Persistent review updated to latest commit 072087b

@gaurav-karki

Copy link
Copy Markdown
Member Author

All six CodeRabbit items addressed in 072087b — 2 actionable, 4 nitpicks, all verified against the code first and all valid.

Item Fix
main exits 0 on failures Returns 1 when any case errored; diverges from siblings, documented, 3 tests. See thread.
README "Two failures" vs "All three" Now "Both" — 401/403 are one credential class.
submit_review hand-built headers Uses _headers("application/json"), matching create_material.
Bare-list branch of reviews_for_slug untested Test added. Pagination is a project-wide DRF setting, so that branch could have gone stale silently.
render_report test row built as a literal Built through _row — the literal still carried the title key dropped in a5ee01d.
--force load-bearing but unexplained in the guard test Comment added: it skips the pre-check GET, which is unguarded, so an unforced run fails on no_sockets before the POST guard fires.

1616 passed, 1 skipped in tests/casework/, ruff clean. No production writes at any point — the script has still only ever run against a loopback DEV_AUTH harness.

@damo-da damo-da closed this Aug 12, 2026
gaurav-karki and others added 6 commits August 11, 2026 22:04
reviews_for_slug answers "has this case been reviewed" off page one of the
list endpoint; review_detail carries `error`, which the list rows omit;
submit_review POSTs through _request so the host write-guard covers it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A case that already has a review of any status is skipped, so a run that dies
part-way resumes instead of re-grading at full LLM cost. 403 aborts the run
(the role check fails identically on every remaining case); any other HTTP
failure is recorded and the batch continues.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--report is read-only and reads the score/disposition straight off the review
list row. Only a failed row costs a detail fetch, because `error` lives on the
detail serializer alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The broad per-case except swallowed the guard's RuntimeError, so a remote run
without --allow-remote-writes logged one error per case and exited 0. Caught by
the end-to-end guard-wiring test, which is the only place the real client runs.

Also documents submit_reviews in the casework README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s 403

Code review found four real defects in submit_reviews:

- The pre-check GET sat outside the try/except, so one transient failure on
  what is half of a run's requests killed the whole batch and discarded the
  stats and footer. It now costs that case only, like the POST does.
- The abort guard checked 403 alone. OIDCAuthentication supplies
  authenticate_header, so an expired token is a 401 -- the commonest
  credential failure was counted once per case and still exited 0.
- --report logged "mode: APPLY" on a run that writes nothing.
- report_rows had the same unguarded read: one blip lost a report over a
  batch that took hours to grade. Failures are now `unreadable` rows.

Also warns when a review comes back filed under a different slug than the one
submitted (retired slug via CaseSlugHistory), which otherwise re-grades that
case on every run, and drops the report row's unused `title` field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w 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>
@damo-da

damo-da commented Aug 12, 2026

Copy link
Copy Markdown
Member

Rebased onto main to clear the merge conflict (was CONFLICTING). Force-pushed: 072087b595865e.

Conflict and how it was resolved. One file, tests/casework/test_api.py, two hunks — an adjacent-addition conflict where both sides appended tests to the end of the file. main added the get_courtcase / list_hearings / patch_case block; this branch added _FakeResponse plus the reviews_for_slug / review_detail / submit_review tests. Git interleaved them because both new tests opened with the same seen = {} / def fake_get(...) preamble. Both families kept in full, each with its own preamble.

Verification. pytest tests/casework/ = 1908 passed, 1 skipped. All 6 commits preserved; diffstat (6 files, +904/−0) unchanged from before the rebase.

Recovery if this rebase is unwanted: git push --force origin 072087b94305701db7b526da2f3fc125621641b0:feat/casework-submit-reviews

@damo-da damo-da reopened this Aug 12, 2026

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

If your organization is eligible for promotional free reviews, this run could not use one — if free runs remain, retrying may succeed without raising the limit.

Once credits are available — or to retry now — push a new commit or reopen this pull request to trigger a review.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Persistent review updated to latest commit 072087b

@damo-da
damo-da force-pushed the feat/casework-submit-reviews branch from 072087b to 595865e Compare August 12, 2026 05:42
@jawafdehi-pr-agent

Copy link
Copy Markdown

Persistent review updated to latest commit 595865e

@jawafdehi-pr-agent

Copy link
Copy Markdown

PR Agent Walkthrough 🤖

Welcome to the PR Agent, an AI-powered tool for automated pull request analysis, feedback, suggestions and more.

Here is a list of tools you can use to interact with the PR Agent:

ToolDescriptionTrigger Interactively 💎

DESCRIBE

Generates PR description - title, type, summary, code walkthrough and labels
  • Run

REVIEW

Adjustable feedback about the PR, possible issues, security concerns, review effort and more
  • Run

IMPROVE

Code suggestions for improving the PR
  • Run

UPDATE CHANGELOG

Automatically updates the changelog
  • Run

HELP DOCS

Answers a question regarding this repository, or a given one, based on given documentation path
  • Run

ADD DOCS

Generates documentation to methods/functions/classes that changed in the PR
  • Run

ASK

Answering free-text questions about the PR

[*]

GENERATE CUSTOM LABELS

Generates custom labels for the PR, based on specific guidelines defined by the user

[*]

(1) Note that each tool can be triggered automatically when a new PR is opened, or called manually by commenting on a PR.

(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the /ask tool, you need to comment on a PR: /ask "<question content>". See the relevant documentation for each tool for more details.

@jawafdehi-pr-agent

Copy link
Copy Markdown

Auto-approved PR

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants