From ef8839365697e7f0758e7e93ca9a7c0727597368 Mon Sep 17 00:00:00 2001
From: Atul Gupta
Date: Fri, 18 Sep 2026 11:55:58 -0700
Subject: [PATCH 1/2] ci: publish extensive test and coverage job summaries
Job summaries listed the highest-coverage packages (almost all 100%)
and never named failing tests. Capture go test -json / Vitest JSON and
lead the summary with failures, then lowest packages, files, functions,
area rollups, and coverage bands so a newly red test is visible on the
Actions page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e
---
.github/scripts/backend_coverage_summary.py | 615 ++++++++++++++++++
.github/scripts/frontend_coverage_summary.mjs | 401 ++++++++++++
.github/workflows/ci.yml | 85 +--
3 files changed, 1046 insertions(+), 55 deletions(-)
create mode 100644 .github/scripts/backend_coverage_summary.py
create mode 100644 .github/scripts/frontend_coverage_summary.mjs
diff --git a/.github/scripts/backend_coverage_summary.py b/.github/scripts/backend_coverage_summary.py
new file mode 100644
index 000000000..759cd2b33
--- /dev/null
+++ b/.github/scripts/backend_coverage_summary.py
@@ -0,0 +1,615 @@
+#!/usr/bin/env python3
+"""Render an extensive GitHub Actions job summary from Go test + cover data.
+
+The previous CI summary listed the *highest* package percentages (almost
+all 100%), which hid gaps and never mentioned failing tests. This script
+leads with failures, then lowest-coverage packages/files/functions.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import sys
+from collections import defaultdict
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Iterable
+
+MODULE = "github.com/ev-dev-labs/teslasync/"
+COVER_LINE = re.compile(
+ r"^(?P.+):(?P\d+\.\d+),(?P\d+\.\d+) (?P\d+) (?P\d+)$"
+)
+FUNC_LINE = re.compile(
+ r"^(?P.+):(?P\d+):\s+(?P\S+)\s+(?P[\d.]+)%$"
+)
+
+
+@dataclass
+class Block:
+ file: str
+ stmts: int
+ covered: int
+
+
+@dataclass
+class Func:
+ file: str
+ name: str
+ pct: float
+
+
+@dataclass
+class TestEvent:
+ action: str
+ package: str
+ test: str = ""
+ elapsed: float = 0.0
+ output: str = ""
+
+
+@dataclass
+class Report:
+ blocks: list[Block] = field(default_factory=list)
+ funcs: list[Func] = field(default_factory=list)
+ events: list[TestEvent] = field(default_factory=list)
+
+
+def short_pkg(path: str) -> str:
+ rel = path
+ if rel.startswith(MODULE):
+ rel = rel[len(MODULE) :]
+ if rel.endswith(".go"):
+ rel = rel.rsplit("/", 1)[0]
+ return rel or path
+
+
+def layer_of(pkg: str) -> str:
+ parts = pkg.split("/")
+ if not parts:
+ return "other"
+ if parts[0] == "cmd":
+ return "cmd"
+ if parts[0] == "tools":
+ return "tools"
+ if parts[0] == "internal" and len(parts) > 1:
+ return f"internal/{parts[1]}"
+ return parts[0]
+
+
+def pct(covered: int, total: int) -> float:
+ if total <= 0:
+ return 100.0
+ return 100.0 * covered / total
+
+
+def parse_coverprofile(text: str) -> list[Block]:
+ out: list[Block] = []
+ for raw in text.splitlines():
+ if raw.startswith("mode:") or not raw.strip():
+ continue
+ m = COVER_LINE.match(raw)
+ if not m:
+ continue
+ stmts = int(m.group("stmts"))
+ count = int(m.group("count"))
+ out.append(
+ Block(
+ file=m.group("file"),
+ stmts=stmts,
+ covered=stmts if count > 0 else 0,
+ )
+ )
+ return out
+
+
+def parse_func(text: str) -> list[Func]:
+ out: list[Func] = []
+ for raw in text.splitlines():
+ if raw.startswith("total:"):
+ continue
+ m = FUNC_LINE.match(raw)
+ if not m:
+ continue
+ out.append(
+ Func(
+ file=m.group("file"),
+ name=m.group("name"),
+ pct=float(m.group("pct")),
+ )
+ )
+ return out
+
+
+def parse_test_json(text: str) -> list[TestEvent]:
+ events: list[TestEvent] = []
+ for raw in text.splitlines():
+ raw = raw.strip()
+ if not raw:
+ continue
+ try:
+ obj = json.loads(raw)
+ except json.JSONDecodeError:
+ continue
+ action = str(obj.get("Action") or "")
+ if action not in {"pass", "fail", "skip", "output"}:
+ continue
+ events.append(
+ TestEvent(
+ action=action,
+ package=str(obj.get("Package") or ""),
+ test=str(obj.get("Test") or ""),
+ elapsed=float(obj.get("Elapsed") or 0),
+ output=str(obj.get("Output") or ""),
+ )
+ )
+ return events
+
+
+def load_report(profile: Path | None, func_path: Path | None, json_path: Path | None) -> Report:
+ r = Report()
+ if profile and profile.exists():
+ r.blocks = parse_coverprofile(profile.read_text(encoding="utf-8", errors="replace"))
+ if func_path and func_path.exists():
+ r.funcs = parse_func(func_path.read_text(encoding="utf-8", errors="replace"))
+ if json_path and json_path.exists():
+ r.events = parse_test_json(json_path.read_text(encoding="utf-8", errors="replace"))
+ return r
+
+
+def aggregate_files(blocks: Iterable[Block]) -> dict[str, tuple[int, int]]:
+ acc: dict[str, list[int]] = defaultdict(lambda: [0, 0])
+ for b in blocks:
+ acc[b.file][0] += b.stmts
+ acc[b.file][1] += b.covered
+ return {k: (v[0], v[1]) for k, v in acc.items()}
+
+
+def aggregate_packages(files: dict[str, tuple[int, int]]) -> dict[str, tuple[int, int]]:
+ acc: dict[str, list[int]] = defaultdict(lambda: [0, 0])
+ for file, (stmts, covered) in files.items():
+ pkg = short_pkg(file)
+ acc[pkg][0] += stmts
+ acc[pkg][1] += covered
+ return {k: (v[0], v[1]) for k, v in acc.items()}
+
+
+def aggregate_layers(packages: dict[str, tuple[int, int]]) -> dict[str, tuple[int, int]]:
+ acc: dict[str, list[int]] = defaultdict(lambda: [0, 0])
+ for pkg, (stmts, covered) in packages.items():
+ layer = layer_of(pkg)
+ acc[layer][0] += stmts
+ acc[layer][1] += covered
+ return {k: (v[0], v[1]) for k, v in acc.items()}
+
+
+def band(p: float) -> str:
+ if p >= 100:
+ return "100%"
+ if p >= 80:
+ return "80–99%"
+ if p >= 50:
+ return "50–79%"
+ if p > 0:
+ return "1–49%"
+ return "0%"
+
+
+def md_table(headers: list[str], rows: list[list[str]]) -> list[str]:
+ lines = [
+ "| " + " | ".join(headers) + " |",
+ "| " + " | ".join(["---"] * len(headers)) + " |",
+ ]
+ for row in rows:
+ lines.append("| " + " | ".join(row) + " |")
+ return lines
+
+
+def fmt_pct(p: float) -> str:
+ return f"{p:.1f}%"
+
+
+def rel_file(path: str) -> str:
+ if path.startswith(MODULE):
+ return path[len(MODULE) :]
+ return path
+
+
+def test_outcome(report: Report) -> dict[str, object]:
+ failed_tests: list[tuple[str, str]] = []
+ passed_tests = 0
+ skipped_tests = 0
+ failed_pkgs: list[str] = []
+ passed_pkgs = 0
+ skipped_pkgs = 0
+ slow: list[tuple[float, str, str]] = []
+ fail_output: dict[tuple[str, str], list[str]] = defaultdict(list)
+
+ for ev in report.events:
+ if ev.action == "output" and ev.test:
+ fail_output[(ev.package, ev.test)].append(ev.output)
+ continue
+ if ev.test:
+ if ev.action == "fail":
+ failed_tests.append((ev.package, ev.test))
+ slow.append((ev.elapsed, ev.package, ev.test))
+ elif ev.action == "pass":
+ passed_tests += 1
+ slow.append((ev.elapsed, ev.package, ev.test))
+ elif ev.action == "skip":
+ skipped_tests += 1
+ continue
+ if ev.action == "fail" and ev.package:
+ failed_pkgs.append(ev.package)
+ elif ev.action == "pass" and ev.package:
+ passed_pkgs += 1
+ elif ev.action == "skip" and ev.package:
+ skipped_pkgs += 1
+
+ slow.sort(reverse=True)
+ snippets: dict[tuple[str, str], str] = {}
+ for key in failed_tests:
+ buf = "".join(fail_output.get(key, []))
+ lines = [ln.rstrip() for ln in buf.splitlines() if ln.strip()]
+ # Keep FAIL lines and assertion context, drop RUN noise.
+ keep = [
+ ln
+ for ln in lines
+ if not ln.startswith("=== RUN") and not ln.startswith("=== PAUSE")
+ ]
+ snippets[key] = "\n".join(keep[-24:])
+ return {
+ "failed_tests": failed_tests,
+ "passed_tests": passed_tests,
+ "skipped_tests": skipped_tests,
+ "failed_pkgs": failed_pkgs,
+ "passed_pkgs": passed_pkgs,
+ "skipped_pkgs": skipped_pkgs,
+ "slow": slow[:15],
+ "snippets": snippets,
+ }
+
+
+def render(report: Report) -> str:
+ files = aggregate_files(report.blocks)
+ packages = aggregate_packages(files)
+ layers = aggregate_layers(packages)
+ total_stmts = sum(s for s, _ in files.values())
+ total_cov = sum(c for _, c in files.values())
+ overall = pct(total_cov, total_stmts)
+ outcome = test_outcome(report)
+
+ lines: list[str] = []
+ lines.append("## Backend tests")
+ lines.append("")
+
+ failed_tests: list[tuple[str, str]] = outcome["failed_tests"] # type: ignore[assignment]
+ failed_pkgs: list[str] = outcome["failed_pkgs"] # type: ignore[assignment]
+ if failed_tests or failed_pkgs:
+ lines.append("### Failures — start here")
+ lines.append("")
+ lines.append(
+ f"**{len(failed_tests)} test(s)** failed in **{len(failed_pkgs)} package(s)**."
+ )
+ lines.append("New red tests show up in this section on the next run.")
+ lines.append("")
+ rows = []
+ for pkg, name in failed_tests[:80]:
+ rows.append([f"`{name}`", f"`{short_pkg(pkg)}`"])
+ if rows:
+ lines.extend(md_table(["Test", "Package"], rows))
+ lines.append("")
+ snippets: dict[tuple[str, str], str] = outcome["snippets"] # type: ignore[assignment]
+ shown = 0
+ for key in failed_tests:
+ snippet = snippets.get(key, "").strip()
+ if not snippet:
+ continue
+ pkg, name = key
+ lines.append(f"Log: `{name}` (`{short_pkg(pkg)}`)
")
+ lines.append("")
+ lines.append("```")
+ lines.append(snippet[:4000])
+ lines.append("```")
+ lines.append(" ")
+ lines.append("")
+ shown += 1
+ if shown >= 12:
+ break
+ if len(failed_tests) > 80:
+ lines.append(f"_…and {len(failed_tests) - 80} more failing tests._")
+ lines.append("")
+ else:
+ lines.append(
+ f"**All reported tests passed** "
+ f"({outcome['passed_tests']} pass, {outcome['skipped_tests']} skip, "
+ f"{outcome['passed_pkgs']} packages)."
+ )
+ lines.append("")
+
+ lines.append("| Result | Tests | Packages |")
+ lines.append("| --- | ---: | ---: |")
+ lines.append(
+ f"| Fail | {len(failed_tests)} | {len(failed_pkgs)} |"
+ )
+ lines.append(
+ f"| Pass | {outcome['passed_tests']} | {outcome['passed_pkgs']} |"
+ )
+ lines.append(
+ f"| Skip | {outcome['skipped_tests']} | {outcome['skipped_pkgs']} |"
+ )
+ lines.append("")
+
+ slow: list[tuple[float, str, str]] = outcome["slow"] # type: ignore[assignment]
+ if slow:
+ lines.append("Slowest tests
")
+ lines.append("")
+ lines.extend(
+ md_table(
+ ["Seconds", "Test", "Package"],
+ [
+ [f"{elapsed:.2f}", f"`{name}`", f"`{short_pkg(pkg)}`"]
+ for elapsed, pkg, name in slow
+ if name
+ ],
+ )
+ )
+ lines.append("")
+ lines.append(" ")
+ lines.append("")
+
+ lines.append("## Backend coverage")
+ lines.append("")
+ if total_stmts == 0:
+ lines.append("_No coverage profile was produced for this run._")
+ lines.append("")
+ return "\n".join(lines).rstrip() + "\n"
+ lines.append(
+ f"**{fmt_pct(overall)}** of statements covered "
+ f"(`{total_cov} / {total_stmts}`)."
+ )
+ lines.append("")
+ lines.append(
+ "Lowest packages and files are listed first. "
+ "HTML + per-function reports are on the **backend-coverage** artifact."
+ )
+ lines.append("")
+
+ band_counts: dict[str, list[int]] = defaultdict(lambda: [0, 0])
+ for stmts, covered in packages.values():
+ b = band(pct(covered, stmts))
+ band_counts[b][0] += 1
+ band_counts[b][1] += stmts
+ order = ["0%", "1–49%", "50–79%", "80–99%", "100%"]
+ lines.append("### Package coverage bands")
+ lines.append("")
+ lines.extend(
+ md_table(
+ ["Band", "Packages", "Statements"],
+ [
+ [b, str(band_counts[b][0]), str(band_counts[b][1])]
+ for b in order
+ if band_counts[b][0]
+ ],
+ )
+ )
+ lines.append("")
+
+ if layers:
+ lines.append("### Coverage by area")
+ lines.append("")
+ layer_rows = []
+ for name, (stmts, covered) in sorted(
+ layers.items(), key=lambda kv: pct(kv[1][1], kv[1][0])
+ ):
+ layer_rows.append(
+ [
+ f"`{name}`",
+ fmt_pct(pct(covered, stmts)),
+ str(covered),
+ str(stmts),
+ str(stmts - covered),
+ ]
+ )
+ lines.extend(
+ md_table(["Area", "Covered", "Hit", "Stmts", "Missed"], layer_rows)
+ )
+ lines.append("")
+
+ ranked_pkgs = sorted(
+ packages.items(),
+ key=lambda kv: (pct(kv[1][1], kv[1][0]), -(kv[1][0] - kv[1][1]), kv[0]),
+ )
+ lowest = [(p, s, c) for p, (s, c) in ranked_pkgs if s > 0 and pct(c, s) < 100][:40]
+ if lowest:
+ lines.append("### Lowest packages (actionable)")
+ lines.append("")
+ lines.extend(
+ md_table(
+ ["Covered", "Hit", "Stmts", "Missed", "Package"],
+ [
+ [
+ fmt_pct(pct(c, s)),
+ str(c),
+ str(s),
+ str(s - c),
+ f"`{p}`",
+ ]
+ for p, s, c in lowest
+ ],
+ )
+ )
+ lines.append("")
+
+ ranked_files = sorted(
+ files.items(),
+ key=lambda kv: (pct(kv[1][1], kv[1][0]), -(kv[1][0] - kv[1][1]), kv[0]),
+ )
+ low_files = [
+ (f, s, c) for f, (s, c) in ranked_files if s > 0 and pct(c, s) < 100
+ ][:30]
+ if low_files:
+ lines.append("### Lowest files")
+ lines.append("")
+ lines.extend(
+ md_table(
+ ["Covered", "Missed", "File"],
+ [
+ [fmt_pct(pct(c, s)), str(s - c), f"`{rel_file(f)}`"]
+ for f, s, c in low_files
+ ],
+ )
+ )
+ lines.append("")
+
+ low_funcs = sorted(
+ [fn for fn in report.funcs if fn.pct < 100],
+ key=lambda fn: (fn.pct, fn.file, fn.name),
+ )[:30]
+ if low_funcs:
+ lines.append("### Lowest functions")
+ lines.append("")
+ lines.extend(
+ md_table(
+ ["Covered", "Function", "File"],
+ [
+ [fmt_pct(fn.pct), f"`{fn.name}`", f"`{rel_file(fn.file)}`"]
+ for fn in low_funcs
+ ],
+ )
+ )
+ lines.append("")
+
+ zero = [p for p, (s, c) in ranked_pkgs if s > 0 and c == 0]
+ full = [p for p, (s, c) in ranked_pkgs if s > 0 and c == s]
+ if zero:
+ lines.append(f"Zero-coverage packages ({len(zero)})
")
+ lines.append("")
+ for p in zero[:80]:
+ lines.append(f"- `{p}`")
+ if len(zero) > 80:
+ lines.append(f"- …and {len(zero) - 80} more")
+ lines.append("")
+ lines.append(" ")
+ lines.append("")
+ if full:
+ lines.append(
+ f"Fully covered packages ({len(full)})
"
+ )
+ lines.append("")
+ for p in full[:80]:
+ lines.append(f"- `{p}`")
+ if len(full) > 80:
+ lines.append(f"- …and {len(full) - 80} more")
+ lines.append("")
+ lines.append(" ")
+ lines.append("")
+
+ return "\n".join(lines).rstrip() + "\n"
+
+
+def write_package_table(packages: dict[str, tuple[int, int]], dest: Path) -> None:
+ rows = sorted(
+ packages.items(),
+ key=lambda kv: (pct(kv[1][1], kv[1][0]), kv[0]),
+ )
+ lines = ["coverage stmts covered missed package"]
+ for pkg, (stmts, covered) in rows:
+ lines.append(
+ f"{pct(covered, stmts):7.2f}% {stmts:5d} {covered:7d} {stmts - covered:6d} {pkg}"
+ )
+ dest.write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+def write_file_table(files: dict[str, tuple[int, int]], dest: Path) -> None:
+ rows = sorted(
+ files.items(),
+ key=lambda kv: (pct(kv[1][1], kv[1][0]), kv[0]),
+ )
+ lines = ["coverage stmts covered missed file"]
+ for file, (stmts, covered) in rows:
+ lines.append(
+ f"{pct(covered, stmts):7.2f}% {stmts:5d} {covered:7d} {stmts - covered:6d} {rel_file(file)}"
+ )
+ dest.write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+SELF_TEST_PROFILE = """mode: set
+github.com/ev-dev-labs/teslasync/internal/api/foo/a.go:10.1,12.2 5 1
+github.com/ev-dev-labs/teslasync/internal/api/foo/a.go:12.2,14.3 5 0
+github.com/ev-dev-labs/teslasync/internal/models/alert/alert.go:1.1,2.2 10 10
+github.com/ev-dev-labs/teslasync/cmd/teslasync/main.go:1.1,2.2 4 0
+"""
+
+SELF_TEST_FUNC = """github.com/ev-dev-labs/teslasync/internal/api/foo/a.go:10:\tServeHTTP\t50.0%
+github.com/ev-dev-labs/teslasync/internal/models/alert/alert.go:1:\tValidate\t100.0%
+github.com/ev-dev-labs/teslasync/cmd/teslasync/main.go:1:\tmain\t0.0%
+total:\t\t(statements)\t60.0%
+"""
+
+SELF_TEST_JSON = """
+{"Action":"fail","Package":"github.com/ev-dev-labs/teslasync/internal/api/foo","Test":"TestWakeUp","Elapsed":0.04}
+{"Action":"output","Package":"github.com/ev-dev-labs/teslasync/internal/api/foo","Test":"TestWakeUp","Output":" client_test.go:250: status 408\\n"}
+{"Action":"fail","Package":"github.com/ev-dev-labs/teslasync/internal/api/foo","Elapsed":0.2}
+{"Action":"pass","Package":"github.com/ev-dev-labs/teslasync/internal/models/alert","Test":"TestValidate","Elapsed":0.01}
+{"Action":"pass","Package":"github.com/ev-dev-labs/teslasync/internal/models/alert","Elapsed":0.02}
+"""
+
+
+def self_test() -> None:
+ report = Report(
+ blocks=parse_coverprofile(SELF_TEST_PROFILE),
+ funcs=parse_func(SELF_TEST_FUNC),
+ events=parse_test_json(SELF_TEST_JSON),
+ )
+ md = render(report)
+ assert "Failures — start here" in md, md
+ assert "TestWakeUp" in md, md
+ assert "Lowest packages" in md, md
+ assert "cmd" in md, md
+ assert "0.0%" in md or "0%" in md
+ files = aggregate_files(report.blocks)
+ pkgs = aggregate_packages(files)
+ assert pkgs["cmd/teslasync"][1] == 0
+ assert pkgs["internal/models/alert"][1] == 10
+ print("backend_coverage_summary self-test OK")
+
+
+def main(argv: list[str] | None = None) -> int:
+ p = argparse.ArgumentParser()
+ p.add_argument("--profile", type=Path, help="go coverprofile (coverage.out)")
+ p.add_argument("--func", type=Path, dest="func_path", help="go tool cover -func output")
+ p.add_argument("--json", type=Path, dest="json_path", help="go test -json NDJSON")
+ p.add_argument("--summary", type=Path, help="append markdown (GITHUB_STEP_SUMMARY)")
+ p.add_argument("--markdown-out", type=Path, help="write markdown file")
+ p.add_argument("--package-out", type=Path)
+ p.add_argument("--file-out", type=Path)
+ p.add_argument("--self-test", action="store_true")
+ args = p.parse_args(argv)
+
+ if args.self_test:
+ self_test()
+ return 0
+
+ report = load_report(args.profile, args.func_path, args.json_path)
+ md = render(report)
+ if args.markdown_out:
+ args.markdown_out.write_text(md, encoding="utf-8")
+ if args.summary:
+ with args.summary.open("a", encoding="utf-8") as fh:
+ fh.write(md if md.endswith("\n") else md + "\n")
+ if not args.summary and not args.markdown_out:
+ sys.stdout.write(md)
+
+ files = aggregate_files(report.blocks)
+ packages = aggregate_packages(files)
+ if args.package_out:
+ write_package_table(packages, args.package_out)
+ if args.file_out:
+ write_file_table(files, args.file_out)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/scripts/frontend_coverage_summary.mjs b/.github/scripts/frontend_coverage_summary.mjs
new file mode 100644
index 000000000..7e35edcf7
--- /dev/null
+++ b/.github/scripts/frontend_coverage_summary.mjs
@@ -0,0 +1,401 @@
+#!/usr/bin/env node
+/**
+ * Extensive GitHub Actions job summary from Vitest JSON + Istanbul
+ * coverage-summary.json. Leads with failing tests so a newly red spec
+ * is visible on the job page without opening logs.
+ */
+
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+function readJson(file) {
+ if (!file || !fs.existsSync(file)) return null
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
+}
+
+function pct(covered, total) {
+ if (!total) return 100
+ return (100 * covered) / total
+}
+
+function fmtPct(n) {
+ return `${Number(n).toFixed(1)}%`
+}
+
+function mdTable(headers, rows) {
+ const lines = [
+ `| ${headers.join(' | ')} |`,
+ `| ${headers.map(() => '---').join(' | ')} |`,
+ ]
+ for (const row of rows) lines.push(`| ${row.join(' | ')} |`)
+ return lines
+}
+
+function relSrc(filePath) {
+ const norm = String(filePath).replaceAll('\\', '/')
+ const idx = norm.lastIndexOf('/src/')
+ if (idx >= 0) return norm.slice(idx + 1)
+ if (norm.startsWith('src/')) return norm
+ return norm
+}
+
+function areaOf(rel) {
+ const parts = rel.split('/')
+ if (parts[0] !== 'src') return 'other'
+ if (parts[1] === 'features' && parts[2]) return `features/${parts[2]}`
+ if (parts[1] === 'components' && parts[2]) return `components/${parts[2]}`
+ if (parts[1]) return parts[1]
+ return 'src'
+}
+
+function band(p) {
+ if (p >= 100) return '100%'
+ if (p >= 80) return '80–99%'
+ if (p >= 50) return '50–79%'
+ if (p > 0) return '1–49%'
+ return '0%'
+}
+
+function collectVitest(results) {
+ const failed = []
+ const slow = []
+ let passed = 0
+ let skipped = 0
+ if (!results) {
+ return { failed, slow, passed, skipped, numTotal: 0, success: true }
+ }
+
+ const files = Array.isArray(results.testResults) ? results.testResults : []
+ for (const file of files) {
+ const fileName = relSrc(file.name || file.file || '')
+ const assertions = file.assertionResults || []
+ for (const a of assertions) {
+ const title = (a.fullName || a.title || a.name || '').trim() || '(unnamed)'
+ const status = a.status || file.status
+ const dur = Number(a.duration || 0)
+ if (status === 'failed' || status === 'fail') {
+ failed.push({
+ title,
+ file: fileName,
+ message: (a.failureMessages || []).join('\n').slice(0, 4000),
+ })
+ } else if (status === 'pending' || status === 'skipped' || status === 'todo') {
+ skipped += 1
+ } else {
+ passed += 1
+ }
+ if (dur) slow.push({ dur, title, file: fileName })
+ }
+ }
+ slow.sort((a, b) => b.dur - a.dur)
+ return {
+ failed,
+ slow: slow.slice(0, 15),
+ passed,
+ skipped,
+ numTotal: Number(results.numTotalTests || passed + skipped + failed.length),
+ success: results.success !== false && failed.length === 0,
+ }
+}
+
+function collectCoverage(summary) {
+ const files = []
+ let total = null
+ if (!summary || typeof summary !== 'object') return { files, total }
+ for (const [key, val] of Object.entries(summary)) {
+ if (!val || typeof val !== 'object') continue
+ if (key === 'total') {
+ total = val
+ continue
+ }
+ files.push({
+ file: relSrc(key),
+ lines: val.lines || { total: 0, covered: 0, pct: 0 },
+ statements: val.statements || { total: 0, covered: 0, pct: 0 },
+ functions: val.functions || { total: 0, covered: 0, pct: 0 },
+ branches: val.branches || { total: 0, covered: 0, pct: 0 },
+ })
+ }
+ return { files, total }
+}
+
+function render({ tests, coverage }) {
+ const lines = []
+ lines.push('## Frontend tests')
+ lines.push('')
+
+ if (tests.failed.length) {
+ lines.push('### Failures — start here')
+ lines.push('')
+ lines.push(
+ `**${tests.failed.length} test(s)** failed. New red specs show up in this section on the next run.`,
+ )
+ lines.push('')
+ lines.push(
+ ...mdTable(
+ ['Test', 'File'],
+ tests.failed.slice(0, 80).map((f) => [`\`${f.title.replaceAll('|', '\\|')}\``, `\`${f.file}\``]),
+ ),
+ )
+ lines.push('')
+ let shown = 0
+ for (const f of tests.failed) {
+ if (!f.message) continue
+ lines.push(`Log: \`${f.title.replaceAll('`', "'")}\`
`)
+ lines.push('')
+ lines.push('```')
+ lines.push(f.message)
+ lines.push('```')
+ lines.push(' ')
+ lines.push('')
+ shown += 1
+ if (shown >= 12) break
+ }
+ } else {
+ lines.push(
+ `**All reported tests passed** (${tests.passed} pass, ${tests.skipped} skip).`,
+ )
+ lines.push('')
+ }
+
+ lines.push('| Result | Count |')
+ lines.push('| --- | ---: |')
+ lines.push(`| Fail | ${tests.failed.length} |`)
+ lines.push(`| Pass | ${tests.passed} |`)
+ lines.push(`| Skip | ${tests.skipped} |`)
+ lines.push(`| Total | ${tests.numTotal || tests.passed + tests.skipped + tests.failed.length} |`)
+ lines.push('')
+
+ if (tests.slow.length) {
+ lines.push('Slowest tests
')
+ lines.push('')
+ lines.push(
+ ...mdTable(
+ ['ms', 'Test', 'File'],
+ tests.slow.map((s) => [
+ String(Math.round(s.dur)),
+ `\`${s.title.replaceAll('|', '\\|')}\``,
+ `\`${s.file}\``,
+ ]),
+ ),
+ )
+ lines.push('')
+ lines.push(' ')
+ lines.push('')
+ }
+
+ const { files, total } = coverage
+ lines.push('## Frontend coverage')
+ lines.push('')
+ if (!total && files.length === 0) {
+ lines.push('_No coverage summary was produced for this run._')
+ lines.push('')
+ return `${lines.join('\n').replace(/\n+$/, '')}\n`
+ }
+ if (total) {
+ lines.push('| Metric | Covered | Total | % |')
+ lines.push('| --- | ---: | ---: | ---: |')
+ for (const k of ['statements', 'branches', 'functions', 'lines']) {
+ const m = total[k] || { covered: 0, total: 0, pct: 0 }
+ lines.push(`| ${k} | ${m.covered} | ${m.total} | ${fmtPct(m.pct)} |`)
+ }
+ lines.push('')
+ }
+ lines.push(
+ 'Lowest files first. Browsable HTML is on the **frontend-coverage** artifact (`coverage/index.html`).',
+ )
+ lines.push('')
+
+ const bandCounts = new Map()
+ const areas = new Map()
+ for (const f of files) {
+ const p = Number(f.lines.pct) || pct(f.lines.covered, f.lines.total)
+ const b = band(p)
+ const cur = bandCounts.get(b) || { files: 0, lines: 0 }
+ cur.files += 1
+ cur.lines += f.lines.total || 0
+ bandCounts.set(b, cur)
+ const area = areaOf(f.file)
+ const a = areas.get(area) || { covered: 0, total: 0 }
+ a.covered += f.lines.covered || 0
+ a.total += f.lines.total || 0
+ areas.set(area, a)
+ }
+
+ const bandOrder = ['0%', '1–49%', '50–79%', '80–99%', '100%']
+ const bandRows = bandOrder
+ .filter((b) => bandCounts.has(b))
+ .map((b) => [b, String(bandCounts.get(b).files), String(bandCounts.get(b).lines)])
+ if (bandRows.length) {
+ lines.push('### File coverage bands')
+ lines.push('')
+ lines.push(...mdTable(['Band', 'Files', 'Lines'], bandRows))
+ lines.push('')
+ }
+
+ const areaRows = [...areas.entries()]
+ .map(([name, v]) => ({
+ name,
+ pct: pct(v.covered, v.total),
+ covered: v.covered,
+ total: v.total,
+ missed: v.total - v.covered,
+ }))
+ .sort((a, b) => a.pct - b.pct || b.missed - a.missed)
+ if (areaRows.length) {
+ lines.push('### Coverage by area')
+ lines.push('')
+ lines.push(
+ ...mdTable(
+ ['Area', 'Covered', 'Hit', 'Lines', 'Missed'],
+ areaRows.map((a) => [
+ `\`${a.name}\``,
+ fmtPct(a.pct),
+ String(a.covered),
+ String(a.total),
+ String(a.missed),
+ ]),
+ ),
+ )
+ lines.push('')
+ }
+
+ const lowest = [...files]
+ .filter((f) => (f.lines.total || 0) > 0 && Number(f.lines.pct) < 100)
+ .sort((a, b) => {
+ const pa = Number(a.lines.pct)
+ const pb = Number(b.lines.pct)
+ const ma = (a.lines.total || 0) - (a.lines.covered || 0)
+ const mb = (b.lines.total || 0) - (b.lines.covered || 0)
+ return pa - pb || mb - ma
+ })
+ .slice(0, 40)
+ if (lowest.length) {
+ lines.push('### Lowest files (actionable)')
+ lines.push('')
+ lines.push(
+ ...mdTable(
+ ['Lines', 'Branches', 'Missed', 'File'],
+ lowest.map((f) => [
+ fmtPct(f.lines.pct),
+ fmtPct(f.branches.pct),
+ String((f.lines.total || 0) - (f.lines.covered || 0)),
+ `\`${f.file}\``,
+ ]),
+ ),
+ )
+ lines.push('')
+ }
+
+ const zero = files.filter((f) => (f.lines.total || 0) > 0 && (f.lines.covered || 0) === 0)
+ if (zero.length) {
+ lines.push(`Zero-coverage files (${zero.length})
`)
+ lines.push('')
+ for (const f of zero.slice(0, 80)) lines.push(`- \`${f.file}\``)
+ if (zero.length > 80) lines.push(`- …and ${zero.length - 80} more`)
+ lines.push('')
+ lines.push(' ')
+ lines.push('')
+ }
+
+ return `${lines.join('\n').replace(/\n+$/, '')}\n`
+}
+
+function selfTest() {
+ const tests = collectVitest({
+ numTotalTests: 3,
+ success: false,
+ testResults: [
+ {
+ name: '/repo/web/src/components/ai/Foo.test.tsx',
+ assertionResults: [
+ {
+ fullName: 'Foo fails loudly',
+ status: 'failed',
+ duration: 12,
+ failureMessages: ['expected true to be false'],
+ },
+ { fullName: 'Foo ok', status: 'passed', duration: 4 },
+ ],
+ },
+ ],
+ })
+ const coverage = collectCoverage({
+ total: {
+ statements: { covered: 8, total: 10, pct: 80 },
+ branches: { covered: 3, total: 5, pct: 60 },
+ functions: { covered: 2, total: 2, pct: 100 },
+ lines: { covered: 8, total: 10, pct: 80 },
+ },
+ '/repo/web/src/features/driving/pages/DrivePage.tsx': {
+ lines: { covered: 1, total: 10, pct: 10 },
+ statements: { covered: 1, total: 10, pct: 10 },
+ functions: { covered: 0, total: 2, pct: 0 },
+ branches: { covered: 0, total: 4, pct: 0 },
+ },
+ '/repo/web/src/lib/cn.ts': {
+ lines: { covered: 7, total: 7, pct: 100 },
+ statements: { covered: 7, total: 7, pct: 100 },
+ functions: { covered: 1, total: 1, pct: 100 },
+ branches: { covered: 2, total: 2, pct: 100 },
+ },
+ })
+ const md = render({ tests, coverage })
+ if (!md.includes('Failures — start here')) throw new Error('missing failures heading')
+ if (!md.includes('Foo fails loudly')) throw new Error('missing failed test')
+ if (!md.includes('features/driving')) throw new Error('missing area rollup')
+ if (!md.includes('Lowest files')) throw new Error('missing lowest files')
+ console.log('frontend_coverage_summary self-test OK')
+}
+
+function parseArgs(argv) {
+ const out = {
+ summaryJson: '',
+ vitestJson: '',
+ stepSummary: '',
+ markdownOut: '',
+ selfTest: false,
+ }
+ for (let i = 0; i < argv.length; i += 1) {
+ const a = argv[i]
+ const next = argv[i + 1]
+ if (a === '--self-test') out.selfTest = true
+ else if (a === '--summary-json' && next) {
+ out.summaryJson = next
+ i += 1
+ } else if (a === '--vitest-json' && next) {
+ out.vitestJson = next
+ i += 1
+ } else if (a === '--step-summary' && next) {
+ out.stepSummary = next
+ i += 1
+ } else if (a === '--markdown-out' && next) {
+ out.markdownOut = next
+ i += 1
+ }
+ }
+ return out
+}
+
+function main(argv) {
+ const args = parseArgs(argv)
+ if (args.selfTest) {
+ selfTest()
+ return 0
+ }
+ const tests = collectVitest(readJson(args.vitestJson))
+ const coverage = collectCoverage(readJson(args.summaryJson))
+ const md = render({ tests, coverage })
+ if (args.markdownOut) fs.writeFileSync(args.markdownOut, md)
+ if (args.stepSummary) fs.appendFileSync(args.stepSummary, md)
+ if (!args.markdownOut && !args.stepSummary) process.stdout.write(md)
+ return 0
+}
+
+const isMain =
+ Boolean(process.argv[1]) &&
+ path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
+if (isMain) {
+ process.exit(main(process.argv.slice(2)))
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ec8439fe6..2fbc1f888 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -114,44 +114,26 @@ jobs:
DATABASE_USER: test
DATABASE_PASS: test
DATABASE_NAME: teslasync_test
- run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
+ run: |
+ set -o pipefail
+ go test -race -coverprofile=coverage.out -covermode=atomic -json ./... | tee test-events.json
- name: Generate backend coverage reports
- if: always() && hashFiles('coverage.out') != ''
+ if: always() && (hashFiles('coverage.out') != '' || hashFiles('test-events.json') != '')
run: |
- go tool cover -func=coverage.out > coverage.txt
- go tool cover -html=coverage.out -o coverage.html
- # Per-package roll-up (statement coverage averaged across the package's funcs)
- awk '
- /^total:/ { next }
- {
- n = split($1, parts, "/")
- file = parts[n]; sub(/:.*$/, "", file)
- pkg = parts[1]
- for (i = 2; i < n; i++) pkg = pkg "/" parts[i]
- gsub("%", "", $3)
- sum[pkg] += $3
- cnt[pkg]++
- }
- END {
- for (p in sum) printf "%6.2f%% %s\n", sum[p]/cnt[p], p
- }
- ' coverage.txt | sort -nr > coverage-by-package.txt
- TOTAL_LINE=$(grep '^total:' coverage.txt || echo "total: (statements) 0.0%")
- {
- echo "## 📊 Backend Coverage"
- echo ""
- echo "**$TOTAL_LINE**"
- echo ""
- echo "Full HTML + per-function reports are attached to this run as the **backend-coverage** artifact."
- echo ""
- echo "Per-package coverage (top 50)
"
- echo ""
- echo '```'
- head -n 50 coverage-by-package.txt
- echo '```'
- echo " "
- } >> "$GITHUB_STEP_SUMMARY"
+ python3 .github/scripts/backend_coverage_summary.py --self-test
+ if [ -f coverage.out ]; then
+ go tool cover -func=coverage.out > coverage.txt
+ go tool cover -html=coverage.out -o coverage.html
+ fi
+ python3 .github/scripts/backend_coverage_summary.py \
+ --profile coverage.out \
+ --func coverage.txt \
+ --json test-events.json \
+ --summary "$GITHUB_STEP_SUMMARY" \
+ --markdown-out coverage-summary.md \
+ --package-out coverage-by-package.txt \
+ --file-out coverage-by-file.txt
- name: Architecture test (no forbidden import edges)
run: |
@@ -213,6 +195,9 @@ jobs:
coverage.html
coverage.txt
coverage-by-package.txt
+ coverage-by-file.txt
+ coverage-summary.md
+ test-events.json
frontend:
name: Frontend (lint + test + build)
@@ -245,29 +230,19 @@ jobs:
# doing this after an upgrade; this boots the real runtime.
run: npm run check:vite-deprecations
- name: Test
- run: npx vitest run --coverage --reporter=verbose
+ run: |
+ mkdir -p coverage
+ npx vitest run --coverage --reporter=verbose --reporter=json --outputFile=coverage/vitest-results.json
- name: Generate frontend coverage report
- if: always() && hashFiles('web/coverage/coverage-summary.json') != ''
+ if: always() && (hashFiles('web/coverage/coverage-summary.json') != '' || hashFiles('web/coverage/vitest-results.json') != '')
run: |
- node -e '
- const fs = require("fs");
- const path = "coverage/coverage-summary.json";
- if (!fs.existsSync(path)) { process.exit(0); }
- const t = JSON.parse(fs.readFileSync(path, "utf8")).total;
- const out = [];
- out.push("## 📊 Frontend Coverage");
- out.push("");
- out.push("| Metric | Covered | Total | % |");
- out.push("|--------|--------:|------:|--:|");
- for (const k of ["statements","branches","functions","lines"]) {
- const m = t[k];
- out.push(`| ${k} | ${m.covered} | ${m.total} | ${m.pct}% |`);
- }
- out.push("");
- out.push("Browsable HTML report attached as the **frontend-coverage** artifact (open `coverage/index.html`).");
- fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, out.join("\n") + "\n");
- '
+ node ../.github/scripts/frontend_coverage_summary.mjs --self-test
+ node ../.github/scripts/frontend_coverage_summary.mjs \
+ --summary-json coverage/coverage-summary.json \
+ --vitest-json coverage/vitest-results.json \
+ --step-summary "$GITHUB_STEP_SUMMARY" \
+ --markdown-out coverage/job-summary.md
- name: Upload coverage
if: always()
From 492c9c9159cdd41c5c6f34c4970ff305c9a59008 Mon Sep 17 00:00:00 2001
From: Atul Gupta
Date: Fri, 18 Sep 2026 11:56:59 -0700
Subject: [PATCH 2/2] docs: add GitHub Actions workflow badges to README
Surface CI, SI, ops, security, frontend quality, Helm, ai-eval, release,
and docs status on main so a red workflow is visible without opening Actions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e
---
README.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/README.md b/README.md
index 357a3b451..da4883bde 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
---
## More than a snapshot of your car