fix: preserve resume setup for scheduled dispatch

This commit is contained in:
2026-06-11 08:50:54 -04:00
parent 7109be8cbf
commit 7ebc3bf560
2 changed files with 215 additions and 23 deletions
+163
View File
@@ -295,6 +295,169 @@ async def test_dispatch_due_prompt_send_failure_does_not_retry_next_poll(
assert "send text failed" in row.error
@pytest.mark.asyncio
async def test_dispatch_due_prompt_missing_worktree_recreates_without_attach(
factory, db, tmux, harnesses, fake_worktree
):
missing = "/tmp/does-not-exist/.worktrees/feature-x"
sess = _seed_worktree_session(db, path=missing)
service = SessionService(factory=factory, tmux=tmux, harnesses=harnesses)
service.schedule_prompt(sess.id, "continue", datetime.now() - timedelta(seconds=1))
harnesses["claude-code"].build_resume_config = MagicMock(
return_value=MagicMock(command=["claude"], env={}, cwd=Path(missing))
)
tmux.is_alive = AsyncMock(return_value=False)
tmux.respawn_verified = AsyncMock(
return_value=SpawnResult(ok=True, window_id=None, error="")
)
tmux.attach = AsyncMock(return_value=True)
results = await service.dispatch_due_prompts(now=datetime.now())
assert results[0].ok is True
assert fake_worktree.add_calls == [(Path("/tmp/myproj"), "feature-x")]
tmux.attach.assert_not_called()
tmux.send_text.assert_awaited_once_with("hqt-wt", "continue")
@pytest.mark.asyncio
async def test_dispatch_due_prompt_late_capture_updates_id_before_resume(
factory, db, tmux, harnesses_capture_fallback
):
harness = harnesses_capture_fallback["claude-code"]
harness.capture_session_id.return_value = "late-codex-id"
sess = _seed_session(db)
sess.harness_session_id = "placeholder-id"
db.commit()
service = SessionService(
factory=factory, tmux=tmux, harnesses=harnesses_capture_fallback
)
service.schedule_prompt(sess.id, "continue", datetime.now() - timedelta(seconds=1))
tmux.is_alive = AsyncMock(return_value=False)
tmux.respawn_verified = AsyncMock(
return_value=SpawnResult(ok=True, window_id="@2", error="")
)
results = await service.dispatch_due_prompts(now=datetime.now())
assert results[0].ok is True
harness.build_resume_config.assert_called_with(
Path("/tmp/myproj"), "late-codex-id", sess.model, sandboxed=False
)
db.expire_all()
assert db.get(Session, sess.id).harness_session_id == "late-codex-id"
@pytest.mark.asyncio
async def test_dispatch_due_prompt_send_exception_marks_failed_and_does_not_retry(
service, db, tmux
):
sess = _seed_session(db)
scheduled = service.schedule_prompt(
sess.id, "continue", datetime.now() - timedelta(seconds=1)
)
tmux.is_alive = AsyncMock(return_value=True)
tmux.send_text = AsyncMock(side_effect=RuntimeError("send exploded"))
first = await service.dispatch_due_prompts(now=datetime.now())
second = await service.dispatch_due_prompts(now=datetime.now())
assert len(first) == 1
assert first[0].ok is False
assert "send exploded" in first[0].error
assert second == []
tmux.send_text.assert_awaited_once()
tmux.send_enter.assert_not_called()
db.expire_all()
row = db.get(ScheduledPrompt, scheduled.id)
assert row.status == "failed"
assert "send exploded" in row.error
@pytest.mark.asyncio
async def test_dispatch_due_prompt_resume_exception_marks_failed_without_send(
service, db, tmux, harnesses
):
sess = _seed_session(db)
scheduled = service.schedule_prompt(
sess.id, "continue", datetime.now() - timedelta(seconds=1)
)
harnesses["claude-code"].build_resume_config = MagicMock(
return_value=MagicMock(
command=["claude", "--resume"], env={}, cwd=Path("/tmp/myproj")
)
)
tmux.is_alive = AsyncMock(return_value=False)
tmux.respawn_verified = AsyncMock(side_effect=RuntimeError("resume exploded"))
results = await service.dispatch_due_prompts(now=datetime.now())
assert len(results) == 1
assert results[0].ok is False
assert "resume exploded" in results[0].error
tmux.send_text.assert_not_called()
tmux.send_enter.assert_not_called()
db.expire_all()
row = db.get(ScheduledPrompt, scheduled.id)
assert row.status == "failed"
assert "resume exploded" in row.error
@pytest.mark.asyncio
async def test_dispatch_due_prompt_send_enter_failure_marks_failed(service, db, tmux):
sess = _seed_session(db)
scheduled = service.schedule_prompt(
sess.id, "continue", datetime.now() - timedelta(seconds=1)
)
tmux.is_alive = AsyncMock(return_value=True)
tmux.send_enter = AsyncMock(return_value=False)
results = await service.dispatch_due_prompts(now=datetime.now())
assert len(results) == 1
assert results[0].ok is False
assert "send enter failed" in results[0].error
tmux.send_text.assert_awaited_once_with("hqt-rename", "continue")
tmux.send_enter.assert_awaited_once_with("hqt-rename")
db.expire_all()
row = db.get(ScheduledPrompt, scheduled.id)
assert row.status == "failed"
assert "send enter failed" in row.error
@pytest.mark.asyncio
async def test_dispatch_due_prompt_resume_fails_then_fallback_spawn_succeeds(
service, db, tmux, harnesses
):
sess = _seed_session(db)
service.schedule_prompt(sess.id, "continue", datetime.now() - timedelta(seconds=1))
harnesses["claude-code"].build_resume_config = MagicMock(
return_value=MagicMock(
command=["claude", "--resume"], env={}, cwd=Path("/tmp/myproj")
)
)
harnesses["claude-code"].build_spawn_config = MagicMock(
return_value=MagicMock(
command=["claude", "--session-id", "sess-seeded"],
env={},
cwd=Path("/tmp/myproj"),
)
)
tmux.is_alive = AsyncMock(return_value=False)
tmux.respawn_verified = AsyncMock(
side_effect=[
SpawnResult(ok=False, window_id=None, error="resume failed"),
SpawnResult(ok=True, window_id="@2", error=""),
]
)
results = await service.dispatch_due_prompts(now=datetime.now())
assert results[0].ok is True
assert tmux.respawn_verified.await_count == 2
tmux.send_text.assert_awaited_once_with("hqt-rename", "continue")
@pytest.mark.asyncio
async def test_stop_session(service, tmux):
await service.create_session(project_id=1, harness_name="claude-code")