first commit
This commit is contained in:
345
.planning/codebase/TESTING.md
Normal file
345
.planning/codebase/TESTING.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user