@decohere/ci-tools 0.0.1-dev.126 → 0.0.1-dev.14
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.
- package/package.json +3 -18
- package/src/audit/enforce.mjs +5 -7
- package/src/audit/summarize.mjs +32 -10
- package/src/audit/upsert-issue.mjs +74 -25
- package/src/bench/bench.mjs +368 -134
- package/src/bench/trace.mjs +14 -18
- package/src/coverage/enforce-thresholds.mjs +144 -0
- package/src/pages/artifact.mjs +183 -38
- package/src/pages/clear-bench-history.mjs +27 -0
- package/src/pages/keep-prior-bench-badge.mjs +38 -0
- package/src/pages/restore-bench-history.mjs +28 -15
- package/src/release/npm-links.mjs +13 -6
- package/src/release/sync-dependencies.mjs +2 -1
- package/tests/lib/lcov.test.mjs +4 -4
- package/LICENSE +0 -15
- package/src/audit/format.mjs +0 -133
- package/src/bench/file-regressions.mjs +0 -76
- package/src/bench/history.mjs +0 -248
- package/src/bench/nightly.mjs +0 -118
- package/src/bench/regression-issue.mjs +0 -44
- package/src/github/credential-nudge.mjs +0 -226
- package/src/github/npm-bootstrap.mjs +0 -179
- package/src/github/oidc-bootstrap-nudge.mjs +0 -214
- package/src/github/publish-verify.mjs +0 -206
- package/src/github/smoke-create-launcher.mjs +0 -70
- package/src/lib/github-issue.mjs +0 -75
- package/src/lib/workspace-packages.mjs +0 -30
- package/src/pages/bench-page.mjs +0 -323
- package/src/pages/restore-badges.mjs +0 -58
- package/tests/audit/format.test.mjs +0 -115
- package/tests/bench/history.test.mjs +0 -279
- package/tests/bench/regression-issue.test.mjs +0 -42
- package/tests/github/credential-nudge.test.mjs +0 -112
- package/tests/github/npm-bootstrap.test.mjs +0 -125
- package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -89
- package/tests/github/package-metadata.test.mjs +0 -69
- package/tests/github/publish-verify.test.mjs +0 -115
- package/tests/github/smoke-create-launcher.test.mjs +0 -71
- package/tests/lib/github-issue.test.mjs +0 -85
- package/tests/pages/bench-page.test.mjs +0 -123
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { join, relative } from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { vol } from "memfs";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { listPublishablePackages } from "../../src/github/publish-verify.mjs";
|
|
6
|
-
|
|
7
|
-
const fs = await vi.importActual("node:fs");
|
|
8
|
-
const REPO_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
9
|
-
const REPO = "https://github.com/typesmiths/decohere";
|
|
10
|
-
|
|
11
|
-
/** The repo's real package.json files, loaded into the in-memory fs the tests run on. */
|
|
12
|
-
function loadRealManifests() {
|
|
13
|
-
/** @type {Record<string, string>} */
|
|
14
|
-
const files = {};
|
|
15
|
-
const walk = (dir) => {
|
|
16
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
17
|
-
const path = join(dir, entry.name);
|
|
18
|
-
if (entry.isDirectory() && !["node_modules", "dist"].includes(entry.name)) {
|
|
19
|
-
walk(path);
|
|
20
|
-
} else if (entry.name === "package.json") {
|
|
21
|
-
files[join("/repo", relative(REPO_ROOT, path))] = fs.readFileSync(path, "utf8");
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
walk(join(REPO_ROOT, "projects"));
|
|
26
|
-
return files;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
vol.reset();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// npm shows these on each package's page, and Trusted Publishing's provenance is
|
|
34
|
-
// checked against `repository.url`, so a package without it fails to publish.
|
|
35
|
-
describe("published package metadata", () => {
|
|
36
|
-
vol.fromJSON(loadRealManifests());
|
|
37
|
-
const packages = listPublishablePackages("/repo").map((pkg) => ({
|
|
38
|
-
...pkg,
|
|
39
|
-
json: JSON.parse(String(vol.readFileSync(`/repo/projects/${pkg.dir}/package.json`))),
|
|
40
|
-
}));
|
|
41
|
-
vol.reset();
|
|
42
|
-
|
|
43
|
-
it("finds the publishable packages", () => {
|
|
44
|
-
expect(packages.length).toBeGreaterThan(0);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it.each(packages.map((pkg) => [pkg.name, pkg]))(
|
|
48
|
-
"%s links back to the repo",
|
|
49
|
-
(_name, pkg) => {
|
|
50
|
-
expect(pkg.json.repository).toEqual({
|
|
51
|
-
type: "git",
|
|
52
|
-
url: `git+${REPO}.git`,
|
|
53
|
-
directory: `projects/${pkg.dir}`,
|
|
54
|
-
});
|
|
55
|
-
expect(pkg.json.homepage).toBe(`${REPO}/tree/main/projects/${pkg.dir}#readme`);
|
|
56
|
-
expect(pkg.json.bugs).toEqual({ url: `${REPO}/issues` });
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
it.each(packages.map((pkg) => [pkg.name, pkg]))(
|
|
61
|
-
"%s has a description, keywords, license and author",
|
|
62
|
-
(_name, pkg) => {
|
|
63
|
-
expect(pkg.json.description).toBeTruthy();
|
|
64
|
-
expect(pkg.json.keywords?.length).toBeGreaterThan(0);
|
|
65
|
-
expect(pkg.json.license).toBe("ISC");
|
|
66
|
-
expect(pkg.json.author).toBeTruthy();
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { vol } from "memfs";
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
collectPublishVerify,
|
|
5
|
-
formatGithubOutput,
|
|
6
|
-
listPublishablePackages,
|
|
7
|
-
parseGhcrRepository,
|
|
8
|
-
} from "../../src/github/publish-verify.mjs";
|
|
9
|
-
|
|
10
|
-
function seedWorkspace() {
|
|
11
|
-
const root = "/workspace";
|
|
12
|
-
vol.fromJSON({
|
|
13
|
-
[`${root}/package.json`]: JSON.stringify({ version: "0.0.1-dev.79" }),
|
|
14
|
-
[`${root}/projects/cli/package.json`]: JSON.stringify({
|
|
15
|
-
name: "@decohere/cli",
|
|
16
|
-
version: "0.0.1-dev.79",
|
|
17
|
-
}),
|
|
18
|
-
[`${root}/projects/scaffold/package.json`]: JSON.stringify({
|
|
19
|
-
name: "@decohere/scaffold",
|
|
20
|
-
version: "0.0.1-dev.79",
|
|
21
|
-
private: true,
|
|
22
|
-
}),
|
|
23
|
-
[`${root}/projects/create/package.json`]: JSON.stringify({
|
|
24
|
-
name: "@decohere/create",
|
|
25
|
-
version: "0.0.1-dev.79",
|
|
26
|
-
decohere: { dockerImageRepository: "ghcr.io/typesmiths/decohere/create" },
|
|
27
|
-
}),
|
|
28
|
-
[`${root}/projects/devcontainer/package.json`]: JSON.stringify({
|
|
29
|
-
name: "@decohere/devcontainer",
|
|
30
|
-
private: true,
|
|
31
|
-
decohere: { dockerImageRepository: "ghcr.io/typesmiths/decohere/devcontainer" },
|
|
32
|
-
}),
|
|
33
|
-
});
|
|
34
|
-
return root;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
vol.reset();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("publish-verify", () => {
|
|
42
|
-
it("lists non-private workspace packages", () => {
|
|
43
|
-
const root = seedWorkspace();
|
|
44
|
-
expect(listPublishablePackages(root).map((pkg) => pkg.name)).toEqual([
|
|
45
|
-
"@decohere/cli",
|
|
46
|
-
"@decohere/create",
|
|
47
|
-
]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("lists grouped packages (projects/<group>/<name>) and skips nested and build folders", () => {
|
|
51
|
-
const root = seedWorkspace();
|
|
52
|
-
vol.fromJSON({
|
|
53
|
-
[`${root}/projects/data/data/package.json`]: JSON.stringify({ name: "@decohere/data" }),
|
|
54
|
-
[`${root}/projects/data/data-typeorm/package.json`]: JSON.stringify({
|
|
55
|
-
name: "@decohere/data-typeorm",
|
|
56
|
-
}),
|
|
57
|
-
[`${root}/projects/cli/examples/demo/package.json`]: JSON.stringify({ name: "nested" }),
|
|
58
|
-
[`${root}/projects/__fixtures__/app/package.json`]: JSON.stringify({ name: "fixture" }),
|
|
59
|
-
[`${root}/projects/node_modules/dep/package.json`]: JSON.stringify({ name: "dep" }),
|
|
60
|
-
[`${root}/projects/dist/out/package.json`]: JSON.stringify({ name: "built" }),
|
|
61
|
-
});
|
|
62
|
-
expect(listPublishablePackages(root).map((pkg) => pkg.name)).toEqual([
|
|
63
|
-
"@decohere/cli",
|
|
64
|
-
"@decohere/create",
|
|
65
|
-
"@decohere/data",
|
|
66
|
-
"@decohere/data-typeorm",
|
|
67
|
-
]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("parses GHCR repository coordinates", () => {
|
|
71
|
-
expect(parseGhcrRepository("ghcr.io/typesmiths/decohere/create")).toEqual({
|
|
72
|
-
org: "typesmiths",
|
|
73
|
-
packageName: "decohere/create",
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("marks names missing from npm as unpublished and skip_npm when every version exists", async () => {
|
|
78
|
-
const root = seedWorkspace();
|
|
79
|
-
const views = [];
|
|
80
|
-
const spawnSync = (_cmd, args) => {
|
|
81
|
-
views.push(args[1]);
|
|
82
|
-
if (args[1] === "@decohere/cli") {
|
|
83
|
-
return { status: 1 };
|
|
84
|
-
}
|
|
85
|
-
return { status: 0 };
|
|
86
|
-
};
|
|
87
|
-
const result = await collectPublishVerify({
|
|
88
|
-
root,
|
|
89
|
-
spawnSync,
|
|
90
|
-
githubToken: "",
|
|
91
|
-
});
|
|
92
|
-
expect(result.unpublished).toEqual(["@decohere/cli"]);
|
|
93
|
-
expect(result.skipNpm).toBe(false);
|
|
94
|
-
expect(views).toContain("@decohere/cli");
|
|
95
|
-
expect(views).toContain("@decohere/create");
|
|
96
|
-
expect(views).toContain("@decohere/create@0.0.1-dev.79");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("sets skip_docker when both GHCR tags exist", async () => {
|
|
100
|
-
const root = seedWorkspace();
|
|
101
|
-
const fetch = async () => ({
|
|
102
|
-
ok: true,
|
|
103
|
-
status: 200,
|
|
104
|
-
json: async () => [{ metadata: { container: { tags: ["0.0.1-dev.79"] } } }],
|
|
105
|
-
});
|
|
106
|
-
const result = await collectPublishVerify({
|
|
107
|
-
root,
|
|
108
|
-
spawnSync: () => ({ status: 0 }),
|
|
109
|
-
githubToken: "ghs_test",
|
|
110
|
-
fetch,
|
|
111
|
-
});
|
|
112
|
-
expect(result.skipDocker).toBe(true);
|
|
113
|
-
expect(formatGithubOutput(result)).toContain("skip_docker=true");
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { runCreateLauncherSmoke } from "../../src/github/smoke-create-launcher.mjs";
|
|
4
|
-
|
|
5
|
-
const fs = await vi.importActual("node:fs");
|
|
6
|
-
const PUBLISH_YML = fileURLToPath(
|
|
7
|
-
new URL("../../../../.github/workflows/publish.yml", import.meta.url),
|
|
8
|
-
);
|
|
9
|
-
|
|
10
|
-
describe("runCreateLauncherSmoke", () => {
|
|
11
|
-
it("execs npx from a temp directory, not the repo root", () => {
|
|
12
|
-
/** @type {{ cmd: string, args: string[], opts: { cwd?: string } }[]} */
|
|
13
|
-
const calls = [];
|
|
14
|
-
const written = [];
|
|
15
|
-
|
|
16
|
-
const result = runCreateLauncherSmoke({
|
|
17
|
-
createVersion: "0.0.1-dev.80",
|
|
18
|
-
authToken: "tok",
|
|
19
|
-
tmpdir: () => "/tmp",
|
|
20
|
-
mkdtempSync: (prefix) => `${prefix}abc`,
|
|
21
|
-
writeFileSync: (path, body) => {
|
|
22
|
-
written.push({ path, body });
|
|
23
|
-
},
|
|
24
|
-
spawnSync: (cmd, args, opts) => {
|
|
25
|
-
calls.push({ cmd, args, opts });
|
|
26
|
-
return { status: 0 };
|
|
27
|
-
},
|
|
28
|
-
stdio: "pipe",
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(result.workDir).toBe("/tmp/decohere-create-smoke-abc");
|
|
32
|
-
expect(result.workDir).not.toBe(process.cwd());
|
|
33
|
-
expect(calls).toHaveLength(1);
|
|
34
|
-
expect(calls[0].cmd).toBe("npx");
|
|
35
|
-
expect(calls[0].args).toEqual(["--yes", "@decohere/create@0.0.1-dev.80", "--help"]);
|
|
36
|
-
expect(calls[0].opts.cwd).toBe("/tmp/decohere-create-smoke-abc");
|
|
37
|
-
expect(calls[0].opts.cwd).not.toBe(process.cwd());
|
|
38
|
-
expect(written).toEqual([
|
|
39
|
-
{
|
|
40
|
-
path: "/tmp/decohere-create-smoke-abc/.npmrc",
|
|
41
|
-
body: "@decohere:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=tok\n",
|
|
42
|
-
},
|
|
43
|
-
]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("fails closed without version or token", () => {
|
|
47
|
-
expect(() => runCreateLauncherSmoke({ createVersion: "", authToken: "tok" })).toThrow(
|
|
48
|
-
"CREATE_VERSION is required",
|
|
49
|
-
);
|
|
50
|
-
expect(() =>
|
|
51
|
-
runCreateLauncherSmoke({ createVersion: "0.0.1-dev.80", authToken: "" }),
|
|
52
|
-
).toThrow("NODE_AUTH_TOKEN is required");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("publish.yml create smoke + tag", () => {
|
|
57
|
-
const yml = fs.readFileSync(PUBLISH_YML, "utf8");
|
|
58
|
-
|
|
59
|
-
it("wires create smoke through the isolated launcher", () => {
|
|
60
|
-
expect(yml).toContain("node projects/ci-tools/src/github/smoke-create-launcher.mjs");
|
|
61
|
-
expect(yml).toContain("CREATE_VERSION:");
|
|
62
|
-
expect(yml).not.toContain("npx --yes");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("grants the tag job actions:read so publish-prepare can download CI artifacts", () => {
|
|
66
|
-
const tag = yml.slice(yml.indexOf("\n tag:"));
|
|
67
|
-
const perms = tag.slice(tag.indexOf("permissions:"), tag.indexOf("steps:"));
|
|
68
|
-
expect(perms).toMatch(/contents:\s*write/);
|
|
69
|
-
expect(perms).toMatch(/actions:\s*read/);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { pickIssue, upsertIssue } from "../../src/lib/github-issue.mjs";
|
|
3
|
-
|
|
4
|
-
const TITLE = "chore(security): rotate NODE_AUTH_TOKEN (expires 2026-11-19)";
|
|
5
|
-
|
|
6
|
-
/** A fake `gh` that answers `issue list` with `issues` and records every call. */
|
|
7
|
-
function fakeGh(issues) {
|
|
8
|
-
/** @type {{ args: string[], input?: string }[]} */
|
|
9
|
-
const calls = [];
|
|
10
|
-
const run = (args, input) => {
|
|
11
|
-
calls.push({ args, input });
|
|
12
|
-
if (args[1] === "list") return JSON.stringify(issues);
|
|
13
|
-
if (args[1] === "create") return "https://github.com/typesmiths/decohere/issues/42\n";
|
|
14
|
-
return "";
|
|
15
|
-
};
|
|
16
|
-
return { run, calls };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("github-issue", () => {
|
|
20
|
-
it("matches the exact title only and prefers an open issue", () => {
|
|
21
|
-
const issues = [
|
|
22
|
-
{ number: 1, title: TITLE, state: "CLOSED", url: "u1" },
|
|
23
|
-
{ number: 2, title: `${TITLE} (old)`, state: "OPEN", url: "u2" },
|
|
24
|
-
{ number: 3, title: TITLE, state: "OPEN", url: "u3" },
|
|
25
|
-
];
|
|
26
|
-
expect(pickIssue(issues, TITLE)?.number).toBe(3);
|
|
27
|
-
expect(pickIssue(issues.slice(0, 2), TITLE)?.number).toBe(1);
|
|
28
|
-
expect(pickIssue([], TITLE)).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// GitHub's search can't match `security(ci): …` unquoted, so the lookup found nothing
|
|
32
|
-
// and a new issue was opened on every run.
|
|
33
|
-
it("searches for the title as a quoted phrase", () => {
|
|
34
|
-
const { run, calls } = fakeGh([]);
|
|
35
|
-
upsertIssue({ title: "security(ci): npm audit vulnerabilities detected", body: "b" }, run);
|
|
36
|
-
const args = calls[0].args;
|
|
37
|
-
expect(args[args.indexOf("--search") + 1]).toBe(
|
|
38
|
-
'"security(ci): npm audit vulnerabilities detected" in:title',
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("opens an issue with assignees when none has the title", () => {
|
|
43
|
-
const { run, calls } = fakeGh([]);
|
|
44
|
-
const result = upsertIssue({ title: TITLE, body: "body", assignees: ["das974"] }, run);
|
|
45
|
-
expect(result).toEqual({
|
|
46
|
-
number: 42,
|
|
47
|
-
url: "https://github.com/typesmiths/decohere/issues/42",
|
|
48
|
-
created: true,
|
|
49
|
-
});
|
|
50
|
-
expect(calls[1]).toEqual({
|
|
51
|
-
args: [
|
|
52
|
-
"issue",
|
|
53
|
-
"create",
|
|
54
|
-
"--title",
|
|
55
|
-
TITLE,
|
|
56
|
-
"--body-file",
|
|
57
|
-
"-",
|
|
58
|
-
"--add-assignee",
|
|
59
|
-
"das974",
|
|
60
|
-
],
|
|
61
|
-
input: "body",
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("updates an open issue's body in place", () => {
|
|
66
|
-
const { run, calls } = fakeGh([{ number: 7, title: TITLE, state: "OPEN", url: "u7" }]);
|
|
67
|
-
const result = upsertIssue({ title: TITLE, body: "new body" }, run);
|
|
68
|
-
expect(result).toEqual({ number: 7, url: "u7", created: false });
|
|
69
|
-
expect(calls.map((call) => call.args[1])).toEqual(["list", "edit"]);
|
|
70
|
-
expect(calls[1]).toEqual({
|
|
71
|
-
args: ["issue", "edit", "7", "--body-file", "-"],
|
|
72
|
-
input: "new body",
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("reopens a closed issue before updating it", () => {
|
|
77
|
-
const { run, calls } = fakeGh([{ number: 7, title: TITLE, state: "CLOSED", url: "u7" }]);
|
|
78
|
-
upsertIssue({ title: TITLE, body: "b", repo: "typesmiths/decohere" }, run);
|
|
79
|
-
expect(calls.map((call) => call.args.slice(0, 3))).toEqual([
|
|
80
|
-
["issue", "list", "--repo"],
|
|
81
|
-
["issue", "reopen", "7"],
|
|
82
|
-
["issue", "edit", "7"],
|
|
83
|
-
]);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
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("leaves out machine types with no run in the 30 days before the newest run", () => {
|
|
85
|
-
const html = buildBenchmarkHtml({
|
|
86
|
-
version: 2,
|
|
87
|
-
contexts: {
|
|
88
|
-
...history.contexts,
|
|
89
|
-
gone: {
|
|
90
|
-
context: context("gh-actions-node24-retired", 2),
|
|
91
|
-
runs: [
|
|
92
|
-
run("2026-08-01T03:17:00Z", "dddddddddddd", [result("flux.serve.start", 70)]),
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
expect(html).not.toContain("gh-actions-node24-retired");
|
|
98
|
-
expect(html).toContain("(local)");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("escapes labels from the history", () => {
|
|
102
|
-
const html = buildBenchmarkHtml(history);
|
|
103
|
-
expect(html).toContain("spawn <to> ready");
|
|
104
|
-
expect(html).not.toContain("spawn <to> ready");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("trendSvg", () => {
|
|
109
|
-
it("places a single point without dividing by a zero time range", () => {
|
|
110
|
-
const svg = trendSvg([
|
|
111
|
-
{
|
|
112
|
-
color: "red",
|
|
113
|
-
points: [{ time: 1, date: "2026-10-20", sha: "abc", median: 5, p90: 6 }],
|
|
114
|
-
},
|
|
115
|
-
]);
|
|
116
|
-
expect(svg).toContain("<circle");
|
|
117
|
-
expect(svg).not.toContain("NaN");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("draws nothing when no machine has a point", () => {
|
|
121
|
-
expect(trendSvg([])).toBe("");
|
|
122
|
-
});
|
|
123
|
-
});
|