Files
sp80/.opencode/get-shit-done/bin/test/set-profile.test.cjs
2026-03-13 15:46:10 +08:00

301 lines
8.6 KiB
JavaScript

/**
* Unit tests for set-profile.cjs
*
* Tests for profile switching, validation, and the three operation modes:
* 1. Mode 1 (no profile name): Validate and apply current profile
* 2. Mode 2 (profile name): Switch to specified profile
* 3. Mode 3 (inline JSON): Create new profile from definition
*
* Includes validation checks, dry-run functionality, and rollback mechanisms.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Mock console.log and console.error to capture output
const originalLog = console.log;
const originalError = console.error;
const originalExit = process.exit;
// Test fixtures
const VALID_CONFIG = {
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: {
planning: 'bailian-coding-plan/qwen3.5-plus',
execution: 'bailian-coding-plan/qwen3.5-plus',
verification: 'bailian-coding-plan/qwen3.5-plus'
},
genius: {
planning: 'bailian-coding-plan/qwen3.5-plus',
execution: 'bailian-coding-plan/qwen3.5-plus',
verification: 'bailian-coding-plan/qwen3.5-plus'
}
}
}
};
describe('set-profile.cjs', () => {
let testDir;
let planningDir;
let configPath;
let opencodePath;
let capturedLog;
let capturedError;
let exitCode;
let allLogs;
let allErrors;
beforeEach(() => {
// Create isolated test directory
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'set-profile-test-'));
planningDir = path.join(testDir, '.planning');
configPath = path.join(planningDir, 'oc_config.json');
opencodePath = path.join(testDir, 'opencode.json');
fs.mkdirSync(planningDir, { recursive: true });
// Reset captured output
capturedLog = null;
capturedError = null;
exitCode = null;
allLogs = [];
allErrors = [];
// 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
}
});
// Import setProfile inside tests to use mocked functions
const importSetProfile = () => {
const modulePath = '../gsd-oc-commands/set-profile.cjs';
delete require.cache[require.resolve(modulePath)];
return require(modulePath);
};
describe('Export verification', () => {
it('exports setProfile function', () => {
const setProfile = importSetProfile();
expect(typeof setProfile).toBe('function');
});
it('function name is setProfile', () => {
const setProfile = importSetProfile();
expect(setProfile.name).toBe('setProfilePhase16'); // Function was renamed from phase16
});
});
describe('Basic functionality', () => {
function writeOpencodeJson() {
const opencode = {
$schema: 'https://opencode.ai/schema.json',
agent: {
'gsd-planner': {
model: 'bailian-coding-plan/qwen3.5-plus',
tools: ['*']
},
'gsd-executor': {
model: 'bailian-coding-plan/qwen3.5-plus',
tools: ['*']
}
}
};
fs.writeFileSync(opencodePath, JSON.stringify(opencode, null, 2) + '\n', 'utf8');
}
beforeEach(() => {
fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2) + '\n', 'utf8');
writeOpencodeJson();
});
it('setProfile updates profile when profile name provided', () => {
const setProfile = importSetProfile();
try {
setProfile(testDir, ['genius']);
} catch (err) {
// Expected to throw due to process.exit mock
}
expect(exitCode).toBe(0);
const output = JSON.parse(capturedLog);
expect(output.success).toBe(true);
expect(output.data.profile).toBe('genius');
});
it('setProfile processes dry-run flag', () => {
const setProfile = importSetProfile();
try {
setProfile(testDir, ['smart', '--dry-run']);
} catch (err) {
// Expected
}
expect(exitCode).toBe(0);
const output = JSON.parse(capturedLog);
expect(output.success).toBe(true);
expect(output.data.dryRun).toBe(true);
expect(output.data.action).toBe('switch_profile');
});
it('setProfile validates required keys for inline profiles', () => {
const setProfile = importSetProfile();
const inlineProfile = 'test_profile:{"planning":"bailian-coding-plan/qwen3.5-plus","execution":"bailian-coding-plan/qwen3.5-plus","verification":"bailian-coding-plan/qwen3.5-plus"}';
try {
setProfile(testDir, [inlineProfile]);
} catch (err) {
// Expected
}
const output = JSON.parse(capturedLog);
expect(output.success).toBe(true);
expect(output.data.profile).toBe('test_profile');
});
it('setProfile handles Mode 1 (no profile name) scenario', () => {
const setProfile = importSetProfile();
try {
setProfile(testDir, []);
} catch (err) {
// Expected
}
expect(exitCode).toBe(0);
const output = JSON.parse(capturedLog);
expect(output.success).toBe(true);
expect(output.data.profile).toBe('smart'); // From initial current_oc_profile
});
it('setProfile validates invalid models before modification', () => {
const setProfile = importSetProfile();
const inlineProfile = 'bad_profile:{"planning":"bad_model","execution":"bad_model","verification":"bad_model"}';
try {
setProfile(testDir, [inlineProfile]);
} catch (err) {
// Expected - should error
}
expect(exitCode).toBe(1);
});
it('setProfile rejects invalid inline profile definitions', () => {
const setProfile = importSetProfile();
// Invalid JSON
const badDef = 'bad_profile:{"planning:"model","execution":"model","verification":"model"}';
try {
setProfile(testDir, [badDef]);
} catch (err) {
// Expected - should error
}
expect(exitCode).toBe(1);
const error = JSON.parse(capturedError);
expect(error.error.code).toBe('INVALID_SYNTAX');
});
it('setProfile rejects incomplete profile definitions', () => {
const setProfile = importSetProfile();
// Missing verification property
const badDef = 'bad_profile:{"planning":"bailian-coding-plan/qwen3.5-plus","execution":"bailian-coding-plan/qwen3.5-plus"}';
try {
setProfile(testDir, [badDef]);
} catch (err) {
// Expected - should error
}
expect(exitCode).toBe(1);
const error = JSON.parse(capturedError);
expect(error.error.code).toBe('INCOMPLETE_PROFILE');
});
});
describe('Error handling', () => {
it('handles missing config.json gracefully', () => {
const setProfile = importSetProfile();
try {
setProfile(testDir, ['test']);
} catch (err) {
// Expected to throw
}
expect(exitCode).toBe(1);
const error = JSON.parse(capturedError);
expect(error.error.code).toBe('CONFIG_NOT_FOUND');
});
it('sets exit code 1 for invalid profile', () => {
const setProfile = importSetProfile();
// Set up a valid config with presets
const configData = {...VALID_CONFIG};
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + '\n', 'utf8');
const opencodeData = {
$schema: 'https://opencode.ai/schema.json',
agent: {}
};
fs.writeFileSync(opencodePath, JSON.stringify(opencodeData, null, 2) + '\n', 'utf8');
try {
setProfile(testDir, ['non-existent-profile']);
} catch (err) {
// Expected
}
expect(exitCode).toBe(1);
});
it('rejects too many arguments', () => {
const setProfile = importSetProfile();
try {
setProfile(testDir, ['profile1', 'profile2']);
} catch (err) {
// Expected
}
expect(exitCode).toBe(1);
const error = JSON.parse(capturedError);
expect(error.error.code).toBe('INVALID_ARGS');
});
});
});