opencode-dux 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/agents/descriptions.d.ts +6 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/interpreter.d.ts +2 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +27 -0
- package/dist/agents/overrides.d.ts +18 -0
- package/dist/agents/prompt-blocks.d.ts +97 -0
- package/dist/agents/steward.d.ts +3 -0
- package/dist/cli/config-io.d.ts +24 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1006 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/mcps.d.ts +13 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +137 -0
- package/dist/cli/skills.d.ts +22 -0
- package/dist/cli/system.d.ts +5 -0
- package/dist/cli/types.d.ts +38 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/discovery/local/types.d.ts +79 -0
- package/dist/discovery/local.d.ts +73 -0
- package/dist/discovery/mcp-servers.d.ts +88 -0
- package/dist/discovery/skills.d.ts +94 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/types.d.ts +22 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +32 -0
- package/dist/hooks/foreground-fallback/index.d.ts +72 -0
- package/dist/hooks/image-hook.d.ts +5 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
- package/dist/hooks/task-session-manager/index.d.ts +52 -0
- package/dist/hooks/todo-continuation/index.d.ts +53 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31782 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/skills/registry.d.ts +29 -0
- package/dist/subscriptions/accounts-store.d.ts +57 -0
- package/dist/subscriptions/index.d.ts +13 -0
- package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
- package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
- package/dist/subscriptions/types.d.ts +115 -0
- package/dist/subscriptions/usage-service.d.ts +74 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/delegate.d.ts +14 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +18 -0
- package/dist/tui-state.d.ts +168 -0
- package/dist/tui.d.ts +37 -0
- package/dist/tui.js +1896 -0
- package/dist/utils/agent-variant.d.ts +63 -0
- package/dist/utils/compat.d.ts +30 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session-manager.d.ts +55 -0
- package/dist/utils/session.d.ts +90 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/index.ts +1 -0
- package/opencode-dux.schema.json +634 -0
- package/package.json +103 -0
- package/src/agents/descriptions.ts +55 -0
- package/src/agents/designer.test.ts +86 -0
- package/src/agents/designer.ts +154 -0
- package/src/agents/display-name.test.ts +186 -0
- package/src/agents/explorer.test.ts +79 -0
- package/src/agents/explorer.ts +144 -0
- package/src/agents/fixer.test.ts +79 -0
- package/src/agents/fixer.ts +145 -0
- package/src/agents/index.test.ts +472 -0
- package/src/agents/index.ts +248 -0
- package/src/agents/interpreter.ts +136 -0
- package/src/agents/librarian.test.ts +80 -0
- package/src/agents/librarian.ts +145 -0
- package/src/agents/oracle.test.ts +89 -0
- package/src/agents/oracle.ts +184 -0
- package/src/agents/orchestrator.test.ts +116 -0
- package/src/agents/orchestrator.ts +574 -0
- package/src/agents/overrides.ts +95 -0
- package/src/agents/prompt-blocks.test.ts +114 -0
- package/src/agents/prompt-blocks.ts +640 -0
- package/src/agents/steward.ts +146 -0
- package/src/cli/config-io.test.ts +536 -0
- package/src/cli/config-io.ts +473 -0
- package/src/cli/config-manager.test.ts +141 -0
- package/src/cli/config-manager.ts +4 -0
- package/src/cli/index.ts +88 -0
- package/src/cli/install.ts +282 -0
- package/src/cli/mcps.test.ts +62 -0
- package/src/cli/mcps.ts +39 -0
- package/src/cli/model-key-normalization.test.ts +21 -0
- package/src/cli/model-key-normalization.ts +60 -0
- package/src/cli/paths.test.ts +167 -0
- package/src/cli/paths.ts +144 -0
- package/src/cli/providers.test.ts +118 -0
- package/src/cli/providers.ts +141 -0
- package/src/cli/skills.test.ts +111 -0
- package/src/cli/skills.ts +103 -0
- package/src/cli/system.test.ts +91 -0
- package/src/cli/system.ts +180 -0
- package/src/cli/types.ts +43 -0
- package/src/config/constants.ts +58 -0
- package/src/config/index.ts +4 -0
- package/src/config/loader.test.ts +1194 -0
- package/src/config/loader.ts +269 -0
- package/src/config/model-resolution.test.ts +176 -0
- package/src/config/runtime-preset.test.ts +61 -0
- package/src/config/runtime-preset.ts +37 -0
- package/src/config/schema.ts +248 -0
- package/src/config/utils.test.ts +41 -0
- package/src/config/utils.ts +23 -0
- package/src/discovery/local/types.ts +85 -0
- package/src/discovery/local.ts +322 -0
- package/src/discovery/mcp-servers.ts +804 -0
- package/src/discovery/skills.ts +959 -0
- package/src/hooks/apply-patch/codec.test.ts +184 -0
- package/src/hooks/apply-patch/codec.ts +352 -0
- package/src/hooks/apply-patch/errors.ts +117 -0
- package/src/hooks/apply-patch/execution-context.ts +432 -0
- package/src/hooks/apply-patch/hook.test.ts +768 -0
- package/src/hooks/apply-patch/index.ts +126 -0
- package/src/hooks/apply-patch/matching.test.ts +215 -0
- package/src/hooks/apply-patch/matching.ts +586 -0
- package/src/hooks/apply-patch/operations.test.ts +1535 -0
- package/src/hooks/apply-patch/operations.ts +3 -0
- package/src/hooks/apply-patch/patch.ts +9 -0
- package/src/hooks/apply-patch/prepared-changes.ts +400 -0
- package/src/hooks/apply-patch/resolution.test.ts +420 -0
- package/src/hooks/apply-patch/resolution.ts +437 -0
- package/src/hooks/apply-patch/rewrite.ts +496 -0
- package/src/hooks/apply-patch/test-helpers.ts +52 -0
- package/src/hooks/apply-patch/types.ts +111 -0
- package/src/hooks/auto-update-checker/cache.test.ts +179 -0
- package/src/hooks/auto-update-checker/cache.ts +188 -0
- package/src/hooks/auto-update-checker/checker.test.ts +159 -0
- package/src/hooks/auto-update-checker/checker.ts +308 -0
- package/src/hooks/auto-update-checker/constants.ts +33 -0
- package/src/hooks/auto-update-checker/index.test.ts +282 -0
- package/src/hooks/auto-update-checker/index.ts +225 -0
- package/src/hooks/auto-update-checker/types.ts +26 -0
- package/src/hooks/chat-headers.test.ts +236 -0
- package/src/hooks/chat-headers.ts +97 -0
- package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
- package/src/hooks/context-pressure-reminder/index.ts +137 -0
- package/src/hooks/delegate-task-retry/guidance.ts +41 -0
- package/src/hooks/delegate-task-retry/hook.ts +23 -0
- package/src/hooks/delegate-task-retry/index.test.ts +38 -0
- package/src/hooks/delegate-task-retry/index.ts +7 -0
- package/src/hooks/delegate-task-retry/patterns.ts +79 -0
- package/src/hooks/filter-available-skills/index.test.ts +297 -0
- package/src/hooks/filter-available-skills/index.ts +160 -0
- package/src/hooks/foreground-fallback/index.test.ts +624 -0
- package/src/hooks/foreground-fallback/index.ts +374 -0
- package/src/hooks/image-hook.ts +6 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/json-error-recovery/hook.ts +73 -0
- package/src/hooks/json-error-recovery/index.test.ts +111 -0
- package/src/hooks/json-error-recovery/index.ts +6 -0
- package/src/hooks/phase-reminder/index.test.ts +74 -0
- package/src/hooks/phase-reminder/index.ts +85 -0
- package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
- package/src/hooks/post-file-tool-nudge/index.ts +63 -0
- package/src/hooks/task-session-manager/index.test.ts +833 -0
- package/src/hooks/task-session-manager/index.ts +434 -0
- package/src/hooks/todo-continuation/index.test.ts +3026 -0
- package/src/hooks/todo-continuation/index.ts +878 -0
- package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
- package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
- package/src/index.ts +1672 -0
- package/src/mcp/context7.ts +14 -0
- package/src/mcp/grep-app.ts +11 -0
- package/src/mcp/index.test.ts +96 -0
- package/src/mcp/index.ts +66 -0
- package/src/mcp/types.ts +16 -0
- package/src/mcp/websearch.ts +47 -0
- package/src/skills/codemap/README.md +60 -0
- package/src/skills/codemap/SKILL.md +174 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/registry.ts +218 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/subscriptions/accounts-store.test.ts +236 -0
- package/src/subscriptions/accounts-store.ts +184 -0
- package/src/subscriptions/index.ts +30 -0
- package/src/subscriptions/neuralwatt-scraper.ts +108 -0
- package/src/subscriptions/opencode-go-scraper.ts +301 -0
- package/src/subscriptions/types.ts +145 -0
- package/src/subscriptions/usage-service.test.ts +202 -0
- package/src/subscriptions/usage-service.ts +651 -0
- package/src/tools/ast-grep/cli.ts +257 -0
- package/src/tools/ast-grep/constants.ts +214 -0
- package/src/tools/ast-grep/downloader.ts +131 -0
- package/src/tools/ast-grep/index.ts +24 -0
- package/src/tools/ast-grep/tools.ts +117 -0
- package/src/tools/ast-grep/types.ts +51 -0
- package/src/tools/ast-grep/utils.ts +126 -0
- package/src/tools/delegate-handoff.test.ts +18 -0
- package/src/tools/delegate.ts +508 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/preset-manager.test.ts +795 -0
- package/src/tools/preset-manager.ts +332 -0
- package/src/tools/smartfetch/binary.ts +58 -0
- package/src/tools/smartfetch/cache.test.ts +34 -0
- package/src/tools/smartfetch/cache.ts +112 -0
- package/src/tools/smartfetch/constants.ts +29 -0
- package/src/tools/smartfetch/index.ts +8 -0
- package/src/tools/smartfetch/network.test.ts +178 -0
- package/src/tools/smartfetch/network.ts +614 -0
- package/src/tools/smartfetch/secondary-model.test.ts +85 -0
- package/src/tools/smartfetch/secondary-model.ts +276 -0
- package/src/tools/smartfetch/tool.test.ts +60 -0
- package/src/tools/smartfetch/tool.ts +832 -0
- package/src/tools/smartfetch/types.ts +135 -0
- package/src/tools/smartfetch/utils.test.ts +24 -0
- package/src/tools/smartfetch/utils.ts +456 -0
- package/src/tui-state.test.ts +867 -0
- package/src/tui-state.ts +1255 -0
- package/src/tui.test.ts +336 -0
- package/src/tui.ts +1539 -0
- package/src/utils/agent-variant.test.ts +244 -0
- package/src/utils/agent-variant.ts +187 -0
- package/src/utils/compat.ts +91 -0
- package/src/utils/env.ts +12 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/internal-initiator.ts +28 -0
- package/src/utils/logger.test.ts +220 -0
- package/src/utils/logger.ts +136 -0
- package/src/utils/polling.test.ts +191 -0
- package/src/utils/polling.ts +67 -0
- package/src/utils/session-manager.test.ts +173 -0
- package/src/utils/session-manager.ts +356 -0
- package/src/utils/session.test.ts +110 -0
- package/src/utils/session.ts +389 -0
- package/src/utils/subagent-depth.test.ts +170 -0
- package/src/utils/subagent-depth.ts +75 -0
- package/src/utils/system-collapse.test.ts +86 -0
- package/src/utils/system-collapse.ts +24 -0
- package/src/utils/task.test.ts +24 -0
- package/src/utils/task.ts +20 -0
- package/src/utils/zip-extractor.ts +102 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import type { PluginConfig } from '../config';
|
|
6
|
+
import {
|
|
7
|
+
getActiveRuntimePreset,
|
|
8
|
+
setActiveRuntimePreset,
|
|
9
|
+
} from '../config/runtime-preset';
|
|
10
|
+
import { createPresetManager } from './preset-manager';
|
|
11
|
+
|
|
12
|
+
function createMockContext() {
|
|
13
|
+
const configUpdate = mock(async () => ({}));
|
|
14
|
+
return {
|
|
15
|
+
client: {
|
|
16
|
+
config: {
|
|
17
|
+
update: configUpdate,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
directory: '/tmp/test',
|
|
21
|
+
} as any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createOutput() {
|
|
25
|
+
return { parts: [] as Array<{ type: string; text?: string }> };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getOutputText(output: ReturnType<typeof createOutput>): string {
|
|
29
|
+
return output.parts
|
|
30
|
+
.filter((p) => p.type === 'text')
|
|
31
|
+
.map((p) => p.text ?? '')
|
|
32
|
+
.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let previousXdgDataHome: string | undefined;
|
|
36
|
+
let tempDir: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
previousXdgDataHome = process.env.XDG_DATA_HOME;
|
|
40
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-preset-manager-'));
|
|
41
|
+
process.env.XDG_DATA_HOME = tempDir;
|
|
42
|
+
setActiveRuntimePreset(null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
if (previousXdgDataHome === undefined) {
|
|
47
|
+
delete process.env.XDG_DATA_HOME;
|
|
48
|
+
} else {
|
|
49
|
+
process.env.XDG_DATA_HOME = previousXdgDataHome;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
53
|
+
setActiveRuntimePreset(null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('createPresetManager', () => {
|
|
57
|
+
describe('handleCommandExecuteBefore', () => {
|
|
58
|
+
test('ignores non-preset commands', async () => {
|
|
59
|
+
const ctx = createMockContext();
|
|
60
|
+
const config: PluginConfig = {};
|
|
61
|
+
const manager = createPresetManager(ctx, config);
|
|
62
|
+
const output = createOutput();
|
|
63
|
+
|
|
64
|
+
await manager.handleCommandExecuteBefore(
|
|
65
|
+
{ command: 'auto-continue', sessionID: 's1', arguments: 'on' },
|
|
66
|
+
output,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(output.parts).toHaveLength(0);
|
|
70
|
+
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('lists available presets when no argument given', async () => {
|
|
74
|
+
const ctx = createMockContext();
|
|
75
|
+
const config: PluginConfig = {
|
|
76
|
+
presets: {
|
|
77
|
+
cheap: {
|
|
78
|
+
orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
79
|
+
},
|
|
80
|
+
powerful: {
|
|
81
|
+
orchestrator: { model: 'openai/gpt-5.5' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const manager = createPresetManager(ctx, config);
|
|
86
|
+
const output = createOutput();
|
|
87
|
+
|
|
88
|
+
await manager.handleCommandExecuteBefore(
|
|
89
|
+
{ command: 'preset', sessionID: 's1', arguments: '' },
|
|
90
|
+
output,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const text = getOutputText(output);
|
|
94
|
+
expect(text).toContain('cheap');
|
|
95
|
+
expect(text).toContain('powerful');
|
|
96
|
+
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('lists presets with active marker when preset is set', async () => {
|
|
100
|
+
const ctx = createMockContext();
|
|
101
|
+
const config: PluginConfig = {
|
|
102
|
+
preset: 'cheap',
|
|
103
|
+
presets: {
|
|
104
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
105
|
+
powerful: { orchestrator: { model: 'openai/gpt-5.5' } },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const manager = createPresetManager(ctx, config);
|
|
109
|
+
const output = createOutput();
|
|
110
|
+
|
|
111
|
+
await manager.handleCommandExecuteBefore(
|
|
112
|
+
{ command: 'preset', sessionID: 's1', arguments: '' },
|
|
113
|
+
output,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const text = getOutputText(output);
|
|
117
|
+
expect(text).toContain('← active');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('shows no-presets message when none configured', async () => {
|
|
121
|
+
const ctx = createMockContext();
|
|
122
|
+
const config: PluginConfig = {};
|
|
123
|
+
const manager = createPresetManager(ctx, config);
|
|
124
|
+
const output = createOutput();
|
|
125
|
+
|
|
126
|
+
await manager.handleCommandExecuteBefore(
|
|
127
|
+
{ command: 'preset', sessionID: 's1', arguments: '' },
|
|
128
|
+
output,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const text = getOutputText(output);
|
|
132
|
+
expect(text).toContain('No presets configured');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('switches preset and calls config.update', async () => {
|
|
136
|
+
const ctx = createMockContext();
|
|
137
|
+
const config: PluginConfig = {
|
|
138
|
+
presets: {
|
|
139
|
+
cheap: {
|
|
140
|
+
orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
141
|
+
explorer: { model: 'openai/gpt-5.4-mini' },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const manager = createPresetManager(ctx, config);
|
|
146
|
+
const output = createOutput();
|
|
147
|
+
|
|
148
|
+
await manager.handleCommandExecuteBefore(
|
|
149
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
150
|
+
output,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const text = getOutputText(output);
|
|
154
|
+
expect(text).toContain('Switched to preset "cheap"');
|
|
155
|
+
expect(text).toContain('orchestrator');
|
|
156
|
+
expect(text).toContain('anthropic/claude-3.5-haiku');
|
|
157
|
+
expect(text).toContain('explorer');
|
|
158
|
+
expect(ctx.client.config.update).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
160
|
+
body: {
|
|
161
|
+
agent: {
|
|
162
|
+
orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
163
|
+
explorer: { model: 'openai/gpt-5.4-mini' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('passes temperature in config update', async () => {
|
|
170
|
+
const ctx = createMockContext();
|
|
171
|
+
const config: PluginConfig = {
|
|
172
|
+
presets: {
|
|
173
|
+
precise: {
|
|
174
|
+
orchestrator: { model: 'openai/o3', temperature: 0.1 },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const manager = createPresetManager(ctx, config);
|
|
179
|
+
const output = createOutput();
|
|
180
|
+
|
|
181
|
+
await manager.handleCommandExecuteBefore(
|
|
182
|
+
{ command: 'preset', sessionID: 's1', arguments: 'precise' },
|
|
183
|
+
output,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
187
|
+
body: {
|
|
188
|
+
agent: {
|
|
189
|
+
orchestrator: { model: 'openai/o3', temperature: 0.1 },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('passes variant in config update', async () => {
|
|
196
|
+
const ctx = createMockContext();
|
|
197
|
+
const config: PluginConfig = {
|
|
198
|
+
presets: {
|
|
199
|
+
thinker: {
|
|
200
|
+
oracle: {
|
|
201
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
202
|
+
variant: 'thinking',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const manager = createPresetManager(ctx, config);
|
|
208
|
+
const output = createOutput();
|
|
209
|
+
|
|
210
|
+
await manager.handleCommandExecuteBefore(
|
|
211
|
+
{ command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
212
|
+
output,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
216
|
+
body: {
|
|
217
|
+
agent: {
|
|
218
|
+
oracle: {
|
|
219
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
220
|
+
variant: 'thinking',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('shows error for unknown preset name', async () => {
|
|
228
|
+
const ctx = createMockContext();
|
|
229
|
+
const config: PluginConfig = {
|
|
230
|
+
presets: {
|
|
231
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
const manager = createPresetManager(ctx, config);
|
|
235
|
+
const output = createOutput();
|
|
236
|
+
|
|
237
|
+
await manager.handleCommandExecuteBefore(
|
|
238
|
+
{ command: 'preset', sessionID: 's1', arguments: 'nonexistent' },
|
|
239
|
+
output,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const text = getOutputText(output);
|
|
243
|
+
expect(text).toContain('not found');
|
|
244
|
+
expect(text).toContain('cheap');
|
|
245
|
+
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('shows error when no presets configured but argument given', async () => {
|
|
249
|
+
const ctx = createMockContext();
|
|
250
|
+
const config: PluginConfig = {};
|
|
251
|
+
const manager = createPresetManager(ctx, config);
|
|
252
|
+
const output = createOutput();
|
|
253
|
+
|
|
254
|
+
await manager.handleCommandExecuteBefore(
|
|
255
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
256
|
+
output,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const text = getOutputText(output);
|
|
260
|
+
expect(text).toContain('not found');
|
|
261
|
+
expect(text).toContain('No presets configured');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('handles config.update error gracefully', async () => {
|
|
265
|
+
const ctx = createMockContext();
|
|
266
|
+
ctx.client.config.update = mock(async () => {
|
|
267
|
+
throw new Error('Server unavailable');
|
|
268
|
+
});
|
|
269
|
+
const config: PluginConfig = {
|
|
270
|
+
presets: {
|
|
271
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
const manager = createPresetManager(ctx, config);
|
|
275
|
+
const output = createOutput();
|
|
276
|
+
|
|
277
|
+
await manager.handleCommandExecuteBefore(
|
|
278
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
279
|
+
output,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const text = getOutputText(output);
|
|
283
|
+
expect(text).toContain('Failed to switch preset');
|
|
284
|
+
expect(text).toContain('Server unavailable');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('shows empty preset message when preset has no valid overrides', async () => {
|
|
288
|
+
const ctx = createMockContext();
|
|
289
|
+
const config: PluginConfig = {
|
|
290
|
+
presets: {
|
|
291
|
+
empty: {
|
|
292
|
+
orchestrator: {},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
const manager = createPresetManager(ctx, config);
|
|
297
|
+
const output = createOutput();
|
|
298
|
+
|
|
299
|
+
await manager.handleCommandExecuteBefore(
|
|
300
|
+
{ command: 'preset', sessionID: 's1', arguments: 'empty' },
|
|
301
|
+
output,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const text = getOutputText(output);
|
|
305
|
+
expect(text).toContain('empty');
|
|
306
|
+
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('forwards options field in config update', async () => {
|
|
310
|
+
const ctx = createMockContext();
|
|
311
|
+
const config: PluginConfig = {
|
|
312
|
+
presets: {
|
|
313
|
+
thinker: {
|
|
314
|
+
oracle: {
|
|
315
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
316
|
+
options: {
|
|
317
|
+
thinking: { type: 'enabled', budgetTokens: 10000 },
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
const manager = createPresetManager(ctx, config);
|
|
324
|
+
const output = createOutput();
|
|
325
|
+
|
|
326
|
+
await manager.handleCommandExecuteBefore(
|
|
327
|
+
{ command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
328
|
+
output,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
332
|
+
body: {
|
|
333
|
+
agent: {
|
|
334
|
+
oracle: {
|
|
335
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
336
|
+
options: {
|
|
337
|
+
thinking: { type: 'enabled', budgetTokens: 10000 },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('trims whitespace from preset name argument', async () => {
|
|
346
|
+
const ctx = createMockContext();
|
|
347
|
+
const config: PluginConfig = {
|
|
348
|
+
presets: {
|
|
349
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
const manager = createPresetManager(ctx, config);
|
|
353
|
+
const output = createOutput();
|
|
354
|
+
|
|
355
|
+
await manager.handleCommandExecuteBefore(
|
|
356
|
+
{ command: 'preset', sessionID: 's1', arguments: ' cheap ' },
|
|
357
|
+
output,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const text = getOutputText(output);
|
|
361
|
+
expect(text).toContain('Switched to preset "cheap"');
|
|
362
|
+
expect(ctx.client.config.update).toHaveBeenCalledTimes(1);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('shows suggestion for multi-word arguments', async () => {
|
|
366
|
+
const ctx = createMockContext();
|
|
367
|
+
const config: PluginConfig = {
|
|
368
|
+
presets: {
|
|
369
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
const manager = createPresetManager(ctx, config);
|
|
373
|
+
const output = createOutput();
|
|
374
|
+
|
|
375
|
+
await manager.handleCommandExecuteBefore(
|
|
376
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap powerful' },
|
|
377
|
+
output,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const text = getOutputText(output);
|
|
381
|
+
expect(text).toContain('cannot contain spaces');
|
|
382
|
+
expect(text).toContain('/preset cheap');
|
|
383
|
+
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('catches tab-separated arguments', async () => {
|
|
387
|
+
const ctx = createMockContext();
|
|
388
|
+
const config: PluginConfig = {
|
|
389
|
+
presets: {
|
|
390
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
const manager = createPresetManager(ctx, config);
|
|
394
|
+
const output = createOutput();
|
|
395
|
+
|
|
396
|
+
await manager.handleCommandExecuteBefore(
|
|
397
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap\tpowerful' },
|
|
398
|
+
output,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const text = getOutputText(output);
|
|
402
|
+
expect(text).toContain('cannot contain spaces');
|
|
403
|
+
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('skips agents with empty overrides in mixed preset', async () => {
|
|
407
|
+
const ctx = createMockContext();
|
|
408
|
+
const config: PluginConfig = {
|
|
409
|
+
presets: {
|
|
410
|
+
mixed: {
|
|
411
|
+
orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
412
|
+
explorer: {},
|
|
413
|
+
oracle: { temperature: 0.3 },
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
const manager = createPresetManager(ctx, config);
|
|
418
|
+
const output = createOutput();
|
|
419
|
+
|
|
420
|
+
await manager.handleCommandExecuteBefore(
|
|
421
|
+
{ command: 'preset', sessionID: 's1', arguments: 'mixed' },
|
|
422
|
+
output,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const text = getOutputText(output);
|
|
426
|
+
expect(text).toContain('Switched to preset "mixed"');
|
|
427
|
+
// Only orchestrator and oracle should be forwarded
|
|
428
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
429
|
+
body: {
|
|
430
|
+
agent: {
|
|
431
|
+
orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
432
|
+
oracle: { temperature: 0.3 },
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('resolves array-form model to first entry', async () => {
|
|
439
|
+
const ctx = createMockContext();
|
|
440
|
+
const config: PluginConfig = {
|
|
441
|
+
presets: {
|
|
442
|
+
fallback: {
|
|
443
|
+
orchestrator: {
|
|
444
|
+
model: ['anthropic/claude-3.5-haiku', 'openai/gpt-5.5'],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
const manager = createPresetManager(ctx, config);
|
|
450
|
+
const output = createOutput();
|
|
451
|
+
|
|
452
|
+
await manager.handleCommandExecuteBefore(
|
|
453
|
+
{ command: 'preset', sessionID: 's1', arguments: 'fallback' },
|
|
454
|
+
output,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const text = getOutputText(output);
|
|
458
|
+
expect(text).toContain('Switched to preset "fallback"');
|
|
459
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
460
|
+
body: {
|
|
461
|
+
agent: {
|
|
462
|
+
orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('resolves array-form model with object entries', async () => {
|
|
469
|
+
const ctx = createMockContext();
|
|
470
|
+
const config: PluginConfig = {
|
|
471
|
+
presets: {
|
|
472
|
+
thinker: {
|
|
473
|
+
oracle: {
|
|
474
|
+
model: [
|
|
475
|
+
{ id: 'anthropic/claude-sonnet-4-6', variant: 'thinking' },
|
|
476
|
+
{ id: 'openai/o3' },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
const manager = createPresetManager(ctx, config);
|
|
483
|
+
const output = createOutput();
|
|
484
|
+
|
|
485
|
+
await manager.handleCommandExecuteBefore(
|
|
486
|
+
{ command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
487
|
+
output,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
491
|
+
body: {
|
|
492
|
+
agent: {
|
|
493
|
+
oracle: {
|
|
494
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
495
|
+
variant: 'thinking',
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('shows variant and options in switch summary', async () => {
|
|
503
|
+
const ctx = createMockContext();
|
|
504
|
+
const config: PluginConfig = {
|
|
505
|
+
presets: {
|
|
506
|
+
thinker: {
|
|
507
|
+
oracle: {
|
|
508
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
509
|
+
variant: 'thinking',
|
|
510
|
+
options: { thinking: { type: 'enabled', budgetTokens: 10000 } },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
const manager = createPresetManager(ctx, config);
|
|
516
|
+
const output = createOutput();
|
|
517
|
+
|
|
518
|
+
await manager.handleCommandExecuteBefore(
|
|
519
|
+
{ command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
520
|
+
output,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const text = getOutputText(output);
|
|
524
|
+
expect(text).toContain('variant: thinking');
|
|
525
|
+
expect(text).toContain('options: yes');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('tracks active preset after switch', async () => {
|
|
529
|
+
const ctx = createMockContext();
|
|
530
|
+
const config: PluginConfig = {
|
|
531
|
+
presets: {
|
|
532
|
+
cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
533
|
+
powerful: { orchestrator: { model: 'openai/gpt-5.5' } },
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const manager = createPresetManager(ctx, config);
|
|
537
|
+
|
|
538
|
+
// Switch to cheap
|
|
539
|
+
const output1 = createOutput();
|
|
540
|
+
await manager.handleCommandExecuteBefore(
|
|
541
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
542
|
+
output1,
|
|
543
|
+
);
|
|
544
|
+
expect(getOutputText(output1)).toContain('Switched');
|
|
545
|
+
|
|
546
|
+
// List presets should now show cheap as active
|
|
547
|
+
const output2 = createOutput();
|
|
548
|
+
await manager.handleCommandExecuteBefore(
|
|
549
|
+
{ command: 'preset', sessionID: 's1', arguments: '' },
|
|
550
|
+
output2,
|
|
551
|
+
);
|
|
552
|
+
expect(getOutputText(output2)).toContain('cheap ← active');
|
|
553
|
+
|
|
554
|
+
// Switch to powerful
|
|
555
|
+
const output3 = createOutput();
|
|
556
|
+
await manager.handleCommandExecuteBefore(
|
|
557
|
+
{ command: 'preset', sessionID: 's1', arguments: 'powerful' },
|
|
558
|
+
output3,
|
|
559
|
+
);
|
|
560
|
+
expect(getOutputText(output3)).toContain('Switched to preset "powerful"');
|
|
561
|
+
|
|
562
|
+
// List should now show powerful as active
|
|
563
|
+
const output4 = createOutput();
|
|
564
|
+
await manager.handleCommandExecuteBefore(
|
|
565
|
+
{ command: 'preset', sessionID: 's1', arguments: '' },
|
|
566
|
+
output4,
|
|
567
|
+
);
|
|
568
|
+
expect(getOutputText(output4)).toContain('powerful ← active');
|
|
569
|
+
|
|
570
|
+
// Cleanup module state
|
|
571
|
+
setActiveRuntimePreset(null);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe('registerCommand', () => {
|
|
576
|
+
test('registers preset command when not present', () => {
|
|
577
|
+
const ctx = createMockContext();
|
|
578
|
+
const config: PluginConfig = {};
|
|
579
|
+
const manager = createPresetManager(ctx, config);
|
|
580
|
+
const opencodeConfig: Record<string, unknown> = {};
|
|
581
|
+
|
|
582
|
+
manager.registerCommand(opencodeConfig);
|
|
583
|
+
|
|
584
|
+
const command = (opencodeConfig.command as Record<string, unknown>)
|
|
585
|
+
.preset as { template: string; description: string };
|
|
586
|
+
expect(command).toBeDefined();
|
|
587
|
+
expect(command.template).toContain('presets');
|
|
588
|
+
expect(command.description).toContain('/preset');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('does not overwrite existing preset command', () => {
|
|
592
|
+
const ctx = createMockContext();
|
|
593
|
+
const config: PluginConfig = {};
|
|
594
|
+
const manager = createPresetManager(ctx, config);
|
|
595
|
+
const existing = { template: 'custom', description: 'custom' };
|
|
596
|
+
const opencodeConfig: Record<string, unknown> = {
|
|
597
|
+
command: { preset: existing },
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
manager.registerCommand(opencodeConfig);
|
|
601
|
+
|
|
602
|
+
expect((opencodeConfig.command as Record<string, unknown>).preset).toBe(
|
|
603
|
+
existing,
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe('preset switching stale state', () => {
|
|
609
|
+
test('reset updates for agents removed when switching presets', async () => {
|
|
610
|
+
const ctx = createMockContext();
|
|
611
|
+
const config: PluginConfig = {
|
|
612
|
+
presets: {
|
|
613
|
+
cheap: {
|
|
614
|
+
oracle: { model: 'cheap-model', temperature: 0.3 },
|
|
615
|
+
},
|
|
616
|
+
powerful: {
|
|
617
|
+
orchestrator: { model: 'powerful-model' },
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
agents: {
|
|
621
|
+
oracle: { model: 'baseline-model' },
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
const manager = createPresetManager(ctx, config);
|
|
625
|
+
const output1 = createOutput();
|
|
626
|
+
|
|
627
|
+
// Switch to cheap first
|
|
628
|
+
await manager.handleCommandExecuteBefore(
|
|
629
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
630
|
+
output1,
|
|
631
|
+
);
|
|
632
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
633
|
+
body: {
|
|
634
|
+
agent: {
|
|
635
|
+
oracle: { model: 'cheap-model', temperature: 0.3 },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Reset mock for next call
|
|
641
|
+
ctx.client.config.update.mockClear();
|
|
642
|
+
|
|
643
|
+
const output2 = createOutput();
|
|
644
|
+
await manager.handleCommandExecuteBefore(
|
|
645
|
+
{ command: 'preset', sessionID: 's1', arguments: 'powerful' },
|
|
646
|
+
output2,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// Second update should reset oracle to baseline and set orchestrator
|
|
650
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
651
|
+
body: {
|
|
652
|
+
agent: {
|
|
653
|
+
oracle: { model: 'baseline-model' },
|
|
654
|
+
orchestrator: { model: 'powerful-model' },
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Cleanup
|
|
660
|
+
setActiveRuntimePreset(null);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test('no reset updates when new preset covers same agents', async () => {
|
|
664
|
+
const ctx = createMockContext();
|
|
665
|
+
const config: PluginConfig = {
|
|
666
|
+
presets: {
|
|
667
|
+
cheap: {
|
|
668
|
+
oracle: { model: 'a' },
|
|
669
|
+
},
|
|
670
|
+
cheaper: {
|
|
671
|
+
oracle: { model: 'b' },
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
const manager = createPresetManager(ctx, config);
|
|
676
|
+
const output1 = createOutput();
|
|
677
|
+
|
|
678
|
+
// Switch to cheap first
|
|
679
|
+
await manager.handleCommandExecuteBefore(
|
|
680
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
681
|
+
output1,
|
|
682
|
+
);
|
|
683
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
684
|
+
body: {
|
|
685
|
+
agent: {
|
|
686
|
+
oracle: { model: 'a' },
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Reset mock for next call
|
|
692
|
+
ctx.client.config.update.mockClear();
|
|
693
|
+
|
|
694
|
+
const output2 = createOutput();
|
|
695
|
+
await manager.handleCommandExecuteBefore(
|
|
696
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheaper' },
|
|
697
|
+
output2,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
// Second update should only have oracle, no reset updates
|
|
701
|
+
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
702
|
+
body: {
|
|
703
|
+
agent: {
|
|
704
|
+
oracle: { model: 'b' },
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Cleanup
|
|
710
|
+
setActiveRuntimePreset(null);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test('preset state rolled back on config.update error', async () => {
|
|
714
|
+
const ctx = createMockContext();
|
|
715
|
+
ctx.client.config.update = mock(async () => {
|
|
716
|
+
throw new Error('Server unavailable');
|
|
717
|
+
});
|
|
718
|
+
const config: PluginConfig = {
|
|
719
|
+
presets: {
|
|
720
|
+
cheap: {
|
|
721
|
+
oracle: { model: 'a' },
|
|
722
|
+
},
|
|
723
|
+
expensive: {
|
|
724
|
+
oracle: { model: 'b' },
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
const manager = createPresetManager(ctx, config);
|
|
729
|
+
|
|
730
|
+
// Reset mock for successful switch
|
|
731
|
+
ctx.client.config.update = mock(async () => ({}));
|
|
732
|
+
|
|
733
|
+
// Switch to cheap successfully
|
|
734
|
+
const output1 = createOutput();
|
|
735
|
+
await manager.handleCommandExecuteBefore(
|
|
736
|
+
{ command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
737
|
+
output1,
|
|
738
|
+
);
|
|
739
|
+
expect(getActiveRuntimePreset()).toBe('cheap');
|
|
740
|
+
|
|
741
|
+
// Reset mock to throw error
|
|
742
|
+
ctx.client.config.update = mock(async () => {
|
|
743
|
+
throw new Error('Server unavailable');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Try to switch to expensive but it fails
|
|
747
|
+
const output2 = createOutput();
|
|
748
|
+
await manager.handleCommandExecuteBefore(
|
|
749
|
+
{ command: 'preset', sessionID: 's1', arguments: 'expensive' },
|
|
750
|
+
output2,
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
// Active preset should still be "cheap" after error
|
|
754
|
+
expect(getActiveRuntimePreset()).toBe('cheap');
|
|
755
|
+
expect(getOutputText(output2)).toContain('Failed to switch preset');
|
|
756
|
+
|
|
757
|
+
// Cleanup
|
|
758
|
+
setActiveRuntimePreset(null);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test('activePreset syncs from runtime-preset state on factory creation', async () => {
|
|
762
|
+
// Set runtime preset before creating manager
|
|
763
|
+
setActiveRuntimePreset('cheap');
|
|
764
|
+
|
|
765
|
+
const ctx = createMockContext();
|
|
766
|
+
const config: PluginConfig = {
|
|
767
|
+
presets: {
|
|
768
|
+
cheap: {
|
|
769
|
+
oracle: { model: 'a' },
|
|
770
|
+
},
|
|
771
|
+
powerful: {
|
|
772
|
+
oracle: { model: 'b' },
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// Create manager - should sync from module-level state
|
|
778
|
+
const manager = createPresetManager(ctx, config);
|
|
779
|
+
|
|
780
|
+
// List presets should show cheap as active
|
|
781
|
+
const output = createOutput();
|
|
782
|
+
await manager.handleCommandExecuteBefore(
|
|
783
|
+
{ command: 'preset', sessionID: 's1', arguments: '' },
|
|
784
|
+
output,
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const text = getOutputText(output);
|
|
788
|
+
expect(text).toContain('cheap ← active');
|
|
789
|
+
expect(text).toContain('powerful');
|
|
790
|
+
|
|
791
|
+
// Cleanup
|
|
792
|
+
setActiveRuntimePreset(null);
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
});
|