diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..15917b1 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,40 @@ +name: E2E Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + e2e-tests: + name: E2E Tests (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: ['18', '20', '22'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install + + - name: Build project + run: pnpm build + + - name: Run E2E tests + run: pnpm test:e2e diff --git a/package.json b/package.json index 01f9e58..f31cdd3 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,17 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "test": "vitest run", - "test:watch": "vitest --watch", - "test:coverage": "vitest run --coverage", + "test": "vitest run --config vitest.config.ts --dir tests/unit", + "test:unit": "vitest run --config vitest.config.ts --dir tests/unit", + "test:e2e": "vitest run --config vitest.config.e2e.ts", + "test:watch": "vitest --watch --config vitest.config.ts --dir tests/unit", + "test:coverage": "vitest run --coverage --config vitest.config.ts --dir tests/unit", + "test:all": "npm run test:unit && npm run test:e2e", "lint": "eslint src --ext .ts", "lint:fix": "eslint src --ext .ts --fix", "typecheck": "tsc --noEmit", "clean": "rm -rf dist", - "prepublishOnly": "npm run clean && npm run build && npm test", + "prepublishOnly": "npm run clean && npm run build && npm run test:unit", "start": "node bin/capiscio.js" }, "dependencies": { diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..c61332e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,116 @@ +# Tests for capiscio-node CLI + +This directory contains unit and E2E tests for the `capiscio` CLI wrapper. + +## Directory Structure + +``` +tests/ +├── unit/ # Unit tests with mocks (no server required) +│ └── cli.test.ts +└── e2e/ # E2E tests (offline mode, no server required) + ├── fixtures/ # Test data files + │ ├── valid-agent-card.json + │ ├── invalid-agent-card.json + │ └── malformed.json + ├── validate.e2e.test.ts # Validation command tests + └── badge.e2e.test.ts # Badge issuance/verification tests +``` + +## Running Tests + +### Run All Tests + +```bash +pnpm test # Unit tests only (default) +pnpm test:all # Both unit and E2E tests +``` + +### Run Only Unit Tests + +```bash +pnpm test:unit +``` + +### Run Only E2E Tests + +```bash +pnpm test:e2e +``` + +### Run with Watch Mode + +```bash +pnpm test:watch +``` + +### Run with Coverage + +```bash +pnpm test:coverage +``` + +## E2E Test Design + +The E2E tests are designed to run **offline** without requiring a server: + +- **Validate tests**: Use `--schema-only` flag for local schema validation +- **Badge tests**: Use `--self-sign` for issuance and `--accept-self-signed --offline` for verification + +This approach allows E2E tests to run in CI without complex server infrastructure. + +## Test Coverage + +### Validate Command (`validate.e2e.test.ts`) + +- ✅ Valid local agent card file (schema-only mode) +- ✅ Invalid local agent card file +- ✅ Malformed JSON file +- ✅ Nonexistent file +- ✅ JSON output format +- ✅ Help command + +### Badge Commands (`badge.e2e.test.ts`) + +- ✅ Issue self-signed badge +- ✅ Issue badge with custom expiration +- ✅ Issue badge with audience restriction +- ✅ Verify self-signed badge (offline) +- ✅ Verify invalid token (error handling) +- ✅ Help commands (badge, issue, verify) + +## CI/CD Integration + +The E2E tests run in GitHub Actions without server dependencies: + +```yaml +# See .github/workflows/e2e.yml +- name: Run E2E tests + run: pnpm test:e2e +``` + +## Notes + +- **Offline Mode**: All E2E tests run offline without server dependencies +- **Timeouts**: Tests have 15-second timeouts to prevent hanging +- **Download Messages**: On first run, the CLI may download the capiscio-core binary; tests handle this gracefully + +## Troubleshooting + +### TypeScript Build Errors + +Ensure the project is built before running E2E tests: + +```bash +pnpm build +pnpm test:e2e +``` + +### Path Issues + +Ensure you're running tests from the project root: + +```bash +cd /path/to/capiscio-node +pnpm test:e2e +``` diff --git a/tests/e2e/badge.e2e.test.ts b/tests/e2e/badge.e2e.test.ts new file mode 100644 index 0000000..a45c52e --- /dev/null +++ b/tests/e2e/badge.e2e.test.ts @@ -0,0 +1,114 @@ +/** + * E2E tests for capiscio badge commands. + * + * Tests badge issuance and verification commands against the CLI. + * These tests focus on the CLI interface itself, not the server. + */ + +import { describe, it, expect } from 'vitest'; +import { runCapiscio, extractToken } from './helpers'; + +describe('badge commands', () => { + describe('badge issue', () => { + it('should issue a self-signed badge', async () => { + const result = await runCapiscio([ + 'badge', 'issue', '--self-sign', '--domain', 'test.example.com' + ]); + + // Self-signed badge issuance should succeed + expect(result.exitCode).toBe(0); + + // Output should contain a JWT token (has dots for header.payload.signature) + // Get last line in case there are download messages + const token = extractToken(result.stdout); + expect(token.split('.').length).toBe(3); // JWT format + }, 15000); + + it('should issue badge with custom expiration', async () => { + const result = await runCapiscio([ + 'badge', 'issue', '--self-sign', '--exp', '10m' + ]); + + expect(result.exitCode).toBe(0); + const token = extractToken(result.stdout); + expect(token.split('.').length).toBe(3); + }, 15000); + + it('should issue badge with audience restriction', async () => { + const result = await runCapiscio([ + 'badge', 'issue', '--self-sign', '--aud', 'https://api.example.com' + ]); + + expect(result.exitCode).toBe(0); + const token = extractToken(result.stdout); + expect(token.split('.').length).toBe(3); + }, 15000); + + it('should display help for badge issue', async () => { + const result = await runCapiscio(['badge', 'issue', '--help']); + + expect(result.exitCode).toBe(0); + const helpText = result.stdout.toLowerCase(); + expect(helpText.includes('issue')).toBe(true); + expect(helpText.includes('self-sign') || helpText.includes('level')).toBe(true); + }, 15000); + }); + + describe('badge verify', () => { + it('should verify a self-signed badge', async () => { + // First issue a badge + const issueResult = await runCapiscio([ + 'badge', 'issue', '--self-sign', '--domain', 'test.example.com' + ]); + expect(issueResult.exitCode).toBe(0); + const token = extractToken(issueResult.stdout); + + // Then verify it with --accept-self-signed + const verifyResult = await runCapiscio([ + 'badge', 'verify', token, '--accept-self-signed', '--offline' + ]); + + expect(verifyResult.exitCode).toBe(0); + const output = verifyResult.stdout.toLowerCase(); + expect( + output.includes('valid') || output.includes('verified') || output.includes('ok') + ).toBe(true); + }, 15000); + + it('should fail for invalid token', async () => { + const invalidToken = 'invalid.jwt.token'; + const result = await runCapiscio(['badge', 'verify', invalidToken, '--accept-self-signed']); + + expect(result.exitCode).not.toBe(0); + const errorOutput = (result.stderr + result.stdout).toLowerCase(); + expect( + errorOutput.includes('invalid') || + errorOutput.includes('verify') || + errorOutput.includes('failed') || + errorOutput.includes('malformed') || + errorOutput.includes('error') + ).toBe(true); + }, 15000); + + it('should display help for badge verify', async () => { + const result = await runCapiscio(['badge', 'verify', '--help']); + + expect(result.exitCode).toBe(0); + const helpText = result.stdout.toLowerCase(); + expect(helpText.includes('verify') || helpText.includes('token')).toBe(true); + }, 15000); + }); + + describe('badge help', () => { + it('should display help for badge command', async () => { + const result = await runCapiscio(['badge', '--help']); + + expect(result.exitCode).toBe(0); + const helpText = result.stdout.toLowerCase(); + expect(helpText.includes('badge')).toBe(true); + expect( + helpText.includes('issue') || helpText.includes('verify') || helpText.includes('usage') + ).toBe(true); + }, 15000); + }); +}); diff --git a/tests/e2e/fixtures/invalid-agent-card.json b/tests/e2e/fixtures/invalid-agent-card.json new file mode 100644 index 0000000..d057737 --- /dev/null +++ b/tests/e2e/fixtures/invalid-agent-card.json @@ -0,0 +1,9 @@ +{ + "version": "1.0", + "did": "invalid-did-format", + "name": "Invalid Agent", + "publicKey": { + "kty": "WRONG", + "x": "invalid-key-data" + } +} diff --git a/tests/e2e/fixtures/malformed.txt b/tests/e2e/fixtures/malformed.txt new file mode 100644 index 0000000..f731ba1 --- /dev/null +++ b/tests/e2e/fixtures/malformed.txt @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "did": "did:web:example.com", + "trailing": "comma", +} diff --git a/tests/e2e/fixtures/valid-agent-card.json b/tests/e2e/fixtures/valid-agent-card.json new file mode 100644 index 0000000..cef05cd --- /dev/null +++ b/tests/e2e/fixtures/valid-agent-card.json @@ -0,0 +1,26 @@ +{ + "protocolVersion": "1.3.0", + "version": "1.0.0", + "name": "Test Agent", + "description": "A test agent for E2E validation tests", + "url": "https://example.com/.well-known/agent.json", + "capabilities": { + "streaming": false, + "pushNotifications": false + }, + "skills": [ + { + "id": "test-skill", + "name": "Test Skill", + "description": "A skill for testing", + "tags": ["test", "validation"] + } + ], + "provider": { + "organization": "Test Organization", + "url": "https://example.com" + }, + "authentication": { + "schemes": ["none"] + } +} diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts new file mode 100644 index 0000000..99b811d --- /dev/null +++ b/tests/e2e/helpers.ts @@ -0,0 +1,43 @@ +/** + * Shared test helpers for E2E tests. + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; +import path from 'path'; + +const execAsync = promisify(exec); + +export const CLI_PATH = path.join(__dirname, '../../bin/capiscio.js'); +export const FIXTURES_DIR = path.join(__dirname, 'fixtures'); + +/** + * Run the capiscio CLI with the given arguments. + */ +export async function runCapiscio( + args: string[], + env?: Record +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + try { + const { stdout, stderr } = await execAsync(`node "${CLI_PATH}" ${args.join(' ')}`, { + env: { ...process.env, ...env }, + }); + return { stdout, stderr, exitCode: 0 }; + } catch (error: unknown) { + const err = error as { stdout?: string; stderr?: string; code?: number }; + return { + stdout: err.stdout || '', + stderr: err.stderr || '', + exitCode: err.code || 1, + }; + } +} + +/** + * Extract token from CLI output - handles potential download messages on first run. + * The CLI may print download progress before the actual output. + */ +export function extractToken(stdout: string): string { + const lines = stdout.trim().split('\n'); + return lines[lines.length - 1].trim(); +} diff --git a/tests/e2e/validate.e2e.test.ts b/tests/e2e/validate.e2e.test.ts new file mode 100644 index 0000000..8f5e0aa --- /dev/null +++ b/tests/e2e/validate.e2e.test.ts @@ -0,0 +1,83 @@ +/** + * E2E tests for capiscio validate command. + * + * Tests the validate command locally, validating agent cards + * from local files. Uses --schema-only for offline validation. + */ + +import { describe, it, expect } from 'vitest'; +import path from 'path'; +import { runCapiscio, FIXTURES_DIR } from './helpers'; + +describe('validate command', () => { + const validCardPath = path.join(FIXTURES_DIR, 'valid-agent-card.json'); + const invalidCardPath = path.join(FIXTURES_DIR, 'invalid-agent-card.json'); + const malformedPath = path.join(FIXTURES_DIR, 'malformed.txt'); + const nonexistentPath = path.join(FIXTURES_DIR, 'does-not-exist.json'); + + it('should validate a valid local agent card file', async () => { + const result = await runCapiscio(['validate', validCardPath, '--schema-only']); + + expect(result.exitCode).toBe(0); + const output = result.stdout.toLowerCase(); + expect( + output.includes('pass') || output.includes('valid') || output.includes('✅') + ).toBe(true); + }, 15000); + + it('should fail for an invalid local agent card file', async () => { + const result = await runCapiscio(['validate', invalidCardPath, '--schema-only']); + + expect(result.exitCode).not.toBe(0); + const errorOutput = (result.stderr + result.stdout).toLowerCase(); + expect( + errorOutput.includes('fail') || errorOutput.includes('error') || errorOutput.includes('❌') + ).toBe(true); + }, 15000); + + it('should fail for malformed JSON', async () => { + const result = await runCapiscio(['validate', malformedPath, '--schema-only']); + + expect(result.exitCode).not.toBe(0); + const errorOutput = (result.stderr + result.stdout).toLowerCase(); + expect( + errorOutput.includes('json') || errorOutput.includes('parse') || errorOutput.includes('invalid') || errorOutput.includes('syntax') + ).toBe(true); + }, 15000); + + it('should fail for nonexistent file', async () => { + const result = await runCapiscio(['validate', nonexistentPath, '--schema-only']); + + expect(result.exitCode).not.toBe(0); + const errorOutput = (result.stderr + result.stdout).toLowerCase(); + expect( + errorOutput.includes('not found') || + errorOutput.includes('no such file') || + errorOutput.includes('does not exist') || + errorOutput.includes('failed to load') || + errorOutput.includes('error') + ).toBe(true); + }, 15000); + + it('should support JSON output format', async () => { + const result = await runCapiscio(['validate', validCardPath, '--schema-only', '--json']); + + expect(result.exitCode).toBe(0); + + // Verify output is valid JSON + expect(() => JSON.parse(result.stdout)).not.toThrow(); + const output = JSON.parse(result.stdout); + expect(typeof output).toBe('object'); + }, 15000); + + it('should display help for validate command', async () => { + const result = await runCapiscio(['validate', '--help']); + + expect(result.exitCode).toBe(0); + const helpText = result.stdout.toLowerCase(); + expect(helpText.includes('validate')).toBe(true); + expect( + helpText.includes('usage') || helpText.includes('options') || helpText.includes('flags') + ).toBe(true); + }, 15000); +}); diff --git a/src/__tests__/cli.test.ts b/tests/unit/cli.test.ts similarity index 87% rename from src/__tests__/cli.test.ts rename to tests/unit/cli.test.ts index f3b3802..63c30dc 100644 --- a/src/__tests__/cli.test.ts +++ b/tests/unit/cli.test.ts @@ -54,7 +54,7 @@ describe('BinaryManager', () => { describe('getInstance', () => { it('should be a singleton', async () => { - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance1 = BinaryManager.getInstance(); const instance2 = BinaryManager.getInstance(); expect(instance1).toBe(instance2); @@ -63,7 +63,7 @@ describe('BinaryManager', () => { it('should create bin directory if it does not exist', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); BinaryManager.getInstance(); expect(fs.mkdirSync).toHaveBeenCalled(); @@ -81,7 +81,7 @@ describe('BinaryManager', () => { return undefined; }); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -92,7 +92,7 @@ describe('BinaryManager', () => { it('should map darwin correctly', async () => { vi.spyOn(os, 'platform').mockReturnValue('darwin'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -101,7 +101,7 @@ describe('BinaryManager', () => { it('should map linux correctly', async () => { vi.spyOn(os, 'platform').mockReturnValue('linux'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -110,7 +110,7 @@ describe('BinaryManager', () => { it('should map win32 to windows', async () => { vi.spyOn(os, 'platform').mockReturnValue('win32'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -119,7 +119,7 @@ describe('BinaryManager', () => { it('should throw for unsupported platform', async () => { vi.spyOn(os, 'platform').mockReturnValue('freebsd' as NodeJS.Platform); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); expect(() => BinaryManager.getInstance()).toThrow('Unsupported platform: freebsd'); }); @@ -129,7 +129,7 @@ describe('BinaryManager', () => { it('should map x64 to amd64', async () => { vi.spyOn(os, 'arch').mockReturnValue('x64'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -138,7 +138,7 @@ describe('BinaryManager', () => { it('should handle arm64', async () => { vi.spyOn(os, 'arch').mockReturnValue('arm64'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -154,7 +154,7 @@ describe('BinaryManager', () => { process.env.CAPISCIO_CORE_PATH = customPath; vi.mocked(fs.existsSync).mockImplementation((p) => String(p) === customPath || true); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); const result = await instance.getBinaryPath(); @@ -173,7 +173,7 @@ describe('BinaryManager', () => { return true; // Binary exists in default location }); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); await instance.getBinaryPath(); @@ -186,7 +186,7 @@ describe('BinaryManager', () => { it('should return existing binary path without downloading', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); const result = await instance.getBinaryPath(); @@ -203,12 +203,12 @@ describe('CLI Package', () => { }); it('should export version', async () => { - const { version } = await import('../index'); + const { version } = await import('../../src/index'); expect(version).toBe('2.2.0'); }); it('should export BinaryManager', async () => { - const { BinaryManager: ExportedBinaryManager } = await import('../index'); + const { BinaryManager: ExportedBinaryManager } = await import('../../src/index'); expect(ExportedBinaryManager).toBeDefined(); expect(typeof ExportedBinaryManager.getInstance).toBe('function'); }); @@ -224,7 +224,7 @@ describe('Binary naming', () => { it('should add .exe extension on Windows', async () => { vi.spyOn(os, 'platform').mockReturnValue('win32'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); const binaryPath = await instance.getBinaryPath(); @@ -234,7 +234,7 @@ describe('Binary naming', () => { it('should not add .exe extension on Unix platforms', async () => { vi.spyOn(os, 'platform').mockReturnValue('darwin'); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); const binaryPath = await instance.getBinaryPath(); @@ -256,7 +256,7 @@ describe('Version handling', () => { it('should use default version when env var not set', async () => { delete process.env.CAPISCIO_CORE_VERSION; - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -266,7 +266,7 @@ describe('Version handling', () => { it('should respect CAPISCIO_CORE_VERSION env var', async () => { process.env.CAPISCIO_CORE_VERSION = 'v2.0.0'; - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -323,7 +323,7 @@ describe('Install functionality', () => { }; vi.mocked(axios.default.get).mockResolvedValue({ data: mockStream }); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); // This should trigger install since binary doesn't exist @@ -356,7 +356,7 @@ describe('Install functionality', () => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); await expect(instance.getBinaryPath()).rejects.toEqual(mockError); @@ -385,7 +385,7 @@ describe('Install functionality', () => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); await expect(instance.getBinaryPath()).rejects.toEqual(mockError); @@ -411,7 +411,7 @@ describe('Install functionality', () => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); await expect(instance.getBinaryPath()).rejects.toThrow('Unknown error'); @@ -434,7 +434,7 @@ describe('findPackageRoot', () => { }); vi.mocked(fs.mkdirSync).mockReturnValue(undefined); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); @@ -448,7 +448,7 @@ describe('findPackageRoot', () => { }); vi.mocked(fs.mkdirSync).mockReturnValue(undefined); - const { BinaryManager } = await import('../utils/binary-manager'); + const { BinaryManager } = await import('../../src/utils/binary-manager'); const instance = BinaryManager.getInstance(); expect(instance).toBeDefined(); diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts new file mode 100644 index 0000000..4f0eeb7 --- /dev/null +++ b/vitest.config.e2e.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // E2E tests run the CLI offline + environment: 'node', + + // Include only E2E tests + include: ['tests/e2e/**/*.e2e.test.ts'], + + // Longer timeouts for E2E tests + testTimeout: 30000, + hookTimeout: 60000, + + // Run tests serially to avoid race conditions + pool: 'forks', + poolOptions: { + forks: { + singleFork: true + } + }, + } +}); diff --git a/vitest.config.ts b/vitest.config.ts index 3041dc8..cb089de 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ 'src/**/*.test.ts', 'src/**/*.spec.ts', 'src/__tests__/**', + 'tests/**', 'src/cli.ts' // CLI entry point is tested via integration ], // Coverage thresholds - fail if below 70%