feat(tasks): SessionStart reads the claim — a compaction gets its work back (milestone 381 step 3)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Canceled after 45s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Canceled after 45s
The claim now pays rent to the session that set it. The SessionStart hook sends the host's `source` and the session id; the server renders a claim section by source (task_claims.render_claims): - compact / clear: the work this session had claimed, each with its two latest log entries, so a compacted session resumes from the record rather than from a count of open tasks. - startup / clear: other sessions' live claims (may still be running) and in-progress tasks whose claim went quiet (abandoned mid-task). - fork: the same, framed as "two sessions may now hold this". - resume, or no source sent: nothing. The task sidebar shows a live claim as "Being worked" and a dead one on open work as "Went quiet", so the operator sees a session die mid-task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -168,3 +168,91 @@ async def test_a_closed_task_binds_nothing(users):
|
||||
task = await notes_svc.create_note(owner, title="claim closed", status="in_progress")
|
||||
await notes_svc.update_note(owner, task.id, status="done")
|
||||
assert await tc.bind_session(owner, task.id, "sess") is None
|
||||
|
||||
|
||||
# --- step 3: the readers ----------------------------------------------------
|
||||
|
||||
from types import SimpleNamespace # noqa: E402
|
||||
|
||||
_NOW = datetime(2026, 9, 24, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _claimed(id, session, touched_ago, status="in_progress", title=None):
|
||||
t = _NOW - touched_ago
|
||||
return SimpleNamespace(
|
||||
id=id, title=title or f"task {id}", status=status,
|
||||
claimed_by=1, claimed_at=t, claim_touched_at=t, claim_session=session,
|
||||
)
|
||||
|
||||
|
||||
def _log(content, ago=timedelta(minutes=5)):
|
||||
return SimpleNamespace(content=content, created_at=_NOW - ago)
|
||||
|
||||
|
||||
def _render(source, claims, logs=None, sid="me"):
|
||||
return "\n".join(tc.render_claims(source, sid, claims, logs or {}, now=_NOW))
|
||||
|
||||
|
||||
def test_a_compaction_gets_back_its_own_claimed_work_and_latest_logs():
|
||||
"""The measurement the milestone names: a compacted session comes back
|
||||
holding its own state, without being told to go looking."""
|
||||
mine = _claimed(10, "me", timedelta(minutes=3), title="wire the reader")
|
||||
out = _render("compact", [mine], {10: [_log("ruled out the cache theory")]})
|
||||
assert "#10" in out and "wire the reader" in out
|
||||
assert "ruled out the cache theory" in out
|
||||
|
||||
|
||||
def test_a_resume_says_nothing():
|
||||
mine = _claimed(10, "me", timedelta(minutes=3))
|
||||
assert _render("resume", [mine]) == ""
|
||||
|
||||
|
||||
def test_a_startup_names_other_sessions_live_and_abandoned_claims():
|
||||
live = _claimed(11, "other", timedelta(minutes=10))
|
||||
gone = _claimed(12, "older", timedelta(days=3))
|
||||
out = _render("startup", [live, gone])
|
||||
assert "#11" in out and "may still be running" in out
|
||||
assert "#12" in out and "went quiet" in out
|
||||
|
||||
|
||||
def test_a_startup_does_not_push_this_sessions_own_work():
|
||||
"""A new session id owns nothing yet; the own-work push is for a context
|
||||
that was lost, not one that never existed."""
|
||||
mine = _claimed(10, "me", timedelta(minutes=3))
|
||||
assert "In flight" not in _render("startup", [mine])
|
||||
|
||||
|
||||
def test_a_fork_is_told_two_sessions_may_hold_the_same_claim():
|
||||
parent = _claimed(13, "parent", timedelta(minutes=2))
|
||||
out = _render("fork", [parent], sid="child")
|
||||
assert "forked" in out and "#13" in out
|
||||
|
||||
|
||||
def test_a_dead_claim_on_finished_work_is_not_news():
|
||||
done = _claimed(14, "older", timedelta(days=3), status="done")
|
||||
assert _render("startup", [done]) == ""
|
||||
|
||||
|
||||
def test_the_session_start_hook_sends_the_source_and_the_session():
|
||||
"""The reader branches on what the hook sends; a hook that stopped sending
|
||||
either would leave every session on the no-claims path, silently."""
|
||||
text = (ROOT / "plugin/hooks/scribe_session_context.sh").read_text()
|
||||
assert re.search(r'q="source=\$\(printf', text)
|
||||
assert "session_id=$(printf" in text
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_session_start_after_a_compaction_carries_the_claimed_task(users):
|
||||
from scribe.services import task_logs
|
||||
from scribe.services.plugin_context import build_session_context
|
||||
|
||||
owner, _ = users
|
||||
task = await notes_svc.create_note(owner, title="claimed then compacted",
|
||||
status="in_progress")
|
||||
await task_logs.create_log(owner, task.id, "halfway: the migration is written")
|
||||
await tc.bind_session(owner, task.id, "sess-compact")
|
||||
|
||||
ctx = (await build_session_context(
|
||||
owner, source="compact", session_id="sess-compact"))["context"]
|
||||
assert "claimed then compacted" in ctx
|
||||
assert "the migration is written" in ctx
|
||||
|
||||
Reference in New Issue
Block a user