Skip to content

Commit c3774b6

Browse files
mo4islonaclaude
andcommitted
feat(pr-quality-gate): bundle Portal coverage tooling with the skill
Makes PR #7 self-contained: the coverage-guard step now ships with its tooling. - portal/vite.portal.config.ts: v8 coverage block scoped to the Portal source (inert unless --coverage; also converts the file to single quotes per Biome) - scripts/sync-upstream.sh: --coverage mode (installs the matching v8 provider, emits portal-coverage/coverage-summary.json) - scripts/coverage-diff.mjs: renders a base-vs-head Portal coverage table for a PR body (head-only fallback when base predates the tooling) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e5d29aa commit c3774b6

3 files changed

Lines changed: 146 additions & 6 deletions

File tree

portal/vite.portal.config.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
import path from "node:path";
2-
import { defineConfig } from "vitest/config";
1+
import path from 'node:path';
2+
import { defineConfig } from 'vitest/config';
33
// run the Portal-layer unit tests in isolation (no Foundry globalSetup): copied next to
44
// packages/core/ by scripts/sync-upstream.sh, where ./src points at the patched core.
55
export default defineConfig({
66
resolve: {
77
alias: {
8-
"@": path.resolve(__dirname, "./src"),
9-
"@ponder/utils": path.resolve(__dirname, "../utils/src"),
8+
'@': path.resolve(__dirname, './src'),
9+
'@ponder/utils': path.resolve(__dirname, '../utils/src'),
1010
},
1111
},
1212
test: {
1313
include: [
14-
"src/sync-historical/portal*.test.ts",
15-
"src/sync-historical/realtime*.test.ts",
14+
'src/sync-historical/portal*.test.ts',
15+
'src/sync-historical/realtime*.test.ts',
1616
],
1717
testTimeout: 15000,
18+
// Coverage is scoped to the Portal layer (the whole diff vs upstream ponder). Inert on a plain
19+
// `--test` run: it only activates under `--coverage` (see scripts/sync-upstream.sh --coverage),
20+
// so the normal path needs no coverage provider installed.
21+
coverage: {
22+
provider: 'v8',
23+
reporter: ['json-summary', 'text-summary'],
24+
reportsDirectory: 'portal-coverage',
25+
include: [
26+
'src/sync-historical/portal.ts',
27+
'src/sync-historical/portal-transform.ts',
28+
'src/sync-historical/portal-realtime.ts',
29+
'src/sync-historical/portal-realtime-wire.ts',
30+
'src/sync-historical/realtime.ts',
31+
],
32+
},
1833
},
1934
});

