feat: dispatch due scheduled prompts
This commit is contained in:
+108
-1
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -188,6 +188,113 @@ async def test_list_sessions_includes_pending_scheduled_prompt(service, db, tmux
|
||||
assert result[0].scheduled_prompt.status == "pending"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_due_prompt_alive_window_sends_and_marks_sent(service, db, tmux):
|
||||
sess = _seed_session(db)
|
||||
due = datetime.now() - timedelta(seconds=1)
|
||||
scheduled = service.schedule_prompt(sess.id, "continue", due)
|
||||
tmux.is_alive = AsyncMock(return_value=True)
|
||||
|
||||
results = await service.dispatch_due_prompts(now=datetime.now())
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].ok is True
|
||||
assert results[0].session_id == sess.id
|
||||
assert results[0].window_name == "hqt-rename"
|
||||
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 == "sent"
|
||||
assert row.sent_at is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_due_prompt_dead_window_resumes_without_attach(
|
||||
service, db, tmux, harnesses
|
||||
):
|
||||
sess = _seed_session(db)
|
||||
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=None, error="")
|
||||
)
|
||||
tmux.attach = AsyncMock(return_value=True)
|
||||
harnesses["claude-code"].build_resume_config = MagicMock(
|
||||
return_value=MagicMock(
|
||||
command=["claude", "--resume"], env={}, cwd=Path("/tmp/myproj")
|
||||
)
|
||||
)
|
||||
|
||||
results = await service.dispatch_due_prompts(now=datetime.now())
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].ok is True
|
||||
tmux.respawn_verified.assert_awaited()
|
||||
tmux.attach.assert_not_called()
|
||||
tmux.send_text.assert_awaited_once_with("hqt-rename", "continue")
|
||||
tmux.send_enter.assert_awaited_once_with("hqt-rename")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_due_prompt_resume_failure_marks_failed(
|
||||
service, db, tmux, harnesses
|
||||
):
|
||||
sess = _seed_session(db)
|
||||
scheduled = 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=False, window_id=None, error="resume failed")
|
||||
)
|
||||
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"], env={}, cwd=Path("/tmp/myproj"))
|
||||
)
|
||||
|
||||
results = await service.dispatch_due_prompts(now=datetime.now())
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].ok is False
|
||||
assert "resume failed" 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 failed" in row.error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_due_prompt_send_failure_does_not_retry_next_poll(
|
||||
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(return_value=False)
|
||||
|
||||
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 second == []
|
||||
assert tmux.send_text.await_count == 1
|
||||
tmux.send_enter.assert_not_called()
|
||||
db.expire_all()
|
||||
row = db.get(ScheduledPrompt, scheduled.id)
|
||||
assert row.status == "failed"
|
||||
assert "send text failed" in row.error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_session(service, tmux):
|
||||
await service.create_session(project_id=1, harness_name="claude-code")
|
||||
|
||||
Reference in New Issue
Block a user