Skip to content

Commit 676e51c

Browse files
feat: add test tracking view to dashboard
- Add /api/tests endpoint for test tracking data - Add test tracking section to dashboard HTML showing: - Pass/fail stats grid - Files needing attention table - Recent test execution history - Add Test Coverage card to main dashboard grid - Convert TelemetryStore records to dicts for JSON serialization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b19451d commit 676e51c

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

src/empathy_os/dashboard/server.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242
list_workflows = None # type: ignore[assignment]
4343
HAS_WORKFLOWS = False
4444

45+
try:
46+
from empathy_os.models.telemetry import TelemetryStore
47+
48+
HAS_TELEMETRY = True
49+
except ImportError:
50+
TelemetryStore = None # type: ignore[misc, assignment]
51+
HAS_TELEMETRY = False
52+
4553

4654
class DashboardHandler(http.server.BaseHTTPRequestHandler):
4755
"""HTTP request handler for the dashboard."""
@@ -69,6 +77,8 @@ def do_GET(self):
6977
self._serve_health()
7078
elif path == "/api/workflows":
7179
self._serve_workflows()
80+
elif path == "/api/tests":
81+
self._serve_tests()
7282
else:
7383
self.send_error(404, "Not Found")
7484

@@ -119,6 +129,62 @@ def _serve_workflows(self):
119129
data = {"error": "Workflows not available"}
120130
self._send_json(data)
121131

132+
def _serve_tests(self):
133+
"""Serve test tracking data as JSON."""
134+
data = self._get_test_stats()
135+
self._send_json(data)
136+
137+
def _get_test_stats(self) -> dict:
138+
"""Get test tracking statistics.
139+
140+
Returns:
141+
Dictionary with test tracking data including:
142+
- total_files: Total files with test records
143+
- passed_files: Files with passing tests
144+
- failed_files: Files with failing tests
145+
- coverage_avg: Average coverage percentage
146+
- recent_tests: Recent test executions
147+
- files_needing_tests: Files that need attention
148+
"""
149+
if not HAS_TELEMETRY or TelemetryStore is None:
150+
return {"error": "Telemetry not available"}
151+
152+
try:
153+
store = TelemetryStore(Path(self.empathy_dir))
154+
155+
# Get file test records and convert to dicts
156+
file_tests_raw = store.get_file_tests(limit=100)
157+
file_tests = [t.to_dict() if hasattr(t, 'to_dict') else t for t in file_tests_raw]
158+
159+
# Calculate stats
160+
total_files = len(file_tests)
161+
passed_files = sum(1 for t in file_tests if t.get("last_test_result") == "passed")
162+
failed_files = sum(1 for t in file_tests if t.get("last_test_result") == "failed")
163+
164+
# Coverage average
165+
coverages = [t.get("coverage_percentage", 0) for t in file_tests if t.get("coverage_percentage")]
166+
coverage_avg = sum(coverages) / len(coverages) if coverages else 0
167+
168+
# Get files needing attention (failed or stale) and convert to dicts
169+
files_needing_raw = store.get_files_needing_tests(stale_only=False, failed_only=False)
170+
files_needing_tests = [t.to_dict() if hasattr(t, 'to_dict') else t for t in files_needing_raw[:10]]
171+
172+
# Get recent test executions and convert to dicts
173+
recent_raw = store.get_test_executions(limit=10)
174+
recent_executions = [t.to_dict() if hasattr(t, 'to_dict') else t for t in recent_raw]
175+
176+
return {
177+
"total_files": total_files,
178+
"passed_files": passed_files,
179+
"failed_files": failed_files,
180+
"coverage_avg": round(coverage_avg, 1),
181+
"files_needing_tests": files_needing_tests,
182+
"recent_executions": recent_executions,
183+
"file_tests": file_tests[:20], # Most recent 20
184+
}
185+
except Exception as e:
186+
return {"error": str(e)}
187+
122188
def _send_json(self, data):
123189
"""Send JSON response."""
124190
content = json.dumps(data, indent=2, default=str)
@@ -201,6 +267,9 @@ def _generate_dashboard_html(self) -> str:
201267
except Exception:
202268
pass
203269

