Files
openchamber/packages/ui/src/stores/useConfigStore.test.ts
T
Bohdan Triapitsyn 2740c7b3a2 feat(projects): pin a thinking level next to a project's model
A project could pin the model new chats start on, but not the level to
run it at: the default cascade dropped any variant as soon as a project
model won, and only ever considered the global one — which belongs to
the global model.

Projects now carry `defaultVariant` alongside `defaultModel`, stored and
sanitized only next to that model, and the cascade passes it through.
Both controls sit in one "Defaults for new chats" group laid out like the
Sessions defaults, and the level appears only for models that offer them.
2026-08-22 20:31:31 +03:00

1122 lines
41 KiB
TypeScript

import { beforeEach, describe, expect, mock, test } from 'bun:test';
import type { Agent } from '@opencode-ai/sdk/v2';
const DIRECTORY = '/workspace/project';
const OTHER_DIRECTORY = '/workspace/other';
const STORAGE_KEY = 'config-store';
type TestAgent = { name: string; mode?: string; hidden?: boolean; model?: { providerID?: string; modelID?: string }; variant?: string };
let storage = new Map<string, string>();
let liveProviderId = 'live';
let liveProviderIdsByDirectory = new Map<string, string>();
let liveProviderVariants: Record<string, Record<string, unknown>> | undefined;
let getProvidersCalls = 0;
let getConfigCalls = 0;
let listAgentsCalls = 0;
let liveAgents: TestAgent[] = [];
let listAgentsImpl: ((directory?: string | null) => Promise<TestAgent[]>) | null = null;
let withDirectoryCalls: Array<string | null> = [];
let currentFetchDirectory: string | null = DIRECTORY;
let configListener: ((event: { scopes: string[]; source?: string; timestamp: number }) => void | Promise<void>) | null = null;
const makeStorage = (): Storage => ({
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => {
storage.set(key, value);
},
removeItem: (key: string) => {
storage.delete(key);
},
clear: () => {
storage.clear();
},
key: (index: number) => Array.from(storage.keys())[index] ?? null,
get length() {
return storage.size;
},
}) as Storage;
const provider = (id: string, modelId = `${id}-model`, variants?: Record<string, Record<string, unknown>>) => ({
id,
name: id,
source: 'config' as const,
env: [],
options: {},
models: [
{
id: modelId,
name: modelId,
providerID: id,
api: { id: 'chat', url: '', npm: '' },
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 0, output: 0 },
options: {},
release_date: '',
status: 'active' as const,
headers: {},
attachment: false,
reasoning: false,
temperature: true,
tool_call: true,
...(variants ? { variants } : {}),
},
],
});
const providerResponse = (id: string, modelId = `${id}-model`, variants?: Record<string, Record<string, unknown>>) => ({
id,
name: id,
source: 'config' as const,
env: [],
options: {},
models: {
[modelId]: {
id: modelId,
name: modelId,
providerID: id,
api: { id: 'chat', url: '', npm: '' },
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 0, output: 0 },
options: {},
release_date: '',
status: 'active' as const,
headers: {},
attachment: false,
reasoning: false,
temperature: true,
tool_call: true,
...(variants ? { variants } : {}),
},
},
});
const testAgent = (name: string, options?: Partial<TestAgent>): Agent => ({
name,
mode: options?.mode ?? 'primary',
hidden: options?.hidden,
model: options?.model,
variant: options?.variant,
permission: {},
options: {},
}) as Agent;
const deferred = <T,>() => {
let resolve!: (value: T) => void;
let reject!: (error: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
mock.module('@/stores/utils/safeStorage', () => ({
getDeferredSafeStorage: () => makeStorage(),
getSafeStorage: () => makeStorage(),
createDeferredSafeJSONStorage: () => {
const testStorage = makeStorage();
return {
getItem: (name: string) => {
const value = testStorage.getItem(name);
return value === null ? null : JSON.parse(value);
},
setItem: (name: string, value: unknown) => {
testStorage.setItem(name, JSON.stringify(value));
},
removeItem: (name: string) => {
testStorage.removeItem(name);
},
};
},
}));
mock.module('@/stores/useProjectsStore', () => ({
useProjectsStore: {
getState: () => ({
activeProjectId: 'project',
projects: [
{ id: 'project', path: DIRECTORY, label: 'Project' },
{ id: 'other', path: OTHER_DIRECTORY, label: 'Other' },
],
}),
},
}));
mock.module('@/lib/opencode/client', () => ({
opencodeClient: {
setDirectory: mock(() => undefined),
getDirectory: mock(() => DIRECTORY),
checkHealth: mock(async () => true),
withDirectory: mock(async (directory: string | null, callback: () => Promise<unknown>) => {
withDirectoryCalls.push(directory);
const previous = currentFetchDirectory;
currentFetchDirectory = directory;
try {
return await callback();
} finally {
currentFetchDirectory = previous;
}
}),
getProviders: mock(async () => {
getProvidersCalls += 1;
const id = liveProviderIdsByDirectory.get(currentFetchDirectory ?? '') ?? liveProviderId;
return { providers: [providerResponse(id, `${id}-model`, liveProviderVariants)], default: { default: id } };
}),
getProvidersForConfig: mock(async (directory?: string | null) => {
getProvidersCalls += 1;
const id = liveProviderIdsByDirectory.get(directory ?? '') ?? liveProviderId;
return { providers: [providerResponse(id, `${id}-model`, liveProviderVariants)], default: { default: id } };
}),
listAgents: mock(async (directory?: string | null) => {
listAgentsCalls += 1;
const impl = listAgentsImpl as ((directory?: string | null) => Promise<TestAgent[]>) | null;
return impl ? impl(directory) : liveAgents;
}),
getConfig: mock(async () => {
getConfigCalls += 1;
return {};
}),
clearConfigCache: mock(() => undefined),
},
}));
mock.module('@/contexts/runtimeAPIRegistry', () => ({
getRegisteredRuntimeAPIs: mock(() => null),
}));
mock.module('@/lib/runtime-fetch', () => ({
runtimeFetch: mock(async () => new Response(JSON.stringify({}), {
headers: { 'Content-Type': 'application/json' },
})),
}));
mock.module('@/lib/persistence', () => ({
updateDesktopSettings: mock(async () => undefined),
}));
mock.module('@/lib/startupTrace', () => ({
markStartupTrace: mock(() => undefined),
measureStartupTrace: mock(async (_name: string, callback: () => Promise<unknown>) => callback()),
}));
mock.module('@/lib/configSync', () => ({
emitConfigChange: mock(() => undefined),
scopeMatches: mock((event: { scopes: string[] }, scope: string) => event.scopes.includes('all') || event.scopes.includes(scope)),
subscribeToConfigChanges: mock((listener: typeof configListener) => {
configListener = listener;
return () => {
if (configListener === listener) {
configListener = null;
}
};
}),
}));
const { useConfigStore } = await import('./useConfigStore');
const { emitSyncConfigChanged, setSyncRefs } = await import('@/sync/sync-refs');
const { useSelectionStore } = await import('@/sync/selection-store');
const { useSessionUIStore } = await import('@/sync/session-ui-store');
describe('useConfigStore provider persistence', () => {
beforeEach(() => {
storage = new Map<string, string>();
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: makeStorage(),
});
liveProviderId = 'live';
liveProviderIdsByDirectory = new Map<string, string>();
liveProviderVariants = undefined;
getProvidersCalls = 0;
getConfigCalls = 0;
listAgentsCalls = 0;
liveAgents = [];
listAgentsImpl = null;
withDirectoryCalls = [];
currentFetchDirectory = DIRECTORY;
setSyncRefs({} as never, { children: new Map(), getState: () => undefined } as never, DIRECTORY);
useSelectionStore.setState({
sessionModelSelections: new Map(),
sessionAgentSelections: new Map(),
sessionAgentModelSelections: new Map(),
lastUsedProvider: null,
});
useSessionUIStore.setState({ currentSessionId: null });
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
directoryScoped: {},
providers: [],
defaultProviders: {},
currentProviderId: '',
currentModelId: '',
currentVariant: undefined,
selectedProviderId: '',
currentAgentName: undefined,
agents: [],
agentModelSelections: {},
opencodeDefaultAgent: undefined,
opencodeDefaultModel: undefined,
selectionSource: 'auto',
isConnected: true,
isInitialized: false,
});
});
test('hydrates persisted provider snapshots for instant paint, then refreshes to live data', async () => {
storage.set(STORAGE_KEY, JSON.stringify({
state: {
activeDirectoryKey: DIRECTORY,
directoryScoped: {
[DIRECTORY]: {
providers: [provider('stale')],
agents: [{ name: 'build', mode: 'primary' }],
currentProviderId: 'stale',
currentModelId: 'stale-model',
currentAgentName: 'build',
selectedProviderId: 'stale',
agentModelSelections: { build: { providerId: 'stale', modelId: 'stale-model' } },
defaultProviders: { default: 'stale' },
},
[OTHER_DIRECTORY]: {
providers: [provider('other-stale')],
agents: [{ name: 'review', mode: 'primary' }],
currentProviderId: 'other-stale',
currentModelId: 'other-stale-model',
currentAgentName: 'review',
selectedProviderId: 'other-stale',
agentModelSelections: {},
defaultProviders: { default: 'other-stale' },
},
},
currentProviderId: 'stale',
currentModelId: 'stale-model',
selectedProviderId: 'stale',
defaultProviders: { default: 'stale' },
},
version: 0,
}));
await useConfigStore.persist.rehydrate();
// Stale-while-revalidate: the persisted snapshot is hydrated as-is so the
// pickers can paint instantly on cold start, instead of being stripped to empty.
const hydrated = useConfigStore.getState();
expect(hydrated.providers.map((entry) => entry.id)).toEqual(['stale']);
expect(hydrated.defaultProviders).toEqual({ default: 'stale' });
expect(hydrated.directoryScoped[DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['stale']);
expect(hydrated.directoryScoped[DIRECTORY]?.defaultProviders).toEqual({ default: 'stale' });
expect(hydrated.directoryScoped[DIRECTORY]?.agents).toEqual([{ name: 'build', mode: 'primary' }]);
expect(hydrated.directoryScoped[DIRECTORY]?.currentAgentName).toBe('build');
expect(hydrated.directoryScoped[OTHER_DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['other-stale']);
liveProviderId = 'fresh';
await hydrated.initializeApp();
const reloaded = useConfigStore.getState();
expect(getProvidersCalls).toBe(1);
expect(reloaded.providers.map((entry) => entry.id)).toEqual(['fresh']);
expect(reloaded.directoryScoped[DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['fresh']);
expect(reloaded.currentProviderId).toBe('fresh');
expect(reloaded.currentModelId).toBe('fresh-model');
});
test('provider config events refresh all known directory provider caches immediately', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('active-stale')],
defaultProviders: { default: 'active-stale' },
currentProviderId: 'active-stale',
currentModelId: 'active-stale-model',
selectedProviderId: 'active-stale',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('active-stale')],
agents: [],
currentProviderId: 'active-stale',
currentModelId: 'active-stale-model',
currentAgentName: undefined,
selectedProviderId: 'active-stale',
agentModelSelections: {},
defaultProviders: { default: 'active-stale' },
},
[OTHER_DIRECTORY]: {
providers: [provider('inactive-cached')],
agents: [],
currentProviderId: 'inactive-cached',
currentModelId: 'inactive-cached-model',
currentAgentName: undefined,
selectedProviderId: 'inactive-cached',
agentModelSelections: {},
defaultProviders: { default: 'inactive-cached' },
},
},
});
liveProviderIdsByDirectory = new Map([
[DIRECTORY, 'active-live'],
[OTHER_DIRECTORY, 'inactive-live'],
]);
expect(configListener).not.toBeNull();
await configListener?.({ scopes: ['providers'], timestamp: Date.now() });
const state = useConfigStore.getState();
expect(getProvidersCalls).toBe(2);
expect(state.directoryScoped[DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['active-live']);
expect(state.directoryScoped[OTHER_DIRECTORY]?.providers.map((entry) => entry.id)).toEqual(['inactive-live']);
expect(state.directoryScoped[OTHER_DIRECTORY]?.defaultProviders).toEqual({ default: 'inactive-live' });
});
test('provider reload preserves a valid current variant', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
currentProviderId: 'live',
currentModelId: 'live-model',
currentVariant: 'fast',
selectedProviderId: 'live',
settingsDefaultVariant: 'slow',
directoryScoped: {},
});
liveProviderId = 'live';
liveProviderVariants = { fast: {}, slow: {} };
await useConfigStore.getState().loadProviders({ source: 'test:variant' });
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('live');
expect(state.currentModelId).toBe('live-model');
expect(state.currentVariant).toBe('fast');
});
test('the settings provider selection survives a refresh that no longer lists it', async () => {
// Plugin-registered providers vanish from the list while OpenCode restarts.
// A refresh in that window used to move the user to another provider while
// they were reading or editing the one they picked.
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
currentProviderId: 'live',
currentModelId: 'live-model',
selectedProviderId: 'plugin-provider',
directoryScoped: {},
});
liveProviderId = 'live';
await useConfigStore.getState().loadProviders({ source: 'test:missing-selection' });
const state = useConfigStore.getState();
expect(state.providers.map((entry) => entry.id)).toEqual(['live']);
expect(state.selectedProviderId).toBe('plugin-provider');
expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('plugin-provider');
});
test('an empty settings provider selection is filled from the refreshed list', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
currentProviderId: '',
currentModelId: '',
selectedProviderId: '',
directoryScoped: {},
});
liveProviderId = 'live';
await useConfigStore.getState().loadProviders({ source: 'test:empty-selection' });
expect(useConfigStore.getState().selectedProviderId).toBe('live');
});
test('changing the chat provider leaves the settings provider selection alone', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('anthropic'), provider('openai')],
currentProviderId: 'anthropic',
currentModelId: 'anthropic-model',
selectedProviderId: 'openai',
directoryScoped: {},
});
useConfigStore.getState().setProvider('anthropic');
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('anthropic');
expect(state.selectedProviderId).toBe('openai');
expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('openai');
});
test('provider reload preserves the add-provider sentinel selection', async () => {
// The user has opened the "Add provider" form, which sets selectedProviderId
// to the sentinel. A background provider refresh must not navigate them away
// (and discard their unsaved input) just because the sentinel is not a real
// provider id. See issue #1765.
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
currentProviderId: 'live',
currentModelId: 'live-model',
selectedProviderId: '__add_provider__',
directoryScoped: {},
});
liveProviderId = 'live';
await useConfigStore.getState().loadProviders({ source: 'test:add-provider' });
expect(useConfigStore.getState().selectedProviderId).toBe('__add_provider__');
});
test('add-provider sentinel is not persisted as a stable provider selection', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
currentProviderId: 'live',
currentModelId: 'live-model',
selectedProviderId: '__add_provider__',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('live')],
agents: [],
currentProviderId: 'live',
currentModelId: 'live-model',
currentAgentName: undefined,
selectedProviderId: '__add_provider__',
agentModelSelections: {},
defaultProviders: { default: 'live' },
},
},
});
const persisted = JSON.parse(storage.get(STORAGE_KEY) ?? '{}');
expect(persisted.state.selectedProviderId).toBe('');
expect(persisted.state.directoryScoped[DIRECTORY].selectedProviderId).toBe('');
});
test('setAgent applies settings default variant for an agent configured model', () => {
useSessionUIStore.setState({ currentSessionId: 'ses_agent_default_variant' });
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5', { low: {}, high: {} })],
agents: [testAgent('plan', { model: { providerID: 'openai', modelID: 'gpt-5.5' } })],
settingsDefaultVariant: 'high',
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentVariant: undefined,
directoryScoped: {},
});
useConfigStore.getState().setAgent('plan');
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('openai');
expect(state.currentModelId).toBe('gpt-5.5');
expect(state.currentVariant).toBe('high');
expect(state.directoryScoped[DIRECTORY]?.currentVariant).toBe('high');
});
test('setAgent prefers saved and agent variants before settings default', () => {
const sessionId = 'ses_agent_saved_variant';
useSessionUIStore.setState({ currentSessionId: sessionId });
useSelectionStore.getState().saveAgentModelVariantForSession(sessionId, 'plan', 'openai', 'gpt-5.5', 'low');
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5', { low: {}, medium: {}, high: {} })],
agents: [testAgent('plan', {
model: { providerID: 'openai', modelID: 'gpt-5.5' },
variant: 'medium',
})],
settingsDefaultVariant: 'high',
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentVariant: undefined,
directoryScoped: {},
});
useConfigStore.getState().setAgent('plan');
expect(useConfigStore.getState().currentVariant).toBe('low');
useSelectionStore.getState().saveAgentModelVariantForSession(sessionId, 'plan', 'openai', 'gpt-5.5', undefined);
useConfigStore.setState({ currentVariant: undefined, directoryScoped: {} });
useConfigStore.getState().setAgent('plan');
expect(useConfigStore.getState().currentVariant).toBe('medium');
});
test('setAgent applies settings default variant for a saved session agent model', () => {
const sessionId = 'ses_existing_agent_model_default_variant';
useSessionUIStore.setState({ currentSessionId: sessionId });
useSelectionStore.getState().saveAgentModelForSession(sessionId, 'plan', 'openai', 'gpt-5.5');
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5', { low: {}, high: {} })],
agents: [testAgent('plan')],
settingsDefaultVariant: 'high',
currentProviderId: 'other',
currentModelId: 'other-model',
currentVariant: undefined,
directoryScoped: {},
});
useConfigStore.getState().setAgent('plan');
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('openai');
expect(state.currentModelId).toBe('gpt-5.5');
expect(state.currentVariant).toBe('high');
});
test('[issue-2404] setAgent keeps session model override over agent default model', () => {
// Custom agent default is model-a; user manually overrode to model-b for this session.
// Re-applying setAgent (e.g. after delegated subtask completion rematerializes the
// parent) must keep model-b rather than resetting to the agent pin.
const sessionId = 'ses_2404_model_override';
const multiModelProvider = {
...provider('provider', 'model-a'),
models: [
provider('provider', 'model-a').models[0],
provider('provider', 'model-b').models[0],
],
};
useSessionUIStore.setState({ currentSessionId: sessionId });
useSelectionStore.getState().saveSessionModelSelection(sessionId, 'provider', 'model-b');
useSelectionStore.getState().saveAgentModelForSession(sessionId, 'custom-agent', 'provider', 'model-b');
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [multiModelProvider],
agents: [testAgent('custom-agent', { model: { providerID: 'provider', modelID: 'model-a' } })],
currentProviderId: 'provider',
currentModelId: 'model-b',
currentAgentName: 'custom-agent',
selectionSource: 'manual',
currentVariant: undefined,
directoryScoped: {},
});
useConfigStore.getState().setAgent('custom-agent');
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('provider');
expect(state.currentModelId).toBe('model-b');
expect(useSelectionStore.getState().getAgentModelForSession(sessionId, 'custom-agent')).toEqual({
providerId: 'provider',
modelId: 'model-b',
});
});
test('[issue-2404] setAgent uses agent default when no session override exists', () => {
const sessionId = 'ses_2404_agent_default';
const multiModelProvider = {
...provider('provider', 'model-a'),
models: [
provider('provider', 'model-a').models[0],
provider('provider', 'model-b').models[0],
],
};
useSessionUIStore.setState({ currentSessionId: sessionId });
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [multiModelProvider],
agents: [testAgent('custom-agent', { model: { providerID: 'provider', modelID: 'model-a' } })],
currentProviderId: 'provider',
currentModelId: 'model-b',
currentAgentName: undefined,
selectionSource: 'auto',
currentVariant: undefined,
directoryScoped: {},
});
useConfigStore.getState().setAgent('custom-agent');
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('provider');
expect(state.currentModelId).toBe('model-a');
});
test('loadAgents does not fetch OpenCode config directly', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5')],
agents: [],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: undefined,
selectedProviderId: 'openai',
agentModelSelections: {},
defaultProviders: {},
selectionSource: 'auto',
},
},
});
liveAgents = [testAgent('build')];
await useConfigStore.getState().loadAgents({ directory: DIRECTORY, source: 'test:noConfigFetch' });
expect(listAgentsCalls).toBe(1);
expect(getConfigCalls).toBe(0);
});
test('a project default carries its own thinking level', async () => {
// The project pins a model plus the level to run it at. Before, the level
// was dropped and only the global settings variant was ever considered —
// and that one belongs to the global model, not this project's.
const projectProvider = provider('anthropic', 'claude-opus-5', { high: {}, low: {} });
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [projectProvider],
agents: [testAgent('build')],
currentProviderId: '',
currentModelId: '',
currentVariant: undefined,
settingsDefaultModel: undefined,
settingsDefaultVariant: 'low',
selectionSource: 'auto',
directoryScoped: {},
});
useConfigStore.getState().applyDefaultModelAgentSelection({
projectDefaultModel: 'anthropic/claude-opus-5',
projectDefaultVariant: 'high',
});
const state = useConfigStore.getState();
expect(state.currentProviderId).toBe('anthropic');
expect(state.currentModelId).toBe('claude-opus-5');
expect(state.currentVariant).toBe('high');
});
test('a thinking level the project model does not offer is ignored', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('anthropic', 'claude-opus-5')],
agents: [testAgent('build')],
currentProviderId: '',
currentModelId: '',
currentVariant: undefined,
settingsDefaultModel: undefined,
settingsDefaultVariant: undefined,
selectionSource: 'auto',
directoryScoped: {},
});
useConfigStore.getState().applyDefaultModelAgentSelection({
projectDefaultModel: 'anthropic/claude-opus-5',
projectDefaultVariant: 'high',
});
expect(useConfigStore.getState().currentVariant).toBe(undefined);
});
test('manual selection survives an in-flight loadAgents refresh', async () => {
const pendingAgents = deferred<TestAgent[]>();
listAgentsImpl = async () => pendingAgents.promise;
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('manual'), provider('default')],
agents: [testAgent('build')],
currentProviderId: 'default',
currentModelId: 'default-model',
currentAgentName: 'build',
selectedProviderId: 'default',
selectionSource: 'auto',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('manual'), provider('default')],
agents: [testAgent('build')],
currentProviderId: 'default',
currentModelId: 'default-model',
currentAgentName: 'build',
selectedProviderId: 'default',
agentModelSelections: {},
defaultProviders: {},
selectionSource: 'auto',
},
},
});
const load = useConfigStore.getState().loadAgents({ directory: DIRECTORY, source: 'test:manualRace' });
useConfigStore.setState((state) => ({
currentProviderId: 'manual',
currentModelId: 'manual-model',
currentAgentName: 'manual-agent',
selectedProviderId: 'manual',
selectionSource: 'manual',
directoryScoped: {
...state.directoryScoped,
[DIRECTORY]: {
...state.directoryScoped[DIRECTORY],
currentProviderId: 'manual',
currentModelId: 'manual-model',
currentAgentName: 'manual-agent',
selectedProviderId: 'manual',
selectionSource: 'manual',
},
},
}));
pendingAgents.resolve([
testAgent('build', { model: { providerID: 'default', modelID: 'default-model' } }),
testAgent('manual-agent'),
]);
await load;
const state = useConfigStore.getState();
expect(state.currentAgentName).toBe('manual-agent');
expect(state.currentProviderId).toBe('manual');
expect(state.currentModelId).toBe('manual-model');
expect(state.selectionSource).toBe('manual');
});
test('worktree sync config applies to the project-scoped snapshot', () => {
const worktree = '/workspace/project-worktree';
storage.set('oc.worktreeProjectMap', JSON.stringify({ [worktree]: DIRECTORY }));
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'build',
selectedProviderId: 'openai',
selectionSource: 'auto',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'build',
selectedProviderId: 'openai',
agentModelSelections: {},
defaultProviders: {},
selectionSource: 'auto',
},
},
});
emitSyncConfigChanged(worktree, { default_agent: 'review', model: 'openai/gpt-5.5' });
const state = useConfigStore.getState();
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultAgent).toBe('review');
expect(state.directoryScoped[worktree]).toBe(undefined);
expect(state.currentAgentName).toBe('review');
});
test('sync config defaults do not close the add-provider settings flow', () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5'), provider('anthropic', 'claude')],
agents: [
testAgent('build', { model: { providerID: 'anthropic', modelID: 'claude' } }),
testAgent('review', { model: { providerID: 'openai', modelID: 'gpt-5.5' } }),
],
currentProviderId: 'anthropic',
currentModelId: 'claude',
currentAgentName: 'build',
selectedProviderId: '__add_provider__',
selectionSource: 'auto',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5'), provider('anthropic', 'claude')],
agents: [
testAgent('build', { model: { providerID: 'anthropic', modelID: 'claude' } }),
testAgent('review', { model: { providerID: 'openai', modelID: 'gpt-5.5' } }),
],
currentProviderId: 'anthropic',
currentModelId: 'claude',
currentAgentName: 'build',
selectedProviderId: '__add_provider__',
agentModelSelections: {},
defaultProviders: {},
selectionSource: 'auto',
},
},
});
emitSyncConfigChanged(DIRECTORY, { default_agent: 'review', model: 'openai/gpt-5.5' });
const state = useConfigStore.getState();
expect(state.currentAgentName).toBe('review');
expect(state.currentProviderId).toBe('openai');
expect(state.currentModelId).toBe('gpt-5.5');
expect(state.selectedProviderId).toBe('__add_provider__');
expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('__add_provider__');
});
test('duplicate sync config event is a no-op when defaults and selection are unchanged', () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'review',
selectedProviderId: 'openai',
opencodeDefaultAgent: 'review',
opencodeDefaultModel: 'openai/gpt-5.5',
selectionSource: 'auto',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'review',
selectedProviderId: 'openai',
agentModelSelections: {},
defaultProviders: {},
opencodeDefaultAgent: 'review',
opencodeDefaultModel: 'openai/gpt-5.5',
selectionSource: 'auto',
},
},
});
let updates = 0;
const unsubscribe = useConfigStore.subscribe(() => {
updates += 1;
});
emitSyncConfigChanged(DIRECTORY, { default_agent: 'review', model: 'openai/gpt-5.5' });
unsubscribe();
expect(updates).toBe(0);
});
test('project loadAgents preserves defaults previously applied from a worktree config event', async () => {
const worktree = '/workspace/project-worktree';
storage.set('oc.worktreeProjectMap', JSON.stringify({ [worktree]: DIRECTORY }));
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'build',
selectedProviderId: 'openai',
selectionSource: 'auto',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'build',
selectedProviderId: 'openai',
agentModelSelections: {},
defaultProviders: {},
selectionSource: 'auto',
},
},
});
liveAgents = [testAgent('build'), testAgent('review')];
emitSyncConfigChanged(worktree, { default_agent: 'review', model: 'openai/gpt-5.5' });
await useConfigStore.getState().loadAgents({ directory: DIRECTORY, source: 'test:preserveWorktreeDefaults' });
const state = useConfigStore.getState();
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultAgent).toBe('review');
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultModel).toBe('openai/gpt-5.5');
expect(state.opencodeDefaultAgent).toBe('review');
expect(state.opencodeDefaultModel).toBe('openai/gpt-5.5');
});
test('in-flight loadAgents does not restore defaults cleared by a sync config event', async () => {
const pendingAgents = deferred<TestAgent[]>();
listAgentsImpl = async () => pendingAgents.promise;
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'review',
selectedProviderId: 'openai',
selectionSource: 'auto',
opencodeDefaultAgent: 'review',
opencodeDefaultModel: 'openai/gpt-5.5',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'review',
selectedProviderId: 'openai',
agentModelSelections: {},
defaultProviders: {},
opencodeDefaultAgent: 'review',
opencodeDefaultModel: 'openai/gpt-5.5',
selectionSource: 'auto',
},
},
});
const load = useConfigStore.getState().loadAgents({ directory: DIRECTORY, source: 'test:staleDefaultsRace' });
emitSyncConfigChanged(DIRECTORY, {});
pendingAgents.resolve([testAgent('build'), testAgent('review')]);
await load;
const state = useConfigStore.getState();
expect(state.opencodeDefaultAgent).toBe(undefined);
expect(state.opencodeDefaultModel).toBe(undefined);
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultAgent).toBe(undefined);
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultModel).toBe(undefined);
});
test('in-flight loadAgents does not restore pre-await sync config defaults after a clearing event', async () => {
const pendingAgents = deferred<TestAgent[]>();
const syncConfigs = new Map<string, Record<string, unknown>>([
[DIRECTORY, { default_agent: 'review', model: 'openai/gpt-5.5' }],
]);
setSyncRefs(
{} as never,
{
children: new Map(),
getState: (directory: string) => ({ config: syncConfigs.get(directory) ?? {} }),
} as never,
DIRECTORY,
);
listAgentsImpl = async () => pendingAgents.promise;
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'review',
selectedProviderId: 'openai',
selectionSource: 'auto',
opencodeDefaultAgent: 'review',
opencodeDefaultModel: 'openai/gpt-5.5',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5')],
agents: [testAgent('build'), testAgent('review')],
currentProviderId: 'openai',
currentModelId: 'gpt-5.5',
currentAgentName: 'review',
selectedProviderId: 'openai',
agentModelSelections: {},
defaultProviders: {},
opencodeDefaultAgent: 'review',
opencodeDefaultModel: 'openai/gpt-5.5',
selectionSource: 'auto',
},
},
});
const load = useConfigStore.getState().loadAgents({ directory: DIRECTORY, source: 'test:preAwaitSyncConfigRace' });
syncConfigs.set(DIRECTORY, {});
emitSyncConfigChanged(DIRECTORY, {});
pendingAgents.resolve([testAgent('build'), testAgent('review')]);
await load;
const state = useConfigStore.getState();
expect(state.opencodeDefaultAgent).toBe(undefined);
expect(state.opencodeDefaultModel).toBe(undefined);
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultAgent).toBe(undefined);
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultModel).toBe(undefined);
});
test('directory activation isolates selection source and OpenCode defaults', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
selectionSource: 'manual',
opencodeDefaultAgent: 'active-default',
opencodeDefaultModel: 'active/model',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('active')],
agents: [testAgent('active-agent')],
currentProviderId: 'active',
currentModelId: 'active-model',
currentAgentName: 'active-agent',
selectedProviderId: 'active',
agentModelSelections: {},
defaultProviders: {},
opencodeDefaultAgent: 'active-default',
opencodeDefaultModel: 'active/model',
selectionSource: 'manual',
},
[OTHER_DIRECTORY]: {
providers: [provider('other')],
agents: [testAgent('other-agent')],
currentProviderId: 'other',
currentModelId: 'other-model',
currentAgentName: 'other-agent',
selectedProviderId: 'other',
agentModelSelections: {},
defaultProviders: {},
opencodeDefaultAgent: 'other-default',
opencodeDefaultModel: 'other/model',
selectionSource: 'auto',
},
},
isConnected: false,
});
await useConfigStore.getState().activateDirectory(OTHER_DIRECTORY);
const state = useConfigStore.getState();
expect(state.activeDirectoryKey).toBe(OTHER_DIRECTORY);
expect(state.selectionSource).toBe('auto');
expect(state.opencodeDefaultAgent).toBe('other-default');
expect(state.opencodeDefaultModel).toBe('other/model');
});
test('sync config without defaults clears stored OpenCode defaults without changing manual selection', () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('manual')],
agents: [testAgent('manual-agent')],
currentProviderId: 'manual',
currentModelId: 'manual-model',
currentAgentName: 'manual-agent',
selectedProviderId: 'manual',
selectionSource: 'manual',
opencodeDefaultAgent: 'old-agent',
opencodeDefaultModel: 'old/model',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('manual')],
agents: [testAgent('manual-agent')],
currentProviderId: 'manual',
currentModelId: 'manual-model',
currentAgentName: 'manual-agent',
selectedProviderId: 'manual',
agentModelSelections: {},
defaultProviders: {},
opencodeDefaultAgent: 'old-agent',
opencodeDefaultModel: 'old/model',
selectionSource: 'manual',
},
},
});
emitSyncConfigChanged(DIRECTORY, {});
const state = useConfigStore.getState();
expect(state.opencodeDefaultAgent).toBe(undefined);
expect(state.opencodeDefaultModel).toBe(undefined);
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultAgent).toBe(undefined);
expect(state.directoryScoped[DIRECTORY]?.opencodeDefaultModel).toBe(undefined);
expect(state.currentAgentName).toBe('manual-agent');
expect(state.currentProviderId).toBe('manual');
expect(state.selectionSource).toBe('manual');
});
});