Skip to content

Commit d3dcfd9

Browse files
test(C6): pin CLI start refused while a web session is grace-detached
Council item C6 (SIGNOFF-M2/ARBITRATION.md), matrix gap #1 of 3 (the other two -- C1's interleaving, C4's restart-with-live-child -- are closed by those items' own tests). No code change: claim_blocks_cli_start never consults _grace state, only pid liveness, so this cell was already correct by construction. The test exists to pin that the two mechanisms compose correctly, not to fix a defect. Test: TestWebThenCli:: test_cli_start_is_refused_while_a_web_session_is_detached_within_grace -- schedules a real grace-release timer for a web claim with a live pid, confirms has_pending_release() is true, then asserts a CLI start still refuses with the existing message and logs no reclaim. Evidence: reviews/2026-09-02-full-repo-review/evidence/M2/step-9/C6/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 6188f3a commit d3dcfd9

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

docs/architecture/session-authority.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ empty) and cell 2/4's stale-claim branch (CLI start, either owner shape):
9898
the recorded owner is provably dead — reclaimed, not blocked, in both
9999
directions.
100100

101+
**C6 (council, matrix gap, closed):** cell 4's live-pid branch is also
102+
exercised while the web session is merely DETACHED within its grace
103+
window (`_grace.has_pending_release()` true — client's tab closed, agent
104+
still running, not yet released) — `TestWebThenCli::
105+
test_cli_start_is_refused_while_a_web_session_is_detached_within_grace`.
106+
`claim_blocks_cli_start` does not consult grace state at all (only pid
107+
liveness), which is exactly why this cell was worth pinning explicitly:
108+
proof the two mechanisms compose correctly, not merely that each is
109+
individually correct.
110+
101111
## 4. End matrix
102112

103113
| Ends | Effect |

packages/studyloop/tests/test_session_authority_matrix.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,40 @@ def test_a_dead_web_claim_is_reclaimed_by_a_cli_start(self, caplog) -> None:
435435
assert len(warnings) == 1
436436
assert "web-sess-crashed" in warnings[0].message
437437

438+
def test_cli_start_is_refused_while_a_web_session_is_detached_within_grace(
439+
self, caplog
440+
) -> None:
441+
"""C6 (matrix gap the council named): a web session merely DETACHED
442+
within its grace window (client's tab closed, agent still running,
443+
not yet released) must still refuse a CLI start with the existing
444+
message -- the grace window is not license to reclaim.
445+
claim_blocks_cli_start does not consult grace state at all (only
446+
pid liveness), which is exactly why this cell was worth pinning:
447+
proof the two mechanisms compose correctly rather than merely each
448+
being individually tested."""
449+
from studyloop.web.routes.session import _grace
450+
451+
state = _fixture("web-live")
452+
state["pid"] = os.getpid()
453+
454+
async def _schedule() -> None:
455+
_grace.schedule_release("web-sess-1", grace=60.0)
456+
457+
run_async(_schedule())
458+
assert _grace.has_pending_release("web-sess-1") is True
459+
460+
with (
461+
patch("studyloop.tmux.is_tmux_available", return_value=True),
462+
patch("studyloop.agent_launcher.shutil.which", return_value="/usr/bin/claude"),
463+
patch("studyloop.session_state.read_session_state", return_value=state),
464+
patch("studyloop.session.cleanup.auto_clean_zombies"),
465+
pytest.raises(SessionStartError) as exc_info,
466+
):
467+
start_session("Async IO", "claude", "study", "elapsed", 5, False)
468+
469+
assert "already active" in exc_info.value.message
470+
assert not any("reclaim" in rec.message.lower() for rec in caplog.records)
471+
438472
def test_a_web_claim_without_a_pid_blocks_conservatively(self) -> None:
439473
"""A web-owned claim with no `pid` key (written by a build before
440474
this fix) blocks rather than silently reclaiming a claim whose

0 commit comments

Comments
 (0)