270+
# Get test stats
271+
test_stats = self._get_test_stats()
272+
204273
return f"""<!DOCTYPE html>
205274
<html lang="en">
206275
<head>
@@ -507,6 +576,12 @@ def _generate_dashboard_html(self) -> str:
507576
<div class="value">${workflow_stats.get("total_savings", 0):.2f}</div>
508577
<div class="label">workflows + API</div>
509578
</div>
579+
580+
<div class="card {'success' if test_stats.get('failed_files', 0) == 0 else 'warning'}">
581+
<h2>Test Coverage</h2>
582+
<div class="value">{test_stats.get('coverage_avg', 0):.0f}%</div>
583+
<div class="label">{test_stats.get('total_files', 0)} files tracked</div>
584+
</div>
510585
</div>
511586
512587
<div class="section">
@@ -539,6 +614,11 @@ def _generate_dashboard_html(self) -> str:
539614
{self._render_recent_runs(workflow_stats)}
540615
</div>
541616
617+
<div class="section">
618+
<h2>Test Tracking</h2>
619+
{self._render_test_tracking(test_stats)}
620+
</div>
621+
542622
<div class="section">
543623
<h2>Quick Commands</h2>
544624
<p>Run these commands for common tasks:</p>
@@ -684,6 +764,108 @@ def _render_recent_runs(self, workflow_stats: dict) -> str:
684764

685765
return "".join(runs_html)
686766

767+
def _render_test_tracking(self, test_stats: dict) -> str:
768+
"""Render test tracking section."""
769+
if "error" in test_stats:
770+
return f'<p style="color: var(--text-muted);">{test_stats["error"]}</p>'
771+
772+
total = test_stats.get("total_files", 0)
773+
passed = test_stats.get("passed_files", 0)
774+
failed = test_stats.get("failed_files", 0)
775+
coverage = test_stats.get("coverage_avg", 0)
776+
777+
if total == 0:
778+
return '<p style="color: var(--text-muted);">No test tracking data yet. Run tests with empathy to start tracking.</p>'
779+
780+
# Calculate pass rate
781+
pass_rate = (passed / total * 100) if total > 0 else 0
782+
783+
# Stats grid
784+
html = f"""
785+
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem;">
786+
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
787+
<div style="font-size: 1.5rem; font-weight: 600; color: var(--success);">{passed}</div>
788+
<div style="font-size: 0.75rem; color: var(--text-muted);">Passing</div>
789+
</div>
790+
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
791+
<div style="font-size: 1.5rem; font-weight: 600; color: var(--danger);">{failed}</div>
792+
<div style="font-size: 0.75rem; color: var(--text-muted);">Failing</div>
793+
</div>
794+
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
795+
<div style="font-size: 1.5rem; font-weight: 600; color: var(--primary);">{pass_rate:.0f}%</div>
796+
<div style="font-size: 0.75rem; color: var(--text-muted);">Pass Rate</div>
797+
</div>
798+
<div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
799+
<div style="font-size: 1.5rem; font-weight: 600; color: var(--warning);">{coverage:.0f}%</div>
800+
<div style="font-size: 0.75rem; color: var(--text-muted);">Coverage</div>
801+
</div>
802+
</div>
803+
"""
804+
805+
# Files needing attention
806+
files_needing = test_stats.get("files_needing_tests", [])
807+
if files_needing:
808+
html += """
809+
<h3 style="margin-bottom: 0.5rem; font-size: 1rem;">Files Needing Attention</h3>
810+
<table>
811+
<thead>
812+
<tr>
813+
<th>File</th>
814+
<th>Status</th>
815+
<th>Last Run</th>
816+
</tr>
817+
</thead>
818+
<tbody>
819+
"""
820+
for file_record in files_needing[:5]:
821+
file_path = file_record.get("file_path", "unknown")
822+
# Truncate long paths
823+
display_path = ("..." + file_path[-40:]) if len(file_path) > 40 else file_path
824+
result = file_record.get("last_test_result", "unknown")
825+
timestamp = file_record.get("timestamp", "")[:10] if file_record.get("timestamp") else "-"
826+
status_class = "resolved" if result == "passed" else "investigating"
827+
html += f"""
828+
<tr>
829+
<td title="{file_path}">{display_path}</td>
830+
<td><span class="status {status_class}">{result}</span></td>
831+
<td>{timestamp}</td>
832+
</tr>
833+
"""
834+
html += "</tbody></table>"
835+
836+
# Recent test executions
837+
recent = test_stats.get("recent_executions", [])
838+
if recent:
839+
html += """
840+
<h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem; font-size: 1rem;">Recent Test Runs</h3>
841+
"""
842+
for execution in recent[:5]:
843+
suite = execution.get("test_suite", "unknown")
844+
total_tests = execution.get("total_tests", 0)
845+
exec_passed = execution.get("passed", 0)
846+
exec_failed = execution.get("failed", 0)
847+
duration = execution.get("duration_seconds", 0)
848+
timestamp = execution.get("timestamp", "")[:16].replace("T", " ") if execution.get("timestamp") else "-"
849+
success = execution.get("success", False)
850+
851+
status_icon = "&#10003;" if success else "&#10007;"
852+
status_color = "var(--success)" if success else "var(--danger)"
853+
854+
html += f"""
855+
<div class="recent-run">
856+
<span style="color: {status_color};">{status_icon}</span>
857+
<span class="name">{suite}</span>
858+
<span class="provider">{total_tests} tests</span>
859+
<span class="result">
860+
<span style="color: var(--success);">{exec_passed} passed</span>
861+
{f'<span style="color: var(--danger);">{exec_failed} failed</span>' if exec_failed > 0 else ''}
862+
<span class="time">{duration:.1f}s | {timestamp}</span>
863+
</span>
864+
</div>
865+
"""
866+
867+
return html
868+
687869

688870
class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
689871
"""Threaded HTTP server."""

0 commit comments

Comments
 (0)