Skip to content

fix(invites): allow revoking expired invites, 409 for terminal states#2071

Merged
jaylfc merged 2 commits into
devfrom
fix/invite-revoke-expired
Jul 21, 2026
Merged

fix(invites): allow revoking expired invites, 409 for terminal states#2071
jaylfc merged 2 commits into
devfrom
fix/invite-revoke-expired

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Problem

Live repro from Jay's e2e test: both pending invites hit their 15-minute TTL and lazily flipped to expired on read. store.revoke only matches status='pending', so the DELETE matched zero rows and the route returned 404 "invite not found" - while the dialog kept listing the rows (it labels everything "Pending" regardless of status, tracked in #2065). Net effect: dead invites, displayed as pending, unrevokable.

Fix

  • invite_store.revoke: matches pending and expired (an expired invite is exactly what a user wants to clean up).
  • revoke_invite route: redeemed/revoked now return 409 with the actual state instead of a misleading 404; 404 remains only for genuinely missing or wrong-project invites.

Testing

  • New: revoke-expired returns 204 (store + route), revoke-redeemed returns false, double-revoke returns 409.
  • Full invite store + route suites: 71 passed.

Related

Summary by CodeRabbit

  • New Features
    • Expired project invitations can now be revoked successfully.
  • Bug Fixes
    • Revocation now returns clearer 409 conflict responses when an invitation is already redeemed or revoked.
    • If an invitation is mid-redeem (“claimed”), revocation is blocked with a 409 including a “mid-redeem” message.
    • If the invitation changes state during the request, the endpoint returns 409 with a retry guidance response.
  • Tests
    • Added coverage for revoke behavior across expired, redeemed, claimed, and already-revoked scenarios.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Project invite revocation now accepts expired invites, rejects redeemed, revoked, or claimed invites with HTTP 409, and reports failed state transitions as conflicts. Store and route tests cover these behaviors.

Changes

Project invite revocation

Layer / File(s) Summary
Store revocation state handling
tinyagentos/projects/invite_store.py, tests/projects/test_invite_store.py
revoke() now updates invites in pending or expired status; redeemed invites remain unsuccessful.
Endpoint conflict responses
tinyagentos/routes/project_invites.py, tests/test_routes_project_invites.py
The DELETE endpoint returns HTTP 409 for redeemed, revoked, claimed, or concurrently changed invites, with coverage for expired and repeated revocation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • jaylfc/taOS#2002: Introduces the invite status model used by the revoke endpoint’s claimed and redeemed handling.

Suggested reviewers: hognek

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main invite revocation changes: expired invites are now revocable and terminal states return 409.
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.
✨ 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 fix/invite-revoke-expired

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.

@gitar-bot

gitar-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Comment thread tinyagentos/routes/project_invites.py Outdated
row = await store.get(invite_id)
if row is None or row.get("project_id") != project_id:
return JSONResponse({"error": "invite not found"}, status_code=404)
if row.get("status") in ("redeemed", "revoked"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Route-level 409 for an already-redeemed invite is untested

The redeemed branch here is only exercised indirectly at the store level (test_revoke_redeemed_invite_returns_false). Add a route test that mints, redeems via store.redeem, then issues DELETE and asserts 409, so this new HTTP path is covered (the PR description claims "revoke-redeemed returns 409" but only the revoked double-revoke path is tested at the route level).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

ok = await store.revoke(invite_id)
if not ok:
return JSONResponse({"error": "invite not found or already redeemed"}, status_code=404)
return JSONResponse({"error": "invite state changed, retry"}, status_code=409)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Generic 409 also fires for a transient claimed invite

A mid-redeem invite has status claimed (see invite_store.redeem, which flips pendingclaimed). It is not in the ("redeemed", "revoked") guard, so it reaches store.revoke, which returns False (only pending/expired match), landing here with "invite state changed, retry". Behavior is safe (revoke is correctly blocked; a later retry works if the claim rolled back to pending, or returns the explicit 409 once it becomes redeemed), but the message is slightly misleading for that case. Consider adding "claimed" to the explicit guard for clearer observability.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • tinyagentos/projects/invite_store.py
  • tinyagentos/routes/project_invites.py
  • tests/projects/test_invite_store.py
  • tests/test_routes_project_invites.py

Prior Suggestions — Resolved

  • project_invites.py redeemed route 409 now covered by test_revoke_redeemed_returns_409.
  • project_invites.py claimed now returns a dedicated 409 ("invite is mid-redeem, retry once it settles") instead of the generic retry text; covered by test_revoke_claimed_returns_409_with_mid_redeem_message.
Previous Review Summaries (2 snapshots, latest commit 22a9684)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 22a9684)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • tinyagentos/routes/project_invites.py - previous suggestions resolved
  • tests/test_routes_project_invites.py - previous suggestions resolved

Resolved (from prior review)

  • tinyagentos/routes/project_invites.py:707 — Route-level 409 for an already-redeemed invite is now covered by test_revoke_redeemed_returns_409.
  • tinyagentos/routes/project_invites.py (claimed branch) — claimed now returns a dedicated, accurate 409 message ("invite is mid-redeem, retry once it settles") instead of the misleading generic "invite state changed, retry".

