7.6 KiB
7.6 KiB
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.cjsfiles - Version:
^3.2.4(from package.json) - Config: None detected (uses defaults)
Run Commands:
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.cjsbin/test/set-profile.test.cjsbin/test/allow-read-config.test.cjsbin/test/oc-profile-config.test.cjsbin/test/pivot-profile.test.cjs
Test Structure
Vitest Style (Recommended)
From bin/test/get-profile.test.cjs:
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:
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:
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:
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
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
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
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
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
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
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.jsfound - No
jest.config.jsfound - Recommendation: Add configuration file
Testing analysis: 2026-03-12