|
|
@@ -4,7 +4,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testi
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
import Schema from '@deepseek-ai/schemastery'
|
|
|
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
-import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
|
|
+import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
|
|
import {
|
|
|
ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile,
|
|
|
} from '../src/client/ModelsSection.tsx'
|
|
|
@@ -90,7 +90,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
|
|
return [
|
|
|
{
|
|
|
ns: 'llm-deepseek',
|
|
|
- schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
|
|
+ schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
|
|
value: {
|
|
|
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
|
baseURL: 'https://base',
|
|
|
@@ -108,7 +108,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
|
|
ns: 'llm-plain',
|
|
|
schema: JSON.parse(JSON.stringify(Schema.object({
|
|
|
profiles: Schema.dict(Schema.object({ note: Schema.string() })),
|
|
|
- }).toJSON())) as unknown,
|
|
|
+ }).toJSON())) as JsonValue,
|
|
|
value: {},
|
|
|
applies: 'live',
|
|
|
secrets: [],
|
|
|
@@ -116,7 +116,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
|
|
},
|
|
|
{
|
|
|
ns: 'llm-pi-ai',
|
|
|
- schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as unknown,
|
|
|
+ schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as JsonValue,
|
|
|
value: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
|
|
|
user: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
|
|
|
applies: 'live',
|
|
|
@@ -125,7 +125,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
|
|
},
|
|
|
{
|
|
|
ns: 'subagent-model-selection',
|
|
|
- schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as unknown,
|
|
|
+ schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as JsonValue,
|
|
|
value: { enabled: false },
|
|
|
applies: 'live',
|
|
|
secrets: [],
|
|
|
@@ -144,20 +144,25 @@ function fail<T>(message: string, code = 'settings-rejected'): RpcResponse<T> {
|
|
|
result: { ok: false, error: { code, message, details: { ns: 'x' } } as never },
|
|
|
}
|
|
|
}
|
|
|
+/** Credentials answers over the Remote carrier, which has no envelope. */
|
|
|
+function remoteOk<T>(value: T) {
|
|
|
+ return { ok: true as const, value }
|
|
|
+}
|
|
|
+function remoteFail(message: string, code = 'credential-rejected') {
|
|
|
+ return { ok: false as const, error: { code, message, details: {} } }
|
|
|
+}
|
|
|
|
|
|
function scriptedFace(overrides: {
|
|
|
update?: ReturnType<typeof vi.fn>
|
|
|
- replace?: ReturnType<typeof vi.fn>
|
|
|
mutate?: ReturnType<typeof vi.fn>
|
|
|
set?: ReturnType<typeof vi.fn>
|
|
|
unset?: ReturnType<typeof vi.fn>
|
|
|
} = {}) {
|
|
|
const providerNamespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')!
|
|
|
- const update = overrides.update ?? vi.fn(() => Promise.resolve(ok(providerNamespace)))
|
|
|
- const replace = overrides.replace ?? vi.fn(() => Promise.resolve(ok(providerNamespace)))
|
|
|
- const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(ok(providerNamespace)))
|
|
|
- const set = overrides.set ?? vi.fn(() => Promise.resolve(ok({})))
|
|
|
- const unset = overrides.unset ?? vi.fn(() => Promise.resolve(ok({})))
|
|
|
+ const update = overrides.update ?? vi.fn(() => Promise.resolve(remoteOk(providerNamespace)))
|
|
|
+ const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(remoteOk(providerNamespace)))
|
|
|
+ const set = overrides.set ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
|
|
+ const unset = overrides.unset ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
|
|
const face = {
|
|
|
llm: {
|
|
|
providers: vi.fn(() => Promise.resolve(ok({
|
|
|
@@ -173,24 +178,23 @@ function scriptedFace(overrides: {
|
|
|
models: vi.fn(() => Promise.resolve(ok({ groups: [], failures: [] }))),
|
|
|
},
|
|
|
settings: {
|
|
|
- describe: vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: wireNamespaces() }))),
|
|
|
+ describe: vi.fn(() => Promise.resolve(remoteOk({ writable: true, hasDocument: false, namespaces: wireNamespaces() }))),
|
|
|
update,
|
|
|
- replace,
|
|
|
mutate,
|
|
|
},
|
|
|
credentials: {
|
|
|
- describe: vi.fn((payload: { refs: string[] }) => Promise.resolve(ok({
|
|
|
- credentials: Object.fromEntries(payload.refs.map(ref => [ref, {
|
|
|
+ describe: vi.fn((refs: string[]) => Promise.resolve(remoteOk(
|
|
|
+ Object.fromEntries(refs.map(ref => [ref, {
|
|
|
configured: ref === 'OPENAI_API_KEY',
|
|
|
...ref === 'OPENAI_API_KEY' ? { source: 'file' } : {},
|
|
|
writable: true,
|
|
|
}])),
|
|
|
- }))),
|
|
|
+ ))),
|
|
|
set,
|
|
|
unset,
|
|
|
},
|
|
|
}
|
|
|
- return { face, update, replace, mutate, set, unset }
|
|
|
+ return { face, update, mutate, set, unset }
|
|
|
}
|
|
|
|
|
|
type WireFace = ConstructorParameters<typeof ModelsSettingsStore>[0]
|
|
|
@@ -218,7 +222,7 @@ function cardSeatCalls(
|
|
|
}
|
|
|
|
|
|
async function mountFace(scripted: ReturnType<typeof scriptedFace>) {
|
|
|
- const { face, update, replace, mutate, set, unset } = scripted
|
|
|
+ const { face, update, mutate, set, unset } = scripted
|
|
|
const mirror = new SettingsDescribeMirror(face as never)
|
|
|
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema, mirror)
|
|
|
await controller.load()
|
|
|
@@ -232,7 +236,7 @@ async function mountFace(scripted: ReturnType<typeof scriptedFace>) {
|
|
|
renderSlot: renderSlot as unknown as ModelsSectionProps['renderSlot'],
|
|
|
}
|
|
|
const view = render(<ModelsSection {...injected} />)
|
|
|
- return { view, face, update, replace, mutate, set, unset, controller, mirror, renderSlot }
|
|
|
+ return { view, face, update, mutate, set, unset, controller, mirror, renderSlot }
|
|
|
}
|
|
|
|
|
|
async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
|
|
@@ -245,10 +249,10 @@ async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {})
|
|
|
*/
|
|
|
async function mountFirstRun(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
|
|
const scripted = scriptedFace(overrides)
|
|
|
- scripted.face.credentials.describe.mockImplementation((payload: { refs: string[] }) =>
|
|
|
- Promise.resolve(ok({
|
|
|
- credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: false, writable: true }])),
|
|
|
- })))
|
|
|
+ scripted.face.credentials.describe.mockImplementation((refs: string[]) =>
|
|
|
+ Promise.resolve(remoteOk(
|
|
|
+ Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
|
|
+ )))
|
|
|
return mountFace(scripted)
|
|
|
}
|
|
|
|
|
|
@@ -295,12 +299,12 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('derives the draft seat\'s key fact from the page\'s conventional reference', async () => {
|
|
|
const scripted = scriptedFace()
|
|
|
- scripted.face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
|
|
- credentials: Object.fromEntries(payload.refs.map(ref => [ref, {
|
|
|
+ scripted.face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
|
|
+ Object.fromEntries(refs.map(ref => [ref, {
|
|
|
configured: ref === 'OPENAI_API_KEY' || ref === 'ANTHROPIC_API_KEY',
|
|
|
writable: true,
|
|
|
}])),
|
|
|
- })))
|
|
|
+ )))
|
|
|
const { renderSlot } = await mountFace(scripted)
|
|
|
renderSlot.mockClear()
|
|
|
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
|
|
@@ -332,7 +336,7 @@ describe('ModelsSection', () => {
|
|
|
user: { enabled: true },
|
|
|
revision: 5,
|
|
|
}
|
|
|
- const update = vi.fn(() => Promise.resolve(ok(enabledNamespace)))
|
|
|
+ const update = vi.fn(() => Promise.resolve(remoteOk(enabledNamespace)))
|
|
|
await mountSection({ update })
|
|
|
|
|
|
const toggle = screen.getByRole('switch', { name: en.subagentModelSelectionToggle })
|
|
|
@@ -340,18 +344,18 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(toggle)
|
|
|
|
|
|
await waitFor(() => { expect(toggle.getAttribute('aria-checked')).toBe('true') })
|
|
|
- expect(update).toHaveBeenCalledWith({
|
|
|
- ns: 'subagent-model-selection',
|
|
|
- patch: { enabled: true },
|
|
|
- expectedRevision: 4,
|
|
|
- })
|
|
|
+ expect(update).toHaveBeenCalledWith(
|
|
|
+ 'subagent-model-selection',
|
|
|
+ { enabled: true },
|
|
|
+ 4,
|
|
|
+ )
|
|
|
expect(screen.getByRole('status').textContent).toBe(en.subagentModelSelectionSaved)
|
|
|
})
|
|
|
|
|
|
it('reports rejected subagent model-selection updates and permits a retry', async () => {
|
|
|
const update = vi.fn()
|
|
|
- .mockResolvedValueOnce(fail<SettingsNamespaceView>('revision changed'))
|
|
|
- .mockResolvedValueOnce(ok({
|
|
|
+ .mockResolvedValueOnce(remoteFail('revision changed', 'settings-rejected'))
|
|
|
+ .mockResolvedValueOnce(remoteOk({
|
|
|
...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!,
|
|
|
value: { enabled: true },
|
|
|
revision: 5,
|
|
|
@@ -372,12 +376,12 @@ describe('ModelsSection', () => {
|
|
|
...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!,
|
|
|
value: null,
|
|
|
} as unknown as SettingsNamespaceView
|
|
|
- const update = vi.fn()
|
|
|
+ const mutate = vi.fn()
|
|
|
render(
|
|
|
<SubagentModelSelectionCard
|
|
|
namespace={namespace}
|
|
|
writable={false}
|
|
|
- api={{ settings: { update } } as never}
|
|
|
+ api={{ settings: { mutate } } as never}
|
|
|
controller={{ acceptNamespace: vi.fn(), load: vi.fn() } as never}
|
|
|
t={t}
|
|
|
/>,
|
|
|
@@ -387,7 +391,7 @@ describe('ModelsSection', () => {
|
|
|
expect(toggle.getAttribute('aria-checked')).toBe('false')
|
|
|
expect((toggle as HTMLButtonElement).disabled).toBe(true)
|
|
|
fireEvent.click(toggle)
|
|
|
- expect(update).not.toHaveBeenCalled()
|
|
|
+ expect(mutate).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it('renders the unkeyed whole-section provider as an open setup card in the first-run posture', async () => {
|
|
|
@@ -420,9 +424,9 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('marks only a confirmed missing reference and leaves native or unavailable state unmarked', async () => {
|
|
|
const { face } = scriptedFace()
|
|
|
- face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
|
|
- credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: false, writable: true }])),
|
|
|
- })))
|
|
|
+ face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
|
|
+ Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
|
|
+ )))
|
|
|
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema, new SettingsDescribeMirror(face as never))
|
|
|
await controller.load()
|
|
|
render(<ModelsSection
|
|
|
@@ -444,9 +448,9 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('turns the setup card into a row once the credential reports configured', async () => {
|
|
|
const { face } = await mountFirstRun()
|
|
|
- face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
|
|
- credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: true, writable: true }])),
|
|
|
- })))
|
|
|
+ face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
|
|
+ Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])),
|
|
|
+ )))
|
|
|
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema, new SettingsDescribeMirror(face as never))
|
|
|
await controller.load()
|
|
|
cleanup()
|
|
|
@@ -503,12 +507,12 @@ describe('ModelsSection', () => {
|
|
|
})
|
|
|
|
|
|
it('stores a typed key write-only from the setup card without touching settings', async () => {
|
|
|
- const { set, update, face } = await mountFirstRun()
|
|
|
+ const { set, mutate, face } = await mountFirstRun()
|
|
|
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
fireEvent.change(key, { target: { value: ' sk-live ' } })
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
- await waitFor(() => { expect(set).toHaveBeenCalledWith({ ref: 'DEEPSEEK_API_KEY', value: 'sk-live' }) })
|
|
|
- expect(update).not.toHaveBeenCalled()
|
|
|
+ await waitFor(() => { expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-live') })
|
|
|
+ expect(mutate).not.toHaveBeenCalled()
|
|
|
// The saved key re-loads the join; the settings answer rides the shared
|
|
|
// mirror, so the reload shows as a directory read rather than a describe.
|
|
|
await waitFor(() => { expect(face.llm.providers.mock.calls.length).toBeGreaterThan(1) })
|
|
|
@@ -520,8 +524,8 @@ describe('ModelsSection', () => {
|
|
|
})
|
|
|
|
|
|
it('reuses the provider editor as a required credential-only onboarding form', async () => {
|
|
|
- let finishSet: ((response: RpcResponse<Record<string, never>>) => void) | undefined
|
|
|
- const set = vi.fn(() => new Promise<RpcResponse<Record<string, never>>>((resolve) => {
|
|
|
+ let finishSet: ((response: { ok: true; value: undefined }) => void) | undefined
|
|
|
+ const set = vi.fn(() => new Promise<{ ok: true; value: undefined }>((resolve) => {
|
|
|
finishSet = resolve
|
|
|
}))
|
|
|
const { face, mutate } = scriptedFace({ set })
|
|
|
@@ -567,13 +571,13 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(save)
|
|
|
|
|
|
expect(await screen.findByText(en.onboardingSaving)).toBeTruthy()
|
|
|
- expect(set).toHaveBeenCalledWith({ ref: 'DEEPSEEK_API_KEY', value: 'sk-onboarding' })
|
|
|
+ expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-onboarding')
|
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
|
|
|
|
if (finishSet === undefined) throw new Error('credential write did not start')
|
|
|
await act(async () => {
|
|
|
- finishSet?.(ok({}))
|
|
|
+ finishSet?.(remoteOk(undefined))
|
|
|
await Promise.resolve()
|
|
|
})
|
|
|
expect(onClose).toHaveBeenCalledWith(true)
|
|
|
@@ -581,7 +585,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('applies customized deepseek fields as path ops', async () => {
|
|
|
const { mutate } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
const baseURL = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
|
|
@@ -593,16 +597,16 @@ describe('ModelsSection', () => {
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
// Only the field that actually changed: reasoningEffort was already
|
|
|
// 'high' in the loaded profile, so it produces no op.
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-deepseek',
|
|
|
- ops: [{ op: 'set', path: ['baseURL'], value: 'https://next2' }],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-deepseek',
|
|
|
+ [{ op: 'set', path: ['baseURL'], value: 'https://next2' }],
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('materializes inherited models and adds an arbitrary DeepSeek id', async () => {
|
|
|
const { mutate } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
expect(screen.getByText(en.modelsInherited)).toBeTruthy()
|
|
|
@@ -620,9 +624,9 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-deepseek',
|
|
|
- ops: [{
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-deepseek',
|
|
|
+ [{
|
|
|
op: 'set',
|
|
|
path: ['models'],
|
|
|
value: [
|
|
|
@@ -630,8 +634,8 @@ describe('ModelsSection', () => {
|
|
|
{ id: 'private-preview', name: 'Private Preview', contextWindow: 131_072 },
|
|
|
],
|
|
|
}],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('rejects duplicate DeepSeek model ids before writing', async () => {
|
|
|
@@ -706,7 +710,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('accepts a suffixed context window and stores the plain count', async () => {
|
|
|
const { mutate } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
expandRow(1)
|
|
|
@@ -730,9 +734,9 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-deepseek',
|
|
|
- ops: [{
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-deepseek',
|
|
|
+ [{
|
|
|
op: 'set',
|
|
|
path: ['models'],
|
|
|
value: [
|
|
|
@@ -740,8 +744,8 @@ describe('ModelsSection', () => {
|
|
|
{ ...DEFAULT_DEEPSEEK_MODELS[1], contextWindow: 256_000 },
|
|
|
],
|
|
|
}],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('keeps unreadable context-window text on screen and refuses the write', async () => {
|
|
|
@@ -773,7 +777,7 @@ describe('ModelsSection', () => {
|
|
|
const stored = { models: [{ id: 'user-only-model', name: 'User Only' }] }
|
|
|
const overridden: SettingsNamespaceView = {
|
|
|
ns: 'llm-deepseek',
|
|
|
- schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
|
|
+ schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
|
|
value: { ...stored, defaultContextWindow: 1_000_000 },
|
|
|
...base === undefined ? {} : { base },
|
|
|
user: stored,
|
|
|
@@ -858,7 +862,7 @@ describe('ModelsSection', () => {
|
|
|
// inherited row displayed text no settings layer stores — and because an
|
|
|
// unreadable buffer never settles, it stayed there indefinitely.
|
|
|
const { mutate } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
expandRow(1)
|
|
|
@@ -881,7 +885,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('edits an output cap per model and carries its text across a removal', async () => {
|
|
|
const { mutate } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
expandRow(1)
|
|
|
@@ -902,15 +906,15 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-deepseek',
|
|
|
- ops: [{
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-deepseek',
|
|
|
+ [{
|
|
|
op: 'set',
|
|
|
path: ['models'],
|
|
|
value: [{ ...DEFAULT_DEEPSEEK_MODELS[1], maxTokens: 64_000 }],
|
|
|
}],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('settles a pasted id and refuses whitespace that would never match', async () => {
|
|
|
@@ -952,7 +956,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('can empty and reset the model override, then clear optional fields without dropping hidden data', async () => {
|
|
|
const { mutate } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
fireEvent.click(screen.getAllByLabelText(new RegExp(en.removeModel))[0] as HTMLElement)
|
|
|
@@ -969,9 +973,9 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-deepseek',
|
|
|
- ops: [{
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-deepseek',
|
|
|
+ [{
|
|
|
op: 'set',
|
|
|
path: ['models'],
|
|
|
value: [
|
|
|
@@ -979,33 +983,33 @@ describe('ModelsSection', () => {
|
|
|
DEFAULT_DEEPSEEK_MODELS[1],
|
|
|
],
|
|
|
}],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('clears an inherited override with an unset op, never a whole-section replace', async () => {
|
|
|
// A whole-section replace would clobber sibling overrides to clear one field.
|
|
|
- const { replace, update, mutate } = await mountDeepSeekCard()
|
|
|
+ const { mutate } = await mountDeepSeekCard()
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
|
|
expect(url.value).toBe('https://base')
|
|
|
fireEvent.change(url, { target: { value: '' } })
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(replace).not.toHaveBeenCalled()
|
|
|
- expect(update).not.toHaveBeenCalled()
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-deepseek',
|
|
|
- ops: [{ op: 'unset', path: ['baseURL'] }],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ // This editor clears one field through an unset op so it cannot clobber
|
|
|
+ // sibling overrides with a whole-section replacement.
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-deepseek',
|
|
|
+ [{ op: 'unset', path: ['baseURL'] }],
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('pins the deepseek placeholder and clears typed input back to inherited', async () => {
|
|
|
const { face } = scriptedFace()
|
|
|
const bare: SettingsNamespaceView = {
|
|
|
ns: 'llm-deepseek',
|
|
|
- schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
|
|
+ schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
|
|
value: {},
|
|
|
applies: 'live',
|
|
|
secrets: [],
|
|
|
@@ -1033,12 +1037,12 @@ describe('ModelsSection', () => {
|
|
|
})
|
|
|
|
|
|
it('rejects an invalid draft before writing', async () => {
|
|
|
- const { update } = await mountDeepSeekCard()
|
|
|
+ const { mutate } = await mountDeepSeekCard()
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'not-a-url' } })
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await screen.findByText(/baseURL/)
|
|
|
- expect(update).not.toHaveBeenCalled()
|
|
|
+ expect(mutate).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it('edits a pi-ai profile with the curated fields only', async () => {
|
|
|
@@ -1057,11 +1061,11 @@ describe('ModelsSection', () => {
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
// Only the edited field travels: apiKeyEnv and headers were already stored
|
|
|
// with these values, so no op restates them.
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-pi-ai',
|
|
|
- ops: [{ op: 'set', path: ['providers', 'openai', 'baseURL'], value: 'https://proxy/v2' }],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-pi-ai',
|
|
|
+ [{ op: 'set', path: ['providers', 'openai', 'baseURL'], value: 'https://proxy/v2' }],
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('adds a dormant provider with a derived reference and stores its key', async () => {
|
|
|
@@ -1079,12 +1083,12 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.change(addKey, { target: { value: 'sk-ant' } })
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-pi-ai',
|
|
|
- ops: [{ op: 'set', path: ['providers', 'anthropic', 'apiKeyEnv'], value: 'ANTHROPIC_API_KEY' }],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
- await waitFor(() => { expect(set).toHaveBeenCalledWith({ ref: 'ANTHROPIC_API_KEY', value: 'sk-ant' }) })
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-pi-ai',
|
|
|
+ [{ op: 'set', path: ['providers', 'anthropic', 'apiKeyEnv'], value: 'ANTHROPIC_API_KEY' }],
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
+ await waitFor(() => { expect(set).toHaveBeenCalledWith('ANTHROPIC_API_KEY', 'sk-ant') })
|
|
|
})
|
|
|
|
|
|
it('keeps pi-ai provider-native authentication when no key is entered', async () => {
|
|
|
@@ -1093,11 +1097,11 @@ describe('ModelsSection', () => {
|
|
|
await screen.findByLabelText(en.provider)
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-pi-ai',
|
|
|
- ops: [{ op: 'set', path: ['providers', 'anthropic'], value: {} }],
|
|
|
- expectedRevision: 0,
|
|
|
- })
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-pi-ai',
|
|
|
+ [{ op: 'set', path: ['providers', 'anthropic'], value: {} }],
|
|
|
+ 0,
|
|
|
+ ])
|
|
|
expect(set).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
@@ -1115,10 +1119,10 @@ describe('ModelsSection', () => {
|
|
|
} },
|
|
|
revision: 1,
|
|
|
}
|
|
|
- const mutate = vi.fn(() => Promise.resolve(ok(afterSettings)))
|
|
|
+ const mutate = vi.fn(() => Promise.resolve(remoteOk(afterSettings)))
|
|
|
const set = vi.fn()
|
|
|
- .mockResolvedValueOnce(fail('credential store unavailable', 'credential-rejected'))
|
|
|
- .mockResolvedValueOnce(ok({}))
|
|
|
+ .mockResolvedValueOnce(remoteFail('credential store unavailable'))
|
|
|
+ .mockResolvedValueOnce(remoteOk(undefined))
|
|
|
const { face, controller, mirror } = await mountSection({ mutate, set })
|
|
|
fireEvent.click(screen.getByText(en.add))
|
|
|
await screen.findByLabelText(en.provider)
|
|
|
@@ -1126,7 +1130,7 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await screen.findByText('credential store unavailable')
|
|
|
expect(mutate).toHaveBeenCalledOnce()
|
|
|
- face.settings.describe.mockResolvedValue(ok({
|
|
|
+ face.settings.describe.mockResolvedValue(remoteOk({
|
|
|
writable: true,
|
|
|
hasDocument: false,
|
|
|
namespaces: wireNamespaces().map(namespace => namespace.ns === 'llm-pi-ai' ? afterSettings : namespace),
|
|
|
@@ -1141,7 +1145,7 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await waitFor(() => { expect(set).toHaveBeenCalledTimes(2) })
|
|
|
expect(mutate).toHaveBeenCalledOnce()
|
|
|
- expect(set).toHaveBeenLastCalledWith({ ref: 'ANTHROPIC_API_KEY', value: 'sk-ant' })
|
|
|
+ expect(set).toHaveBeenLastCalledWith('ANTHROPIC_API_KEY', 'sk-ant')
|
|
|
})
|
|
|
|
|
|
it('switches the add card target and degrades unknown or broken targets loudly', async () => {
|
|
|
@@ -1161,7 +1165,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('surfaces a rejected settings write and never stores the key after it', async () => {
|
|
|
const { set } = await mountSection({
|
|
|
- mutate: vi.fn(() => Promise.resolve(fail('llm-pi-ai: unknown pi-ai provider "bogus"'))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteFail('llm-pi-ai: unknown pi-ai provider "bogus"', 'settings-rejected'))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.add))
|
|
|
await screen.findByLabelText(en.provider)
|
|
|
@@ -1202,7 +1206,7 @@ describe('ModelsSection', () => {
|
|
|
// The stale-draft overwrite: two tabs open the same card, the other saves,
|
|
|
// and this one must be refused rather than replay its opening snapshot.
|
|
|
const { set } = await mountDeepSeekCard({
|
|
|
- mutate: vi.fn(() => Promise.resolve(fail('changed since it was read', 'settings-conflict'))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteFail('changed since it was read', 'settings-conflict'))),
|
|
|
})
|
|
|
fireEvent.click(screen.getByText(en.customized))
|
|
|
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.baseUrl), { target: { value: 'https://mine' } })
|
|
|
@@ -1226,7 +1230,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('surfaces a shadowed credential write on the card', async () => {
|
|
|
await mountFirstRun({
|
|
|
- set: vi.fn(() => Promise.resolve(fail('credentials: DEEPSEEK_API_KEY is shadowed by the read-only environment', 'credential-rejected'))),
|
|
|
+ set: vi.fn(() => Promise.resolve(remoteFail('credentials: DEEPSEEK_API_KEY is shadowed by the read-only environment'))),
|
|
|
})
|
|
|
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
fireEvent.change(key, { target: { value: 'sk-live' } })
|
|
|
@@ -1237,11 +1241,11 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('locks the key input when the launch environment provides the credential', async () => {
|
|
|
const { face } = await mountSection()
|
|
|
- face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
|
|
- credentials: Object.fromEntries(payload.refs.map(ref => [ref, {
|
|
|
+ face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
|
|
+ Object.fromEntries(refs.map(ref => [ref, {
|
|
|
configured: ref === 'OPENAI_API_KEY', source: 'env', writable: false,
|
|
|
}])),
|
|
|
- })))
|
|
|
+ )))
|
|
|
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
|
|
const editorKey = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
await waitFor(() => { expect(editorKey.placeholder).toBe(en.keyEnvLocked) })
|
|
|
@@ -1250,7 +1254,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('keeps a failed credential describe silent and the input usable', async () => {
|
|
|
const { face, set } = await mountSection()
|
|
|
- face.credentials.describe.mockImplementation(() => Promise.resolve(fail('down', 'internal')) as never)
|
|
|
+ face.credentials.describe.mockImplementation(() => Promise.resolve(remoteFail('down', 'internal')) as never)
|
|
|
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
|
|
const editorKey = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
expect(editorKey.placeholder).toBe(en.keyPlaceholderNative)
|
|
|
@@ -1260,7 +1264,7 @@ describe('ModelsSection', () => {
|
|
|
})
|
|
|
|
|
|
it('requires confirmation before removing a user-added provider', async () => {
|
|
|
- const { replace, mutate, unset } = await mountSection()
|
|
|
+ const { mutate, unset } = await mountSection()
|
|
|
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
|
|
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
|
|
expect(dialog.textContent).toContain(openaiCopy(en.deleteDescriptionWithCredential))
|
|
|
@@ -1280,20 +1284,20 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
|
|
fireEvent.click(within(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) }))
|
|
|
.getByRole('button', { name: openaiCopy(en.deleteConfirm) }))
|
|
|
- await waitFor(() => { expect(unset).toHaveBeenCalledWith({ ref: 'OPENAI_API_KEY' }) })
|
|
|
+ await waitFor(() => { expect(unset).toHaveBeenCalledWith('OPENAI_API_KEY') })
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
expect(unset.mock.invocationCallOrder[0]).toBeLessThan(mutate.mock.invocationCallOrder[0] as number)
|
|
|
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
|
|
- expect(replace).not.toHaveBeenCalled()
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-pi-ai',
|
|
|
- ops: [{ op: 'unset', path: ['providers', 'openai'] }],
|
|
|
- })
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-pi-ai',
|
|
|
+ [{ op: 'unset', path: ['providers', 'openai'] }],
|
|
|
+ undefined,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('blocks duplicate deletion while the confirmed removal is pending', async () => {
|
|
|
- let resolveRemoval!: (response: RpcResponse<SettingsNamespaceView>) => void
|
|
|
- const mutate = vi.fn(() => new Promise<RpcResponse<SettingsNamespaceView>>((resolve) => {
|
|
|
+ let resolveRemoval!: (response: { ok: true; value: SettingsNamespaceView }) => void
|
|
|
+ const mutate = vi.fn(() => new Promise<{ ok: true; value: SettingsNamespaceView }>((resolve) => {
|
|
|
resolveRemoval = resolve
|
|
|
}))
|
|
|
await mountSection({ mutate })
|
|
|
@@ -1309,7 +1313,7 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: en.close }))
|
|
|
expect(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBe(dialog)
|
|
|
expect(mutate).toHaveBeenCalledOnce()
|
|
|
- await act(async () => { resolveRemoval(ok(wireNamespaces()[2]!)) })
|
|
|
+ await act(async () => { resolveRemoval(remoteOk(wireNamespaces()[2]!)) })
|
|
|
await waitFor(() => {
|
|
|
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
|
|
})
|
|
|
@@ -1336,7 +1340,7 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('shows the read-only notice and disables mutations for a read-only provider', async () => {
|
|
|
const { face } = await mountSection()
|
|
|
- face.settings.describe.mockImplementation(() => Promise.resolve(ok({
|
|
|
+ face.settings.describe.mockImplementation(() => Promise.resolve(remoteOk({
|
|
|
writable: false,
|
|
|
hasDocument: false,
|
|
|
namespaces: wireNamespaces(),
|
|
|
@@ -1358,7 +1362,7 @@ describe('ModelsSection', () => {
|
|
|
})
|
|
|
|
|
|
it('toggles the row editor closed on a second edit click and on cancel', async () => {
|
|
|
- const { update } = await mountSection()
|
|
|
+ const { mutate } = await mountSection()
|
|
|
const edit = screen.getByRole('button', { name: openaiCopy(en.editProvider) })
|
|
|
fireEvent.click(edit)
|
|
|
await waitFor(() => { expect(screen.queryAllByLabelText(en.keyInput).length).toBe(1) })
|
|
|
@@ -1368,7 +1372,7 @@ describe('ModelsSection', () => {
|
|
|
await waitFor(() => { expect(screen.queryAllByLabelText(en.keyInput).length).toBe(1) })
|
|
|
fireEvent.click(screen.getByText(en.cancel))
|
|
|
expect(screen.queryAllByLabelText(en.keyInput)).toHaveLength(0)
|
|
|
- expect(update).not.toHaveBeenCalled()
|
|
|
+ expect(mutate).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it('cancels the add card back to the add button', async () => {
|
|
|
@@ -1420,22 +1424,22 @@ describe('ModelsSection', () => {
|
|
|
it('removes by unsetting the profile path, never by rebuilding the section', async () => {
|
|
|
// The page only needs to name the profile path; rebuilding the section
|
|
|
// would widen the write for no benefit.
|
|
|
- const { face, mutate, replace, controller } = await mountSection()
|
|
|
+ const { face, mutate, controller } = await mountSection()
|
|
|
await removeProviderProfile(
|
|
|
face as unknown as Parameters<typeof removeProviderProfile>[0],
|
|
|
controller,
|
|
|
{ settingsNs: 'llm-plain', settingsPath: ['ghost-profile'] },
|
|
|
)
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-plain',
|
|
|
- ops: [{ op: 'unset', path: ['ghost-profile'] }],
|
|
|
- })
|
|
|
- expect(replace).not.toHaveBeenCalled()
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-plain',
|
|
|
+ [{ op: 'unset', path: ['ghost-profile'] }],
|
|
|
+ undefined,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('keeps the snapshot untouched and reports the message when a removal write is refused', async () => {
|
|
|
const { face, controller } = await mountSection({
|
|
|
- mutate: vi.fn(() => Promise.resolve(fail('read-only'))),
|
|
|
+ mutate: vi.fn(() => Promise.resolve(remoteFail('read-only', 'settings-rejected'))),
|
|
|
})
|
|
|
const before = controller.store.getSnapshot().rows
|
|
|
const failure = await removeProviderProfile(
|
|
|
@@ -1449,8 +1453,8 @@ describe('ModelsSection', () => {
|
|
|
|
|
|
it('keeps a failed identified deletion recoverable in its confirmation dialog', async () => {
|
|
|
const mutate = vi.fn()
|
|
|
- .mockResolvedValueOnce(fail('the host refused'))
|
|
|
- .mockResolvedValueOnce(ok(wireNamespaces()[2]!))
|
|
|
+ .mockResolvedValueOnce(remoteFail('the host refused', 'settings-rejected'))
|
|
|
+ .mockResolvedValueOnce(remoteOk(wireNamespaces()[2]!))
|
|
|
const { unset } = await mountSection({ mutate })
|
|
|
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
|
|
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
|
|
@@ -1478,15 +1482,16 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: providerCopy(en.deleteConfirm, target) }))
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
|
|
expect(unset).not.toHaveBeenCalled()
|
|
|
- expect(mutate.mock.calls[0]?.[0]).toEqual({
|
|
|
- ns: 'llm-pi-ai',
|
|
|
- ops: [{ op: 'unset', path: ['providers', 'zombie'] }],
|
|
|
- })
|
|
|
+ expect(mutate.mock.calls[0]).toEqual([
|
|
|
+ 'llm-pi-ai',
|
|
|
+ [{ op: 'unset', path: ['providers', 'zombie'] }],
|
|
|
+ undefined,
|
|
|
+ ])
|
|
|
})
|
|
|
|
|
|
it('does not remove provider settings when its managed credential removal is refused', async () => {
|
|
|
const { face, controller, mutate } = await mountSection({
|
|
|
- unset: vi.fn(() => Promise.resolve(fail('credential is read-only', 'credential-rejected'))),
|
|
|
+ unset: vi.fn(() => Promise.resolve(remoteFail('credential is read-only'))),
|
|
|
})
|
|
|
const failure = await removeProviderProfile(
|
|
|
face as unknown as Parameters<typeof removeProviderProfile>[0],
|