from __future__ import annotations import json from datetime import UTC, datetime from host_agent.history import ConsoleHistoryStore def test_history_store_records_assignment_and_heartbeat_newest_first(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3") store.record_assignment( task_id="task-a", attempt=1, status="done", failure_reason=None, device_id="device-a", ) store.record_heartbeat(device_count=2) entries = store.list_recent() assert len(entries) == 2 assert entries[0] == { "kind": "heartbeat", "occurred_at": entries[0]["occurred_at"], "summary": "heartbeat: 2 devices", "detail": {"device_count": 2}, } assert entries[1] == { "kind": "assignment", "occurred_at": entries[1]["occurred_at"], "summary": "task-a attempt 1 on device-a: done", "detail": { "task_id": "task-a", "attempt": 1, "status": "done", "failure_reason": None, "device_id": "device-a", }, } def test_history_store_prunes_oldest_entries_beyond_limit(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3", limit=3) for index in range(5): store.record_heartbeat(device_count=index) entries = store.list_recent() assert len(entries) == 3 assert [entry["detail"]["device_count"] for entry in entries] == [4, 3, 2] def test_history_store_returns_empty_list_when_no_entries(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3") assert store.list_recent() == [] def test_history_store_uses_injected_now_for_occurred_at(tmp_path) -> None: fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) store = ConsoleHistoryStore(tmp_path / "history.sqlite3", now=lambda: fixed_now) store.record_heartbeat(device_count=1) entries = store.list_recent() assert entries[0]["occurred_at"] == fixed_now.isoformat() def test_history_store_records_task_submission_with_device(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3") store.record_task_submission(task_id="task-cloud-1", device_id="device-cloud-a") entries = store.list_recent() assert len(entries) == 1 assert entries[0]["kind"] == "task_submission" assert entries[0]["summary"] == "task submitted: task-cloud-1 on device-cloud-a" assert entries[0]["detail"] == { "task_id": "task-cloud-1", "device_id": "device-cloud-a", } def test_history_store_records_task_submission_without_device(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3") store.record_task_submission(task_id="task-cloud-2", device_id=None) entries = store.list_recent() assert entries[0]["summary"] == ("task submitted: task-cloud-2 (automatic device)") assert entries[0]["detail"] == {"task_id": "task-cloud-2"} def test_history_store_task_submission_redacts_goal_and_secrets(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3") secret_goal = ( "rotate secret-XYZ-abcdef-very-secret cookie=session-abc; lease=lease-stale" ) store.record_task_submission(task_id="task-cloud-3", device_id="device-cloud-a") store.record_heartbeat(device_count=1) entries = store.list_recent() rendered = "\n".join( repr(entry["summary"]) + " " + json.dumps(entry["detail"]) for entry in entries ) assert secret_goal not in rendered assert "session-abc" not in rendered assert "lease-stale" not in rendered def test_history_store_task_submissions_prune_beyond_limit(tmp_path) -> None: store = ConsoleHistoryStore(tmp_path / "history.sqlite3", limit=3) for index in range(5): store.record_task_submission( task_id=f"task-{index}", device_id=f"device-{index}" ) entries = store.list_recent() assert len(entries) == 3 assert [entry["detail"]["task_id"] for entry in entries] == [ "task-4", "task-3", "task-2", ]