fix(kanban): respawn guard defers blocker_auth instead of auto-blocking (#28683)
Follow-up to #28455. The respawn guard's blocker_auth rule (last error matched a quota/auth/429 pattern) was auto-blocking the task on first occurrence. That's too aggressive: transient rate limits typically clear in seconds to minutes, but the auto-block puts the task in 'blocked' status which requires manual unblock. Now treats blocker_auth the same as recent_success and active_pr: defer the spawn this tick, leave the task in 'ready', let the next tick try again. If the auth error genuinely persists, the existing consecutive_failures counter trips the auto-block circuit breaker after failure_limit failures via the normal path — so a persistent 401/403/quota-exhausted still ends up blocked, just not on first hit. Also documents the respawn_guarded event in kanban.md's events table with the three guard reasons. Updated test_dispatch_respawn_guard_auto_blocks_auth_error → renamed to test_dispatch_respawn_guard_defers_auth_error_without_auto_block; asserts task stays in 'ready' and the guard reason is recorded.
This commit is contained in:
@@ -1174,10 +1174,20 @@ def test_respawn_guard_old_pr_comment_not_guarded(kanban_home):
|
||||
assert reason is None
|
||||
|
||||
|
||||
def test_dispatch_respawn_guard_auto_blocks_auth_error(
|
||||
def test_dispatch_respawn_guard_defers_auth_error_without_auto_block(
|
||||
kanban_home, all_assignees_spawnable
|
||||
):
|
||||
"""dispatch_once auto-blocks a ready task whose last error is a blocker_auth."""
|
||||
"""dispatch_once defers (does NOT auto-block) a ready task whose last
|
||||
error is a blocker_auth.
|
||||
|
||||
The old behaviour auto-blocked on first occurrence, which was too
|
||||
aggressive: a transient 429 rate-limit (which typically clears in
|
||||
seconds to minutes) would end up requiring manual unblock. The new
|
||||
behaviour defers the spawn this tick; the task stays in ``ready``
|
||||
and gets another chance next tick. If the auth error genuinely
|
||||
persists, the existing ``consecutive_failures`` circuit breaker
|
||||
will auto-block via the normal failure-limit path.
|
||||
"""
|
||||
spawned_ids = []
|
||||
|
||||
def fake_spawn(task, workspace):
|
||||
@@ -1191,10 +1201,22 @@ def test_dispatch_respawn_guard_auto_blocks_auth_error(
|
||||
)
|
||||
res = kb.dispatch_once(conn, spawn_fn=fake_spawn)
|
||||
|
||||
assert t in res.auto_blocked
|
||||
# Critical: task is NOT auto-blocked on first occurrence.
|
||||
assert t not in res.auto_blocked, (
|
||||
f"blocker_auth should defer, not auto-block on first occurrence; "
|
||||
f"got auto_blocked={res.auto_blocked!r}"
|
||||
)
|
||||
# It IS recorded as respawn_guarded with the reason.
|
||||
assert (t, "blocker_auth") in res.respawn_guarded, (
|
||||
f"expected (task_id, 'blocker_auth') in respawn_guarded; "
|
||||
f"got {res.respawn_guarded!r}"
|
||||
)
|
||||
# And it's NOT spawned this tick.
|
||||
assert t not in spawned_ids
|
||||
# Status stays ``ready`` so a future tick (or operator action) can
|
||||
# retry without manual unblock.
|
||||
with kb.connect() as conn:
|
||||
assert kb.get_task(conn, t).status == "blocked"
|
||||
assert kb.get_task(conn, t).status == "ready"
|
||||
|
||||
|
||||
def test_dispatch_respawn_guard_skips_recent_success(
|
||||
|
||||
Reference in New Issue
Block a user