@decohere/ci-tools 0.0.1-dev.109 → 0.0.1-dev.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { issueBody, issueTitle } from "../../src/bench/regression-issue.mjs";
3
+
4
+ const regression = {
5
+ id: "flux.load.cold",
6
+ label: "flux load, first import (cold process)",
7
+ baseline: 300,
8
+ percent: 33.3,
9
+ recent: [
10
+ { date: "2026-10-22T03:17:00Z", sha: "cccccccccccccccc", median: 400 },
11
+ { date: "2026-10-21T03:17:00Z", sha: "bbbbbbbbbbbbbbbb", median: 390 },
12
+ ],
13
+ lastGood: { date: "2026-10-20T03:17:00Z", sha: "aaaaaaaaaaaaaaaa" },
14
+ };
15
+ const context = {
16
+ repoUrl: "https://github.com/typesmiths/decohere",
17
+ runUrl: "https://github.com/typesmiths/decohere/actions/runs/1",
18
+ machine: "gh-actions-node24, Test CPU × 4",
19
+ threshold: 0.2,
20
+ };
21
+
22
+ describe("regression issue", () => {
23
+ it("has one stable title per benchmark, so the nightly run finds and updates it", () => {
24
+ expect(issueTitle(regression)).toBe("perf(bench): flux.load.cold is slower");
25
+ });
26
+
27
+ it("gives the numbers, the commits to look at, and how to reproduce", () => {
28
+ const body = issueBody(regression, context);
29
+ expect(body).toContain(
30
+ "median 33% above its baseline, on each of the last 2 nightly runs",
31
+ );
32
+ expect(body).toContain("- Baseline (median of the runs before): **300.0 ms**");
33
+ expect(body).toContain(
34
+ "- 2026-10-22, [`ccccccc`](https://github.com/typesmiths/decohere/commit/cccccccccccccccc): **400.0 ms**",
35
+ );
36
+ expect(body).toContain(
37
+ "(https://github.com/typesmiths/decohere/compare/aaaaaaaaaaaaaaaa...cccccccccccccccc)",
38
+ );
39
+ expect(body).toContain("`pnpm bench:save`");
40
+ expect(body).toContain(context.runUrl);
41
+ });
42
+
43
+ it("says the range is unknown when the baseline run has no commit (converted old history)", () => {
44
+ const body = issueBody(
45
+ { ...regression, lastGood: { date: "2026-08-12T00:00:00Z", sha: null } },
46
+ context,
47
+ );
48
+ expect(body).toContain("unknown (no baseline commit recorded)");
49
+ });
50
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildBenchmarkHtml, trendSvg } from "../../src/pages/bench-page.mjs";
3
+
4
+ const context = (benchEnv, cores) => ({
5
+ benchEnv,
6
+ os: { platform: "linux", arch: "x64" },
7
+ cpu: { model: "Test CPU", count: cores },
8
+ memoryGb: 16,
9
+ node: "v24.1.0",
10
+ });
11
+
12
+ const run = (date, sha, results) => ({ date, sha, results });
13
+ const result = (id, median, label = id) => ({
14
+ id,
15
+ label,
16
+ unit: "ms",
17
+ median,
18
+ p90: median * 1.2,
19
+ min: median,
20
+ max: median,
21
+ samples: 5,
22
+ });
23
+
24
+ const history = {
25
+ version: 2,
26
+ contexts: {
27
+ ci: {
28
+ context: context("gh-actions-node24", 4),
29
+ runs: [
30
+ run("2026-10-20T03:17:00Z", "bbbbbbbbbbbb", [
31
+ result("flux.serve.start", 90, "spawn <to> ready"),
32
+ result("forge.build.debug", 9),
33
+ ]),
34
+ run("2026-10-19T03:17:00Z", "aaaaaaaaaaaa", [
35
+ result("flux.serve.start", 100, "spawn <to> ready"),
36
+ ]),
37
+ ],
38
+ },
39
+ laptop: {
40
+ context: context("(local)", 24),
41
+ runs: [run("2026-10-01T10:00:00Z", "cccccccccccc", [result("flux.serve.start", 40)])],
42
+ },
43
+ },
44
+ };
45
+
46
+ describe("buildBenchmarkHtml", () => {
47
+ it("explains an empty history instead of rendering empty charts", () => {
48
+ const html = buildBenchmarkHtml({ version: 2, contexts: {} });
49
+ expect(html).toContain("No benchmark history yet");
50
+ expect(html).not.toContain("<svg");
51
+ });
52
+
53
+ it("draws one card per benchmark, grouped, with a line per machine that ran it", () => {
54
+ const html = buildBenchmarkHtml(history, { branch: "main", sha: "0123456789abcdef" });
55
+
56
+ expect(html.match(/<article class="card">/g)).toHaveLength(2);
57
+ expect(html.indexOf("flux serve (real process)")).toBeLessThan(
58
+ html.indexOf("<h2>forge build</h2>"),
59
+ );
60
+ const serveCard = html.slice(
61
+ html.indexOf("<code>flux.serve.start</code>"),
62
+ html.indexOf("<code>forge.build.debug</code>"),
63
+ );
64
+ expect(serveCard.match(/<polyline/g)).toHaveLength(2);
65
+ expect(html).toContain("built from main @ <code>0123456789ab</code>");
66
+ });
67
+
68
+ it("shows the latest median and the change from the previous run on the newest machine", () => {
69
+ const html = buildBenchmarkHtml(history);
70
+ expect(html).toContain("<strong>90.0 ms</strong> median · p90 108 ms");
71
+ expect(html).toContain('<span class="change better">10.0% faster</span>');
72
+ });
73
+
74
+ it("dates each result and links its commit, in the latest table and on each card", () => {
75
+ const html = buildBenchmarkHtml(history);
76
+ expect(html).toContain("<th>Run</th><th>Commit</th>");
77
+ expect(html).toContain("<td>2026-10-20 03:17 UTC</td>");
78
+ expect(html).toContain(
79
+ '<a href="https://github.com/typesmiths/decohere/commit/bbbbbbbbbbbb"><code>bbbbbbb</code></a>',
80
+ );
81
+ expect(html).toContain('<p class="when">Last run 2026-10-20 03:17 UTC</p>');
82
+ });
83
+
84
+ it("escapes labels from the history", () => {
85
+ const html = buildBenchmarkHtml(history);
86
+ expect(html).toContain("spawn &lt;to&gt; ready");
87
+ expect(html).not.toContain("spawn <to> ready");
88
+ });
89
+ });
90
+
91
+ describe("trendSvg", () => {
92
+ it("places a single point without dividing by a zero time range", () => {
93
+ const svg = trendSvg([
94
+ {
95
+ color: "red",
96
+ points: [{ time: 1, date: "2026-10-20", sha: "abc", median: 5, p90: 6 }],
97
+ },
98
+ ]);
99
+ expect(svg).toContain("<circle");
100
+ expect(svg).not.toContain("NaN");
101
+ });
102
+
103
+ it("draws nothing when no machine has a point", () => {
104
+ expect(trendSvg([])).toBe("");
105
+ });
106
+ });
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Keep prior bench badge if no code changes; create fallback if none exists
4
-
5
- import { execSync } from "node:child_process";
6
- import { existsSync, writeFileSync } from "node:fs";
7
- import { mkdir } from "node:fs/promises";
8
-
9
- const BADGES_DIR = "projects/ci-tools/dist/ci/badges";
10
- const BADGE_FILE = `${BADGES_DIR}/bench.svg`;
11
- const PAGES_URL = "https://typesmiths.github.io/decohere/badges/bench.svg";
12
-
13
- const FALLBACK_BADGE = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20" role="img" aria-label="bench: unavailable">
14
- <title>bench: unavailable</title>
15
- <rect width="120" height="20" fill="#555"/>
16
- <rect x="46" width="74" height="20" fill="#9f9f9f"/>
17
- <text x="24" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">bench</text>
18
- <text x="83" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">unavailable</text>
19
- </svg>`;
20
-
21
- try {
22
- if (!existsSync(BADGE_FILE)) {
23
- await mkdir(BADGES_DIR, { recursive: true });
24
-
25
- try {
26
- execSync(`curl -fsSL "${PAGES_URL}" -o "${BADGE_FILE}"`, {
27
- stdio: "pipe",
28
- });
29
- console.log("✓ Restored prior bench badge from Pages");
30
- } catch {
31
- console.warn("::warning::No prior bench badge found on Pages; creating fallback badge.");
32
- writeFileSync(BADGE_FILE, FALLBACK_BADGE);
33
- }
34
- }
35
- } catch (error) {
36
- console.error("Error keeping prior bench badge:", error.message);
37
- process.exit(1);
38
- }