mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🪶 refactor: Ground Default Model Spec Selection in Conversation Recency (#13915)
Resolve the new-chat default spec from the most recent conversation setup
(LAST_CONVO_SETUP_0) instead of reconstructing intent from accumulated
cross-endpoint history. Removes hasStoredModelValue, hasStoredPrefixValue,
hasStoredModelSelection, the sticky LAST_SPEC read, the nested
resolveSoftDefault closure, and the duplicated prioritize/modelSelect branches.
Fixes the soft default being dropped on New Chat ("Select a model") when its
preset endpoint sits outside modelSpecs.addedEndpoints alongside a custom
endpoint: a model lingering in LAST_MODEL for that endpoint no longer
suppresses the soft default.
Clear All Chats now also clears LAST_SPEC/LAST_MODEL/LAST_TOOLS so a new chat
afterward cleanly returns to the soft default. Adds the cross-endpoint unit
case, a clearAllConversationStorage test, and a cold-load e2e regression test.
This commit is contained in:
parent
33d7b0070c
commit
f14309e087
5 changed files with 172 additions and 147 deletions
|
|
@ -105,7 +105,7 @@ describe('getDefaultModelSpec', () => {
|
|||
it('keeps the last selected spec before applying the soft default', () => {
|
||||
const lastSpec = createModelSpec('last-spec');
|
||||
const softSpec = createModelSpec('soft-spec', { softDefault: true });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_SPEC, lastSpec.name);
|
||||
persistAppliedSpec(lastSpec);
|
||||
|
||||
const result = getDefaultModelSpec(createStartupConfig([softSpec, lastSpec]));
|
||||
|
||||
|
|
@ -114,10 +114,7 @@ describe('getDefaultModelSpec', () => {
|
|||
|
||||
it('does not apply the soft default when a prior model selection exists', () => {
|
||||
const softSpec = createModelSpec('soft-spec', { softDefault: true });
|
||||
localStorage.setItem(
|
||||
LocalStorageKeys.LAST_MODEL,
|
||||
JSON.stringify({ [EModelEndpoint.openAI]: 'gpt-4o' }),
|
||||
);
|
||||
persistEphemeralSelection(EModelEndpoint.openAI, 'gpt-4o');
|
||||
|
||||
const result = getDefaultModelSpec(createStartupConfig([softSpec]), fullEndpointsConfig);
|
||||
|
||||
|
|
@ -126,7 +123,7 @@ describe('getDefaultModelSpec', () => {
|
|||
|
||||
it('does not apply the soft default when a prior agent selection exists', () => {
|
||||
const softSpec = createModelSpec('soft-spec', { softDefault: true });
|
||||
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_123');
|
||||
persistAgentSelection('agent_123');
|
||||
|
||||
const result = getDefaultModelSpec(createStartupConfig([softSpec]), fullEndpointsConfig);
|
||||
|
||||
|
|
@ -136,7 +133,7 @@ describe('getDefaultModelSpec', () => {
|
|||
it('keeps hard admin defaults ahead of user history and soft defaults', () => {
|
||||
const hardSpec = createModelSpec('hard-spec', { default: true });
|
||||
const softSpec = createModelSpec('soft-spec', { softDefault: true });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_SPEC, softSpec.name);
|
||||
persistAppliedSpec(softSpec);
|
||||
|
||||
const result = getDefaultModelSpec(createStartupConfig([softSpec, hardSpec]));
|
||||
|
||||
|
|
@ -231,7 +228,7 @@ describe('getDefaultModelSpec', () => {
|
|||
softDefault: true,
|
||||
preset: { endpoint: EModelEndpoint.agents, agent_id: 'agent_soft' },
|
||||
} as Partial<TModelSpec>);
|
||||
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_soft');
|
||||
persistAppliedSpec(softAgentSpec);
|
||||
|
||||
const result = getDefaultModelSpec(
|
||||
createStartupConfig([softAgentSpec], { prioritize: false }),
|
||||
|
|
@ -242,10 +239,7 @@ describe('getDefaultModelSpec', () => {
|
|||
});
|
||||
|
||||
it('treats a stored model matching the soft default preset as residue', () => {
|
||||
localStorage.setItem(
|
||||
LocalStorageKeys.LAST_MODEL,
|
||||
JSON.stringify({ [softSpec.preset.endpoint as string]: softSpec.preset.model }),
|
||||
);
|
||||
persistAppliedSpec(softSpec);
|
||||
|
||||
const result = getDefaultModelSpec(
|
||||
createStartupConfig([otherSpec, softSpec], { prioritize: false }),
|
||||
|
|
@ -266,7 +260,7 @@ describe('getDefaultModelSpec', () => {
|
|||
expect(result).toEqual({ softDefault: softSpec });
|
||||
});
|
||||
|
||||
it('does not re-arm from viewing an old soft conversation after an ephemeral pick', () => {
|
||||
it('re-arms after viewing the soft conversation, even when an ephemeral pick lingers', () => {
|
||||
persistEphemeralSelection(EModelEndpoint.anthropic, 'claude-sonnet-4-6');
|
||||
persistAppliedSpec(softSpec, 'a8b1c2d3-e4f5-4a6b-8c7d-9e0f1a2b3c4d');
|
||||
|
||||
|
|
@ -275,10 +269,10 @@ describe('getDefaultModelSpec', () => {
|
|||
fullEndpointsConfig,
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toEqual({ softDefault: softSpec });
|
||||
});
|
||||
|
||||
it('does not re-arm from viewing an old soft conversation after an agent pick', () => {
|
||||
it('re-arms after viewing the soft conversation, even when an agent pick lingers', () => {
|
||||
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_abc');
|
||||
persistAppliedSpec(softSpec, 'a8b1c2d3-e4f5-4a6b-8c7d-9e0f1a2b3c4d');
|
||||
|
||||
|
|
@ -287,7 +281,7 @@ describe('getDefaultModelSpec', () => {
|
|||
fullEndpointsConfig,
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toEqual({ softDefault: softSpec });
|
||||
});
|
||||
|
||||
it('yields when a different agent is stored than the soft default agent spec', () => {
|
||||
|
|
@ -295,7 +289,7 @@ describe('getDefaultModelSpec', () => {
|
|||
softDefault: true,
|
||||
preset: { endpoint: EModelEndpoint.agents, agent_id: 'agent_soft' },
|
||||
} as Partial<TModelSpec>);
|
||||
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_other');
|
||||
persistAgentSelection('agent_other');
|
||||
|
||||
const result = getDefaultModelSpec(
|
||||
createStartupConfig([softAgentSpec], { prioritize: false }),
|
||||
|
|
@ -306,6 +300,48 @@ describe('getDefaultModelSpec', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('soft default endpoint outside an added-endpoints allow-list', () => {
|
||||
// Mirrors a softDefault spec on `bedrock` with `addedEndpoints: [agents, <custom>]`.
|
||||
// The custom endpoint turns on the ephemeral-options gate, and a model used on
|
||||
// that endpoint lingers under a different key than the spec preset — which used
|
||||
// to suppress the soft default and strand New Chat on the unselectable bedrock.
|
||||
const softSpec = createModelSpec('clickhouse-agent', {
|
||||
softDefault: true,
|
||||
preset: { endpoint: 'bedrock', model: 'claude-sonnet-4-6' },
|
||||
} as Partial<TModelSpec>);
|
||||
const otherSpec = createModelSpec('other-spec');
|
||||
const allowListConfig = { addedEndpoints: ['agents', 'ClickHouse'] };
|
||||
const allowListEndpoints = {
|
||||
bedrock: { order: 0 },
|
||||
ClickHouse: { order: 1 },
|
||||
[EModelEndpoint.agents]: { order: 2 },
|
||||
} as TEndpointsConfig;
|
||||
|
||||
it('re-arms on New Chat after viewing the soft conversation last', () => {
|
||||
writeLastModel('ClickHouse', 'kimi-k2p7-code');
|
||||
persistAppliedSpec(softSpec, 'a8b1c2d3-e4f5-4a6b-8c7d-9e0f1a2b3c4d');
|
||||
|
||||
const result = getDefaultModelSpec(
|
||||
createStartupConfig([otherSpec, softSpec], allowListConfig),
|
||||
allowListEndpoints,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ softDefault: softSpec });
|
||||
});
|
||||
|
||||
it('still yields when a selectable endpoint was the last conversation', () => {
|
||||
persistAppliedSpec(softSpec, 'a8b1c2d3-e4f5-4a6b-8c7d-9e0f1a2b3c4d');
|
||||
persistEphemeralSelection('ClickHouse', 'kimi-k2p7-code');
|
||||
|
||||
const result = getDefaultModelSpec(
|
||||
createStartupConfig([otherSpec, softSpec], allowListConfig),
|
||||
allowListEndpoints,
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('explicit soft default selection', () => {
|
||||
const softSpec = createModelSpec('soft-spec', { softDefault: true });
|
||||
const otherSpec = createModelSpec('other-spec');
|
||||
|
|
|
|||
29
client/src/utils/__tests__/localStorage.test.ts
Normal file
29
client/src/utils/__tests__/localStorage.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { clearAllConversationStorage } from '../localStorage';
|
||||
|
||||
describe('clearAllConversationStorage', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('wipes the selection and conversation state but keeps unrelated keys', () => {
|
||||
localStorage.setItem(LocalStorageKeys.LAST_SPEC, 'some-spec');
|
||||
localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify({ openAI: 'gpt-4o' }));
|
||||
localStorage.setItem(LocalStorageKeys.LAST_TOOLS, JSON.stringify(['web_search']));
|
||||
localStorage.setItem(
|
||||
`${LocalStorageKeys.LAST_CONVO_SETUP}_0`,
|
||||
JSON.stringify({ spec: 'some-spec' }),
|
||||
);
|
||||
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_1');
|
||||
localStorage.setItem('unrelated-key', 'keep-me');
|
||||
|
||||
clearAllConversationStorage();
|
||||
|
||||
expect(localStorage.getItem(LocalStorageKeys.LAST_SPEC)).toBeNull();
|
||||
expect(localStorage.getItem(LocalStorageKeys.LAST_MODEL)).toBeNull();
|
||||
expect(localStorage.getItem(LocalStorageKeys.LAST_TOOLS)).toBeNull();
|
||||
expect(localStorage.getItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_0`)).toBeNull();
|
||||
expect(localStorage.getItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`)).toBeNull();
|
||||
expect(localStorage.getItem('unrelated-key')).toBe('keep-me');
|
||||
});
|
||||
});
|
||||
|
|
@ -139,7 +139,7 @@ interface InitiatedTemplateResult {
|
|||
|
||||
type StoredModelSelection = Pick<
|
||||
t.TConversation,
|
||||
'endpoint' | 'model' | 'spec' | 'agent_id' | 'assistant_id' | 'conversationId'
|
||||
'endpoint' | 'model' | 'spec' | 'agent_id' | 'assistant_id'
|
||||
>;
|
||||
|
||||
function hasSelectionValue(value?: string | null): boolean {
|
||||
|
|
@ -160,40 +160,6 @@ function parseStoredModelSelection(
|
|||
}
|
||||
}
|
||||
|
||||
function hasStoredPrefixValue(prefix: string, ignoreValue?: string | null): boolean {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!key?.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = localStorage.getItem(key);
|
||||
if (hasSelectionValue(value) && value !== ignoreValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasStoredModelValue(softPreset?: t.TModelSpecPreset): boolean {
|
||||
const storedModelValue = localStorage.getItem(LocalStorageKeys.LAST_MODEL);
|
||||
if (!storedModelValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const storedModels = JSON.parse(storedModelValue) as Record<string, string | null | undefined>;
|
||||
return Object.entries(storedModels).some(
|
||||
([endpoint, model]) =>
|
||||
hasSelectionValue(model) &&
|
||||
!(endpoint === softPreset?.endpoint && model === softPreset?.model),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasModelSelection(selection?: Partial<StoredModelSelection> | null): boolean {
|
||||
if (!selection) {
|
||||
return false;
|
||||
|
|
@ -208,39 +174,6 @@ export function hasModelSelection(selection?: Partial<StoredModelSelection> | nu
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether localStorage holds a model selection the user actually made.
|
||||
* State matching what applying `softDefaultSpec` writes is residue of the
|
||||
* soft default itself, not evidence of a user choice, and is ignored.
|
||||
*/
|
||||
function hasStoredModelSelection(softDefaultSpec?: t.TModelSpec): boolean {
|
||||
const softPreset = softDefaultSpec?.preset;
|
||||
const lastSpecName = localStorage.getItem(LocalStorageKeys.LAST_SPEC);
|
||||
if (hasSelectionValue(lastSpecName) && lastSpecName !== softDefaultSpec?.name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasStoredModelValue(softPreset)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
hasStoredPrefixValue(LocalStorageKeys.AGENT_ID_PREFIX, softPreset?.agent_id) ||
|
||||
hasStoredPrefixValue(LocalStorageKeys.ASST_ID_PREFIX, softPreset?.assistant_id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastConversationSetup = parseStoredModelSelection(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'),
|
||||
);
|
||||
if (softDefaultSpec && lastConversationSetup?.spec === softDefaultSpec.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasModelSelection(lastConversationSetup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the selector offers any ephemeral endpoint → model options.
|
||||
* False when the endpoints menu is hidden (`modelSelect` disabled) or only
|
||||
|
|
@ -410,13 +343,16 @@ export function applyModelSpecEphemeralAgent({
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets default model spec from config and user preferences.
|
||||
* Priority: hard admin default → prior user selection → soft default.
|
||||
* The soft default yields only to selections the user actually made — its
|
||||
* stored state counts as the live selection only when set on a new chat, so
|
||||
* merely viewing an old soft-spec conversation never re-arms it — and it acts
|
||||
* as the fallback default when no ephemeral endpoint → model options exist.
|
||||
* Legacy first-spec prioritization remains only when no soft default is configured.
|
||||
* Resolves the default model spec for a new chat. Priority: hard admin default →
|
||||
* the most recent conversation's own selection → soft default → legacy first spec.
|
||||
*
|
||||
* `LAST_CONVO_SETUP_0` is the single source of truth for prior intent: a setup naming
|
||||
* the soft spec is the soft default re-arming, any other spec/agent/endpoint is a
|
||||
* selection to carry forward, and an empty setup is a fresh start (clearing chats
|
||||
* wipes the selection, so a new chat then falls to the soft default). The soft default
|
||||
* also wins whenever the selector offers no ephemeral endpoint → model options, so a
|
||||
* stale agent never strands it. The legacy first-spec fallback applies only when specs
|
||||
* are prioritized (or the model menu is hidden) and no soft default is configured.
|
||||
*/
|
||||
export function getDefaultModelSpec(
|
||||
startupConfig?: t.TStartupConfig,
|
||||
|
|
@ -433,59 +369,42 @@ export function getDefaultModelSpec(
|
|||
if (!list) {
|
||||
return;
|
||||
}
|
||||
const defaultSpec = list?.find((spec) => spec.default);
|
||||
const softDefaultSpec = list?.find((spec) => spec.softDefault);
|
||||
const lastConversationSetup = parseStoredModelSelection(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'),
|
||||
);
|
||||
const resolveSoftDefault = (): { softDefault: t.TModelSpec } | undefined => {
|
||||
if (!softDefaultSpec) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
lastConversationSetup?.spec === softDefaultSpec.name &&
|
||||
lastConversationSetup?.conversationId === Constants.NEW_CONVO
|
||||
) {
|
||||
return { softDefault: softDefaultSpec };
|
||||
}
|
||||
const ephemeralOptions = hasEphemeralModelOptions({
|
||||
endpointsConfig,
|
||||
addedEndpoints,
|
||||
modelSelect: interfaceConfig?.modelSelect,
|
||||
});
|
||||
if (!ephemeralOptions) {
|
||||
return { softDefault: softDefaultSpec };
|
||||
}
|
||||
return hasStoredModelSelection(softDefaultSpec) ? undefined : { softDefault: softDefaultSpec };
|
||||
};
|
||||
if (prioritize === true || !interfaceConfig?.modelSelect) {
|
||||
const lastSelectedSpecName = localStorage.getItem(LocalStorageKeys.LAST_SPEC);
|
||||
const lastSelectedSpec = list?.find((spec) => spec.name === lastSelectedSpecName);
|
||||
if (defaultSpec) {
|
||||
return { default: defaultSpec };
|
||||
}
|
||||
if (lastSelectedSpec && lastSelectedSpec.name === softDefaultSpec?.name) {
|
||||
return resolveSoftDefault();
|
||||
}
|
||||
if (lastSelectedSpec) {
|
||||
return { last: lastSelectedSpec };
|
||||
}
|
||||
if (softDefaultSpec) {
|
||||
return resolveSoftDefault();
|
||||
}
|
||||
return { default: list?.[0] };
|
||||
} else if (defaultSpec) {
|
||||
|
||||
const defaultSpec = list.find((spec) => spec.default);
|
||||
if (defaultSpec) {
|
||||
return { default: defaultSpec };
|
||||
}
|
||||
const lastConversationSpecName = lastConversationSetup?.spec;
|
||||
if (!hasSelectionValue(lastConversationSpecName)) {
|
||||
return resolveSoftDefault();
|
||||
|
||||
const softDefaultSpec = list.find((spec) => spec.softDefault);
|
||||
const lastSetup = parseStoredModelSelection(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'),
|
||||
);
|
||||
const lastSpec = hasSelectionValue(lastSetup?.spec)
|
||||
? list.find((spec) => spec.name === lastSetup?.spec)
|
||||
: undefined;
|
||||
|
||||
if (lastSpec && lastSpec.name !== softDefaultSpec?.name) {
|
||||
return { last: lastSpec };
|
||||
}
|
||||
const lastSpec = list?.find((spec) => spec.name === lastConversationSpecName);
|
||||
if (lastSpec && lastSpec.name === softDefaultSpec?.name) {
|
||||
return resolveSoftDefault();
|
||||
|
||||
if (softDefaultSpec) {
|
||||
if (lastSpec?.name === softDefaultSpec.name) {
|
||||
return { softDefault: softDefaultSpec };
|
||||
}
|
||||
const yieldsToSelection =
|
||||
hasModelSelection(lastSetup) &&
|
||||
hasEphemeralModelOptions({
|
||||
endpointsConfig,
|
||||
addedEndpoints,
|
||||
modelSelect: interfaceConfig?.modelSelect,
|
||||
});
|
||||
return yieldsToSelection ? undefined : { softDefault: softDefaultSpec };
|
||||
}
|
||||
return { last: lastSpec };
|
||||
|
||||
if (prioritize === true || interfaceConfig?.modelSelect !== true) {
|
||||
return { default: list[0] };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function getModelSpecPreset(modelSpec?: t.TModelSpec) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@ export function clearAllConversationStorage() {
|
|||
key.startsWith(LocalStorageKeys.TEXT_DRAFT) ||
|
||||
key.startsWith(LocalStorageKeys.ASST_ID_PREFIX) ||
|
||||
key.startsWith(LocalStorageKeys.AGENT_ID_PREFIX) ||
|
||||
key.startsWith(LocalStorageKeys.LAST_CONVO_SETUP)
|
||||
key.startsWith(LocalStorageKeys.LAST_CONVO_SETUP) ||
|
||||
key === LocalStorageKeys.LAST_SPEC ||
|
||||
key === LocalStorageKeys.LAST_MODEL ||
|
||||
key === LocalStorageKeys.LAST_TOOLS
|
||||
) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { NEW_CHAT_PATH, getAccessToken, mockReply, requestJson, sendMessage } from './helpers';
|
||||
import {
|
||||
NEW_CHAT_PATH,
|
||||
getAccessToken,
|
||||
mockReply,
|
||||
requestJson,
|
||||
selectModelSpec,
|
||||
sendMessage,
|
||||
} from './helpers';
|
||||
|
||||
/** Label of the `softDefault: true` spec in e2e/config/librechat.e2e.yaml. */
|
||||
const SOFT_DEFAULT_LABEL = 'E2E Soft Default';
|
||||
|
|
@ -127,9 +134,7 @@ test.describe('soft default model spec', () => {
|
|||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
});
|
||||
|
||||
test('viewing an old soft conversation does not re-arm it over a prior selection', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('viewing the soft conversation re-arms it on the next New Chat', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
await startFresh(page);
|
||||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
|
|
@ -143,7 +148,40 @@ test.describe('soft default model spec', () => {
|
|||
await page.goto(softConvoUrl, { timeout: 10000 });
|
||||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
|
||||
// Fresh load (not the in-memory SPA transition, which masks the regression): the
|
||||
// cold ChatRoute path resolves the New Chat purely from getDefaultModelSpec.
|
||||
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
|
||||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
await expect(modelTrigger(page)).not.toHaveText('Select a model');
|
||||
});
|
||||
|
||||
// Regression: softDefault spec on an endpoint kept out of `addedEndpoints` (e.g. a
|
||||
// bedrock spec with `addedEndpoints: [agents, <custom>]`). Using the custom endpoint
|
||||
// leaves a model in history under a key the spec preset never matches, which used to
|
||||
// suppress the soft default and strand a freshly loaded New Chat on the unselectable
|
||||
// endpoint ("Select a model"). The spec must re-arm when it was the conversation used
|
||||
// last. A cold load is used because the SPA New Chat transition resolves non-
|
||||
// deterministically and can mask the dropped spec.
|
||||
test('re-arms on a fresh New Chat when the spec endpoint is outside the allow-list', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(120000);
|
||||
await startFresh(page);
|
||||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
|
||||
await selectEphemeralModel(page);
|
||||
await sendAndAwaitReply(page, 'history on a different endpoint');
|
||||
|
||||
await newChat(page);
|
||||
await expect(modelTrigger(page)).not.toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
await selectModelSpec(page, SOFT_DEFAULT_LABEL);
|
||||
await sendAndAwaitReply(page, 'soft spec used last');
|
||||
const specConvoUrl = page.url();
|
||||
|
||||
await page.goto(specConvoUrl, { timeout: 10000 });
|
||||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
|
||||
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
|
||||
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
|
||||
await expect(modelTrigger(page)).not.toHaveText('Select a model');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue