fix(kanban): live-claim guard keys on a live worker process, not on any claim
The first cut refused every claim-less complete of a running+claimed card, which also refused the flows that have no worker to protect: a library or CLI claim that never spawned a worker, and a worker whose process is gone (12 sibling tests exercise exactly that shape). The guard now fires only when tasks.worker_pid names a process that is still alive under its spawn fingerprint (_worker_alive), which is the run the issue asked us to keep open. Test updated to stand in as the live worker via _set_worker_pid.
This commit is contained in:
+11
-3
@@ -2653,15 +2653,23 @@ def complete_task(
|
||||
return False
|
||||
if acceptance is not None and not record_acceptance(conn, task_id, acceptance):
|
||||
return False
|
||||
trow = conn.execute("SELECT status, claim_lock FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
||||
trow = conn.execute(
|
||||
"SELECT status, claim_lock, worker_pid, worker_started_at FROM tasks WHERE id = ?",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
prior_status = trow["status"] if trow else None
|
||||
# Refuse to close a live worker's run without proof of ownership
|
||||
# (expected_run_id) or an explicit human override (force=True).
|
||||
# Refuse to close a LIVE worker's run without proof of ownership
|
||||
# (expected_run_id) or an explicit human override (force=True). "Live"
|
||||
# means the spawned worker process still exists: a claim whose worker
|
||||
# is gone (or a library claim that never spawned one) has no run to
|
||||
# protect, so manual completion keeps working there.
|
||||
if (
|
||||
expected_run_id is None
|
||||
and not force
|
||||
and prior_status == "running"
|
||||
and trow["claim_lock"] is not None
|
||||
and trow["worker_pid"]
|
||||
and _worker_alive(trow["worker_pid"], trow["worker_started_at"])
|
||||
):
|
||||
raise LiveClaimError(task_id)
|
||||
sql = """
|
||||
|
||||
@@ -10,11 +10,13 @@ row while that worker kept executing. The guard mirrors ``request_review``'s: a
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli import kanban_db as kb
|
||||
from hermes_cli import kanban_db_dispatch as kbd
|
||||
from hermes_cli import kanban_db_connect as kbc
|
||||
|
||||
|
||||
@@ -31,9 +33,12 @@ def conn(tmp_path, monkeypatch):
|
||||
yield c
|
||||
|
||||
|
||||
def _claimed_running_task(conn) -> tuple[str, int]:
|
||||
def _claimed_running_task(conn, *, live_worker: bool = True) -> tuple[str, int]:
|
||||
tid = kb.create_task(conn, title="live", assignee="coder")
|
||||
assert kb.claim_task(conn, tid, claimer=kb._claimer_id()) is not None
|
||||
if live_worker:
|
||||
# This process stands in for the spawned worker: alive, fingerprinted.
|
||||
kbd._set_worker_pid(conn, tid, os.getpid())
|
||||
return tid, kb._current_run_id(conn, tid)
|
||||
|
||||
|
||||
@@ -56,6 +61,15 @@ def test_claimless_complete_refuses_live_run_until_forced(conn):
|
||||
assert run["ended_at"] is not None and run["outcome"] == "completed"
|
||||
|
||||
|
||||
def test_claimless_complete_of_claim_without_live_worker_unchanged(conn):
|
||||
"""A claim whose worker never spawned (or is gone) protects no live run: the
|
||||
library / CLI flow that claims and then completes keeps working."""
|
||||
tid, run_id = _claimed_running_task(conn, live_worker=False)
|
||||
assert kb.complete_task(conn, tid, result="done") is True
|
||||
run = conn.execute("SELECT ended_at FROM task_runs WHERE id = ?", (run_id,)).fetchone()
|
||||
assert run["ended_at"] is not None
|
||||
|
||||
|
||||
def test_claimless_complete_of_unclaimed_card_unchanged(conn):
|
||||
"""The legitimate manual flow — completing a card nobody is working on — needs no proof."""
|
||||
tid = kb.create_task(conn, title="admin", assignee="coder")
|
||||
|
||||
Reference in New Issue
Block a user