From 6e210cf084029c25d950aee77439b951aae11b42 Mon Sep 17 00:00:00 2001 From: Tabish Bidiwale Date: Sun, 7 Sep 2025 02:13:21 +1000 Subject: [PATCH 1/2] fix(test): ensure dist exists before spawning CLI subprocesses --- test/commands/change.interactive-show.test.ts | 3 +- .../change.interactive-validate.test.ts | 3 +- test/commands/show.test.ts | 3 +- test/commands/spec.interactive-show.test.ts | 3 +- .../spec.interactive-validate.test.ts | 3 +- test/commands/spec.test.ts | 3 +- .../commands/validate.enriched-output.test.ts | 3 +- test/commands/validate.test.ts | 3 +- test/helpers/ensure-build.ts | 34 +++++++++++++++++++ 9 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 test/helpers/ensure-build.ts diff --git a/test/commands/change.interactive-show.test.ts b/test/commands/change.interactive-show.test.ts index 34c6f259..9cb7f1c1 100644 --- a/test/commands/change.interactive-show.test.ts +++ b/test/commands/change.interactive-show.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; +import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,7 +11,7 @@ describe('change show (interactive behavior)', () => { const bin = path.join(projectRoot, 'bin', 'openspec.js'); beforeAll(() => { - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/commands/change.interactive-validate.test.ts b/test/commands/change.interactive-validate.test.ts index bd43367c..f6d9f1af 100644 --- a/test/commands/change.interactive-validate.test.ts +++ b/test/commands/change.interactive-validate.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; +import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -13,7 +14,7 @@ describe('change validate (interactive behavior)', () => { const bin = path.join(projectRoot, 'bin', 'openspec.js'); beforeAll(() => { - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/commands/show.test.ts b/test/commands/show.test.ts index c9a9b569..90d88bf9 100644 --- a/test/commands/show.test.ts +++ b/test/commands/show.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import { execSync } from 'child_process'; +import { ensureBuild } from '../helpers/ensure-build.js'; describe('top-level show command', () => { const projectRoot = process.cwd(); @@ -11,7 +12,7 @@ describe('top-level show command', () => { const openspecBin = path.join(projectRoot, 'bin', 'openspec.js'); beforeAll(() => { - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/commands/spec.interactive-show.test.ts b/test/commands/spec.interactive-show.test.ts index fd0be69d..60eb5ec2 100644 --- a/test/commands/spec.interactive-show.test.ts +++ b/test/commands/spec.interactive-show.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; +import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,7 +11,7 @@ describe('spec show (interactive behavior)', () => { const bin = path.join(projectRoot, 'bin', 'openspec.js'); beforeAll(() => { - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/commands/spec.interactive-validate.test.ts b/test/commands/spec.interactive-validate.test.ts index 70d3dc60..9ade30a6 100644 --- a/test/commands/spec.interactive-validate.test.ts +++ b/test/commands/spec.interactive-validate.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; +import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,7 +11,7 @@ describe('spec validate (interactive behavior)', () => { const bin = path.join(projectRoot, 'bin', 'openspec.js'); beforeAll(() => { - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/commands/spec.test.ts b/test/commands/spec.test.ts index 60802cf4..8dcfd6e8 100644 --- a/test/commands/spec.test.ts +++ b/test/commands/spec.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import { execSync } from 'child_process'; +import { ensureBuild } from '../helpers/ensure-build.js'; describe('spec command', () => { const projectRoot = process.cwd(); @@ -11,7 +12,7 @@ describe('spec command', () => { beforeAll(() => { // Ensure CLI is built so bin/openspec.js loads latest logic from dist/ - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/commands/validate.enriched-output.test.ts b/test/commands/validate.enriched-output.test.ts index 6e88ce80..12811416 100644 --- a/test/commands/validate.enriched-output.test.ts +++ b/test/commands/validate.enriched-output.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; +import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -11,7 +12,7 @@ describe('validate command enriched human output', () => { beforeAll(() => { // Build once so the bin can resolve dist - try { execSync('pnpm -s build', { stdio: 'pipe' }); } catch {} + try { ensureBuild(); } catch {} }); beforeEach(async () => { diff --git a/test/commands/validate.test.ts b/test/commands/validate.test.ts index 2f536844..de44f8d4 100644 --- a/test/commands/validate.test.ts +++ b/test/commands/validate.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; +import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -11,7 +12,7 @@ describe('top-level validate command', () => { const bin = path.join(projectRoot, 'bin', 'openspec.js'); beforeAll(() => { - execSync('pnpm -s build', { stdio: 'pipe' }); + ensureBuild(); }); beforeEach(async () => { diff --git a/test/helpers/ensure-build.ts b/test/helpers/ensure-build.ts new file mode 100644 index 00000000..b5247f74 --- /dev/null +++ b/test/helpers/ensure-build.ts @@ -0,0 +1,34 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import path from 'path'; + +/** + * Ensures the project is built before running tests that spawn CLI subprocesses. + * This is needed because tests that use execSync to run the CLI binary + * require the dist directory to exist. + */ +export function ensureBuild(): void { + const projectRoot = process.cwd(); + const distPath = path.join(projectRoot, 'dist'); + const cliPath = path.join(distPath, 'cli', 'index.js'); + + // Check if dist/cli/index.js exists + if (!existsSync(cliPath)) { + console.log('Building project for tests that spawn CLI subprocesses...'); + try { + // Run build without silent flag to see any errors + execSync('pnpm run build', { + stdio: 'inherit', + cwd: projectRoot + }); + + // Verify the build succeeded + if (!existsSync(cliPath)) { + throw new Error(`Build completed but ${cliPath} still does not exist`); + } + } catch (error) { + console.error('Failed to build project:', error); + throw new Error('Project build failed. Tests that spawn CLI subprocesses will fail.'); + } + } +} \ No newline at end of file From 57216a78240e16b4885eeae5d9ecc8cc802c071b Mon Sep 17 00:00:00 2001 From: Tabish Bidiwale Date: Sun, 7 Sep 2025 02:16:17 +1000 Subject: [PATCH 2/2] refactor(test): use vitest globalSetup for build instead of per-test builds --- test/commands/change.interactive-show.test.ts | 6 +--- .../change.interactive-validate.test.ts | 6 +--- test/commands/show.test.ts | 6 +--- test/commands/spec.interactive-show.test.ts | 6 +--- .../spec.interactive-validate.test.ts | 6 +--- test/commands/spec.test.ts | 7 +--- .../commands/validate.enriched-output.test.ts | 7 +--- test/commands/validate.test.ts | 6 +--- test/helpers/ensure-build.ts | 34 ------------------- vitest.config.ts | 1 + vitest.setup.ts | 21 ++++++++++++ 11 files changed, 30 insertions(+), 76 deletions(-) delete mode 100644 test/helpers/ensure-build.ts create mode 100644 vitest.setup.ts diff --git a/test/commands/change.interactive-show.test.ts b/test/commands/change.interactive-show.test.ts index 9cb7f1c1..b4dee52d 100644 --- a/test/commands/change.interactive-show.test.ts +++ b/test/commands/change.interactive-show.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; -import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,9 +9,6 @@ describe('change show (interactive behavior)', () => { const changesDir = path.join(testDir, 'openspec', 'changes'); const bin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(changesDir, { recursive: true }); diff --git a/test/commands/change.interactive-validate.test.ts b/test/commands/change.interactive-validate.test.ts index f6d9f1af..33484ab2 100644 --- a/test/commands/change.interactive-validate.test.ts +++ b/test/commands/change.interactive-validate.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; -import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -13,9 +12,6 @@ describe('change validate (interactive behavior)', () => { const changesDir = path.join(testDir, 'openspec', 'changes'); const bin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(changesDir, { recursive: true }); diff --git a/test/commands/show.test.ts b/test/commands/show.test.ts index 90d88bf9..67de310c 100644 --- a/test/commands/show.test.ts +++ b/test/commands/show.test.ts @@ -1,8 +1,7 @@ -import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import { execSync } from 'child_process'; -import { ensureBuild } from '../helpers/ensure-build.js'; describe('top-level show command', () => { const projectRoot = process.cwd(); @@ -11,9 +10,6 @@ describe('top-level show command', () => { const specsDir = path.join(testDir, 'openspec', 'specs'); const openspecBin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(changesDir, { recursive: true }); diff --git a/test/commands/spec.interactive-show.test.ts b/test/commands/spec.interactive-show.test.ts index 60eb5ec2..f41fdb63 100644 --- a/test/commands/spec.interactive-show.test.ts +++ b/test/commands/spec.interactive-show.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; -import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,9 +9,6 @@ describe('spec show (interactive behavior)', () => { const specsDir = path.join(testDir, 'openspec', 'specs'); const bin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(specsDir, { recursive: true }); diff --git a/test/commands/spec.interactive-validate.test.ts b/test/commands/spec.interactive-validate.test.ts index 9ade30a6..14949d6c 100644 --- a/test/commands/spec.interactive-validate.test.ts +++ b/test/commands/spec.interactive-validate.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; -import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,9 +9,6 @@ describe('spec validate (interactive behavior)', () => { const specsDir = path.join(testDir, 'openspec', 'specs'); const bin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(specsDir, { recursive: true }); diff --git a/test/commands/spec.test.ts b/test/commands/spec.test.ts index 8dcfd6e8..2a93d18f 100644 --- a/test/commands/spec.test.ts +++ b/test/commands/spec.test.ts @@ -1,8 +1,7 @@ -import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import { execSync } from 'child_process'; -import { ensureBuild } from '../helpers/ensure-build.js'; describe('spec command', () => { const projectRoot = process.cwd(); @@ -10,10 +9,6 @@ describe('spec command', () => { const specsDir = path.join(testDir, 'openspec', 'specs'); const openspecBin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - // Ensure CLI is built so bin/openspec.js loads latest logic from dist/ - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(specsDir, { recursive: true }); diff --git a/test/commands/validate.enriched-output.test.ts b/test/commands/validate.enriched-output.test.ts index 12811416..90b4d1b4 100644 --- a/test/commands/validate.enriched-output.test.ts +++ b/test/commands/validate.enriched-output.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; -import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -10,10 +9,6 @@ describe('validate command enriched human output', () => { const changesDir = path.join(testDir, 'openspec', 'changes'); const bin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - // Build once so the bin can resolve dist - try { ensureBuild(); } catch {} - }); beforeEach(async () => { await fs.mkdir(changesDir, { recursive: true }); diff --git a/test/commands/validate.test.ts b/test/commands/validate.test.ts index de44f8d4..5e4254c3 100644 --- a/test/commands/validate.test.ts +++ b/test/commands/validate.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { promises as fs } from 'fs'; -import { ensureBuild } from '../helpers/ensure-build.js'; import path from 'path'; import { execSync } from 'child_process'; @@ -11,9 +10,6 @@ describe('top-level validate command', () => { const specsDir = path.join(testDir, 'openspec', 'specs'); const bin = path.join(projectRoot, 'bin', 'openspec.js'); - beforeAll(() => { - ensureBuild(); - }); beforeEach(async () => { await fs.mkdir(changesDir, { recursive: true }); diff --git a/test/helpers/ensure-build.ts b/test/helpers/ensure-build.ts deleted file mode 100644 index b5247f74..00000000 --- a/test/helpers/ensure-build.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync } from 'fs'; -import path from 'path'; - -/** - * Ensures the project is built before running tests that spawn CLI subprocesses. - * This is needed because tests that use execSync to run the CLI binary - * require the dist directory to exist. - */ -export function ensureBuild(): void { - const projectRoot = process.cwd(); - const distPath = path.join(projectRoot, 'dist'); - const cliPath = path.join(distPath, 'cli', 'index.js'); - - // Check if dist/cli/index.js exists - if (!existsSync(cliPath)) { - console.log('Building project for tests that spawn CLI subprocesses...'); - try { - // Run build without silent flag to see any errors - execSync('pnpm run build', { - stdio: 'inherit', - cwd: projectRoot - }); - - // Verify the build succeeded - if (!existsSync(cliPath)) { - throw new Error(`Build completed but ${cliPath} still does not exist`); - } - } catch (error) { - console.error('Failed to build project:', error); - throw new Error('Project build failed. Tests that spawn CLI subprocesses will fail.'); - } - } -} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index 5e92c39b..2d6f6588 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { globals: true, environment: 'node', + globalSetup: './vitest.setup.ts', // Keep default pool settings; some tests rely on process.chdir, // which is not supported in worker threads include: ['test/**/*.test.ts'], diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 00000000..03bc27e7 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,21 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import path from 'path'; + +// Run once before all tests +export async function setup() { + const distPath = path.join(process.cwd(), 'dist', 'cli', 'index.js'); + + if (!existsSync(distPath)) { + console.log('Building project before tests...'); + try { + execSync('pnpm run build', { + stdio: 'inherit', + cwd: process.cwd() + }); + } catch (error) { + console.error('Failed to build project:', error); + process.exit(1); + } + } +} \ No newline at end of file