fix(cli): fail closed on mid-pagination 404 in project resolution - #146
Conversation
fetch_projects_page maps every 404 to Ok(None), and the pagination loop in resolve_project_by_repo turned that into a clean "no match" on any page. A 404 on page 2+ — Django pagination when a concurrent delete shrinks the filtered set — therefore discarded the exact matches already collected from page 1, and the caller fell back to the checkout basename, letting list/wait query an unrelated same-basename project and exit 0. The page-1 404 stays a soft None: that is the intentional compatibility path for a backend without the /projects endpoint. Any later page is now a hard error.
There was a problem hiding this comment.
No actionable findings. I verified the merge-base diff and traced the fetch_projects_page 404 sentinel through resolve_project_by_repo and its fatal list/wait call paths. Page 1 remains the old-backend compatibility soft miss, while every page 2+ 404 now returns before collected matches can be discarded into the legacy-name fallback. The focused test exercises that exact transition and asserts the hard error; formatting/diff checks pass, and CI's strict clippy, dependency audit, and 595-test coverage gate are green. I found no production blocker and approve this change.
Sent by Cursor Automation: pr-flow
corgea-security
left a comment
There was a problem hiding this comment.
Automated review risk: 1/5.
No actionable findings. The change correctly preserves the page-1 compatibility behavior while treating later 404 responses as pagination failures, with focused regression coverage.
No critical or high-priority changes were found.
corgea-security
left a comment
There was a problem hiding this comment.
Approved by Dennis: high policy risk and automated risk 1/5.


Bug
fetch_projects_pagemaps every HTTP 404 toOk(None), and the pagination loop inresolve_project_by_repotreated that as a clean "no match" regardless of which page it came from:So a 404 on page 2+ silently discarded the exact matches already accumulated from page 1 and returned a clean miss.
resolve_projectthen falls back to the checkout basename, solist/waitcan query an unrelated same-basename project and exit 0 — the exact failure mode this hardening code exists to prevent.The concurrent-deletion scenario
The backend filters
repo_url__icontainsover a paginated list. If a project is deleted between the page-1 and page-2 requests, the filtered result set shrinks and Django's paginator 404s the now out-of-range page. Nothing is wrong with the CLI's request — but the walk was incomplete, and the answer is no longer trustworthy.Fix
Page 1 keeps its soft
Ok(None): that 404 is the intentional compatibility path for a backend that does not have the/projectsendpoint at all, and the caller correctly falls back to CWD-name resolution there. Any page after the walk has started is now a hardErr— the same fail-closed posture as thePROJECTS_MAX_PAGESceiling right below it.Test
resolve_project_by_repo_mid_pagination_404_is_hard_err: page 1 returnstotal_pages: 2with the exact match, page 2 returns 404 →Errmentioning page 2 and pagination.spawn_paged_projects_stubgained a status-aware variant, mirroring the existingspawn_projects_stub/spawn_projects_stub_statuspair.resolve_project_by_repo_empty_and_404_are_soft_nonestill passes.cargo test: 595 passed, 0 failed.Reference
Review comment on #138: #138 (comment)