test(ui): pin who may record an explicit Default effort
Nothing covered ModelControls, which is where the effort restore decides between "the user chose Default" and "nothing was found". Three cases: a concrete effort in the session history is restored, history without an effort records no choice, and a preserved manual override keeps a recorded Default. The middle one fails on the previous commit's parent — it recorded `null` and then `undefined` for the same restore. Also drops the `fetchAll` binding GitView stopped using when the post-bootstrap refresh moved to a forced `fetchStatus`; it was failing `bun run lint:ui`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZuVVgziiLjD81W5vaxdH2
This commit is contained in:
co-authored by
Claude Opus 5
parent
025cd3d46c
commit
4dfbb5bd1c
@@ -0,0 +1,373 @@
|
||||
import React, { act } from 'react';
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Window } from 'happy-dom';
|
||||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Restoring a session must not invent an effort choice.
|
||||
*
|
||||
* The selection store keeps three states for a session's effort: an effort
|
||||
* name, `null` for an explicit "Default", and `undefined` for no choice at all.
|
||||
* Only the picker may write `null`. When a restore path writes it instead, the
|
||||
* session latches onto "Default" — `null` outranks the agent and settings
|
||||
* defaults by design — and the concrete effort the session's own history
|
||||
* carries can never come back. These tests pin who writes what.
|
||||
*/
|
||||
|
||||
type VariantChoice = string | null | undefined;
|
||||
|
||||
type UserModelChoice = {
|
||||
id: string;
|
||||
agent?: string;
|
||||
providerID: string;
|
||||
modelID: string;
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
const PROVIDER_ID = 'openai';
|
||||
const MODEL_ID = 'gpt-5.5';
|
||||
const AGENT = 'build';
|
||||
const SESSION_ID = 'ses_restore';
|
||||
|
||||
const model = {
|
||||
id: MODEL_ID,
|
||||
name: MODEL_ID,
|
||||
providerID: PROVIDER_ID,
|
||||
variants: { low: {}, high: {} },
|
||||
};
|
||||
const provider = { id: PROVIDER_ID, name: PROVIDER_ID, models: [model] };
|
||||
const agent = { name: AGENT, mode: 'primary' as const };
|
||||
|
||||
let latestUserChoice: UserModelChoice | null = null;
|
||||
let preserveManualOverride = false;
|
||||
|
||||
/** Every effort written for the session, in order, including `undefined`. */
|
||||
const variantWrites: VariantChoice[] = [];
|
||||
/** Every `(override, inherited)` pair pushed into the config store. */
|
||||
const overrideWrites: Array<{ override: VariantChoice; inherited: string | undefined }> = [];
|
||||
|
||||
type ConfigState = {
|
||||
providers: typeof provider[];
|
||||
agents: typeof agent[];
|
||||
modelsMetadata: Record<string, never>;
|
||||
currentProviderId: string;
|
||||
currentModelId: string;
|
||||
currentVariant: string | undefined;
|
||||
currentVariantSelection: { override: VariantChoice; inherited: string | undefined };
|
||||
currentAgentName: string | undefined;
|
||||
settingsDefaultVariant: string | undefined;
|
||||
settingsDefaultAgent: string | undefined;
|
||||
selectionSource: 'auto' | 'manual';
|
||||
setProvider: (providerId: string) => void;
|
||||
setSelectedProvider: (providerId: string) => void;
|
||||
setModel: (modelId: string) => void;
|
||||
setAgent: (agentName: string) => void;
|
||||
setCurrentVariant: (variant: string | undefined) => void;
|
||||
setCurrentVariantOverride: (override: VariantChoice, inherited: string | undefined) => void;
|
||||
getCurrentProvider: () => typeof provider;
|
||||
getCurrentAgent: () => typeof agent;
|
||||
getVisibleAgents: () => typeof agent[];
|
||||
getCurrentModelVariants: () => string[];
|
||||
getModelMetadata: () => undefined;
|
||||
};
|
||||
|
||||
const useConfigStore = create<ConfigState>((set) => ({
|
||||
providers: [provider],
|
||||
agents: [agent],
|
||||
modelsMetadata: {},
|
||||
currentProviderId: PROVIDER_ID,
|
||||
currentModelId: MODEL_ID,
|
||||
currentVariant: undefined,
|
||||
currentVariantSelection: { override: undefined, inherited: undefined },
|
||||
currentAgentName: AGENT,
|
||||
settingsDefaultVariant: undefined,
|
||||
settingsDefaultAgent: undefined,
|
||||
selectionSource: 'auto',
|
||||
setProvider: (providerId) => set({ currentProviderId: providerId }),
|
||||
setSelectedProvider: () => undefined,
|
||||
setModel: (modelId) => set({ currentModelId: modelId }),
|
||||
setAgent: (agentName) => set({ currentAgentName: agentName }),
|
||||
// Mirrors the real store, including its no-op guard: without that guard an
|
||||
// unchanged write returns a fresh state object every render and the
|
||||
// component's variant effects never settle.
|
||||
setCurrentVariant: (variant) => {
|
||||
useConfigStore.getState().setCurrentVariantOverride(undefined, variant);
|
||||
},
|
||||
setCurrentVariantOverride: (override, inherited) => {
|
||||
set((state) => {
|
||||
const currentVariant = override === null ? undefined : override ?? inherited;
|
||||
if (
|
||||
state.currentVariant === currentVariant
|
||||
&& state.currentVariantSelection.override === override
|
||||
&& state.currentVariantSelection.inherited === inherited
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
overrideWrites.push({ override, inherited });
|
||||
return { currentVariant, currentVariantSelection: { override, inherited } };
|
||||
});
|
||||
},
|
||||
getCurrentProvider: () => provider,
|
||||
getCurrentAgent: () => agent,
|
||||
getVisibleAgents: () => [agent],
|
||||
getCurrentModelVariants: () => Object.keys(model.variants),
|
||||
getModelMetadata: () => undefined,
|
||||
}));
|
||||
|
||||
type SelectionState = {
|
||||
savedVariant: VariantChoice;
|
||||
sessionAgentSelections: Map<string, string>;
|
||||
getSessionModelSelection: () => { providerId: string; modelId: string } | null;
|
||||
getSessionAgentSelection: () => string | null;
|
||||
getAgentModelForSession: () => { providerId: string; modelId: string } | null;
|
||||
getAgentModelVariantForSession: () => VariantChoice;
|
||||
saveSessionModelSelection: () => void;
|
||||
saveSessionAgentSelection: () => void;
|
||||
saveAgentModelForSession: () => void;
|
||||
saveAgentModelVariantForSession: (
|
||||
sessionId: string,
|
||||
agentName: string,
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
variant: VariantChoice,
|
||||
) => void;
|
||||
};
|
||||
|
||||
const useSelectionStore = create<SelectionState>((set, get) => ({
|
||||
savedVariant: undefined,
|
||||
sessionAgentSelections: new Map([[SESSION_ID, AGENT]]),
|
||||
getSessionModelSelection: () => ({ providerId: PROVIDER_ID, modelId: MODEL_ID }),
|
||||
getSessionAgentSelection: () => AGENT,
|
||||
getAgentModelForSession: () => ({ providerId: PROVIDER_ID, modelId: MODEL_ID }),
|
||||
getAgentModelVariantForSession: () => get().savedVariant,
|
||||
saveSessionModelSelection: () => undefined,
|
||||
saveSessionAgentSelection: () => undefined,
|
||||
saveAgentModelForSession: () => undefined,
|
||||
saveAgentModelVariantForSession: (_sessionId, _agentName, _providerId, _modelId, variant) => {
|
||||
variantWrites.push(variant);
|
||||
set({ savedVariant: variant });
|
||||
},
|
||||
}));
|
||||
|
||||
const useSessionUIStore = create(() => ({
|
||||
currentSessionId: SESSION_ID,
|
||||
getDirectoryForSession: () => '/workspace/project',
|
||||
}));
|
||||
|
||||
const useUIStore = create(() => ({
|
||||
isMobile: false,
|
||||
isModelSelectorOpen: false,
|
||||
hiddenModels: [],
|
||||
providerOrder: [],
|
||||
shortcutOverrides: {},
|
||||
isFavoriteModel: () => false,
|
||||
toggleFavoriteModel: () => undefined,
|
||||
reorderFavoriteModel: () => undefined,
|
||||
setProviderOrder: () => undefined,
|
||||
setModelSelectorOpen: () => undefined,
|
||||
setSettingsDialogOpen: () => undefined,
|
||||
setSettingsPage: () => undefined,
|
||||
addRecentAgent: () => undefined,
|
||||
addRecentModel: () => undefined,
|
||||
addRecentEffort: () => undefined,
|
||||
}));
|
||||
|
||||
const passthrough = ({ children }: React.PropsWithChildren) => <div>{children}</div>;
|
||||
|
||||
mock.module('@/lib/messages/userModelChoice', () => ({
|
||||
findLatestUserModelChoice: () => latestUserChoice,
|
||||
shouldPreserveManualModelOverride: () => preserveManualOverride,
|
||||
}));
|
||||
|
||||
mock.module('@/stores/useConfigStore', () => ({ useConfigStore }));
|
||||
mock.module('@/sync/selection-store', () => ({ useSelectionStore }));
|
||||
mock.module('@/sync/session-ui-store', () => ({ useSessionUIStore }));
|
||||
mock.module('@/stores/useUIStore', () => ({ useUIStore }));
|
||||
mock.module('@/stores/contextStore', () => ({
|
||||
useContextStore: <T,>(selector: (state: { hasHydrated: boolean }) => T): T => selector({ hasHydrated: true }),
|
||||
}));
|
||||
|
||||
mock.module('@/sync/sync-context', () => ({
|
||||
useSessionMessages: () => [],
|
||||
useSessionRenderable: () => true,
|
||||
}));
|
||||
mock.module('@/sync/use-sync', () => ({ useSync: () => ({ sessions: [] }) }));
|
||||
mock.module('@/sync/sync-refs', () => ({ getSyncParts: () => [] }));
|
||||
|
||||
mock.module('@/components/ui/dropdown-menu', () => ({
|
||||
DropdownMenu: passthrough,
|
||||
DropdownMenuContent: passthrough,
|
||||
DropdownMenuItem: passthrough,
|
||||
DropdownMenuLabel: passthrough,
|
||||
DropdownMenuSeparator: () => null,
|
||||
DropdownMenuTrigger: passthrough,
|
||||
}));
|
||||
mock.module('@/components/ui/input', () => ({
|
||||
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||
}));
|
||||
mock.module('@/components/ui/MobileOverlayPanel', () => ({ MobileOverlayPanel: passthrough }));
|
||||
mock.module('@/components/ui/ProviderLogo', () => ({ ProviderLogo: () => null }));
|
||||
mock.module('@/components/ui/ScrollableOverlay', () => ({ ScrollableOverlay: passthrough }));
|
||||
mock.module('@/components/ui/tooltip', () => ({
|
||||
Tooltip: passthrough,
|
||||
TooltipContent: passthrough,
|
||||
TooltipTrigger: passthrough,
|
||||
}));
|
||||
mock.module('@/components/icon/Icon', () => ({ Icon: () => null }));
|
||||
mock.module('@/components/model-picker/ModelPickerList', () => ({ ModelPickerList: () => null }));
|
||||
mock.module('@/hooks/useRuntimeAPIs', () => ({ useIsVSCodeRuntime: () => false }));
|
||||
mock.module('@/hooks/useModelLists', () => ({ useModelLists: () => ({ favoriteModels: [], recentModels: [] }) }));
|
||||
mock.module('@/hooks/useIsTextTruncated', () => ({ useIsTextTruncated: () => false }));
|
||||
mock.module('@/hooks/useOpenCodeReadiness', () => ({
|
||||
useOpenCodeReadiness: () => ({ isReady: true, isUnavailable: false }),
|
||||
}));
|
||||
mock.module('@/lib/device', () => ({ useDeviceInfo: () => ({ isTouch: false }) }));
|
||||
mock.module('@/lib/desktop', () => ({ isDesktopShell: () => false }));
|
||||
mock.module('@/lib/startupTrace', () => ({ markStartupTrace: () => undefined }));
|
||||
|
||||
const { ModelControls } = await import('./ModelControls');
|
||||
const { I18nProvider } = await import('@/lib/i18n');
|
||||
|
||||
const DOM_GLOBAL_NAMES = [
|
||||
'window',
|
||||
'document',
|
||||
'navigator',
|
||||
'Node',
|
||||
'Element',
|
||||
'HTMLElement',
|
||||
'HTMLIFrameElement',
|
||||
'localStorage',
|
||||
'requestAnimationFrame',
|
||||
'cancelAnimationFrame',
|
||||
'IS_REACT_ACT_ENVIRONMENT',
|
||||
] as const;
|
||||
|
||||
const frameTimers = new Map<number, ReturnType<Window['setTimeout']>>();
|
||||
let nextFrameHandle = 1;
|
||||
|
||||
const installDom = () => {
|
||||
const happyWindow = new Window({ url: 'http://localhost' });
|
||||
const previous = DOM_GLOBAL_NAMES.map(
|
||||
(name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)] as const,
|
||||
);
|
||||
const values = {
|
||||
window: happyWindow,
|
||||
document: happyWindow.document,
|
||||
navigator: happyWindow.navigator,
|
||||
Node: happyWindow.Node,
|
||||
Element: happyWindow.Element,
|
||||
HTMLElement: happyWindow.HTMLElement,
|
||||
HTMLIFrameElement: happyWindow.HTMLIFrameElement,
|
||||
localStorage: happyWindow.localStorage,
|
||||
// The component focuses the composer through rAF on several paths.
|
||||
requestAnimationFrame: (callback: FrameRequestCallback) => {
|
||||
const handle = nextFrameHandle++;
|
||||
frameTimers.set(handle, happyWindow.setTimeout(() => {
|
||||
frameTimers.delete(handle);
|
||||
callback(0);
|
||||
}, 0));
|
||||
return handle;
|
||||
},
|
||||
cancelAnimationFrame: (handle: number) => {
|
||||
const timer = frameTimers.get(handle);
|
||||
if (timer === undefined) return;
|
||||
frameTimers.delete(handle);
|
||||
happyWindow.clearTimeout(timer);
|
||||
},
|
||||
IS_REACT_ACT_ENVIRONMENT: true,
|
||||
};
|
||||
for (const name of DOM_GLOBAL_NAMES) {
|
||||
Object.defineProperty(globalThis, name, { value: values[name], configurable: true, writable: true });
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
return {
|
||||
container,
|
||||
restore: () => {
|
||||
for (const [name, descriptor] of previous) {
|
||||
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
||||
else Reflect.deleteProperty(globalThis, name);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const renderModelControls = async () => {
|
||||
const dom = installDom();
|
||||
const root = createRoot(dom.container);
|
||||
await act(async () => root.render(
|
||||
<I18nProvider>
|
||||
<ModelControls />
|
||||
</I18nProvider>,
|
||||
));
|
||||
return {
|
||||
dom,
|
||||
cleanup: async () => {
|
||||
await act(async () => root.unmount());
|
||||
dom.restore();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('ModelControls effort restore', () => {
|
||||
beforeEach(() => {
|
||||
variantWrites.length = 0;
|
||||
overrideWrites.length = 0;
|
||||
latestUserChoice = null;
|
||||
preserveManualOverride = false;
|
||||
useSelectionStore.setState({ savedVariant: undefined });
|
||||
useConfigStore.setState({
|
||||
currentProviderId: PROVIDER_ID,
|
||||
currentModelId: MODEL_ID,
|
||||
currentAgentName: AGENT,
|
||||
currentVariant: undefined,
|
||||
currentVariantSelection: { override: undefined, inherited: undefined },
|
||||
settingsDefaultVariant: undefined,
|
||||
selectionSource: 'auto',
|
||||
});
|
||||
});
|
||||
|
||||
test('restores the concrete effort the session history carries', async () => {
|
||||
latestUserChoice = { id: 'msg-1', agent: AGENT, providerID: PROVIDER_ID, modelID: MODEL_ID, variant: 'low' };
|
||||
|
||||
const { cleanup } = await renderModelControls();
|
||||
try {
|
||||
expect(variantWrites).toContain('low');
|
||||
expect(variantWrites).not.toContain(null);
|
||||
expect(useSelectionStore.getState().savedVariant).toBe('low');
|
||||
expect(useConfigStore.getState().currentVariantSelection.override).toBe('low');
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('history without an effort records no choice instead of an explicit Default', async () => {
|
||||
latestUserChoice = { id: 'msg-2', agent: AGENT, providerID: PROVIDER_ID, modelID: MODEL_ID };
|
||||
|
||||
const { cleanup } = await renderModelControls();
|
||||
try {
|
||||
expect(variantWrites).not.toContain(null);
|
||||
expect(useSelectionStore.getState().savedVariant).toBe(undefined);
|
||||
expect(useConfigStore.getState().currentVariantSelection.override).toBe(undefined);
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('a preserved manual override keeps a recorded explicit Default', async () => {
|
||||
latestUserChoice = { id: 'msg-3', agent: AGENT, providerID: PROVIDER_ID, modelID: MODEL_ID, variant: 'high' };
|
||||
preserveManualOverride = true;
|
||||
useSelectionStore.setState({ savedVariant: null });
|
||||
useConfigStore.setState({ selectionSource: 'manual' });
|
||||
|
||||
const { cleanup } = await renderModelControls();
|
||||
try {
|
||||
expect(useSelectionStore.getState().savedVariant).toBeNull();
|
||||
expect(useConfigStore.getState().currentVariantSelection.override).toBeNull();
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -292,7 +292,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
const isLogLoading = useGitLoadingLog(gitDirectory ?? null);
|
||||
const {
|
||||
setActiveDirectory,
|
||||
fetchAll,
|
||||
ensureAll,
|
||||
fetchStatus,
|
||||
fetchBranches,
|
||||
@@ -308,7 +307,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
selectNestedRepo,
|
||||
} = useGitStore(useShallow((state) => ({
|
||||
setActiveDirectory: state.setActiveDirectory,
|
||||
fetchAll: state.fetchAll,
|
||||
ensureAll: state.ensureAll,
|
||||
fetchStatus: state.fetchStatus,
|
||||
fetchBranches: state.fetchBranches,
|
||||
|
||||
Reference in New Issue
Block a user