fix: serialize Codex spawn->capture with a lock
Hold an asyncio.Lock across the spawn->capture window for harnesses that capture a session id, so two concurrent hqt starts in the same project never produce overlapping capture windows. Completes the P2 capture-race fix alongside the ambiguity guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1106,3 +1106,53 @@ async def test_list_sessions_skips_capture_for_deleted_project(
|
||||
harnesses_capture["claude-code"].capture_session_id.return_value = "would-capture"
|
||||
infos = await service.list_sessions(1)
|
||||
assert len(infos) == 1 # no ServiceError escaped
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 3 (P2): serialize the spawn->capture window with a lock
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_session_holds_capture_lock_for_capturing_harness(
|
||||
factory, db, tmux
|
||||
):
|
||||
observed = {}
|
||||
|
||||
def _capture(project_path, since):
|
||||
# capture runs inside the spawn->capture critical section
|
||||
observed["locked_during_capture"] = service._capture_lock.locked()
|
||||
return "real-codex-id"
|
||||
|
||||
h = MagicMock()
|
||||
h.captures_session_id = True
|
||||
h.generate_session_id.return_value = "placeholder"
|
||||
h.build_spawn_config.return_value = MagicMock(
|
||||
command=["codex"], env={}, cwd=Path("/tmp/myproj")
|
||||
)
|
||||
h.capture_session_id.side_effect = _capture
|
||||
service = SessionService(factory=factory, tmux=tmux, harnesses={"claude-code": h})
|
||||
|
||||
result = await service.create_session(project_id=1, harness_name="claude-code")
|
||||
|
||||
assert observed["locked_during_capture"] is True
|
||||
assert result.session.harness_session_id == "real-codex-id"
|
||||
# Lock is released after create_session returns.
|
||||
assert service._capture_lock.locked() is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_session_no_lock_for_non_capturing_harness(service, db, tmux):
|
||||
observed = {}
|
||||
|
||||
async def _spawn(req):
|
||||
observed["locked_during_spawn"] = service._capture_lock.locked()
|
||||
return SpawnResult(ok=True, window_id="@1", error="")
|
||||
|
||||
tmux.spawn.side_effect = _spawn
|
||||
|
||||
await service.create_session(project_id=1, harness_name="claude-code")
|
||||
|
||||
# The `service` fixture's harness has captures_session_id = False, so the
|
||||
# spawn must NOT be serialized under the capture lock.
|
||||
assert observed["locked_during_spawn"] is False
|
||||
|
||||
Reference in New Issue
Block a user