gsd-opencode 1.20.2 → 1.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/commands/gsd/gsd-check-profile.md +30 -0
  2. package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
  3. package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
  4. package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
  5. package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
  6. package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
  7. package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
  8. package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
  9. package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
  10. package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
  11. package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
  12. package/get-shit-done/bin/gsd-oc-tools.cjs +130 -0
  13. package/get-shit-done/bin/lib/oc-config.cjs +200 -0
  14. package/get-shit-done/bin/lib/oc-core.cjs +114 -0
  15. package/get-shit-done/bin/lib/oc-models.cjs +133 -0
  16. package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
  17. package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
  18. package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
  19. package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
  20. package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
  21. package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
  22. package/get-shit-done/workflows/oc-check-profile.md +181 -0
  23. package/get-shit-done/workflows/oc-set-profile.md +83 -243
  24. package/get-shit-done/workflows/settings.md +4 -3
  25. package/package.json +2 -2
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Unit tests for oc-profile-config.cjs
3
+ *
4
+ * Tests for loadOcProfileConfig, validateProfile, and applyProfileWithValidation
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+
12
+ import {
13
+ loadOcProfileConfig,
14
+ validateProfile,
15
+ applyProfileWithValidation,
16
+ getAgentsForProfile,
17
+ ERROR_CODES
18
+ } from '../gsd-oc-lib/oc-profile-config.cjs';
19
+
20
+ // Test fixtures
21
+ import VALID_CONFIG from './fixtures/oc-config-valid.json' assert { type: 'json' };
22
+ import INVALID_CONFIG from './fixtures/oc-config-invalid.json' assert { type: 'json' };
23
+
24
+ // Mock model catalog (simulates opencode models output)
25
+ const MOCK_MODELS = [
26
+ 'bailian-coding-plan/qwen3.5-plus',
27
+ 'bailian-coding-plan/qwen3.5-pro',
28
+ 'opencode/gpt-5-nano',
29
+ 'kilo/anthropic/claude-3.7-sonnet',
30
+ 'kilo/anthropic/claude-3.5-haiku'
31
+ ];
32
+
33
+ describe('oc-profile-config.cjs', () => {
34
+ let testDir;
35
+ let planningDir;
36
+ let configPath;
37
+
38
+ beforeEach(() => {
39
+ // Create isolated test directory
40
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-profile-test-'));
41
+ planningDir = path.join(testDir, '.planning');
42
+ configPath = path.join(planningDir, 'oc_config.json');
43
+ fs.mkdirSync(planningDir, { recursive: true });
44
+ });
45
+
46
+ afterEach(() => {
47
+ // Cleanup test directory
48
+ try {
49
+ fs.rmSync(testDir, { recursive: true, force: true });
50
+ } catch (err) {
51
+ // Ignore cleanup errors
52
+ }
53
+ });
54
+
55
+ describe('loadOcProfileConfig', () => {
56
+ it('returns CONFIG_NOT_FOUND when file does not exist', () => {
57
+ const result = loadOcProfileConfig(testDir);
58
+
59
+ expect(result.success).toBe(false);
60
+ expect(result.error.code).toBe(ERROR_CODES.CONFIG_NOT_FOUND);
61
+ expect(result.error.message).toContain('oc_config.json not found');
62
+ });
63
+
64
+ it('returns INVALID_JSON for malformed JSON', () => {
65
+ fs.writeFileSync(configPath, '{ invalid json }', 'utf8');
66
+
67
+ const result = loadOcProfileConfig(testDir);
68
+
69
+ expect(result.success).toBe(false);
70
+ expect(result.error.code).toBe(ERROR_CODES.INVALID_JSON);
71
+ expect(result.error.message).toContain('Invalid JSON');
72
+ });
73
+
74
+ it('returns config and configPath for valid file', () => {
75
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
76
+
77
+ const result = loadOcProfileConfig(testDir);
78
+
79
+ expect(result.success).toBe(true);
80
+ expect(result.config).toEqual(VALID_CONFIG);
81
+ expect(result.configPath).toBe(configPath);
82
+ });
83
+ });
84
+
85
+ describe('validateProfile', () => {
86
+ it('returns valid: true for existing profile with valid models', () => {
87
+ const result = validateProfile(VALID_CONFIG, 'simple', MOCK_MODELS);
88
+
89
+ expect(result.valid).toBe(true);
90
+ expect(result.errors).toHaveLength(0);
91
+ });
92
+
93
+ it('returns PROFILE_NOT_FOUND for non-existent profile', () => {
94
+ const result = validateProfile(VALID_CONFIG, 'nonexistent', MOCK_MODELS);
95
+
96
+ expect(result.valid).toBe(false);
97
+ expect(result.errors).toHaveLength(1);
98
+ expect(result.errors[0].code).toBe(ERROR_CODES.PROFILE_NOT_FOUND);
99
+ expect(result.errors[0].message).toContain('not found');
100
+ });
101
+
102
+ it('returns INVALID_MODELS for profile with invalid model IDs', () => {
103
+ const result = validateProfile(INVALID_CONFIG, 'invalid-models', MOCK_MODELS);
104
+
105
+ expect(result.valid).toBe(false);
106
+ expect(result.errors).toHaveLength(1);
107
+ expect(result.errors[0].code).toBe(ERROR_CODES.INVALID_MODELS);
108
+ expect(result.errors[0].invalidModels).toHaveLength(3);
109
+ });
110
+
111
+ it('returns INCOMPLETE_PROFILE for missing planning/execution/verification', () => {
112
+ const result = validateProfile(INVALID_CONFIG, 'incomplete', MOCK_MODELS);
113
+
114
+ expect(result.valid).toBe(false);
115
+ expect(result.errors).toHaveLength(1);
116
+ expect(result.errors[0].code).toBe(ERROR_CODES.INCOMPLETE_PROFILE);
117
+ expect(result.errors[0].missingKeys).toContain('execution');
118
+ expect(result.errors[0].missingKeys).toContain('verification');
119
+ });
120
+ });
121
+
122
+ describe('applyProfileWithValidation', () => {
123
+ it('dry-run mode returns preview without file modifications', () => {
124
+ // Setup opencode.json for applyProfileToOpencode to work
125
+ const opencodePath = path.join(testDir, 'opencode.json');
126
+ fs.writeFileSync(opencodePath, JSON.stringify({
127
+ "$schema": "https://opencode.ai/config.json",
128
+ "agent": {}
129
+ }, null, 2), 'utf8');
130
+
131
+ // Write config file
132
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
133
+
134
+ const result = applyProfileWithValidation(testDir, 'smart', {
135
+ dryRun: true,
136
+ verbose: false
137
+ });
138
+
139
+ expect(result.success).toBe(true);
140
+ expect(result.dryRun).toBe(true);
141
+ expect(result.preview).toBeDefined();
142
+ expect(result.preview.profile).toBe('smart');
143
+ expect(result.preview.models).toHaveProperty('planning');
144
+ expect(result.preview.models).toHaveProperty('execution');
145
+ expect(result.preview.models).toHaveProperty('verification');
146
+
147
+ // Verify no backup was created in dry-run
148
+ const backupDir = path.join(testDir, '.planning', 'backups');
149
+ expect(fs.existsSync(backupDir)).toBe(false);
150
+ });
151
+
152
+ it('creates backups before modifications', () => {
153
+ // Setup opencode.json
154
+ const opencodePath = path.join(testDir, 'opencode.json');
155
+ fs.writeFileSync(opencodePath, JSON.stringify({
156
+ "$schema": "https://opencode.ai/config.json",
157
+ "agent": {}
158
+ }, null, 2), 'utf8');
159
+
160
+ // Write config file
161
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
162
+
163
+ const result = applyProfileWithValidation(testDir, 'simple', {
164
+ dryRun: false,
165
+ verbose: false
166
+ });
167
+
168
+ expect(result.success).toBe(true);
169
+ expect(result.data.backup).toBeDefined();
170
+ expect(fs.existsSync(result.data.backup)).toBe(true);
171
+ expect(result.data.backup).toContain('.planning/backups');
172
+ });
173
+
174
+ it('updates oc_config.json with current_oc_profile', () => {
175
+ // Setup initial config with different current profile
176
+ const initialConfig = {
177
+ current_oc_profile: 'simple',
178
+ profiles: VALID_CONFIG.profiles
179
+ };
180
+ fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
181
+
182
+ // Setup opencode.json
183
+ const opencodePath = path.join(testDir, 'opencode.json');
184
+ fs.writeFileSync(opencodePath, JSON.stringify({
185
+ "$schema": "https://opencode.ai/config.json",
186
+ "agent": {}
187
+ }, null, 2), 'utf8');
188
+
189
+ const result = applyProfileWithValidation(testDir, 'genius', {
190
+ dryRun: false,
191
+ verbose: false
192
+ });
193
+
194
+ expect(result.success).toBe(true);
195
+
196
+ // Verify config was updated
197
+ const updatedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
198
+ expect(updatedConfig.current_oc_profile).toBe('genius');
199
+ });
200
+
201
+ it('applies to opencode.json via applyProfileToOpencode', () => {
202
+ // Setup opencode.json
203
+ const opencodePath = path.join(testDir, 'opencode.json');
204
+ fs.writeFileSync(opencodePath, JSON.stringify({
205
+ "$schema": "https://opencode.ai/config.json",
206
+ "agent": {}
207
+ }, null, 2), 'utf8');
208
+
209
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
210
+
211
+ const result = applyProfileWithValidation(testDir, 'smart', {
212
+ dryRun: false,
213
+ verbose: false
214
+ });
215
+
216
+ expect(result.success).toBe(true);
217
+ expect(result.data.updated).toBeDefined();
218
+ expect(Array.isArray(result.data.updated)).toBe(true);
219
+
220
+ // Verify opencode.json was updated with gsd-* agents
221
+ const updatedOpencode = JSON.parse(fs.readFileSync(opencodePath, 'utf8'));
222
+ expect(updatedOpencode.agent).toBeDefined();
223
+ expect(updatedOpencode.agent['gsd-planner']).toBeDefined();
224
+ expect(updatedOpencode.agent['gsd-executor']).toBeDefined();
225
+ expect(updatedOpencode.agent['gsd-verifier']).toBeDefined();
226
+ });
227
+
228
+ it('returns error for non-existent profile', () => {
229
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
230
+
231
+ const result = applyProfileWithValidation(testDir, 'nonexistent', {
232
+ dryRun: false,
233
+ verbose: false
234
+ });
235
+
236
+ expect(result.success).toBe(false);
237
+ expect(result.error.code).toBe(ERROR_CODES.PROFILE_NOT_FOUND);
238
+ });
239
+
240
+ it('validates models before file modifications', () => {
241
+ // Config with invalid models
242
+ const invalidConfig = {
243
+ current_oc_profile: 'simple',
244
+ profiles: {
245
+ presets: {
246
+ 'bad-profile': {
247
+ planning: 'invalid-model',
248
+ execution: 'invalid-model',
249
+ verification: 'invalid-model'
250
+ }
251
+ }
252
+ }
253
+ };
254
+ fs.writeFileSync(configPath, JSON.stringify(invalidConfig, null, 2), 'utf8');
255
+
256
+ const result = applyProfileWithValidation(testDir, 'bad-profile', {
257
+ dryRun: false,
258
+ verbose: false
259
+ });
260
+
261
+ expect(result.success).toBe(false);
262
+ expect(result.error.code).toBe(ERROR_CODES.INVALID_MODELS);
263
+
264
+ // Verify config was NOT modified (validation happened first)
265
+ const configAfter = JSON.parse(fs.readFileSync(configPath, 'utf8'));
266
+ expect(configAfter.current_oc_profile).toBe('simple');
267
+ });
268
+
269
+ it('supports inline profile definition', () => {
270
+ // Setup opencode.json
271
+ const opencodePath = path.join(testDir, 'opencode.json');
272
+ fs.writeFileSync(opencodePath, JSON.stringify({
273
+ "$schema": "https://opencode.ai/config.json",
274
+ "agent": {}
275
+ }, null, 2), 'utf8');
276
+
277
+ // Start with empty profiles
278
+ const initialConfig = {
279
+ profiles: {
280
+ presets: {}
281
+ }
282
+ };
283
+ fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
284
+
285
+ const inlineProfile = {
286
+ planning: 'bailian-coding-plan/qwen3.5-plus',
287
+ execution: 'bailian-coding-plan/qwen3.5-plus',
288
+ verification: 'bailian-coding-plan/qwen3.5-plus'
289
+ };
290
+
291
+ const result = applyProfileWithValidation(testDir, 'custom', {
292
+ dryRun: false,
293
+ verbose: false,
294
+ inlineProfile
295
+ });
296
+
297
+ expect(result.success).toBe(true);
298
+
299
+ // Verify profile was added
300
+ const updatedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
301
+ expect(updatedConfig.profiles.presets.custom).toEqual(inlineProfile);
302
+ expect(updatedConfig.current_oc_profile).toBe('custom');
303
+ });
304
+
305
+ it('rejects incomplete inline profile definition', () => {
306
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2), 'utf8');
307
+
308
+ const incompleteProfile = {
309
+ planning: 'bailian-coding-plan/qwen3.5-plus'
310
+ // Missing execution and verification
311
+ };
312
+
313
+ const result = applyProfileWithValidation(testDir, 'new-profile', {
314
+ dryRun: false,
315
+ verbose: false,
316
+ inlineProfile: incompleteProfile
317
+ });
318
+
319
+ expect(result.success).toBe(false);
320
+ expect(result.error.code).toBe(ERROR_CODES.INCOMPLETE_PROFILE);
321
+ expect(result.error.missingKeys).toContain('execution');
322
+ });
323
+ });
324
+
325
+ describe('getAgentsForProfile', () => {
326
+ it('returns all agents for complete profile', () => {
327
+ const profile = {
328
+ planning: 'bailian-coding-plan/qwen3.5-plus',
329
+ execution: 'opencode/gpt-5-nano',
330
+ verification: 'kilo/anthropic/claude-3.7-sonnet'
331
+ };
332
+
333
+ const agents = getAgentsForProfile(profile);
334
+
335
+ expect(agents).toBeInstanceOf(Array);
336
+ expect(agents.length).toBeGreaterThan(10); // Should have 11 agents
337
+
338
+ // Check planning agents
339
+ const planningAgents = agents.filter(a => a.model === 'bailian-coding-plan/qwen3.5-plus');
340
+ expect(planningAgents.length).toBe(7);
341
+
342
+ // Check execution agents
343
+ const executionAgents = agents.filter(a => a.model === 'opencode/gpt-5-nano');
344
+ expect(executionAgents.length).toBe(2);
345
+
346
+ // Check verification agents
347
+ const verificationAgents = agents.filter(a => a.model === 'kilo/anthropic/claude-3.7-sonnet');
348
+ expect(verificationAgents.length).toBe(2);
349
+ });
350
+
351
+ it('handles profile with missing categories', () => {
352
+ const profile = {
353
+ planning: 'bailian-coding-plan/qwen3.5-plus'
354
+ // Missing execution and verification
355
+ };
356
+
357
+ const agents = getAgentsForProfile(profile);
358
+
359
+ expect(agents).toBeInstanceOf(Array);
360
+ expect(agents.length).toBe(7); // Only planning agents
361
+ expect(agents.every(a => a.model === 'bailian-coding-plan/qwen3.5-plus')).toBe(true);
362
+ });
363
+ });
364
+
365
+ describe('ERROR_CODES', () => {
366
+ it('exports all expected error codes', () => {
367
+ expect(ERROR_CODES).toHaveProperty('CONFIG_NOT_FOUND');
368
+ expect(ERROR_CODES).toHaveProperty('INVALID_JSON');
369
+ expect(ERROR_CODES).toHaveProperty('PROFILE_NOT_FOUND');
370
+ expect(ERROR_CODES).toHaveProperty('INVALID_MODELS');
371
+ expect(ERROR_CODES).toHaveProperty('INCOMPLETE_PROFILE');
372
+ expect(ERROR_CODES).toHaveProperty('WRITE_FAILED');
373
+ expect(ERROR_CODES).toHaveProperty('APPLY_FAILED');
374
+ expect(ERROR_CODES).toHaveProperty('ROLLBACK_FAILED');
375
+ });
376
+ });
377
+ });
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Unit tests for pivot-profile.cjs
3
+ *
4
+ * Tests for the thin wrapper that delegates to setProfilePhase16
5
+ * Focus: Verify correct import and delegation, not re-testing underlying functionality
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ // Mock console.log and console.error to capture output
14
+ const originalLog = console.log;
15
+ const originalError = console.error;
16
+ const originalExit = process.exit;
17
+
18
+ // Test fixtures
19
+ const VALID_CONFIG = {
20
+ current_oc_profile: 'smart',
21
+ profiles: {
22
+ presets: {
23
+ simple: {
24
+ planning: 'bailian-coding-plan/qwen3.5-plus',
25
+ execution: 'bailian-coding-plan/qwen3.5-plus',
26
+ verification: 'bailian-coding-plan/qwen3.5-plus'
27
+ },
28
+ smart: {
29
+ planning: 'bailian-coding-plan/qwen3.5-plus',
30
+ execution: 'bailian-coding-plan/qwen3.5-plus',
31
+ verification: 'bailian-coding-plan/qwen3.5-plus'
32
+ },
33
+ genius: {
34
+ planning: 'bailian-coding-plan/qwen3.5-plus',
35
+ execution: 'bailian-coding-plan/qwen3.5-plus',
36
+ verification: 'bailian-coding-plan/qwen3.5-plus'
37
+ }
38
+ }
39
+ }
40
+ };
41
+
42
+ describe('pivot-profile.cjs', () => {
43
+ let testDir;
44
+ let planningDir;
45
+ let configPath;
46
+ let opencodePath;
47
+ let capturedLog;
48
+ let capturedError;
49
+ let exitCode;
50
+ let allLogs;
51
+ let allErrors;
52
+
53
+ beforeEach(() => {
54
+ // Create isolated test directory
55
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pivot-profile-test-'));
56
+ planningDir = path.join(testDir, '.planning');
57
+ configPath = path.join(planningDir, 'oc_config.json');
58
+ opencodePath = path.join(testDir, 'opencode.json');
59
+
60
+ fs.mkdirSync(planningDir, { recursive: true });
61
+
62
+ // Reset captured output
63
+ capturedLog = null;
64
+ capturedError = null;
65
+ exitCode = null;
66
+ allLogs = [];
67
+ allErrors = [];
68
+
69
+ // Mock console.log to capture all output
70
+ console.log = (msg) => {
71
+ allLogs.push(msg);
72
+ capturedLog = msg;
73
+ };
74
+ console.error = (msg) => {
75
+ allErrors.push(msg);
76
+ capturedError = msg;
77
+ };
78
+ process.exit = (code) => {
79
+ exitCode = code;
80
+ throw new Error(`process.exit(${code})`);
81
+ };
82
+ });
83
+
84
+ afterEach(() => {
85
+ // Restore original functions
86
+ console.log = originalLog;
87
+ console.error = originalError;
88
+ process.exit = originalExit;
89
+
90
+ // Cleanup test directory
91
+ try {
92
+ fs.rmSync(testDir, { recursive: true, force: true });
93
+ } catch (err) {
94
+ // Ignore cleanup errors
95
+ }
96
+ });
97
+
98
+ // Import pivotProfile inside tests to use mocked functions
99
+ const importPivotProfile = () => {
100
+ const modulePath = '../gsd-oc-commands/pivot-profile.cjs';
101
+ delete require.cache[require.resolve(modulePath)];
102
+ return require(modulePath);
103
+ };
104
+
105
+ describe('Export verification', () => {
106
+ it('exports pivotProfile function', () => {
107
+ const pivotProfile = importPivotProfile();
108
+ expect(typeof pivotProfile).toBe('function');
109
+ });
110
+
111
+ it('function name is pivotProfile', () => {
112
+ const pivotProfile = importPivotProfile();
113
+ expect(pivotProfile.name).toBe('pivotProfile');
114
+ });
115
+ });
116
+
117
+ describe('Delegation tests', () => {
118
+ function writeOpencodeJson() {
119
+ const opencode = {
120
+ $schema: 'https://opencode.ai/schema.json',
121
+ agent: {
122
+ 'gsd-planner': {
123
+ model: 'bailian-coding-plan/qwen3.5-plus',
124
+ tools: ['*']
125
+ },
126
+ 'gsd-executor': {
127
+ model: 'bailian-coding-plan/qwen3.5-plus',
128
+ tools: ['*']
129
+ }
130
+ }
131
+ };
132
+ fs.writeFileSync(opencodePath, JSON.stringify(opencode, null, 2) + '\n', 'utf8');
133
+ }
134
+
135
+ beforeEach(() => {
136
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2) + '\n', 'utf8');
137
+ writeOpencodeJson();
138
+ });
139
+
140
+ it('pivotProfile delegates to setProfilePhase16', () => {
141
+ const pivotProfile = importPivotProfile();
142
+
143
+ try {
144
+ pivotProfile(testDir, ['smart']);
145
+ } catch (err) {
146
+ // Expected to throw due to process.exit mock
147
+ }
148
+
149
+ expect(exitCode).toBe(0);
150
+ const output = JSON.parse(capturedLog);
151
+ expect(output.success).toBe(true);
152
+ expect(output.data.profile).toBe('smart');
153
+ });
154
+
155
+ it('pivotProfile accepts cwd and args parameters', () => {
156
+ const pivotProfile = importPivotProfile();
157
+
158
+ // Should not throw except for process.exit mock
159
+ try {
160
+ pivotProfile(testDir, ['smart']);
161
+ } catch (err) {
162
+ // Expected - only process.exit should throw
163
+ expect(err.message).toContain('process.exit');
164
+ }
165
+ });
166
+
167
+ it('pivotProfile passes arguments through unchanged', () => {
168
+ const pivotProfile = importPivotProfile();
169
+
170
+ try {
171
+ pivotProfile(testDir, ['genius']);
172
+ } catch (err) {
173
+ // Expected
174
+ }
175
+
176
+ const output = JSON.parse(capturedLog);
177
+ expect(output.data.profile).toBe('genius');
178
+ });
179
+
180
+ it('pivotProfile returns same output structure as setProfilePhase16', () => {
181
+ const pivotProfile = importPivotProfile();
182
+
183
+ try {
184
+ pivotProfile(testDir, ['simple']);
185
+ } catch (err) {
186
+ // Expected
187
+ }
188
+
189
+ const output = JSON.parse(capturedLog);
190
+ expect(output).toHaveProperty('success', true);
191
+ expect(output.data).toHaveProperty('profile');
192
+ expect(output.data).toHaveProperty('models');
193
+ expect(output.data.models).toHaveProperty('planning');
194
+ expect(output.data.models).toHaveProperty('execution');
195
+ expect(output.data.models).toHaveProperty('verification');
196
+ });
197
+
198
+ it('pivotProfile handles --dry-run flag', () => {
199
+ const pivotProfile = importPivotProfile();
200
+
201
+ try {
202
+ pivotProfile(testDir, ['--dry-run', 'genius']);
203
+ } catch (err) {
204
+ // Expected
205
+ }
206
+
207
+ expect(exitCode).toBe(0);
208
+ const output = JSON.parse(capturedLog);
209
+ expect(output.success).toBe(true);
210
+ expect(output.data.dryRun).toBe(true);
211
+ });
212
+
213
+ it('pivotProfile returns error for invalid profile', () => {
214
+ const pivotProfile = importPivotProfile();
215
+
216
+ try {
217
+ pivotProfile(testDir, ['nonexistent']);
218
+ } catch (err) {
219
+ // Expected
220
+ }
221
+
222
+ expect(exitCode).toBe(1);
223
+ const error = JSON.parse(capturedError);
224
+ expect(error.error.code).toBe('PROFILE_NOT_FOUND');
225
+ });
226
+
227
+ it('pivotProfile works in Mode 1 (no profile name)', () => {
228
+ const configWithCurrent = {
229
+ ...VALID_CONFIG,
230
+ current_oc_profile: 'smart'
231
+ };
232
+ fs.writeFileSync(configPath, JSON.stringify(configWithCurrent, null, 2) + '\n', 'utf8');
233
+
234
+ const pivotProfile = importPivotProfile();
235
+
236
+ try {
237
+ pivotProfile(testDir, []);
238
+ } catch (err) {
239
+ // Expected
240
+ }
241
+
242
+ expect(exitCode).toBe(0);
243
+ const output = JSON.parse(capturedLog);
244
+ expect(output.success).toBe(true);
245
+ expect(output.data.profile).toBe('smart');
246
+ });
247
+
248
+ it('pivotProfile handles inline profile creation (Mode 3)', () => {
249
+ const pivotProfile = importPivotProfile();
250
+ const profileDef = 'test:{"planning":"bailian-coding-plan/qwen3.5-plus","execution":"bailian-coding-plan/qwen3.5-plus","verification":"bailian-coding-plan/qwen3.5-plus"}';
251
+
252
+ try {
253
+ pivotProfile(testDir, [profileDef]);
254
+ } catch (err) {
255
+ // Expected
256
+ }
257
+
258
+ expect(exitCode).toBe(0);
259
+ const output = JSON.parse(capturedLog);
260
+ expect(output.success).toBe(true);
261
+ expect(output.data.profile).toBe('test');
262
+
263
+ // Verify profile was added to config
264
+ const updatedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
265
+ expect(updatedConfig.profiles.presets.test).toBeDefined();
266
+ expect(updatedConfig.current_oc_profile).toBe('test');
267
+ });
268
+ });
269
+
270
+ describe('Integration with gsd-oc-tools.cjs', () => {
271
+ it('pivot-profile module can be imported', () => {
272
+ const pivotProfile = importPivotProfile();
273
+ expect(typeof pivotProfile).toBe('function');
274
+ });
275
+ });
276
+ });