Files
rtu_v5/.opencode/get-shit-done/bin/test/oc-profile-config.test.cjs
2026-05-29 14:48:36 +08:00

378 lines
13 KiB
JavaScript

/**
* Unit tests for oc-profile-config.cjs
*
* Tests for loadOcProfileConfig, validateProfile, and applyProfileWithValidation
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';
import {
loadOcProfileConfig,
validateProfile,
applyProfileWithValidation,
getAgentsForProfile,
ERROR_CODES
} from '../gsd-oc-lib/oc-profile-config.cjs';
// Test fixtures
import VALID_CONFIG from './fixtures/oc-config-valid.json' assert { type: 'json' };
import INVALID_CONFIG from './fixtures/oc-config-invalid.json' assert { type: 'json' };
// Mock model catalog (simulates opencode models output)
const MOCK_MODELS = [
'bailian-coding-plan/qwen3.5-plus',
'bailian-coding-plan/qwen3.5-pro',
'opencode/gpt-5-nano',
'kilo/anthropic/claude-3.7-sonnet',
'kilo/anthropic/claude-3.5-haiku'
];
describe('oc-profile-config.cjs', () => {
let testDir;
let planningDir;
let configPath;
beforeEach(() => {
// Create isolated test directory
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-profile-test-'));
planningDir = path.join(testDir, '.planning');
configPath = path.join(planningDir, 'oc_config.json');
fs.mkdirSync(planningDir, { recursive: true });
});
afterEach(() => {
// Cleanup test directory
try {
fs.rmSync(testDir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors
}
});
describe('loadOcProfileConfig', () => {
it('returns CONFIG_NOT_FOUND when file does not exist', () => {
const result = loadOcProfileConfig(testDir);
expect(result.success).toBe(false);
expect(result.error.code).toBe(ERROR_CODES.CONFIG_NOT_FOUND);
expect(result.error.message).toContain('oc_config.json not found');
});
it('returns INVALID_JSON for malformed JSON', () => {
fs.writeFileSync(configPath, '{ invalid json }', 'utf8');
const result = loadOcProfileConfig(testDir);
expect(result.success).toBe(false);
expect(result.error.code).toBe(ERROR_CODES.INVALID_JSON);
expect(result.error.message).toContain('Invalid JSON');
});
it('returns config and configPath for valid file', () => {
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
const result = loadOcProfileConfig(testDir);
expect(result.success).toBe(true);
expect(result.config).toEqual(VALID_CONFIG);
expect(result.configPath).toBe(configPath);
});
});
describe('validateProfile', () => {
it('returns valid: true for existing profile with valid models', () => {
const result = validateProfile(VALID_CONFIG, 'simple', MOCK_MODELS);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('returns PROFILE_NOT_FOUND for non-existent profile', () => {
const result = validateProfile(VALID_CONFIG, 'nonexistent', MOCK_MODELS);
expect(result.valid).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].code).toBe(ERROR_CODES.PROFILE_NOT_FOUND);
expect(result.errors[0].message).toContain('not found');
});
it('returns INVALID_MODELS for profile with invalid model IDs', () => {
const result = validateProfile(INVALID_CONFIG, 'invalid-models', MOCK_MODELS);
expect(result.valid).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].code).toBe(ERROR_CODES.INVALID_MODELS);
expect(result.errors[0].invalidModels).toHaveLength(3);
});
it('returns INCOMPLETE_PROFILE for missing planning/execution/verification', () => {
const result = validateProfile(INVALID_CONFIG, 'incomplete', MOCK_MODELS);
expect(result.valid).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].code).toBe(ERROR_CODES.INCOMPLETE_PROFILE);
expect(result.errors[0].missingKeys).toContain('execution');
expect(result.errors[0].missingKeys).toContain('verification');
});
});
describe('applyProfileWithValidation', () => {
it('dry-run mode returns preview without file modifications', () => {
// Setup opencode.json for applyProfileToOpencode to work
const opencodePath = path.join(testDir, 'opencode.json');
fs.writeFileSync(opencodePath, JSON.stringify({
"$schema": "https://opencode.ai/config.json",
"agent": {}
}, null, 2), 'utf8');
// Write config file
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
const result = applyProfileWithValidation(testDir, 'smart', {
dryRun: true,
verbose: false
});
expect(result.success).toBe(true);
expect(result.dryRun).toBe(true);
expect(result.preview).toBeDefined();
expect(result.preview.profile).toBe('smart');
expect(result.preview.models).toHaveProperty('planning');
expect(result.preview.models).toHaveProperty('execution');
expect(result.preview.models).toHaveProperty('verification');
// Verify no backup was created in dry-run
const backupDir = path.join(testDir, '.planning', 'backups');
expect(fs.existsSync(backupDir)).toBe(false);
});
it('creates backups before modifications', () => {
// Setup opencode.json
const opencodePath = path.join(testDir, 'opencode.json');
fs.writeFileSync(opencodePath, JSON.stringify({
"$schema": "https://opencode.ai/config.json",
"agent": {}
}, null, 2), 'utf8');
// Write config file
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
const result = applyProfileWithValidation(testDir, 'simple', {
dryRun: false,
verbose: false
});
expect(result.success).toBe(true);
expect(result.data.backup).toBeDefined();
expect(fs.existsSync(result.data.backup)).toBe(true);
expect(result.data.backup).toContain('.planning/backups');
});
it('updates oc_config.json with current_oc_profile', () => {
// Setup initial config with different current profile
const initialConfig = {
current_oc_profile: 'simple',
profiles: VALID_CONFIG.profiles
};
fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
// Setup opencode.json
const opencodePath = path.join(testDir, 'opencode.json');
fs.writeFileSync(opencodePath, JSON.stringify({
"$schema": "https://opencode.ai/config.json",
"agent": {}
}, null, 2), 'utf8');
const result = applyProfileWithValidation(testDir, 'genius', {
dryRun: false,
verbose: false
});
expect(result.success).toBe(true);
// Verify config was updated
const updatedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
expect(updatedConfig.current_oc_profile).toBe('genius');
});
it('applies to opencode.json via applyProfileToOpencode', () => {
// Setup opencode.json
const opencodePath = path.join(testDir, 'opencode.json');
fs.writeFileSync(opencodePath, JSON.stringify({
"$schema": "https://opencode.ai/config.json",
"agent": {}
}, null, 2), 'utf8');
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
const result = applyProfileWithValidation(testDir, 'smart', {
dryRun: false,
verbose: false
});
expect(result.success).toBe(true);
expect(result.data.updated).toBeDefined();
expect(Array.isArray(result.data.updated)).toBe(true);
// Verify opencode.json was updated with gsd-* agents
const updatedOpencode = JSON.parse(fs.readFileSync(opencodePath, 'utf8'));
expect(updatedOpencode.agent).toBeDefined();
expect(updatedOpencode.agent['gsd-planner']).toBeDefined();
expect(updatedOpencode.agent['gsd-executor']).toBeDefined();
expect(updatedOpencode.agent['gsd-verifier']).toBeDefined();
});
it('returns error for non-existent profile', () => {
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
const result = applyProfileWithValidation(testDir, 'nonexistent', {
dryRun: false,
verbose: false
});
expect(result.success).toBe(false);
expect(result.error.code).toBe(ERROR_CODES.PROFILE_NOT_FOUND);
});
it('validates models before file modifications', () => {
// Config with invalid models
const invalidConfig = {
current_oc_profile: 'simple',
profiles: {
presets: {
'bad-profile': {
planning: 'invalid-model',
execution: 'invalid-model',
verification: 'invalid-model'
}
}
}
};
fs.writeFileSync(configPath, JSON.stringify(invalidConfig, null, 2), 'utf8');
const result = applyProfileWithValidation(testDir, 'bad-profile', {
dryRun: false,
verbose: false
});
expect(result.success).toBe(false);
expect(result.error.code).toBe(ERROR_CODES.INVALID_MODELS);
// Verify config was NOT modified (validation happened first)
const configAfter = JSON.parse(fs.readFileSync(configPath, 'utf8'));
expect(configAfter.current_oc_profile).toBe('simple');
});
it('supports inline profile definition', () => {
// Setup opencode.json
const opencodePath = path.join(testDir, 'opencode.json');
fs.writeFileSync(opencodePath, JSON.stringify({
"$schema": "https://opencode.ai/config.json",
"agent": {}
}, null, 2), 'utf8');
// Start with empty profiles
const initialConfig = {
profiles: {
presets: {}
}
};
fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
const inlineProfile = {
planning: 'bailian-coding-plan/qwen3.5-plus',
execution: 'bailian-coding-plan/qwen3.5-plus',
verification: 'bailian-coding-plan/qwen3.5-plus'
};
const result = applyProfileWithValidation(testDir, 'custom', {
dryRun: false,
verbose: false,
inlineProfile
});
expect(result.success).toBe(true);
// Verify profile was added
const updatedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
expect(updatedConfig.profiles.presets.custom).toEqual(inlineProfile);
expect(updatedConfig.current_oc_profile).toBe('custom');
});
it('rejects incomplete inline profile definition', () => {
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
const incompleteProfile = {
planning: 'bailian-coding-plan/qwen3.5-plus'
// Missing execution and verification
};
const result = applyProfileWithValidation(testDir, 'new-profile', {
dryRun: false,
verbose: false,
inlineProfile: incompleteProfile
});
expect(result.success).toBe(false);
expect(result.error.code).toBe(ERROR_CODES.INCOMPLETE_PROFILE);
expect(result.error.missingKeys).toContain('execution');
});
});
describe('getAgentsForProfile', () => {
it('returns all agents for complete profile', () => {
const profile = {
planning: 'bailian-coding-plan/qwen3.5-plus',
execution: 'opencode/gpt-5-nano',
verification: 'kilo/anthropic/claude-3.7-sonnet'
};
const agents = getAgentsForProfile(profile);
expect(agents).toBeInstanceOf(Array);
expect(agents.length).toBeGreaterThan(10); // Should have 11 agents
// Check planning agents
const planningAgents = agents.filter(a => a.model === 'bailian-coding-plan/qwen3.5-plus');
expect(planningAgents.length).toBe(7);
// Check execution agents
const executionAgents = agents.filter(a => a.model === 'opencode/gpt-5-nano');
expect(executionAgents.length).toBe(2);
// Check verification agents
const verificationAgents = agents.filter(a => a.model === 'kilo/anthropic/claude-3.7-sonnet');
expect(verificationAgents.length).toBe(2);
});
it('handles profile with missing categories', () => {
const profile = {
planning: 'bailian-coding-plan/qwen3.5-plus'
// Missing execution and verification
};
const agents = getAgentsForProfile(profile);
expect(agents).toBeInstanceOf(Array);
expect(agents.length).toBe(7); // Only planning agents
expect(agents.every(a => a.model === 'bailian-coding-plan/qwen3.5-plus')).toBe(true);
});
});
describe('ERROR_CODES', () => {
it('exports all expected error codes', () => {
expect(ERROR_CODES).toHaveProperty('CONFIG_NOT_FOUND');
expect(ERROR_CODES).toHaveProperty('INVALID_JSON');
expect(ERROR_CODES).toHaveProperty('PROFILE_NOT_FOUND');
expect(ERROR_CODES).toHaveProperty('INVALID_MODELS');
expect(ERROR_CODES).toHaveProperty('INCOMPLETE_PROFILE');
expect(ERROR_CODES).toHaveProperty('WRITE_FAILED');
expect(ERROR_CODES).toHaveProperty('APPLY_FAILED');
expect(ERROR_CODES).toHaveProperty('ROLLBACK_FAILED');
});
});
});