diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml index e107a8c..dbb957a 100644 --- a/.github/workflows/dashboard.yml +++ b/.github/workflows/dashboard.yml @@ -35,3 +35,4 @@ jobs: events: "2026-10-10: integrase-bot (CI shepherd) on" max-wait-p90: "15" cost-max: "10" + clones-db: https://raw.githubusercontent.com/squid-protocol/squid-telemetry/main/traffic_metrics.db diff --git a/README.md b/README.md index ad8eef4..31de4a2 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ squid-bot keeps the swarm inside its habitat's carrying capacity. What it actual ## 📊 Live Fleet Dashboard -![GitGalaxy promoter-bot fleet over time: delivery, cumulative bot PRs, speed, cost, spend and efficiency](https://raw.githubusercontent.com/squid-protocol/squid-bot/dashboard/dashboard.svg) +![GitGalaxy promoter-bot fleet over time: delivery, cumulative repo clones, speed, CI load, dollar spend, cost per PR and efficiency](https://raw.githubusercontent.com/squid-protocol/squid-bot/dashboard/dashboard.svg) Live numbers from the GitHub API for [GitGalaxy](https://github.com/squid-protocol/gitgalaxy), the repository squid-bot runs on, refreshed every 6 hours. Raw data: [`metrics.json`](https://github.com/squid-protocol/squid-bot/blob/dashboard/metrics.json). -It's made by this repo's **[Fleet CI report action](actions/fleet-report)**. The dashboard lines up six panels over time, with solid lines where integrase-bot (Oct 10) and promoter-bot (Oct 11) went live: merged PRs per day by origin; promoter-bot's merged PRs, cumulative since it started; job queue wait; runner hours per merged PR, with the before and with-bot averages; runner hours spent per day; and the share of run minutes saved by early cancellation against the share of runs that failed. The weekly report also includes reruns by workflow and, when the orchestrator publishes its own data, agent factory time and slot use. It does not yet include historical host pressure or post-merge rework. +It's made by this repo's **[Fleet CI report action](actions/fleet-report)**. The dashboard lines up seven panels over time, with solid lines where integrase-bot (Oct 10) and promoter-bot (Oct 11) went live: merged PRs per day by origin; clones of the repos the fleet works in, added up since promoter-bot started (GitHub's counts, via [squid-telemetry](https://github.com/squid-protocol/squid-telemetry)); job queue wait; CI runner hours per merged PR, with the before and with-bot averages; promoter-bot's total spend in dollars since it started; its dollars per merged PR each day, with the average; and the share of run minutes saved by early cancellation against the share of runs that failed. The weekly report also includes reruns by workflow and, when the orchestrator publishes its own data, agent factory time and slot use. It does not yet include historical host pressure or post-merge rework. --- diff --git a/actions/fleet-report/README.md b/actions/fleet-report/README.md index fc258b9..bf85330 100644 --- a/actions/fleet-report/README.md +++ b/actions/fleet-report/README.md @@ -2,7 +2,7 @@ A GitHub Action that shows what an agent bot changed in your repo. It publishes a live dashboard (SVG, light and dark) you can embed in a README, and writes a report to each run's summary page. -Every chart shows one mark per UTC day: merged PRs by origin, job queue wait, runner time per merged PR and per day, and the share of run minutes saved by early cancellation and of runs that failed. Name your bot's branch prefixes and its PRs get their own color and a cumulative chart; a line marks the day it started, through every chart, and the cards compare each measure's average with the bot against the 14 days before it. Without a bot, they compare the last 7 days with the 7 before. It also publishes its source data and explains which measures are exact or sampled. +Every chart shows one mark per UTC day: merged PRs by origin, job queue wait, runner time per merged PR, and the share of run minutes saved by early cancellation and of runs that failed. Name your bot's branch prefixes and its PRs get their own color, and its dollar spend and repo clones are added up from the day it started; a line marks the day it started, through every chart, and the cards compare each measure's average with the bot against the 14 days before it. Without a bot, they compare the last 7 days with the 7 before. It also publishes its source data and explains which measures are exact or sampled. ![Example: GitGalaxy's agent fleet](https://raw.githubusercontent.com/squid-protocol/squid-bot/dashboard/dashboard.svg) @@ -48,7 +48,9 @@ The first run backfills 30 days of exact counts, plus the sampled detail for the | `detail-backfill` | `4` | Older days to fill with sampled detail per run | | `max-wait-p90` | off | Warn when the 7-day average p90 queue wait (min) exceeds this | | `max-runner-hours-per-pr` | off | Warn when 7-day runner hours per merged PR exceed this | -| `cost-max` | fit the data | Top of the cost-per-merged-PR axis, in hours | +| `cost-max` | fit the data | Top of the runner-hours-per-merged-PR axis | +| `clones-db` | none | Path or https URL of a squid-telemetry `traffic_metrics.db`; adds the cumulative repo clones chart | +| `clones-repos` | the fleet's CI targets | Comma-separated `owner/repo` to count in the clones chart | | `fail-on-alert` | `false` | Fail the step on a warning | Output: `dashboard-url`, the raw URL of the published SVG. @@ -69,10 +71,10 @@ Output: `dashboard-url`, the raw URL of the published SVG. ### The charts - **Delivery:** merged PRs per day, split by recorded origin (bot, other agents, other, automation). Attribution depends on configured branch prefixes or PR-body markers. -- **Bot PRs, cumulative** (with a bot): the bot's merged PRs added up day by day since it started. +- **Repo clones, cumulative** (with `clones-db`): GitHub's clone counts for the repos the fleet works in, added up since the bot started, from a [squid-telemetry](https://github.com/squid-protocol/squid-telemetry) database. GitHub publishes clone traffic about two days late, so the line ends on its last reported day. - **Speed:** each day's sampled p90 job queue wait. -- **Cost:** each day's estimated runner hours per merged PR, with dashed lines at the average before and with the bot (the cards' figures). Set `cost-max` to cap the axis; days above it are drawn at the top and marked with their value. -- **Spend:** each day's estimated runner hours in total, the bot's days in its color and labelled. +- **CI load:** each day's estimated runner hours per merged PR, with dashed lines at the average before and with the bot (the cards' figures). Set `cost-max` to cap the axis; days above it are drawn at the top and marked with their value. +- **Spend** and **Cost per PR** (when the orchestrator publishes `factory.json` with `usd`): the bot's dollar cost added up since it started, and each day's dollars per merged bot PR with the overall average. - **Efficiency:** two percentages per day. **Runs failed:** failed ÷ (passed + failed) workflow runs, exact. **Run minutes saved (est.):** a run cancelled early (usually because a newer push replaced it) would have taken about as long as that day's median successful run of the same workflow; what it saved is that minus what it ran. The chart shows saved ÷ (spent + saved), in run wall-clock minutes. Runs of the configured `bot-workflows` are left out of both sides, and cancelled runs of a workflow with no successful run that day aren't priced. It is an estimate, labelled as one. Today is drawn faded, as incomplete. The "with bot" averages and cards use finished days only. diff --git a/actions/fleet-report/action.yml b/actions/fleet-report/action.yml index 26d6bc9..89d5014 100644 --- a/actions/fleet-report/action.yml +++ b/actions/fleet-report/action.yml @@ -64,6 +64,14 @@ inputs: cost-max: description: Top of the cost-per-merged-PR chart axis, in runner hours; days above it are drawn at the top and marked. Empty = fit the data. default: "" + clones-db: + description: >- + Path or https URL of a squid-telemetry traffic_metrics.db. Adds a panel of the fleet's repo clones (GitHub's counts), + added up since the bot started. Empty = no clones panel. + default: "" + clones-repos: + description: Comma-separated owner/repo to count in the clones panel. Empty = every repo the fleet's CI fetches. + default: "" fail-on-alert: description: Fail the step when an alert fires. default: "false" @@ -111,6 +119,8 @@ runs: I_MAX_H: ${{ inputs.max-runner-hours-per-pr }} I_FAIL: ${{ inputs.fail-on-alert }} I_COST_MAX: ${{ inputs.cost-max }} + I_CLONES_DB: ${{ inputs.clones-db }} + I_CLONES_REPOS: ${{ inputs.clones-repos }} run: | args=(--repo "$I_REPO" --days "$I_DAYS" --out "$OUT" --title "$I_TITLE" --bot-workflows "$I_BOTS" --agent-branch-prefixes "$I_PREFIXES" --sample-runs "$I_SAMPLE" --detail-backfill "$I_BACKFILL" @@ -119,6 +129,14 @@ runs: --cost-max "$I_COST_MAX") [ -n "$I_MARKERS" ] && args+=(--agent-markers "$I_MARKERS") [ "$I_FAIL" = "true" ] && args+=(--fail-on-alert) + if [ -n "$I_CLONES_DB" ]; then + db="$I_CLONES_DB" + if [[ "$db" == https://* ]]; then + db="$RUNNER_TEMP/clones.db" + curl -sfL --retry 3 -o "$db" "$I_CLONES_DB" || { echo "::warning title=Fleet CI report::could not download clones-db"; db=""; } + fi + [ -n "$db" ] && args+=(--clones-db "$db" --clones-repos "$I_CLONES_REPOS") + fi python3 "$GITHUB_ACTION_PATH/fleet_report.py" "${args[@]}" - name: Publish diff --git a/actions/fleet-report/fleet_report.py b/actions/fleet-report/fleet_report.py index 20dfc28..85f5a16 100644 --- a/actions/fleet-report/fleet_report.py +++ b/actions/fleet-report/fleet_report.py @@ -497,6 +497,28 @@ def factory_stats(rows: list[tuple[str, dict]]) -> dict: } +def clone_days(db: Path, repos: list[str]) -> dict[str, dict[str, int]]: + """{day: {repo: clones}} from a squid-telemetry traffic_metrics.db (GitHub's own clone counts per repo and day). + Repos default to every repo the fleet's CI fetches (its own_ci_checkouts table), else every repo in the database.""" + import sqlite3 + + con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) + try: + if not repos: + try: + repos = [r for (r,) in con.execute("SELECT DISTINCT target_repo FROM own_ci_checkouts")] + except sqlite3.OperationalError: # an older database without the own-CI estimate + repos = [r for (r,) in con.execute("SELECT DISTINCT repo_name FROM traffic_clones")] + out: dict[str, dict[str, int]] = {} + rows = con.execute(f"SELECT date, repo_name, total_clones FROM traffic_clones WHERE repo_name IN " + f"({','.join('?' * len(repos))})", repos) + for day, repo, n in rows: + out.setdefault(day, {})[repo] = int(n or 0) + return out + finally: + con.close() + + def runner_split(rows: list[dict], key: str) -> list[tuple[str, float]]: """Share of sampled runner minutes by workflow or by origin, largest first.""" c: Counter[str] = Counter() @@ -869,32 +891,50 @@ def legend(y: int, items: list[tuple[str, str]]) -> None: y = top + chart_h + 70 panel = 2 - # 2 · The bot's merged PRs, cumulative since it started - if start_i is not None: - made = sorted((d, (r.get("merged_by_class") or {}).get("bot", 0)) for d, r in data["days"].items() if d >= start) - cum_values, cum_tips, total = [], [], 0 + def running_total(top: int, per_day: dict[str, float], title: str, axis_fmt, end_fmt, tip_fmt, note: str = "") -> int: + """A running total of per_day from the bot's start, as a filled line ending in its total; days past the last one + with data are left off. Returns the y of the next panel.""" + last = max(per_day, default="") + values, tips, total = [], [], 0.0 for day, _ in rows: - if day < start: - cum_values.append(None) - cum_tips.append("") + if day < start or day > last: + values.append(None) + tips.append("") continue - total = sum(n for d, n in made if d <= day) - that_day = next((n for d, n in made if d == day), 0) - cum_values.append(float(total)) - cum_tips.append(f"{total:,} {cfg.bot_name} PRs merged since {_fmt_day(start)} (+{that_day} this day)") - top, chart_h = y, 150 - cum_axis = _nice_axis(max(1.0, total)) - frame(top, chart_h, cum_axis, f"{panel} · {cfg.bot_name} PRs merged, cumulative since {_fmt_day(start)}", - lambda v: f"{v:,.0f}") - pts = [(x_mid(i), top + chart_h - chart_h * v / cum_axis[0]) for i, v in enumerate(cum_values) if v is not None] + total += per_day.get(day, 0) + values.append(total) + tips.append(tip_fmt(day, per_day.get(day, 0), total)) + chart_h = 150 + axis = _nice_axis(max(1.0, total)) + frame(top, chart_h, axis, title, axis_fmt, note) + pts = [(x_mid(i), top + chart_h - chart_h * v / axis[0]) for i, v in enumerate(values) if v is not None] if len(pts) > 1: edge = " ".join(f"L{x:.1f},{py:.1f}" for x, py in pts) out.append(f'') - line_series(top, chart_h, cum_values, cum_axis[0], tips=cum_tips, cls="cum") + line_series(top, chart_h, values, axis[0], tips=tips, cls="cum") if pts: out.append(f'' - f'{total:,} PRs') - y = top + chart_h + 70 + f'{escape(end_fmt(total))}') + if note: + out.append(f'{escape(note)}') + return top + chart_h + 70 + + # 2 · Clones of the repos the fleet works in, added up since the bot started (squid-telemetry's GitHub traffic) + clones = data.get("clones") or {} + if start_i is not None and clones: + per_day = {d: float(sum(by_repo.values())) for d, by_repo in clones.items() if d >= start} + by_repo = Counter() + for d, counts in clones.items(): + if d >= start: + by_repo.update(counts) + top_repos = " · ".join(f"{r.split('/')[-1]} {n:,}" for r, n in by_repo.most_common(4)) + y = running_total( + y, per_day, f"{panel} · Repo clones by the fleet, cumulative since {_fmt_day(start)} " + f"(GitHub's counts, through {_fmt_day(max(per_day))})" if per_day else f"{panel} · Repo clones", + lambda v: f"{v / 1000:g}k" if v >= 1000 else f"{v:g}", lambda t: f"{t:,.0f} clones", + lambda d, n, t: f"{n:,.0f} clones this day · {t:,.0f} since {_fmt_day(start)} · " + + ", ".join(f"{r.split('/')[-1]} {c:,}" for r, c in sorted(clones[d].items(), key=lambda x: -x[1]) if c), + top_repos) panel += 1 # Queue wait, CI cost per merged PR and runner spend per day (sampled) @@ -922,7 +962,7 @@ def legend(y: int, items: list[tuple[str, str]]) -> None: runner_axis = (_nice_axis(cfg.cost_max) if cfg.cost_max else _nice_axis(max((v for v in runner_values if v is not None), default=1.0))) runner_max = runner_axis[0] - frame(top, chart_h, runner_axis, f"{panel} · Cost — runner hours per merged PR (est., sampled)", lambda v: f"{v:g}") + frame(top, chart_h, runner_axis, f"{panel} · CI load — runner hours per merged PR (est., sampled)", lambda v: f"{v:g}") panel += 1 cost_tips = [f"{v:.2f} runner hours / merged PR" + (f" (above the {runner_max:g} h axis)" if v > runner_max else "") if v is not None else "" for v in runner_values] @@ -944,39 +984,45 @@ def legend(y: int, items: list[tuple[str, str]]) -> None: if v is not None and v > runner_max: out.append(f'▲ {v:.0f} h') y = top + chart_h + 70 - - # Spend: estimated runner hours each day, the bot's days in its colour and labelled - spend = [detail["runner_min_est"] / 60 if detail.get("runner_min_est") is not None else None for detail in details] - top = y - spend_axis = _nice_axis(max((v for v in spend if v is not None), default=1.0)) - with_bot = [v for (day, _), v in zip(rows, spend) if v is not None and start and start <= day < today_s] - spend_note = (f"with {cfg.bot_name} (finished days): {sum(with_bot):,.0f} h, avg {sum(with_bot) / len(with_bot):,.0f} h a day" - if with_bot else "") - frame(top, chart_h, spend_axis, f"{panel} · Spend — runner hours per day (est., sampled)", lambda v: f"{v:,.0f}") - panel += 1 - for i, ((day, row), v) in enumerate(zip(rows, spend)): - if v is None: - continue - h = chart_h * v / spend_axis[0] - x = x_left(i) + (slot - bar_w) / 2 - kind = "bot" if start and day >= start else "automation" - partial = day == today_s - per_pr = f" · {v / row['merged']:.1f} h per merged PR" if row["merged"] else "" - tip = escape(f"{_fmt_day(day)}: {v:,.0f} runner hours (est.){per_pr}{' (today so far)' if partial else ''}") - fade = ' class="partial"' if partial else "" - out.append(f'{tip}' - f'') - if kind == "bot": - out.append(f'{v:,.0f}') - if spend_note: - out.append(f'{escape(spend_note)}') - y = top + chart_h + 70 else: out.append(f'{panel} · Speed and cost') panel += 1 out.append(f'No sampled CI capacity history yet.') y += 20 + # The bot's own spend in dollars (factory.json: what its agent sessions cost, all hosts) and per merged bot PR + fdays = {d: r for d, r in ((data.get("factory") or {}).get("days") or {}).items() + if start and d >= start and r.get("usd") is not None} + if start_i is not None and fdays: + usd = {d: float(r["usd"]) for d, r in fdays.items()} + prs = {d: r.get("churn_merged") or 0 for d, r in fdays.items()} + spent, made = sum(usd.values()), sum(prs.values()) + y = running_total( + y, usd, f"{panel} · Spend — {cfg.bot_name} cost in dollars, cumulative since {_fmt_day(start)}", + lambda v: f"${v:,.0f}", lambda t: f"${t:,.0f} total", + lambda d, n, t: f"${n:,.2f} this day ({prs[d]} merged PRs) · ${t:,.2f} since {_fmt_day(start)}" + + (" (today so far)" if d == today_s else ""), + f"{made:,} merged {cfg.bot_name} PRs for ${spent:,.0f}") + panel += 1 + per_pr = [usd[d] / prs[d] if prs.get(d) else None for d, _ in rows] + top, chart_h = y, 120 + pr_axis = _nice_axis(max((v for v in per_pr if v is not None), default=1.0)) + frame(top, chart_h, pr_axis, f"{panel} · Cost per PR — {cfg.bot_name} dollars per merged PR, each day", + lambda v: f"${v:g}") + panel += 1 + line_series(top, chart_h, per_pr, pr_axis[0], + tips=[f"${v:.2f} per merged PR (${usd[d]:,.2f} for {prs[d]})" if v is not None else "" + for (d, _), v in zip(rows, per_pr)], cls="cum") + if made: + avg = spent / made + spots = [index[d] for d in sorted(fdays) if d in index] + ya = top + chart_h - chart_h * avg / pr_axis[0] + out.append(f'') + out.append(f'' + f'{escape(f"avg ${avg:.2f} per PR since {_fmt_day(start)}")}') + y = top + chart_h + 70 + # 4 · Efficiency: share of run minutes early cancellation saved (est.) and share of runs that failed proof_detail = ["CI workflow-run outcomes; not a per-PR quality score"] reruns = rerun_split(finished(rows, today)[-7:]) @@ -1055,6 +1101,8 @@ def main(argv: list[str] | None = None) -> int: p.add_argument("--max-wait-p90", default="", help="alert when the 7-day p90 job queue wait (min) exceeds this") p.add_argument("--max-runner-hours-per-pr", default="", help="alert when 7-day runner hours per merged PR exceed this") p.add_argument("--cost-max", default="", help="top of the cost-per-PR chart axis, hours (default: fit the data)") + p.add_argument("--clones-db", type=Path, help="a squid-telemetry traffic_metrics.db: adds the fleet's cumulative repo clones") + p.add_argument("--clones-repos", default="", help="comma-separated owner/repo to count (default: the fleet's CI targets)") p.add_argument("--fail-on-alert", action="store_true") p.add_argument("--render-only", action="store_true", help="redraw from metrics.json without fetching") a = p.parse_args(argv) @@ -1080,6 +1128,11 @@ def main(argv: list[str] | None = None) -> int: data = {**data, "factory": json.loads(factory_file.read_text())} except ValueError as e: print(f"::warning title=Fleet CI report::factory.json unreadable: {e}", file=sys.stderr) + if a.clones_db: + try: + data = {**data, "clones": clone_days(a.clones_db, _csv(a.clones_repos))} + except Exception as e: # noqa: BLE001 - a missing or odd database drops the panel, not the report + print(f"::warning title=Fleet CI report::clones database unreadable: {e}", file=sys.stderr) (a.out / "dashboard.svg").write_text(render(data, a.days, today, cfg), encoding="utf-8") alerts = check_alerts(data, a.days, today, _opt_float(a.max_wait_p90), _opt_float(a.max_runner_hours_per_pr)) for msg in alerts: diff --git a/tests/test_fleet_report.py b/tests/test_fleet_report.py index cc8dde2..f029bc1 100644 --- a/tests/test_fleet_report.py +++ b/tests/test_fleet_report.py @@ -57,7 +57,7 @@ def test_render_adds_the_ci_cost_row_and_wait_chart_when_detail_exists(): svg = dashboard.render(data, 30, TODAY) ET.fromstring(svg) assert "job queue wait (p90)" in svg and "4.0 min" in svg - assert "runner hours per merged PR" in svg and "runner hours per day" in svg + assert "runner hours per merged PR" in svg assert svg.count("CI change(s) merged") == 3 # without detail the extra row and chart stay out assert "job queue wait" not in dashboard.render(_data(), 30, TODAY) @@ -240,18 +240,49 @@ def test_render_tells_the_before_and_with_bot_story(): svg = dashboard.render(_with_bot(), 30, TODAY, BOT) ET.fromstring(svg) assert "promoter-bot live · Oct 8" in svg and "Oct 6 · shepherd on" in svg - # no shaded band: a start line (and the event lines) through every panel, now six of them - assert 'class="band"' not in svg and svg.count('class="start"') == 6 and svg.count('class="mark"') == 6 + # no shaded band: a start line (and the event lines) through every panel, four without clones or factory.json + assert 'class="band"' not in svg and svg.count('class="start"') == 4 and svg.count('class="mark"') == 4 assert "PROMOTER-BOT-MADE SHARE" in svg and "62.5%" in svg # 25 of 40 merges a day since it started assert "before promoter-bot: 15.0 · +167%" in svg # merged / day: 40 with vs 15 before assert "175 of the 280 PRs merged since then were its own" in svg - # 2 · cumulative bot PRs: 25 a day from Oct 8 through today (Oct 14) = 175 - assert "2 · promoter-bot PRs merged, cumulative since Oct 8" in svg and ">175 PRs" in svg - assert "175 promoter-bot PRs merged since Oct 8 (+25 this day)" in svg - # cost per PR with its before/with averages (600 min over 40 vs 15 merges); spend per day as bars - assert "4 · Cost" in svg and "avg 0.2 h / PR with promoter-bot" in svg and "avg 0.7 h / PR before promoter-bot" in svg - assert "5 · Spend" in svg and "with promoter-bot (finished days): 60 h, avg 10 h a day" in svg - assert "6 · Efficiency" in svg + # runner hours per PR with its before/with averages (600 min over 40 vs 15 merges) + assert "3 · CI load" in svg and "avg 0.2 h / PR with promoter-bot" in svg and "avg 0.7 h / PR before promoter-bot" in svg + assert "4 · Efficiency" in svg # no clones database or factory.json: no clones or dollar panels + assert "Repo clones" not in svg and "Spend" not in svg + + +def test_clones_and_dollar_panels_add_up_from_the_bots_start(): + data = _with_bot() + data["clones"] = {"2026-10-07": {"o/a": 999}, "2026-10-08": {"o/a": 100, "o/b": 20}, "2026-10-09": {"o/a": 80}} + data["factory"] = {"days": {"2026-10-07": {"usd": 50.0, "churn_merged": 1}, + "2026-10-08": {"usd": 30.0, "churn_merged": 10}, "2026-10-09": {"usd": 20.0, "churn_merged": 15}}} + svg = dashboard.render(data, 30, TODAY, BOT) + ET.fromstring(svg) + # 2 · clones from the start (Oct 8) on, the day before left out, the line ending on the last day with data + assert "2 · Repo clones by the fleet, cumulative since Oct 8 (GitHub's counts, through Oct 9)" in svg \ + or "2 · Repo clones by the fleet, cumulative since Oct 8 (GitHub's counts, through Oct 9)" in svg + assert ">200 clones" in svg and "a 180 · b 20" in svg + # 5 · dollars spent since the start, and 6 · per merged PR with the overall average ($50 / 25 PRs) + assert "5 · Spend — promoter-bot cost in dollars, cumulative since Oct 8" in svg and ">$50 total" in svg + assert "25 merged promoter-bot PRs for $50" in svg + assert "6 · Cost per PR" in svg and "$3.00 per merged PR ($30.00 for 10)" in svg and "avg $2.00 per PR since Oct 8" in svg + assert "7 · Efficiency" in svg + + +def test_clone_days_reads_the_fleets_ci_targets_from_a_telemetry_db(tmp_path): + import sqlite3 + db = tmp_path / "t.db" + con = sqlite3.connect(db) + con.execute("CREATE TABLE traffic_clones (repo_name TEXT, date TEXT, total_clones INTEGER, unique_cloners INTEGER)") + con.execute("CREATE TABLE own_ci_checkouts (date TEXT, ci_repo TEXT, workflow TEXT, target_repo TEXT, runs INTEGER, " + "checkouts_per_run INTEGER, est_clones INTEGER)") + con.executemany("INSERT INTO traffic_clones VALUES (?, ?, ?, 0)", + [("o/a", "2026-10-08", 5), ("o/b", "2026-10-08", 7), ("o/other", "2026-10-08", 99)]) + con.executemany("INSERT INTO own_ci_checkouts VALUES ('2026-10-08', 'o/ci', 'ci.yml', ?, 1, 1, 1)", [("o/a",), ("o/b",)]) + con.commit() + con.close() + assert dashboard.clone_days(db, []) == {"2026-10-08": {"o/a": 5, "o/b": 7}} # repos the fleet's CI fetches + assert dashboard.clone_days(db, ["o/other"]) == {"2026-10-08": {"o/other": 99}} md = dashboard.summary_md(_with_bot(), BOT, 30, TODAY, []) assert "| Measure | With promoter-bot | Before promoter-bot |" in md assert "| Delivery · promoter-bot-made share | 62.5% | 0.0% |" in md