Previous review (commit 915f977)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/routes/project_invites.py 707 Route-level 409 for an already-redeemed invite is untested (only covered at store level)
tinyagentos/routes/project_invites.py 713 Generic 409 also fires for a transient claimed invite; message slightly misleading for that case
Files Reviewed (4 files)
  • tinyagentos/projects/invite_store.py - 0 issues (revoke IN ('pending','expired') correct)
  • tinyagentos/routes/project_invites.py - 2 suggestions
  • tests/projects/test_invite_store.py - 0 issues
  • tests/test_routes_project_invites.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 74.1K · Output: 9.3K · Cached: 173.5K

@jaylfc

jaylfc commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Both Kilo suggestions folded at the new head:

  1. Route-level 409 for redeemed untested - added test_revoke_redeemed_returns_409 (mint, redeem via store, DELETE, assert 409). You were right that the PR description claimed coverage the store-level test did not provide.
  2. Generic 409 for a transient claimed invite - claimed now has its own branch returning "invite is mid-redeem, retry once it settles" rather than falling through to "invite state changed, retry", with test_revoke_claimed_returns_409_with_mid_redeem_message covering it. Behaviour was already safe (revoke correctly refuses mid-redeem); this just stops the message from misdescribing why.

Suite: 73 passed.

jaylfc added 2 commits July 20, 2026 23:53
store.revoke only matched status='pending', so an invite that lazily
flipped to expired could never be revoked: the route reported 404 while
the row kept showing in the pending list. Revoke now covers pending and
expired; the route returns 409 with the actual state for redeemed or
already-revoked invites instead of a misleading 404.

Tests: revoke-expired succeeds (store + route), revoke-redeemed refused,
double-revoke 409.
Folds both Kilo suggestions: a 'claimed' invite is mid-redeem, so it now
returns a 409 that says so instead of the generic retry text, and both
409 route paths (redeemed and claimed) have direct tests rather than only
store-level coverage.
@jaylfc
jaylfc force-pushed the fix/invite-revoke-expired branch from 22a9684 to fb2a1df Compare July 20, 2026 23:54

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

Actionable comments posted: 1

🤖 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 `@tests/test_routes_project_invites.py`:
- Around line 664-668: Update the invite state setup in this test to directly
set the stored invite’s status to redeemed, following the database-update
approach used by the other state-transition tests, instead of calling
store.redeem(). Assert the response’s specific error message in addition to
status code 409 to verify the route exercised the redeemed branch rather than
claimed.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2064bd83-74c9-4a37-895d-56121258ec0f

📥 Commits

Reviewing files that changed from the base of the PR and between 915f977 and fb2a1df.

📒 Files selected for processing (4)
  • tests/projects/test_invite_store.py
  • tests/test_routes_project_invites.py
  • tinyagentos/projects/invite_store.py
  • tinyagentos/routes/project_invites.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tinyagentos/projects/invite_store.py
  • tinyagentos/routes/project_invites.py
  • tests/projects/test_invite_store.py

Comment on lines +664 to +668
body = mint_resp.json()
store = app.state.project_invites
await store.redeem(body["invite_id"], body["pin"])
resp = await client.delete(f"/api/projects/{pid}/invites/{body['invite_id']}")
assert resp.status_code == 409, resp.text

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Set status to redeemed to test the correct code path.

Calling store.redeem() only transitions the invite to the claimed state (mid-redeem), based on the store's implementation in tinyagentos/projects/invite_store.py. This causes the test to accidentally hit the claimed branch in the route, which also returns a 409, yielding a false positive for the redeemed state coverage.

To correctly test the redeemed branch, update the database status directly, matching the approach used in the other state-transition tests. Asserting the specific error message will also guarantee the correct branch is exercised.

🛠️ Proposed fix to correctly test the redeemed state
-    body = mint_resp.json()
-    store = app.state.project_invites
-    await store.redeem(body["invite_id"], body["pin"])
-    resp = await client.delete(f"/api/projects/{pid}/invites/{body['invite_id']}")
-    assert resp.status_code == 409, resp.text
+    iid = mint_resp.json()["invite_id"]
+    store = app.state.project_invites
+    await store._db.execute(
+        "UPDATE project_invites SET status = 'redeemed' WHERE invite_id = ?", (iid,)
+    )
+    await store._db.commit()
+    resp = await client.delete(f"/api/projects/{pid}/invites/{iid}")
+    assert resp.status_code == 409, resp.text
+    assert "already redeemed" in resp.json()["error"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
body = mint_resp.json()
store = app.state.project_invites
await store.redeem(body["invite_id"], body["pin"])
resp = await client.delete(f"/api/projects/{pid}/invites/{body['invite_id']}")
assert resp.status_code == 409, resp.text
iid = mint_resp.json()["invite_id"]
store = app.state.project_invites
await store._db.execute(
"UPDATE project_invites SET status = 'redeemed' WHERE invite_id = ?", (iid,)
)
await store._db.commit()
resp = await client.delete(f"/api/projects/{pid}/invites/{iid}")
assert resp.status_code == 409, resp.text
assert "already redeemed" in resp.json()["error"]
🤖 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/test_routes_project_invites.py` around lines 664 - 668, Update the
invite state setup in this test to directly set the stored invite’s status to
redeemed, following the database-update approach used by the other
state-transition tests, instead of calling store.redeem(). Assert the response’s
specific error message in addition to status code 409 to verify the route
exercised the redeemed branch rather than claimed.

@jaylfc
jaylfc merged commit 6ee9f63 into dev Jul 21, 2026
9 checks passed
@jaylfc
jaylfc deleted the fix/invite-revoke-expired branch July 21, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant