diff --git a/README.md b/README.md index 5a131860..a4b71e6a 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ ccs ollama "summarize these logs" ## Contribute And Report Safely - Contributing guide: [CONTRIBUTING.md](./CONTRIBUTING.md) -- Daily local gate: `bun run format && bun run lint:fix && bun run validate` +- Daily local gate: `bun run format && bun run lint:fix && bun run validate` (`validate` is the fast path only) - Before review or merge confidence: `bun run validate:ci-parity` - If PR checks stay queued for more than 10 minutes, assume the self-hosted runner is offline and notify a maintainer instead of retrying blindly - Starter work: diff --git a/scripts/ci-parity-gate.sh b/scripts/ci-parity-gate.sh index c9b31a53..36b66b28 100755 --- a/scripts/ci-parity-gate.sh +++ b/scripts/ci-parity-gate.sh @@ -56,6 +56,8 @@ if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then fi echo "[i] Running CI-parity local checks..." +# `set -euo pipefail` above makes every step fail fast. Keep these commands +# explicit so parity drift is visible when CI changes. bun run typecheck bun run lint bun run format:check diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index 0f3c0212..da2cdc68 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -4,9 +4,11 @@ const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); -const bucket = process.argv[2]; const rootDir = path.resolve(__dirname, '..'); const candidateRoots = ['tests/unit', 'tests/integration', 'tests/npm']; +// Keep this list in sync with any newly added dist-dependent or long-running +// tests. `tests/unit/scripts/run-test-bucket.test.js` verifies every path here +// exists so bucket drift fails loudly instead of silently slowing `test:fast`. const slowTests = [ 'tests/integration/cursor-daemon-lifecycle.test.ts', 'tests/integration/proxy/daemon-lifecycle.test.ts', @@ -23,11 +25,6 @@ const slowTests = [ 'tests/unit/web-server/websearch-routes.test.ts', ]; -if (!['fast', 'slow', 'all'].includes(bucket)) { - console.error('[X] Usage: node scripts/run-test-bucket.js '); - process.exit(1); -} - const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/; function collectFiles(dir, files = []) { @@ -51,19 +48,30 @@ function readsBuiltDist(relativePath) { return source.includes('dist/'); } -const discovered = candidateRoots - .flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir))) - .sort(); -const forceSlow = discovered.filter((file) => { +function getDiscoveredTests() { + return candidateRoots + .flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir))) + .sort(); +} + +function shouldForceSlow(file) { if (file.startsWith('tests/npm/')) { return true; } return readsBuiltDist(file); -}); -const slowSet = new Set([...slowTests, ...forceSlow]); +} + +function getSlowSet() { + const discovered = getDiscoveredTests(); + const forceSlow = discovered.filter((file) => shouldForceSlow(file)); + return new Set([...slowTests, ...forceSlow]); +} function selectBucket(name) { + const discovered = getDiscoveredTests(); + const slowSet = getSlowSet(); + return name === 'slow' ? [...slowSet].sort() : discovered.filter((file) => !slowSet.has(file)); @@ -111,17 +119,40 @@ function runBucket(name) { return result.status ?? 1; } -if (bucket === 'all') { - let exitCode = 0; +function main(args = process.argv.slice(2)) { + const bucket = args[0]; - for (const name of ['fast', 'slow']) { - const status = runBucket(name); - if (status !== 0) { - exitCode = status; - } + if (!['fast', 'slow', 'all'].includes(bucket)) { + console.error('[X] Usage: node scripts/run-test-bucket.js '); + return 1; } - process.exit(exitCode); + if (bucket === 'all') { + let exitCode = 0; + + for (const name of ['fast', 'slow']) { + const status = runBucket(name); + if (status !== 0) { + exitCode = status; + } + } + + return exitCode; + } + + return runBucket(bucket); } -process.exit(runBucket(bucket)); +if (require.main === module) { + process.exit(main()); +} + +module.exports = { + slowTests, + readsBuiltDist, + shouldForceSlow, + getDiscoveredTests, + getSlowSet, + selectBucket, + main, +}; diff --git a/tests/unit/scripts/run-test-bucket.test.js b/tests/unit/scripts/run-test-bucket.test.js new file mode 100644 index 00000000..b4a1213d --- /dev/null +++ b/tests/unit/scripts/run-test-bucket.test.js @@ -0,0 +1,24 @@ +const { describe, expect, test } = require('bun:test'); +const path = require('node:path'); +const bucket = require('../../../scripts/run-test-bucket.js'); + +describe('run-test-bucket', () => { + test('all declared slow tests still exist on disk', () => { + for (const relativePath of bucket.slowTests) { + const absolutePath = path.resolve(__dirname, '../../../', relativePath); + expect(Bun.file(absolutePath).exists()).resolves.toBe(true); + } + }); + + test('forces npm tests into the slow bucket', () => { + expect(bucket.shouldForceSlow('tests/npm/cli.test.js')).toBe(true); + }); + + test('keeps dist-independent javascript tests in the fast bucket', () => { + expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false); + }); + + test('still forces dist-dependent tests into the slow bucket', () => { + expect(bucket.shouldForceSlow('tests/unit/config-dir-override.test.js')).toBe(true); + }); +});