scripts/coverage-diff.mjs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env node
2+
// Render a Portal-layer coverage diff as a markdown table for a PR body.
3+
//
4+
// node scripts/coverage-diff.mjs <head-summary.json> # absolute table
5+
// node scripts/coverage-diff.mjs <base-summary.json> <head-summary.json> # diff vs base
6+
//
7+
// Inputs are vitest v8 `coverage-summary.json` files (produced by
8+
// `scripts/sync-upstream.sh <ver> --coverage`, at <core>/portal-coverage/coverage-summary.json).
9+
// Per-file keys are absolute paths inside the grafted tree; we key rows by basename so base and
10+
// head line up regardless of the temp workdir. With one argument (e.g. the base branch doesn't have
11+
// the coverage tooling yet) it prints head absolutes with no deltas.
12+
import { readFileSync } from 'node:fs';
13+
14+
const args = process.argv.slice(2);
15+
16+
if (args.length === 0 || args.length > 2) {
17+
console.error(
18+
'usage: node scripts/coverage-diff.mjs [<base-summary.json>] <head-summary.json>',
19+
);
20+
process.exit(2);
21+
}
22+
23+
const load = (p) => JSON.parse(readFileSync(p, 'utf8'));
24+
const headArg = args[args.length - 1];
25+
const baseArg = args.length === 2 ? args[0] : null;
26+
const head = load(headArg);
27+
const base = baseArg ? load(baseArg) : { total: null };
28+
29+
const pct = (entry, metric) => entry?.[metric]?.pct ?? null;
30+
const fmtPct = (v) => (v === null ? '—' : `${v.toFixed(1)}%`);
31+
const fmtDelta = (b, h) => {
32+
if (b === null || h === null) {
33+
return '';
34+
}
35+
36+
const d = h - b;
37+
38+
if (Math.abs(d) < 0.1) {
39+
return '±0';
40+
}
41+
42+
return `${d > 0 ? '+' : ''}${d.toFixed(1)}`;
43+
};
44+
45+
// Row keys: 'All files' for total, basename for each file (union of base + head).
46+
const fileKeys = new Map(); // basename -> { base, head }
47+
48+
const collect = (summary, side) => {
49+
for (const [k, entry] of Object.entries(summary)) {
50+
if (k === 'total') {
51+
continue;
52+
}
53+
54+
const name = k.split('/').pop();
55+
const row = fileKeys.get(name) ?? {};
56+
row[side] = entry;
57+
fileKeys.set(name, row);
58+
}
59+
};
60+
61+
collect(base, 'base');
62+
collect(head, 'head');
63+
64+
const rows = [];
65+
rows.push({
66+
label: '**All files**',
67+
base: base.total,
68+
head: head.total,
69+
changed: true,
70+
});
71+
72+
for (const [name, { base: b, head: h }] of [...fileKeys].sort()) {
73+
const sB = pct(b, 'statements');
74+
const sH = pct(h, 'statements');
75+
const changed =
76+
sB === null || sH === null || Math.abs((sH ?? 0) - (sB ?? 0)) >= 0.1;
77+
rows.push({ label: name, base: b, head: h, changed });
78+
}
79+
80+
const lines = [];
81+
lines.push('## Coverage (Portal layer)');
82+
lines.push('');
83+
lines.push('| File | Stmts | Δ | Branch | Δ | Funcs | Δ |');
84+
lines.push('|------|-------|---|--------|---|-------|---|');
85+
86+
for (const r of rows) {
87+
if (!r.changed) {
88+
continue;
89+
}
90+
91+
const sB = pct(r.base, 'statements');
92+
const sH = pct(r.head, 'statements');
93+
const bB = pct(r.base, 'branches');
94+
const bH = pct(r.head, 'branches');
95+
const fB = pct(r.base, 'functions');
96+
const fH = pct(r.head, 'functions');
97+
lines.push(
98+
`| ${r.label} | ${fmtPct(sH)} | ${fmtDelta(sB, sH)} | ${fmtPct(bH)} | ${fmtDelta(bB, bH)} | ${fmtPct(fH)} | ${fmtDelta(fB, fH)} |`,
99+
);
100+
}
101+
102+
if (base.total) {
103+
const totalDelta = head.total.statements.pct - base.total.statements.pct;
104+
105+
if (totalDelta < -0.1) {
106+
lines.push('');
107+
lines.push('⚠️ Overall statement coverage decreased.');
108+
}
109+
}
110+
111+
console.log(lines.join('\n'));

scripts/sync-upstream.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,19 @@ if [ "${2:-}" = "--test" ]; then
5555
( cd "$CORE" && pnpm exec vitest run --config vite.portal.config.ts )
5656
fi
5757

58+
# --coverage: run the same suite with v8 coverage scoped to the Portal source (see the `coverage`
59+
# block in vite.portal.config.ts). The provider must match the tree's vitest version; the renamed
60+
# core breaks a workspace-wide `pnpm add` (benchmark still deps ponder@workspace:*), so install it
61+
# standalone with --ignore-workspace. Writes portal-coverage/coverage-summary.json under $CORE.
62+
if [ "${2:-}" = "--coverage" ]; then
63+
VITEST_VER="$(node -e "const p=require('$CORE/package.json');console.log((p.devDependencies&&p.devDependencies.vitest)||(p.dependencies&&p.dependencies.vitest))")"
64+
echo "▶ installing @vitest/coverage-v8@$VITEST_VER (matching vitest)"
65+
( cd "$CORE" && $PNPM add -D "@vitest/coverage-v8@$VITEST_VER" --ignore-workspace )
66+
67+
echo "▶ running Portal-layer tests with coverage"
68+
( cd "$CORE" && pnpm exec vitest run --config vite.portal.config.ts --coverage )
69+
echo "✓ coverage summary → $CORE/portal-coverage/coverage-summary.json"
70+
fi
71+
5872
echo "✓ built @subsquid/ponder@$VER-sqd.$REV$CORE"
5973
echo " publish: cd $CORE && npm publish --access public --tag latest # --tag latest: make this prerelease the default install"

0 commit comments

Comments
 (0)