# Testing Patterns **Analysis Date:** 2026-03-12 ## Test Framework **Mixed approach detected:** ### Primary: Node.js built-in `node:test` - Used in `bin/gsd-tools.test.cjs` - Version: Node.js built-in (no package needed) - Config: None detected ### Secondary: Vitest - Used in `bin/test/*.test.cjs` files - Version: `^3.2.4` (from package.json) - Config: None detected (uses defaults) **Run Commands:** ```bash npm test # Run all tests (vitest run) npm run test:watch # Watch mode (vitest) ``` ## Test File Organization **Location:** `bin/test/` **Naming:** `{command-name}.test.cjs` **Example files:** - `bin/test/get-profile.test.cjs` - `bin/test/set-profile.test.cjs` - `bin/test/allow-read-config.test.cjs` - `bin/test/oc-profile-config.test.cjs` - `bin/test/pivot-profile.test.cjs` ## Test Structure ### Vitest Style (Recommended) From `bin/test/get-profile.test.cjs`: ```javascript import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import fs from 'fs'; import path from 'path'; import os from 'os'; describe('get-profile.cjs', () => { let testDir; let planningDir; let configPath; let capturedLog; let capturedError; let exitCode; beforeEach(() => { // Create isolated test directory testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-profile-test-')); planningDir = path.join(testDir, '.planning'); configPath = path.join(planningDir, 'oc_config.json'); fs.mkdirSync(planningDir, { recursive: true }); // Reset captured output capturedLog = null; capturedError = null; exitCode = null; // Mock console.log to capture all output console.log = (msg) => { allLogs.push(msg); capturedLog = msg; }; console.error = (msg) => { allErrors.push(msg); capturedError = msg; }; process.exit = (code) => { exitCode = code; throw new Error(`process.exit(${code})`); }; }); afterEach(() => { // Restore original functions console.log = originalLog; console.error = originalError; process.exit = originalExit; // Cleanup test directory try { fs.rmSync(testDir, { recursive: true, force: true }); } catch (err) { // Ignore cleanup errors } }); it('returns current profile when current_oc_profile is set', () => { fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2)); const getProfile = importGetProfile(); try { getProfile(testDir, []); } catch (err) { // Expected to throw due to process.exit mock } expect(exitCode).toBe(0); const output = JSON.parse(capturedLog); expect(output.success).toBe(true); }); }); ``` ### Node:test Style From `bin/gsd-tools.test.cjs`: ```javascript const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); describe('history-digest command', () => { let tmpDir; beforeEach(() => { tmpDir = createTempProject(); }); afterEach(() => { cleanup(tmpDir); }); test('empty phases directory returns valid schema', () => { const result = runGsdTools('history-digest', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const digest = JSON.parse(result.output); assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object'); }); }); ``` ### Manual Test Runner Style From `bin/test/allow-read-config.test.cjs`: ```javascript function testCreatePermission() { console.log('Test: Create new opencode.json with permission...'); const testDir = createTestDir(); try { const result = runCLI(testDir, []); if (!result.success) { throw new Error(`Expected success, got: ${JSON.stringify(result)}`); } // Verify opencode.json was created const opencodePath = path.join(testDir, 'opencode.json'); if (!fs.existsSync(opencodePath)) { throw new Error('opencode.json was not created'); } console.log('✓ PASS: Create permission\n'); return true; } catch (err) { console.error('✗ FAIL:', err.message, '\n'); return false; } finally { cleanupTestDir(testDir); } } function runTests() { console.log('Running allow-read-config tests...\n'); const results = [ testCreatePermission(), testIdempotency(), testDryRun(), ]; const passed = results.filter(r => r).length; console.log(`Results: ${passed}/${total} tests passed`); if (passed === total) { process.exit(0); } else { process.exit(1); } } runTests(); ``` ## Mocking Patterns ### Console/Process Mocking **Vitest approach:** ```javascript const originalLog = console.log; const originalError = console.error; const originalExit = process.exit; beforeEach(() => { console.log = (msg) => { allLogs.push(msg); capturedLog = msg; }; console.error = (msg) => { allErrors.push(msg); capturedError = msg; }; process.exit = (code) => { exitCode = code; throw new Error(`process.exit(${code})`); }; }); afterEach(() => { console.log = originalLog; console.error = originalError; process.exit = originalExit; }); ``` ### Require Cache Clearing ```javascript const importGetProfile = () => { const modulePath = '../gsd-oc-commands/get-profile.cjs'; delete require.cache[require.resolve(modulePath)]; return require(modulePath); }; ``` ## Test Fixtures **Location:** `bin/test/fixtures/` **Pattern:** In-file constants ```javascript const VALID_CONFIG_WITH_CURRENT = { current_oc_profile: 'smart', profiles: { presets: { simple: { planning: 'bailian-coding-plan/qwen3.5-plus', execution: 'bailian-coding-plan/qwen3.5-plus', verification: 'bailian-coding-plan/qwen3.5-plus' }, smart: { /* ... */ } } } }; ``` ## Test Helpers ### CLI Execution ```javascript function runCLI(testDir, args) { const cmd = `node ${TOOLS_PATH} allow-read-config ${args.join(' ')}`; const output = execSync(cmd, { cwd: testDir, encoding: 'utf8' }); return JSON.parse(output); } ``` ### Temp Directory Creation ```javascript function createTestDir() { const testDir = path.join(os.tmpdir(), `gsd-oc-test-${Date.now()}`); fs.mkdirSync(testDir, { recursive: true }); return testDir; } function cleanupTestDir(testDir) { if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true, force: true }); } } ``` ## Assertion Patterns ### Vitest ```javascript expect(exitCode).toBe(0); expect(output.success).toBe(true); expect(output.data).toHaveProperty('smart'); expect(output.data.smart).toEqual(VALID_CONFIG_WITH_CURRENT.profiles.presets.smart); ``` ### Node.js assert ```javascript assert.ok(result.success, `Command failed: ${result.error}`); assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object'); ``` ## Coverage **No coverage configuration detected.** No enforcement of coverage thresholds. ## Test Types ### Unit Tests - Individual function testing - Use mocking for dependencies - Test isolated behavior ### Integration/CLI Tests - Execute CLI commands via `execSync` - Test with real temporary directories - Verify file creation/state changes ### Patterns - Test both success and error paths - Test flag combinations (--verbose, --dry-run) - Test idempotency (running same command twice) ## Common Issues ### Inconsistent Test Frameworks - Some tests use Vitest, others use Node.js built-in `node:test` - Some tests use manual test runner (no framework) - **Recommendation:** Standardize on one framework ### Missing Test Config - No `vitest.config.js` found - No `jest.config.js` found - **Recommendation:** Add configuration file --- *Testing analysis: 2026-03-12*