191 lines
5.8 KiB
Python
191 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from rdb_discovery.telemetry import TelemetryEvent, record_event, read_events
|
|
|
|
|
|
# -- TelemetryEvent --
|
|
|
|
def test_telemetry_event_to_dict() -> None:
|
|
event = TelemetryEvent(
|
|
timestamp="2026-06-02T12:00:00+00:00",
|
|
event_type="command",
|
|
target="rdb prompt",
|
|
details={"flag": True},
|
|
)
|
|
d = event.to_dict()
|
|
assert d["event_type"] == "command"
|
|
assert d["target"] == "rdb prompt"
|
|
assert d["details"]["flag"] is True
|
|
|
|
|
|
def test_telemetry_event_defaults_to_empty_details() -> None:
|
|
event = TelemetryEvent(
|
|
timestamp="2026-06-02T12:00:00+00:00",
|
|
event_type="command",
|
|
target="rdb prompt",
|
|
)
|
|
assert event.details == {}
|
|
|
|
|
|
def test_telemetry_event_required_fields() -> None:
|
|
event = TelemetryEvent(
|
|
timestamp="2026-06-02T12:00:00+00:00",
|
|
event_type="command",
|
|
target="rdb prompt",
|
|
)
|
|
assert event.timestamp == "2026-06-02T12:00:00+00:00"
|
|
assert event.event_type == "command"
|
|
assert event.target == "rdb prompt"
|
|
|
|
|
|
# -- record_event (file creation) --
|
|
|
|
def test_record_event_creates_session_log(tmp_path: Path) -> None:
|
|
log = tmp_path / ".rdb" / "session-log.jsonl"
|
|
assert not log.exists()
|
|
record_event(tmp_path, "command", "rdb prompt")
|
|
assert log.exists()
|
|
|
|
|
|
# -- record_event (appending) --
|
|
|
|
def test_record_event_appends_jsonl_line(tmp_path: Path) -> None:
|
|
record_event(tmp_path, "command", "rdb prompt")
|
|
record_event(tmp_path, "task", "TASK-012")
|
|
lines = log_lines(tmp_path)
|
|
assert len(lines) == 2
|
|
|
|
|
|
def test_record_event_writes_valid_json_per_line(tmp_path: Path) -> None:
|
|
record_event(tmp_path, "command", "rdb prompt", {"arg": "value"})
|
|
record_event(tmp_path, "task", "TASK-012")
|
|
for line in log_lines(tmp_path):
|
|
data = json.loads(line)
|
|
assert data["event_type"] in ("command", "task")
|
|
|
|
|
|
def test_record_event_has_required_keys(tmp_path: Path) -> None:
|
|
record_event(tmp_path, "command", "rdb prompt")
|
|
event = TelemetryEvent(**json.loads(log_lines(tmp_path)[0]))
|
|
assert hasattr(event, "timestamp")
|
|
assert hasattr(event, "event_type")
|
|
assert hasattr(event, "target")
|
|
assert hasattr(event, "details")
|
|
|
|
|
|
def test_record_event_details_default_empty_dict(tmp_path: Path) -> None:
|
|
record_event(tmp_path, "command", "rdb prompt")
|
|
event = TelemetryEvent(**json.loads(log_lines(tmp_path)[0]))
|
|
assert event.details == {}
|
|
|
|
|
|
# -- read_events (no file) --
|
|
|
|
def test_read_events_returns_empty_when_no_log(tmp_path: Path) -> None:
|
|
events = read_events(tmp_path)
|
|
assert events == []
|
|
|
|
|
|
# -- read_events (round-trip) --
|
|
|
|
def test_read_events_returns_recorded_events(tmp_path: Path) -> None:
|
|
record_event(tmp_path, "command", "rdb prompt", {"count": 42})
|
|
events = read_events(tmp_path)
|
|
assert len(events) == 1
|
|
assert events[0].event_type == "command"
|
|
assert events[0].target == "rdb prompt"
|
|
assert events[0].details["count"] == 42
|
|
|
|
|
|
def test_read_events_returns_multiple_events(tmp_path: Path) -> None:
|
|
record_event(tmp_path, "task", "TASK-001")
|
|
record_event(tmp_path, "task", "TASK-002")
|
|
record_event(tmp_path, "command", "rdb status")
|
|
events = read_events(tmp_path)
|
|
assert len(events) == 3
|
|
|
|
|
|
# -- read_events (malformed lines) --
|
|
|
|
def test_read_events_skips_malformed_lines(tmp_path: Path) -> None:
|
|
log_path = tmp_path / ".rdb" / "session-log.jsonl"
|
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
log_path.write_text(
|
|
'{"timestamp":"2026-01-01T00:00:00+00:00","event_type":"ok","target":"x","details":{}}\n'
|
|
'this is not json\n'
|
|
'{"timestamp":"2026-01-01T00:00:00+00:00","event_type":"also_ok","target":"y","details":{}}\n',
|
|
encoding="utf-8",
|
|
)
|
|
events = read_events(tmp_path)
|
|
assert len(events) == 2
|
|
assert events[0].event_type == "ok"
|
|
assert events[1].event_type == "also_ok"
|
|
|
|
|
|
# -- CLI command events --
|
|
|
|
COMMANDS = [
|
|
"rdb init",
|
|
"rdb discover",
|
|
"rdb ask_more",
|
|
"rdb status",
|
|
"rdb next",
|
|
"rdb start",
|
|
"rdb complete",
|
|
"rdb handoff",
|
|
"rdb prompt",
|
|
"rdb guardrails",
|
|
]
|
|
|
|
|
|
def test_record_event_creates_command_events(tmp_path: Path) -> None:
|
|
"""Each CLI command records an event with event_type 'command' and target 'rdb <name>'."""
|
|
for target in COMMANDS:
|
|
record_event(tmp_path, "command", target)
|
|
|
|
events = read_events(tmp_path)
|
|
assert len(events) == len(COMMANDS)
|
|
for event in events:
|
|
assert event.event_type == "command"
|
|
|
|
|
|
def test_record_event_target_matches_command_name(tmp_path: Path) -> None:
|
|
"""Event target matches 'rdb <cmd>' format."""
|
|
for expected in COMMANDS:
|
|
record_event(tmp_path, "command", expected)
|
|
|
|
events = read_events(tmp_path)
|
|
targets = [e.target for e in events]
|
|
assert set(targets) == set(COMMANDS)
|
|
|
|
|
|
def test_record_command_event_has_timestamp(tmp_path: Path) -> None:
|
|
"""Each command event has a non-empty timestamp."""
|
|
record_event(tmp_path, "command", "rdb status")
|
|
events = read_events(tmp_path)
|
|
assert len(events) == 1
|
|
assert events[0].timestamp
|
|
assert "+" in events[0].timestamp # UTC offset
|
|
|
|
|
|
def test_record_command_event_with_details(tmp_path: Path) -> None:
|
|
"""Commands like start/complete can pass details dict."""
|
|
record_event(tmp_path, "command", "rdb start", {"task_id": "TASK-013"})
|
|
events = read_events(tmp_path)
|
|
assert events[0].details["task_id"] == "TASK-013"
|
|
|
|
record_event(tmp_path, "command", "rdb complete", {"task_id": "TASK-007"})
|
|
events = read_events(tmp_path)
|
|
assert events[-1].details["task_id"] == "TASK-007"
|
|
|
|
|
|
# -- Helpers --
|
|
|
|
def log_lines(root: Path) -> list[str]:
|
|
return (root / ".rdb" / "session-log.jsonl").read_text(encoding="utf-8").strip().splitlines()
|