from datetime import timedelta import pytest from arena.models import ( CodebaseArchive, Criteria, ModelProviderConfig, RunEvent, RunRequest, RunTask, ValidationCheck, ValidationReport, utc_now, ) from arena.run_store import InMemoryRunStore, InvalidRunToken, RedisRunStore, create_run_store class FakeRedisClient: def __init__(self) -> None: self.data: dict[str, str] = {} self.commands: list[tuple[str, ...]] = [] def execute(self, *args: str): self.commands.append(tuple(args)) command = args[0].upper() if command == "GET": return self.data.get(args[1]) if command == "SET": self.data[args[1]] = args[2] return "OK" raise AssertionError(f"unexpected command: {args}") def test_create_run_returns_rejoin_token_and_public_view_without_secret() -> None: store = InMemoryRunStore() access = store.create_run( RunRequest( prompt="Build a small FastAPI app", criteria=[Criteria(text="pytest must pass")], user_tests=["uv run pytest"], ) ) assert access.token assert access.run.status == "queued" assert access.run.prompt == "Build a small FastAPI app" assert access.run.criteria[0].text == "pytest must pass" assert access.run.user_tests == ["uv run pytest"] assert access.token not in access.run.model_dump_json() def test_create_run_persists_model_provider_without_api_key() -> None: store = InMemoryRunStore() access = store.create_run( RunRequest( prompt="Build a CLI", model_provider=ModelProviderConfig( provider="openrouter", model="qwen/model", api_key="sk-user-secret", base_url="https://openrouter.ai/api/v1", ), ) ) payload = access.run.model_dump_json() assert access.run.model_provider is not None assert access.run.model_provider.model == "qwen/model" assert "sk-user-secret" not in payload def test_rejoin_run_restores_access_by_token() -> None: store = InMemoryRunStore() access = store.create_run(RunRequest(prompt="Build a CLI")) rejoined = store.rejoin(access.token) assert rejoined.token == access.token assert rejoined.run.id == access.run.id def test_rejoin_run_rejects_unknown_token() -> None: store = InMemoryRunStore() with pytest.raises(InvalidRunToken): store.rejoin("not-a-real-token") def test_store_appends_events_and_saves_task_state() -> None: store = InMemoryRunStore() access = store.create_run(RunRequest(prompt="Build a CLI")) created_at = access.run.updated_at earlier = utc_now() later = earlier + timedelta(seconds=1) store.save_tasks( access.run.id, [RunTask(id="task-1", title="Implement CLI", status="active")], ) store.append_event(access.run.id, RunEvent(id="event-2", message="Coding", created_at=later)) store.append_event(access.run.id, RunEvent(id="event-1", message="Planning", created_at=earlier)) view = store.get_run_view(access.run.id) assert view.tasks[0].status == "active" assert [event.id for event in view.events] == ["event-1", "event-2"] assert view.updated_at > created_at def test_store_deduplicates_appended_events_by_id() -> None: store = InMemoryRunStore() access = store.create_run(RunRequest(prompt="Build a CLI")) event = RunEvent(id="event-1", message="Planning") store.append_event(access.run.id, event) store.append_event(access.run.id, event) view = store.get_run_view(access.run.id) assert [item.id for item in view.events] == ["event-1"] def test_store_status_mutation_updates_timestamp() -> None: store = InMemoryRunStore() access = store.create_run(RunRequest(prompt="Build a CLI")) view = store.save_status(access.run.id, "working") assert view.status == "working" assert view.updated_at > access.run.updated_at def test_store_saves_archive_checks_and_validation_report() -> None: store = InMemoryRunStore() access = store.create_run(RunRequest(prompt="Build a CLI")) store.save_codebase_archive( access.run.id, CodebaseArchive(pointer="local-archive://run-1", file_count=2, size_bytes=256), ) store.save_validation_checks( access.run.id, [ ValidationCheck( id="check-1", name="tests", command="uv run pytest", source="generated", ) ], ) view = store.save_validation_report( access.run.id, ValidationReport(run_id=access.run.id, passed=True, summary="ok"), ) assert view.codebase_archive is not None assert view.codebase_archive.pointer == "local-archive://run-1" assert view.validation_checks[0].name == "tests" assert view.validation_report is not None assert view.validation_report.passed is True def test_redis_run_store_restores_run_after_restart() -> None: client = FakeRedisClient() store = RedisRunStore(client=client) access = store.create_run(RunRequest(prompt="Build a CLI")) store.save_tasks( access.run.id, [RunTask(id="task-1", title="Implement CLI", status="completed")], ) store.append_event(access.run.id, RunEvent(id="event-1", message="Done")) restarted = RedisRunStore(client=client) rejoined = restarted.rejoin(access.token) assert rejoined.run.id == access.run.id assert rejoined.run.tasks[0].status == "completed" assert rejoined.run.events[0].message == "Done" assert any(command[0] == "SET" and command[1].startswith("arena:run:") for command in client.commands) def test_store_marks_active_tasks_interrupted() -> None: store = InMemoryRunStore() access = store.create_run(RunRequest(prompt="Build a CLI")) store.save_tasks( access.run.id, [ RunTask(id="task-1", title="Implement CLI", status="active"), RunTask(id="task-2", title="Write docs", status="completed"), ], ) view = store.mark_active_tasks_interrupted(access.run.id) assert [task.status for task in view.tasks] == ["interrupted", "completed"] def test_create_run_store_defaults_to_memory(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("RUN_STORE_PROVIDER", raising=False) assert isinstance(create_run_store(), InMemoryRunStore) def test_create_run_store_requires_redis_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RUN_STORE_PROVIDER", "redis") monkeypatch.delenv("UPSTASH_REDIS_REST_URL", raising=False) monkeypatch.delenv("UPSTASH_REDIS_REST_TOKEN", raising=False) with pytest.raises(ValueError): create_run_store()