From 346aac09c435dd1c31823395e22983b71e2ff855 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sat, 14 Nov 2026 01:42:03 -0300 Subject: [PATCH] ci: unit test coverage in the job summary, held to a floor vitest run --coverage writes coverage/ on every task test, and fails below the statements floor in the vitest config, a little under today's number. The Gate job writes the totals to its summary and keeps the HTML as coverage-web for 7 days, even when a test fails. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 10 ++++ .gitignore | 1 + Taskfile.yml | 7 ++- hack/coverage-summary.mjs | 24 +++++++++ package.json | 3 +- pnpm-lock.yaml | 108 +++++++++++++++++++++++++++++++++++++- vitest.config.ts | 13 +++++ 7 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 hack/coverage-summary.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2119bd..3688fae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,3 +42,13 @@ jobs: with: go-version-file: go.mod - run: go tool task ci + # The coverage task test wrote, even when a test failed: the summary says so when there is none. + - if: always() + run: go tool task coverage:summary + - if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage-web + path: coverage + retention-days: 7 + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 5fa7dde..30bb193 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /.vscode-test/ /*.vsix /.wt/ +/coverage/ diff --git a/Taskfile.yml b/Taskfile.yml index 9265c4b..ec8eff8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -32,10 +32,15 @@ tasks: - pnpm typecheck test: - desc: The unit tests - Vitest, with vscode replaced by test/mocks/vscode.ts. + desc: The unit tests - Vitest, with vscode replaced by test/mocks/vscode.ts, and coverage held to the floor in vitest.config.ts. cmds: - pnpm test + coverage:summary: + desc: The coverage task test left in coverage/, as the job's summary (hack/coverage-summary.mjs); the HTML is coverage/index.html. + cmds: + - node hack/coverage-summary.mjs + test:e2e: desc: >- The Spinbox resolver against spin's e2e stack and the real spin CLI (test/e2e/). Bring the diff --git a/hack/coverage-summary.mjs b/hack/coverage-summary.mjs new file mode 100644 index 0000000..583a2d2 --- /dev/null +++ b/hack/coverage-summary.mjs @@ -0,0 +1,24 @@ +// The unit tests' coverage, from what `vitest run --coverage` writes into coverage/, as the job's +// summary (stdout outside a workflow). The HTML beside it is the coverage-web artifact. +import { appendFileSync, existsSync, readFileSync } from 'node:fs'; + +const file = 'coverage/coverage-summary.json'; +const write = (text) => { + const summary = process.env.GITHUB_STEP_SUMMARY; + if (summary) { + appendFileSync(summary, `${text}\n`); + } else { + console.log(text); + } +}; + +if (!existsSync(file)) { + write(`${file} was not produced; the tests may have failed before coverage ran.`); +} else { + const { total } = JSON.parse(readFileSync(file, 'utf8')); + const rows = ['statements', 'branches', 'functions', 'lines'].map((key) => { + const { covered, total: all, pct } = total[key]; + return `| ${key} | ${covered} | ${all} | ${pct}% |`; + }); + write(['## Unit test coverage', '', '| Metric | Covered | Total | % |', '|---|---:|---:|---:|', ...rows].join('\n')); +} diff --git a/package.json b/package.json index 82a6f78..c884239 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --write --error-on-warnings .", "typecheck": "tsc --noEmit", - "test": "vitest run", + "test": "vitest run --coverage", "test:e2e": "vitest run --config vitest.e2e.config.mts", "package": "vsce package --no-dependencies --out spinbox-remote.vsix" }, @@ -31,6 +31,7 @@ "@biomejs/biome": "2.6.3", "@types/node": "22.20.5", "@types/vscode": "1.146.0", + "@vitest/coverage-v8": "5.0.5", "@vscode/vsce": "4.1.0", "esbuild": "0.28.2", "typescript": "7.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8c1f36..2e3e3a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@types/vscode': specifier: 1.146.0 version: 1.146.0 + '@vitest/coverage-v8': + specifier: 5.0.5 + version: 5.0.5(vitest@5.0.5) '@vscode/vsce': specifier: 4.1.0 version: 4.1.0 @@ -31,7 +34,7 @@ importers: version: 7.0.2 vitest: specifier: 5.0.5 - version: 5.0.5(@types/node@22.20.5)(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2)) + version: 5.0.5(@types/node@22.20.5)(@vitest/coverage-v8@5.0.5)(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2)) packages: @@ -83,6 +86,27 @@ packages: resolution: {integrity: sha512-6mntsfCNq7H1eAPSZik74jT3C5at4nRqiVk7OC+D5Hy3Pe3s76uwE4+J0xvDvlVPmoX2kasf3mSwUmzeq9rW/A==} engines: {node: '>=20'} + '@babel/helper-string-parser@7.29.14': + resolution: {integrity: sha512-CXvKMFvM80ihI7Vox9jb+kQjnyjESQNPM+vB7U0Zz6TWuZA+Z/FL0QPMH300h33qJtIx+REeBhXqWOPW48TcAQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.14': + resolution: {integrity: sha512-VL220vXuyO2z3rNIQwY7ftj9UwpqOt+33sH+sBWkOekbsSR2g7EEMnOx24ZlCH0xOA3DM/33SifVFGbnJD8r/w==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.14': + resolution: {integrity: sha512-UjI/dKmwJwT9pOszJitq2G6EObZPnsZUfdCr92z2TlF3EZK9J3EdfOKUrCzHl6wFthbzwhzl1Soc/UIwxRiq6w==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@biomejs/biome@2.6.3': resolution: {integrity: sha512-va/0xPku7wnfzv+D8Zpdm5uKiGJWiV8neqoEPnRjvNQQwhl4tVXS4fETs4H0CCB2rGeqJ4cu9y4iOZOnOYKAUA==} engines: {node: '>=14.21.3'} @@ -690,6 +714,23 @@ packages: resolution: {integrity: sha512-vddvvtZ7sejSlCv+OIkn2ztjB1O31Fc2JsxI/YK725r3elXXlF/J3o3lbXJFRhoeWRWK1tjdZ+GTi6Fdtrp+EA==} engines: {node: '>=22.0.0'} + '@vitest/coverage-v8@5.0.5': + resolution: {integrity: sha512-4KpkGdIrSqBihx/JyP/e+S+IKB98etg1XDQiLAxksLfcShnwi1VReGeOT9NI43hLHbknosKp/1CCLn9Zgcs8mg==} + peerDependencies: + '@vitest/browser': 5.0.5 + vitest: 5.0.5 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/istanbul-lib-coverage@1.0.2': + resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==} + engines: {node: '>=22'} + + '@vitest/istanbul-lib-report@1.0.2': + resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==} + engines: {node: '>=22'} + '@vitest/mocker@5.0.5': resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} peerDependencies: @@ -765,6 +806,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@1.0.8: + resolution: {integrity: sha512-lPdpagk13ANXV+HClJEpCZM7kUsl5XcvgqKzZhaj6Ve1ZFICcY2sGhAN1n1vT0tML6EclT7bfVgTxqL57Gkpow==} + azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} @@ -954,6 +998,9 @@ packages: resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} engines: {node: '>=4'} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -1069,6 +1116,9 @@ packages: magic-string@1.4.5: resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + marked@18.2.0: resolution: {integrity: sha512-KaeL+tAbtvI930q7HE2PN5lYXYdT8EiSmxHlN6F3rsfb57wd5mp5W5TLazpdj4inTQBJdgjYwbs40W/0VUGU+Q==} engines: {node: '>= 20'} @@ -1221,6 +1271,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} + tslib@2.9.1: resolution: {integrity: sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==} @@ -1453,6 +1507,21 @@ snapshots: '@azure/msal-common': 16.16.1 jsonwebtoken: 9.0.3 + '@babel/helper-string-parser@7.29.14': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.14': + dependencies: + '@babel/types': 7.29.14 + + '@babel/types@7.29.14': + dependencies: + '@babel/helper-string-parser': 7.29.14 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + '@biomejs/biome@2.6.3': optionalDependencies: '@biomejs/cli-android-arm64': 2.6.3 @@ -1828,6 +1897,24 @@ snapshots: - kerberos - supports-color + '@vitest/coverage-v8@5.0.5(vitest@5.0.5)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/istanbul-lib-coverage': 1.0.2 + '@vitest/istanbul-lib-report': 1.0.2 + ast-v8-to-istanbul: 1.0.8 + magicast: 0.5.5 + obug: 3.0.0 + std-env: 4.3.0 + tinyrainbow: 3.2.0 + vitest: 5.0.5(@types/node@22.20.5)(@vitest/coverage-v8@5.0.5)(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2)) + + '@vitest/istanbul-lib-coverage@1.0.2': {} + + '@vitest/istanbul-lib-report@1.0.2': + dependencies: + '@vitest/istanbul-lib-coverage': 1.0.2 + '@vitest/mocker@5.0.5(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2))': dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -1914,6 +2001,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@1.0.8: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + azure-devops-node-api@12.5.0: dependencies: tunnel: 0.0.6 @@ -2108,6 +2201,8 @@ snapshots: editions: 6.22.0 textextensions: 6.11.0 + js-tokens@10.0.0: {} + jsonc-parser@3.3.1: {} jsonwebtoken@9.0.3: @@ -2205,6 +2300,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.14 + '@babel/types': 7.29.14 + source-map-js: 1.2.2 + marked@18.2.0: {} math-intrinsics@1.1.0: {} @@ -2353,6 +2454,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinyrainbow@3.2.0: {} + tslib@2.9.1: {} tunnel@0.0.6: {} @@ -2406,7 +2509,7 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 - vitest@5.0.5(@types/node@22.20.5)(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2)): + vitest@5.0.5(@types/node@22.20.5)(@vitest/coverage-v8@5.0.5)(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2)): dependencies: '@types/chai': 5.2.3 '@vitest/mocker': 5.0.5(vite@8.3.8(@types/node@22.20.5)(esbuild@0.28.2)) @@ -2424,6 +2527,7 @@ snapshots: why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 22.20.5 + '@vitest/coverage-v8': 5.0.5(vitest@5.0.5) transitivePeerDependencies: - msw diff --git a/vitest.config.ts b/vitest.config.ts index ad08ba4..495b2b1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,19 @@ export default defineConfig({ // test:e2e's, which need spin's e2e stack. exclude: ['test/e2e/**'], environment: 'node', + coverage: { + provider: 'v8', + // Every file under src/, so one no unit test imports counts as uncovered rather than + // absent. + include: ['src/**'], + // json-summary is what CI's job summary reads (hack/coverage-summary.mjs); + // reportOnFailure keeps a red suite from leaving it nothing to read. + reporter: ['text', 'json-summary', 'html'], + reportOnFailure: true, + // A floor below the 92.0% of statements measured on 2026-11-14. Raise it when the + // number has moved up and stayed; never lower it to let a change through. + thresholds: { statements: 90 }, + }, }, resolve: { alias: {