mirror of
https://github.com/codeflash-ai/codeflash-agent.git
synced 2026-05-04 18:25:19 +00:00
* 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.
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from blackbox.dashboard.routes import build_session_info, mark_live
|
|
from blackbox.models import SessionInfo
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mark_live
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMarkLive:
|
|
def make_session(self, session_id: str = "sess-1") -> SessionInfo:
|
|
return SessionInfo(
|
|
session_id=session_id,
|
|
project_path="proj",
|
|
project_name="proj",
|
|
transcript_path="/tmp/proj/sess.jsonl",
|
|
started_at=1_000_000.0,
|
|
)
|
|
|
|
def test_no_live_ids_returns_same_list(self) -> None:
|
|
watcher = MagicMock()
|
|
watcher.live_session_ids.return_value = set()
|
|
sessions = [self.make_session()]
|
|
result = mark_live(sessions, watcher)
|
|
assert result is sessions
|
|
|
|
def test_marks_matching_session_as_live(self) -> None:
|
|
watcher = MagicMock()
|
|
watcher.live_session_ids.return_value = {"sess-1"}
|
|
sessions = [self.make_session("sess-1"), self.make_session("sess-2")]
|
|
result = mark_live(sessions, watcher)
|
|
assert result[0].is_live is True
|
|
assert result[1].is_live is False
|
|
|
|
def test_non_matching_sessions_unchanged(self) -> None:
|
|
watcher = MagicMock()
|
|
watcher.live_session_ids.return_value = {"other"}
|
|
sessions = [self.make_session("sess-1")]
|
|
result = mark_live(sessions, watcher)
|
|
assert result[0].is_live is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build_session_info
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBuildSessionInfo:
|
|
def write_transcript(self, path: Path, entries: list[dict[str, Any]]) -> None:
|
|
path.write_text("\n".join(json.dumps(e) for e in entries))
|
|
|
|
def test_returns_info_from_transcript(self, tmp_path: Path) -> None:
|
|
path = tmp_path / "abc.jsonl"
|
|
self.write_transcript(
|
|
path,
|
|
[{"type": "user", "timestamp": "2024-01-01T00:00:00Z", "message": {"content": "hello"}}],
|
|
)
|
|
info = build_session_info(path, "abc", "proj")
|
|
assert "abc" == info.session_id
|
|
assert "hello" == info.first_prompt
|
|
|
|
def test_fallback_for_empty_transcript(self, tmp_path: Path) -> None:
|
|
path = tmp_path / "empty.jsonl"
|
|
path.write_text("")
|
|
info = build_session_info(path, "empty", "proj")
|
|
assert "empty" == info.session_id
|
|
assert "proj" == info.project_name
|
|
assert "" == info.first_prompt
|