|
|
@@ -1,6 +1,10 @@
|
|
|
-import { describe, expect, mock, test } from "bun:test";
|
|
|
-import type { PluginConfig } from "../config";
|
|
|
-import { createPresetManager } from "./preset-manager";
|
|
|
+import { describe, expect, mock, test } from 'bun:test';
|
|
|
+import type { PluginConfig } from '../config';
|
|
|
+import {
|
|
|
+ getActiveRuntimePreset,
|
|
|
+ setActiveRuntimePreset,
|
|
|
+} from '../config/runtime-preset';
|
|
|
+import { createPresetManager } from './preset-manager';
|
|
|
|
|
|
function createMockContext() {
|
|
|
const configUpdate = mock(async () => ({}));
|
|
|
@@ -10,7 +14,7 @@ function createMockContext() {
|
|
|
update: configUpdate,
|
|
|
},
|
|
|
},
|
|
|
- directory: "/tmp/test",
|
|
|
+ directory: '/tmp/test',
|
|
|
} as any;
|
|
|
}
|
|
|
|
|
|
@@ -20,37 +24,37 @@ function createOutput() {
|
|
|
|
|
|
function getOutputText(output: ReturnType<typeof createOutput>): string {
|
|
|
return output.parts
|
|
|
- .filter((p) => p.type === "text")
|
|
|
- .map((p) => p.text ?? "")
|
|
|
- .join("\n");
|
|
|
+ .filter((p) => p.type === 'text')
|
|
|
+ .map((p) => p.text ?? '')
|
|
|
+ .join('\n');
|
|
|
}
|
|
|
|
|
|
-describe("createPresetManager", () => {
|
|
|
- describe("handleCommandExecuteBefore", () => {
|
|
|
- test("ignores non-preset commands", async () => {
|
|
|
+describe('createPresetManager', () => {
|
|
|
+ describe('handleCommandExecuteBefore', () => {
|
|
|
+ test('ignores non-preset commands', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "auto-continue", sessionID: "s1", arguments: "on" },
|
|
|
- output
|
|
|
+ { command: 'auto-continue', sessionID: 's1', arguments: 'on' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
expect(output.parts).toHaveLength(0);
|
|
|
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- test("lists available presets when no argument given", async () => {
|
|
|
+ test('lists available presets when no argument given', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
cheap: {
|
|
|
- orchestrator: { model: "anthropic/claude-3.5-haiku" },
|
|
|
+ orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
|
},
|
|
|
powerful: {
|
|
|
- orchestrator: { model: "openai/gpt-5.5" },
|
|
|
+ orchestrator: { model: 'openai/gpt-5.5' },
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
@@ -58,59 +62,59 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: '' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("cheap");
|
|
|
- expect(text).toContain("powerful");
|
|
|
+ expect(text).toContain('cheap');
|
|
|
+ expect(text).toContain('powerful');
|
|
|
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- test("lists presets with active marker when preset is set", async () => {
|
|
|
+ test('lists presets with active marker when preset is set', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
- preset: "cheap",
|
|
|
+ preset: 'cheap',
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
- powerful: { orchestrator: { model: "openai/gpt-5.5" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
+ powerful: { orchestrator: { model: 'openai/gpt-5.5' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: '' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("← active");
|
|
|
+ expect(text).toContain('← active');
|
|
|
});
|
|
|
|
|
|
- test("shows no-presets message when none configured", async () => {
|
|
|
+ test('shows no-presets message when none configured', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: '' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("No presets configured");
|
|
|
+ expect(text).toContain('No presets configured');
|
|
|
});
|
|
|
|
|
|
- test("switches preset and calls config.update", async () => {
|
|
|
+ test('switches preset and calls config.update', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
cheap: {
|
|
|
- orchestrator: { model: "anthropic/claude-3.5-haiku" },
|
|
|
- explorer: { model: "openai/gpt-5.4-mini" },
|
|
|
+ orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
|
+ explorer: { model: 'openai/gpt-5.4-mini' },
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
@@ -118,32 +122,32 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "cheap" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
expect(text).toContain('Switched to preset "cheap"');
|
|
|
- expect(text).toContain("orchestrator");
|
|
|
- expect(text).toContain("anthropic/claude-3.5-haiku");
|
|
|
- expect(text).toContain("explorer");
|
|
|
+ expect(text).toContain('orchestrator');
|
|
|
+ expect(text).toContain('anthropic/claude-3.5-haiku');
|
|
|
+ expect(text).toContain('explorer');
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledTimes(1);
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
- orchestrator: { model: "anthropic/claude-3.5-haiku" },
|
|
|
- explorer: { model: "openai/gpt-5.4-mini" },
|
|
|
+ orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
|
+ explorer: { model: 'openai/gpt-5.4-mini' },
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("passes temperature in config update", async () => {
|
|
|
+ test('passes temperature in config update', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
precise: {
|
|
|
- orchestrator: { model: "openai/o3", temperature: 0.1 },
|
|
|
+ orchestrator: { model: 'openai/o3', temperature: 0.1 },
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
@@ -151,27 +155,27 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "precise" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'precise' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
- orchestrator: { model: "openai/o3", temperature: 0.1 },
|
|
|
+ orchestrator: { model: 'openai/o3', temperature: 0.1 },
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("passes variant in config update", async () => {
|
|
|
+ test('passes variant in config update', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
thinker: {
|
|
|
oracle: {
|
|
|
- model: "anthropic/claude-sonnet-4-6",
|
|
|
- variant: "thinking",
|
|
|
+ model: 'anthropic/claude-sonnet-4-6',
|
|
|
+ variant: 'thinking',
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
@@ -180,83 +184,83 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "thinker" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
oracle: {
|
|
|
- model: "anthropic/claude-sonnet-4-6",
|
|
|
- variant: "thinking",
|
|
|
+ model: 'anthropic/claude-sonnet-4-6',
|
|
|
+ variant: 'thinking',
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("shows error for unknown preset name", async () => {
|
|
|
+ test('shows error for unknown preset name', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "nonexistent" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'nonexistent' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("not found");
|
|
|
- expect(text).toContain("cheap");
|
|
|
+ expect(text).toContain('not found');
|
|
|
+ expect(text).toContain('cheap');
|
|
|
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- test("shows error when no presets configured but argument given", async () => {
|
|
|
+ test('shows error when no presets configured but argument given', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "cheap" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("not found");
|
|
|
- expect(text).toContain("No presets configured");
|
|
|
+ expect(text).toContain('not found');
|
|
|
+ expect(text).toContain('No presets configured');
|
|
|
});
|
|
|
|
|
|
- test("handles config.update error gracefully", async () => {
|
|
|
+ test('handles config.update error gracefully', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
ctx.client.config.update = mock(async () => {
|
|
|
- throw new Error("Server unavailable");
|
|
|
+ throw new Error('Server unavailable');
|
|
|
});
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "cheap" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("Failed to switch preset");
|
|
|
- expect(text).toContain("Server unavailable");
|
|
|
+ expect(text).toContain('Failed to switch preset');
|
|
|
+ expect(text).toContain('Server unavailable');
|
|
|
});
|
|
|
|
|
|
- test("shows empty preset message when preset has no valid overrides", async () => {
|
|
|
+ test('shows empty preset message when preset has no valid overrides', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
@@ -269,24 +273,24 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "empty" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'empty' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("empty");
|
|
|
+ expect(text).toContain('empty');
|
|
|
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- test("forwards options field in config update", async () => {
|
|
|
+ test('forwards options field in config update', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
thinker: {
|
|
|
oracle: {
|
|
|
- model: "anthropic/claude-sonnet-4-6",
|
|
|
+ model: 'anthropic/claude-sonnet-4-6',
|
|
|
options: {
|
|
|
- thinking: { type: "enabled", budgetTokens: 10000 },
|
|
|
+ thinking: { type: 'enabled', budgetTokens: 10000 },
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
@@ -296,17 +300,17 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "thinker" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
oracle: {
|
|
|
- model: "anthropic/claude-sonnet-4-6",
|
|
|
+ model: 'anthropic/claude-sonnet-4-6',
|
|
|
options: {
|
|
|
- thinking: { type: "enabled", budgetTokens: 10000 },
|
|
|
+ thinking: { type: 'enabled', budgetTokens: 10000 },
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
@@ -314,19 +318,19 @@ describe("createPresetManager", () => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("trims whitespace from preset name argument", async () => {
|
|
|
+ test('trims whitespace from preset name argument', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: " cheap " },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: ' cheap ' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
@@ -334,53 +338,53 @@ describe("createPresetManager", () => {
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledTimes(1);
|
|
|
});
|
|
|
|
|
|
- test("shows suggestion for multi-word arguments", async () => {
|
|
|
+ test('shows suggestion for multi-word arguments', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "cheap powerful" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap powerful' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("cannot contain spaces");
|
|
|
- expect(text).toContain("/preset cheap");
|
|
|
+ expect(text).toContain('cannot contain spaces');
|
|
|
+ expect(text).toContain('/preset cheap');
|
|
|
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- test("catches tab-separated arguments", async () => {
|
|
|
+ test('catches tab-separated arguments', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "cheap\tpowerful" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap\tpowerful' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("cannot contain spaces");
|
|
|
+ expect(text).toContain('cannot contain spaces');
|
|
|
expect(ctx.client.config.update).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- test("skips agents with empty overrides in mixed preset", async () => {
|
|
|
+ test('skips agents with empty overrides in mixed preset', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
mixed: {
|
|
|
- orchestrator: { model: "anthropic/claude-3.5-haiku" },
|
|
|
+ orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
|
explorer: {},
|
|
|
oracle: { temperature: 0.3 },
|
|
|
},
|
|
|
@@ -390,8 +394,8 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "mixed" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'mixed' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
@@ -400,20 +404,20 @@ describe("createPresetManager", () => {
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
- orchestrator: { model: "anthropic/claude-3.5-haiku" },
|
|
|
+ orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
|
oracle: { temperature: 0.3 },
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("resolves array-form model to first entry", async () => {
|
|
|
+ test('resolves array-form model to first entry', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
fallback: {
|
|
|
orchestrator: {
|
|
|
- model: ["anthropic/claude-3.5-haiku", "openai/gpt-5.5"],
|
|
|
+ model: ['anthropic/claude-3.5-haiku', 'openai/gpt-5.5'],
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
@@ -422,8 +426,8 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "fallback" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'fallback' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
@@ -431,21 +435,21 @@ describe("createPresetManager", () => {
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
- orchestrator: { model: "anthropic/claude-3.5-haiku" },
|
|
|
+ orchestrator: { model: 'anthropic/claude-3.5-haiku' },
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("resolves array-form model with object entries", async () => {
|
|
|
+ test('resolves array-form model with object entries', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
thinker: {
|
|
|
oracle: {
|
|
|
model: [
|
|
|
- { id: "anthropic/claude-sonnet-4-6", variant: "thinking" },
|
|
|
- { id: "openai/o3" },
|
|
|
+ { id: 'anthropic/claude-sonnet-4-6', variant: 'thinking' },
|
|
|
+ { id: 'openai/o3' },
|
|
|
],
|
|
|
},
|
|
|
},
|
|
|
@@ -455,31 +459,31 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "thinker" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
body: {
|
|
|
agent: {
|
|
|
oracle: {
|
|
|
- model: "anthropic/claude-sonnet-4-6",
|
|
|
- variant: "thinking",
|
|
|
+ model: 'anthropic/claude-sonnet-4-6',
|
|
|
+ variant: 'thinking',
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- test("shows variant and options in switch summary", async () => {
|
|
|
+ test('shows variant and options in switch summary', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
thinker: {
|
|
|
oracle: {
|
|
|
- model: "anthropic/claude-sonnet-4-6",
|
|
|
- variant: "thinking",
|
|
|
- options: { thinking: { type: "enabled", budgetTokens: 10000 } },
|
|
|
+ model: 'anthropic/claude-sonnet-4-6',
|
|
|
+ variant: 'thinking',
|
|
|
+ options: { thinking: { type: 'enabled', budgetTokens: 10000 } },
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
@@ -488,21 +492,21 @@ describe("createPresetManager", () => {
|
|
|
const output = createOutput();
|
|
|
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "thinker" },
|
|
|
- output
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'thinker' },
|
|
|
+ output,
|
|
|
);
|
|
|
|
|
|
const text = getOutputText(output);
|
|
|
- expect(text).toContain("variant: thinking");
|
|
|
- expect(text).toContain("options: yes");
|
|
|
+ expect(text).toContain('variant: thinking');
|
|
|
+ expect(text).toContain('options: yes');
|
|
|
});
|
|
|
|
|
|
- test("tracks active preset after switch", async () => {
|
|
|
+ test('tracks active preset after switch', async () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {
|
|
|
presets: {
|
|
|
- cheap: { orchestrator: { model: "anthropic/claude-3.5-haiku" } },
|
|
|
- powerful: { orchestrator: { model: "openai/gpt-5.5" } },
|
|
|
+ cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
|
|
|
+ powerful: { orchestrator: { model: 'openai/gpt-5.5' } },
|
|
|
},
|
|
|
};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
@@ -510,39 +514,39 @@ describe("createPresetManager", () => {
|
|
|
// Switch to cheap
|
|
|
const output1 = createOutput();
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "cheap" },
|
|
|
- output1
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output1,
|
|
|
);
|
|
|
- expect(getOutputText(output1)).toContain("Switched");
|
|
|
+ expect(getOutputText(output1)).toContain('Switched');
|
|
|
|
|
|
// List presets should now show cheap as active
|
|
|
const output2 = createOutput();
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "" },
|
|
|
- output2
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: '' },
|
|
|
+ output2,
|
|
|
);
|
|
|
- expect(getOutputText(output2)).toContain("cheap ← active");
|
|
|
+ expect(getOutputText(output2)).toContain('cheap ← active');
|
|
|
|
|
|
// Switch to powerful
|
|
|
const output3 = createOutput();
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "powerful" },
|
|
|
- output3
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'powerful' },
|
|
|
+ output3,
|
|
|
);
|
|
|
expect(getOutputText(output3)).toContain('Switched to preset "powerful"');
|
|
|
|
|
|
// List should now show powerful as active
|
|
|
const output4 = createOutput();
|
|
|
await manager.handleCommandExecuteBefore(
|
|
|
- { command: "preset", sessionID: "s1", arguments: "" },
|
|
|
- output4
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: '' },
|
|
|
+ output4,
|
|
|
);
|
|
|
- expect(getOutputText(output4)).toContain("powerful ← active");
|
|
|
+ expect(getOutputText(output4)).toContain('powerful ← active');
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- describe("registerCommand", () => {
|
|
|
- test("registers preset command when not present", () => {
|
|
|
+ describe('registerCommand', () => {
|
|
|
+ test('registers preset command when not present', () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
@@ -553,15 +557,15 @@ describe("createPresetManager", () => {
|
|
|
const command = (opencodeConfig.command as Record<string, unknown>)
|
|
|
.preset as { template: string; description: string };
|
|
|
expect(command).toBeDefined();
|
|
|
- expect(command.template).toContain("presets");
|
|
|
- expect(command.description).toContain("/preset");
|
|
|
+ expect(command.template).toContain('presets');
|
|
|
+ expect(command.description).toContain('/preset');
|
|
|
});
|
|
|
|
|
|
- test("does not overwrite existing preset command", () => {
|
|
|
+ test('does not overwrite existing preset command', () => {
|
|
|
const ctx = createMockContext();
|
|
|
const config: PluginConfig = {};
|
|
|
const manager = createPresetManager(ctx, config);
|
|
|
- const existing = { template: "custom", description: "custom" };
|
|
|
+ const existing = { template: 'custom', description: 'custom' };
|
|
|
const opencodeConfig: Record<string, unknown> = {
|
|
|
command: { preset: existing },
|
|
|
};
|
|
|
@@ -569,8 +573,196 @@ describe("createPresetManager", () => {
|
|
|
manager.registerCommand(opencodeConfig);
|
|
|
|
|
|
expect((opencodeConfig.command as Record<string, unknown>).preset).toBe(
|
|
|
- existing
|
|
|
+ existing,
|
|
|
);
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+ describe('preset switching stale state', () => {
|
|
|
+ test('reset updates for agents removed when switching presets', async () => {
|
|
|
+ const ctx = createMockContext();
|
|
|
+ const config: PluginConfig = {
|
|
|
+ presets: {
|
|
|
+ cheap: {
|
|
|
+ oracle: { model: 'cheap-model', temperature: 0.3 },
|
|
|
+ },
|
|
|
+ powerful: {
|
|
|
+ orchestrator: { model: 'powerful-model' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ agents: {
|
|
|
+ oracle: { model: 'baseline-model' },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ const manager = createPresetManager(ctx, config);
|
|
|
+ const output1 = createOutput();
|
|
|
+
|
|
|
+ // Switch to cheap first
|
|
|
+ await manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output1,
|
|
|
+ );
|
|
|
+ expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
+ body: {
|
|
|
+ agent: {
|
|
|
+ oracle: { model: 'cheap-model', temperature: 0.3 },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // Reset mock for next call
|
|
|
+ ctx.client.config.update.mockClear();
|
|
|
+
|
|
|
+ const output2 = createOutput();
|
|
|
+ await manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'powerful' },
|
|
|
+ output2,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Second update should reset oracle to baseline and set orchestrator
|
|
|
+ expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
+ body: {
|
|
|
+ agent: {
|
|
|
+ oracle: { model: 'baseline-model' },
|
|
|
+ orchestrator: { model: 'powerful-model' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // Cleanup
|
|
|
+ setActiveRuntimePreset(null);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('no reset updates when new preset covers same agents', async () => {
|
|
|
+ const ctx = createMockContext();
|
|
|
+ const config: PluginConfig = {
|
|
|
+ presets: {
|
|
|
+ cheap: {
|
|
|
+ oracle: { model: 'a' },
|
|
|
+ },
|
|
|
+ cheaper: {
|
|
|
+ oracle: { model: 'b' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ const manager = createPresetManager(ctx, config);
|
|
|
+ const output1 = createOutput();
|
|
|
+
|
|
|
+ // Switch to cheap first
|
|
|
+ await manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output1,
|
|
|
+ );
|
|
|
+ expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
+ body: {
|
|
|
+ agent: {
|
|
|
+ oracle: { model: 'a' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // Reset mock for next call
|
|
|
+ ctx.client.config.update.mockClear();
|
|
|
+
|
|
|
+ const output2 = createOutput();
|
|
|
+ await manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheaper' },
|
|
|
+ output2,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Second update should only have oracle, no reset updates
|
|
|
+ expect(ctx.client.config.update).toHaveBeenCalledWith({
|
|
|
+ body: {
|
|
|
+ agent: {
|
|
|
+ oracle: { model: 'b' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // Cleanup
|
|
|
+ setActiveRuntimePreset(null);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('preset state rolled back on config.update error', async () => {
|
|
|
+ const ctx = createMockContext();
|
|
|
+ ctx.client.config.update = mock(async () => {
|
|
|
+ throw new Error('Server unavailable');
|
|
|
+ });
|
|
|
+ const config: PluginConfig = {
|
|
|
+ presets: {
|
|
|
+ cheap: {
|
|
|
+ oracle: { model: 'a' },
|
|
|
+ },
|
|
|
+ expensive: {
|
|
|
+ oracle: { model: 'b' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ const manager = createPresetManager(ctx, config);
|
|
|
+
|
|
|
+ // Reset mock for successful switch
|
|
|
+ ctx.client.config.update = mock(async () => ({}));
|
|
|
+
|
|
|
+ // Switch to cheap successfully
|
|
|
+ const output1 = createOutput();
|
|
|
+ await manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'cheap' },
|
|
|
+ output1,
|
|
|
+ );
|
|
|
+ expect(getActiveRuntimePreset()).toBe('cheap');
|
|
|
+
|
|
|
+ // Reset mock to throw error
|
|
|
+ ctx.client.config.update = mock(async () => {
|
|
|
+ throw new Error('Server unavailable');
|
|
|
+ });
|
|
|
+
|
|
|
+ // Try to switch to expensive but it fails
|
|
|
+ const output2 = createOutput();
|
|
|
+ await manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: 'expensive' },
|
|
|
+ output2,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Active preset should still be "cheap" after error
|
|
|
+ expect(getActiveRuntimePreset()).toBe('cheap');
|
|
|
+ expect(getOutputText(output2)).toContain('Failed to switch preset');
|
|
|
+
|
|
|
+ // Cleanup
|
|
|
+ setActiveRuntimePreset(null);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('activePreset syncs from runtime-preset state on factory creation', () => {
|
|
|
+ // Set runtime preset before creating manager
|
|
|
+ setActiveRuntimePreset('cheap');
|
|
|
+
|
|
|
+ const ctx = createMockContext();
|
|
|
+ const config: PluginConfig = {
|
|
|
+ presets: {
|
|
|
+ cheap: {
|
|
|
+ oracle: { model: 'a' },
|
|
|
+ },
|
|
|
+ powerful: {
|
|
|
+ oracle: { model: 'b' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // Create manager - should sync from module-level state
|
|
|
+ const manager = createPresetManager(ctx, config);
|
|
|
+
|
|
|
+ // List presets should show cheap as active
|
|
|
+ const output = createOutput();
|
|
|
+ manager.handleCommandExecuteBefore(
|
|
|
+ { command: 'preset', sessionID: 's1', arguments: '' },
|
|
|
+ output,
|
|
|
+ );
|
|
|
+
|
|
|
+ const text = getOutputText(output);
|
|
|
+ expect(text).toContain('cheap ← active');
|
|
|
+ expect(text).toContain('powerful');
|
|
|
+
|
|
|
+ // Cleanup
|
|
|
+ setActiveRuntimePreset(null);
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|