Skip to content

Commit 73d99b2

Browse files
authored
ci(test): fail integration run when all tests are skipped (#63)
* ci(test): fail integration run when all tests are skipped Adds a custom vitest reporter that exits with code 1 in CI when all tests in a module are skipped (0 passed, 0 failed). Catches the silent-green scenario where secrets are accidentally removed. Local dev is unaffected — reporter only activates when CI=true. Refs: TOO-117 * ci(test): harden reporter against silent failure edge cases Wrap onTestRunEnd in try-catch (vitest swallows reporter exceptions), null-check t.result(), and guard against zero collected modules and unhandled errors. Refs: TOO-117 * fix(test): use optional chain for t.result() in reporter Simplifies null check per Greptile review feedback. Refs: TOO-117
1 parent 97370be commit 73d99b2

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { SerializedError } from 'vitest';
2+
import type { Reporter, TestModule, TestRunEndReason } from 'vitest/node';
3+
4+
/**
5+
* Custom vitest reporter that fails CI when all tests in a module are skipped.
6+
*
7+
* This catches the scenario where CI secrets are accidentally removed —
8+
* integration tests silently skip via `it.skipIf()` and vitest reports
9+
* a green build with "N skipped, 0 failed".
10+
*
11+
* Only active when CI=true. Individual skips (e.g. Fireblocks RAW mode)
12+
* are allowed as long as at least one test in the module executes.
13+
*
14+
* Note: vitest swallows reporter exceptions (try-catch internally), so
15+
* this method wraps all logic in try-catch to ensure any unexpected error
16+
* still results in a non-zero exit.
17+
*/
18+
export default class NoAllSkippedReporter implements Reporter {
19+
onTestRunEnd(
20+
testModules: ReadonlyArray<TestModule>,
21+
unhandledErrors: ReadonlyArray<SerializedError>,
22+
_reason: TestRunEndReason,
23+
) {
24+
if (!process.env.CI) return;
25+
26+
try {
27+
if (unhandledErrors.length > 0) {
28+
console.error(`\nCI FAILURE: ${unhandledErrors.length} unhandled error(s) during test run.\n`);
29+
process.exitCode = 1;
30+
process.exit(1);
31+
}
32+
33+
if (testModules.length === 0) {
34+
console.error(
35+
'\nCI FAILURE: No test modules were collected.\n' +
36+
'This may indicate a configuration or import error.\n',
37+
);
38+
process.exitCode = 1;
39+
process.exit(1);
40+
}
41+
42+
const allSkippedModules: string[] = [];
43+
44+
for (const mod of testModules) {
45+
const allTests = [...mod.children.allTests()];
46+
if (allTests.length === 0) continue;
47+
48+
const hasExecuted = allTests.some(t => {
49+
const state = t.result()?.state;
50+
return state === 'passed' || state === 'failed';
51+
});
52+
53+
if (!hasExecuted) {
54+
allSkippedModules.push(mod.moduleId);
55+
}
56+
}
57+
58+
if (allSkippedModules.length > 0) {
59+
const filenames = allSkippedModules.map(f => ` - ${f}`).join('\n');
60+
console.error(
61+
`\nCI FAILURE: All tests were skipped in:\n${filenames}\n` +
62+
'Required secrets may be missing from CI configuration.\n',
63+
);
64+
process.exitCode = 1;
65+
process.exit(1);
66+
}
67+
} catch (err) {
68+
console.error('\nCI FAILURE: NoAllSkippedReporter encountered an unexpected error:\n', err, '\n');
69+
process.exitCode = 1;
70+
process.exit(1);
71+
}
72+
}
73+
}

typescript/vitest.config.integration.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { defineConfig } from 'vitest/config';
22

3+
import NoAllSkippedReporter from './packages/test-utils/src/no-all-skipped-reporter.js';
4+
35
export default defineConfig({
46
test: {
57
globals: true,
68
environment: 'node',
79
include: ['**/src/**/*.integration.test.ts'],
810
exclude: ['**/node_modules/**', '**/dist/**'],
911
testTimeout: 30000, // 30 second timeout for integration tests
10-
fileParallelism: false, // Disable for CI
11-
maxWorkers: 1, // Disable for CI
12+
fileParallelism: false, // Disable for CI
13+
maxWorkers: 1, // Disable for CI
14+
reporters: process.env.CI ? ['default', new NoAllSkippedReporter()] : ['default'],
1215
coverage: {
1316
provider: 'v8',
1417
reporter: ['text', 'json', 'html'],

0 commit comments

Comments
 (0)