codeflash-agent/packages/blackbox/tests/test_watcher.py
Kevin Turcios 0ad5e60523
Add blackbox package: session flight recorder with HTMX dashboard (#39)
* feat(blackbox): add package with models, CLI, and HTMX dashboard

* test(blackbox): add comprehensive test coverage for dashboard

* feat(blackbox): cache session scanning via watcher invalidation

* docs(blackbox): add README and use fastapi[standard] for dev server

* refactor(blackbox): extract presentation logic into formatter classes

* refactor(blackbox): extract classify_error helpers

* feat(blackbox): wire analytics into session detail view

Show token usage, tool breakdowns, and session stats in a
collapsible panel when viewing a session.

* feat(blackbox): add codeflash plugin detection

Detect codeflash agent names, skills, and commands in transcripts.
Surface language, optimization domain, and capability badges in
the analytics panel.

* refactor(blackbox): remove underscore prefixes from internal functions

* chore: add ty python-version to root pyproject.toml

* chore(blackbox): fix lint errors in test files

* style(blackbox): apply ruff formatting to analytics

* feat(blackbox): add Playwright E2E tests for dashboard

Refactor app.py to expose create_app() factory accepting a projects_dir
override, enabling tests to run against fixture data instead of the real
~/.claude/projects/ directory. Routes now read projects_dir from
app.state instead of the module-level constant.

Add 26 Playwright tests across 5 files covering dashboard loading,
session list, session detail with filters and analytics, sidebar
collapse/localStorage persistence, and SSE log streaming. All tests
pass on chromium, firefox, and webkit (78 total).

CI gets a new e2e-blackbox job with a browser matrix strategy running
all three engines in parallel, conditional on blackbox path changes,
with trace upload on failure.

* fix(ci): sync only blackbox package in e2e job

* fix(ci): exclude e2e tests from unit test job

The test job doesn't install Playwright browsers, so e2e tests error
when pytest collects them. Ignore tests/e2e/ directories in the test
job — those are handled by the dedicated e2e-blackbox job.
2026-04-28 19:58:43 -05:00

169 lines
5.4 KiB
Python

from __future__ import annotations
import time
from pathlib import Path
from unittest.mock import MagicMock
from blackbox.dashboard.watcher import LIVE_THRESHOLD_S, SessionWatcher
class TestSessionWatcherLiveIds:
def test_empty_initially(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
assert set() == w.live_session_ids()
def test_recent_modification_is_live(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "abc123.jsonl")
w.on_modified(event)
assert {"abc123"} == w.live_session_ids()
def test_old_modification_expires(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
with w._lock:
w._last_modified["old-session"] = time.time() - LIVE_THRESHOLD_S - 10
assert set() == w.live_session_ids()
def test_non_jsonl_ignored(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "notes.txt")
w.on_modified(event)
assert set() == w.live_session_ids()
def test_directory_events_ignored(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
event = MagicMock()
event.is_directory = True
event.src_path = str(tmp_path / "somedir")
w.on_modified(event)
assert set() == w.live_session_ids()
def test_on_created_delegates_to_on_modified(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "new-session.jsonl")
w.on_created(event)
assert {"new-session"} == w.live_session_ids()
def test_expired_entries_cleaned_up(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
with w._lock:
w._last_modified["expired"] = time.time() - LIVE_THRESHOLD_S - 100
w._last_modified["fresh"] = time.time()
live = w.live_session_ids()
assert {"fresh"} == live
with w._lock:
assert "expired" not in w._last_modified
class TestSessionWatcherStartStop:
def test_start_without_directory_is_noop(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path / "nonexistent")
w.start()
assert w._observer is None
def test_stop_without_start_is_safe(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.stop()
def test_start_and_stop(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.start()
assert w._observer is not None
w.stop()
assert w._observer is None
def test_multiple_stops_safe(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.start()
w.stop()
w.stop()
class TestSessionWatcherCache:
def test_get_sessions_calls_scan_on_first_call(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
sentinel: list[object] = []
result = w.get_sessions(lambda _: sentinel)
assert result is sentinel
def test_get_sessions_returns_cached_on_second_call(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
call_count = 0
def counting_scan(_: object) -> list[object]:
nonlocal call_count
call_count += 1
return []
w.get_sessions(counting_scan)
w.get_sessions(counting_scan)
assert 1 == call_count
def test_on_modified_invalidates_cache(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
call_count = 0
def counting_scan(_: object) -> list[object]:
nonlocal call_count
call_count += 1
return []
w.get_sessions(counting_scan)
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "sess.jsonl")
w.on_modified(event)
w.get_sessions(counting_scan)
assert 2 == call_count
def test_on_created_invalidates_cache(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.get_sessions(lambda _: [])
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "new.jsonl")
w.on_created(event)
assert w._cached_sessions is None
def test_on_deleted_invalidates_cache(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.get_sessions(lambda _: [])
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "removed.jsonl")
w.on_deleted(event)
assert w._cached_sessions is None
def test_on_deleted_ignores_non_jsonl(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.get_sessions(lambda _: [])
event = MagicMock()
event.is_directory = False
event.src_path = str(tmp_path / "notes.txt")
w.on_deleted(event)
assert w._cached_sessions is not None
def test_on_deleted_ignores_directories(self, tmp_path: Path) -> None:
w = SessionWatcher(tmp_path)
w.get_sessions(lambda _: [])
event = MagicMock()
event.is_directory = True
event.src_path = str(tmp_path / "somedir")
w.on_deleted(event)
assert w._cached_sessions is not None