|
|
@@ -5,7 +5,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
import Schema from 'schemastery'
|
|
|
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
|
|
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client'
|
|
|
-import { ModelsSection, needsSetup, removeProviderProfile } from '../src/client/ModelsSection.tsx'
|
|
|
+import {
|
|
|
+ ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile,
|
|
|
+} from '../src/client/ModelsSection.tsx'
|
|
|
import type { ModelsSectionInjected, ModelsSectionProps } from '../src/client/ModelsSection.tsx'
|
|
|
import { pathOps } from '../src/client/ProviderEditor.tsx'
|
|
|
import {
|
|
|
@@ -18,6 +20,8 @@ import { en } from '../src/client/locales.ts'
|
|
|
afterEach(cleanup)
|
|
|
|
|
|
const t: ModelsSectionInjected['t'] = key => en[key]
|
|
|
+const OPENAI_TARGET = { provider: 'openai', displayName: 'openai' }
|
|
|
+const openaiCopy = (template: string): string => providerCopy(template, OPENAI_TARGET)
|
|
|
|
|
|
/** Open one row's capacity disclosure (1-based, as the labels read). */
|
|
|
function expandRow(position: number): void {
|
|
|
@@ -136,11 +140,13 @@ function scriptedFace(overrides: {
|
|
|
replace?: ReturnType<typeof vi.fn>
|
|
|
mutate?: ReturnType<typeof vi.fn>
|
|
|
set?: ReturnType<typeof vi.fn>
|
|
|
+ unset?: ReturnType<typeof vi.fn>
|
|
|
} = {}) {
|
|
|
const update = overrides.update ?? vi.fn(() => Promise.resolve(ok(wireNamespaces()[2])))
|
|
|
const replace = overrides.replace ?? vi.fn(() => Promise.resolve(ok(wireNamespaces()[2])))
|
|
|
const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(ok(wireNamespaces()[2])))
|
|
|
const set = overrides.set ?? vi.fn(() => Promise.resolve(ok({})))
|
|
|
+ const unset = overrides.unset ?? vi.fn(() => Promise.resolve(ok({})))
|
|
|
const face = {
|
|
|
llm: {
|
|
|
providers: vi.fn(() => Promise.resolve(ok({
|
|
|
@@ -170,16 +176,16 @@ function scriptedFace(overrides: {
|
|
|
}])),
|
|
|
}))),
|
|
|
set,
|
|
|
- unset: vi.fn(() => Promise.resolve(ok({}))),
|
|
|
+ unset,
|
|
|
},
|
|
|
}
|
|
|
- return { face, update, replace, mutate, set }
|
|
|
+ return { face, update, replace, mutate, set, unset }
|
|
|
}
|
|
|
|
|
|
type WireFace = ConstructorParameters<typeof ModelsSettingsStore>[0]
|
|
|
|
|
|
async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
|
|
- const { face, update, replace, mutate, set } = scriptedFace(overrides)
|
|
|
+ const { face, update, replace, mutate, set, unset } = scriptedFace(overrides)
|
|
|
const controller = new ModelsSettingsStore(face as unknown as WireFace)
|
|
|
await controller.load()
|
|
|
const injected: ModelsSectionInjected = {
|
|
|
@@ -189,7 +195,7 @@ async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {})
|
|
|
t,
|
|
|
}
|
|
|
const view = render(<ModelsSection {...injected} />)
|
|
|
- return { view, face, update, replace, mutate, set, controller }
|
|
|
+ return { view, face, update, replace, mutate, set, unset, controller }
|
|
|
}
|
|
|
|
|
|
describe('ModelsSection', () => {
|
|
|
@@ -207,9 +213,36 @@ describe('ModelsSection', () => {
|
|
|
expect(screen.getByText('openai')).toBeTruthy()
|
|
|
expect(screen.queryByText('Active')).toBeNull()
|
|
|
expect(screen.queryByText('Inactive')).toBeNull()
|
|
|
+ const configured = screen.getByRole('img', { name: en.credentialConfigured })
|
|
|
+ expect(configured.getAttribute('title')).toBe(en.credentialConfigured)
|
|
|
+ expect(configured.className).toContain('credentialDotConfigured')
|
|
|
+ expect(configured.closest('li')?.textContent).toContain('openai')
|
|
|
+ expect(screen.queryByRole('img', { name: en.credentialMissing })).toBeNull()
|
|
|
expect(screen.getByText(en.add)).toBeTruthy()
|
|
|
})
|
|
|
|
|
|
+ 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 }])),
|
|
|
+ })))
|
|
|
+ const controller = new ModelsSettingsStore(face as unknown as WireFace)
|
|
|
+ await controller.load()
|
|
|
+ render(<ModelsSection
|
|
|
+ controller={controller}
|
|
|
+ useSnapshot={bindSnapshotSelector(controller.store)}
|
|
|
+ api={face as never}
|
|
|
+ t={t}
|
|
|
+ />)
|
|
|
+
|
|
|
+ const missing = screen.getByRole('img', { name: en.credentialMissing })
|
|
|
+ expect(missing.getAttribute('title')).toBe(en.credentialMissing)
|
|
|
+ expect(missing.className).toContain('credentialDotMissing')
|
|
|
+ expect(missing.closest('li')?.textContent).toContain('openai')
|
|
|
+ expect(screen.queryByRole('img', { name: en.credentialConfigured })).toBeNull()
|
|
|
+ expect(screen.getByText('zombie').closest('li')?.querySelector('[role="img"]')).toBeNull()
|
|
|
+ })
|
|
|
+
|
|
|
it('turns the setup card into a row once the credential reports configured', async () => {
|
|
|
const { face } = await mountSection()
|
|
|
face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
|
|
@@ -254,6 +287,13 @@ describe('ModelsSection', () => {
|
|
|
expect(deriveKeyRef('minimax-cn')).toBe('MINIMAX_CN_API_KEY')
|
|
|
})
|
|
|
|
|
|
+ it('uses one stable provider identity in action copy', () => {
|
|
|
+ const target = { provider: 'deepseek-official', displayName: 'DeepSeek' }
|
|
|
+ expect(providerTargetLabel(target)).toBe('DeepSeek (deepseek-official)')
|
|
|
+ expect(providerCopy(en.deleteTitle, target)).toBe('Delete DeepSeek (deepseek-official)?')
|
|
|
+ expect(providerTargetLabel(OPENAI_TARGET)).toBe('openai')
|
|
|
+ })
|
|
|
+
|
|
|
it('names only the fields the card can see, so an unseen secret survives', () => {
|
|
|
// `before` is the REDACTED subtree: a stored literal apiKey is in neither
|
|
|
// side, so no op mentions it and the seam leaves it alone.
|
|
|
@@ -268,11 +308,16 @@ describe('ModelsSection', () => {
|
|
|
it('stores a typed key write-only from the setup card without touching settings', async () => {
|
|
|
const { set, update, face } = await mountSection()
|
|
|
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
- fireEvent.change(key, { target: { value: 'sk-live' } })
|
|
|
+ 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(face.settings.describe.mock.calls.length).toBeGreaterThan(1) })
|
|
|
+ expect((await screen.findByRole('status')).textContent).toBe(
|
|
|
+ providerCopy(en.savedProvider, { provider: 'deepseek-official', displayName: 'DeepSeek' }),
|
|
|
+ )
|
|
|
+ fireEvent.click(screen.getByText(en.add))
|
|
|
+ expect(screen.queryByRole('status')).toBeNull()
|
|
|
})
|
|
|
|
|
|
it('applies customized deepseek fields as path ops', async () => {
|
|
|
@@ -777,6 +822,7 @@ describe('ModelsSection', () => {
|
|
|
expect((urls[1] as HTMLInputElement).placeholder).toBe(en.baseUrlDefault)
|
|
|
const keys = screen.getAllByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
const addKey = keys[keys.length - 1] as HTMLInputElement
|
|
|
+ expect(addKey.placeholder).toBe(en.keyPlaceholderNative)
|
|
|
fireEvent.change(addKey, { target: { value: 'sk-ant' } })
|
|
|
fireEvent.click(screen.getAllByText(en.apply)[1] as HTMLElement)
|
|
|
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
|
|
@@ -788,6 +834,59 @@ describe('ModelsSection', () => {
|
|
|
await waitFor(() => { expect(set).toHaveBeenCalledWith({ ref: 'ANTHROPIC_API_KEY', value: 'sk-ant' }) })
|
|
|
})
|
|
|
|
|
|
+ it('keeps pi-ai provider-native authentication when no key is entered', async () => {
|
|
|
+ const { mutate, set } = await mountSection()
|
|
|
+ fireEvent.click(screen.getByText(en.add))
|
|
|
+ await screen.findByLabelText(en.provider)
|
|
|
+ fireEvent.click(screen.getAllByText(en.apply)[1] as HTMLElement)
|
|
|
+ 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(set).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('retries only the credential after refreshed settings already committed', async () => {
|
|
|
+ const committed = wireNamespaces()[2]!
|
|
|
+ const afterSettings: SettingsNamespaceView = {
|
|
|
+ ...committed,
|
|
|
+ value: { providers: {
|
|
|
+ ...(committed.value as { providers: object }).providers,
|
|
|
+ anthropic: { apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
|
+ } },
|
|
|
+ user: { providers: {
|
|
|
+ ...(committed.user as { providers: object }).providers,
|
|
|
+ anthropic: { apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
|
+ } },
|
|
|
+ revision: 1,
|
|
|
+ }
|
|
|
+ const mutate = vi.fn(() => Promise.resolve(ok(afterSettings)))
|
|
|
+ const set = vi.fn()
|
|
|
+ .mockResolvedValueOnce(fail('credential store unavailable', 'credential-rejected'))
|
|
|
+ .mockResolvedValueOnce(ok({}))
|
|
|
+ const { face, controller } = await mountSection({ mutate, set })
|
|
|
+ fireEvent.click(screen.getByText(en.add))
|
|
|
+ await screen.findByLabelText(en.provider)
|
|
|
+ const keys = screen.getAllByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
+ fireEvent.change(keys[keys.length - 1] as HTMLInputElement, { target: { value: 'sk-ant' } })
|
|
|
+ fireEvent.click(screen.getAllByText(en.apply)[1] as HTMLElement)
|
|
|
+ await screen.findByText('credential store unavailable')
|
|
|
+ expect(mutate).toHaveBeenCalledOnce()
|
|
|
+ face.settings.describe.mockResolvedValue(ok({
|
|
|
+ writable: true,
|
|
|
+ hasDocument: false,
|
|
|
+ namespaces: wireNamespaces().map(namespace => namespace.ns === 'llm-pi-ai' ? afterSettings : namespace),
|
|
|
+ }))
|
|
|
+ await act(async () => { await controller.load() })
|
|
|
+ expect(controller.store.getSnapshot().namespaces.get('llm-pi-ai')?.revision).toBe(1)
|
|
|
+ fireEvent.click(screen.getAllByText(en.apply)[1] as HTMLElement)
|
|
|
+ await waitFor(() => { expect(set).toHaveBeenCalledTimes(2) })
|
|
|
+ expect(mutate).toHaveBeenCalledOnce()
|
|
|
+ expect(set).toHaveBeenLastCalledWith({ ref: 'ANTHROPIC_API_KEY', value: 'sk-ant' })
|
|
|
+ })
|
|
|
+
|
|
|
it('switches the add card target and degrades unknown or broken targets loudly', async () => {
|
|
|
await mountSection()
|
|
|
fireEvent.click(screen.getByText(en.add))
|
|
|
@@ -876,6 +975,7 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.change(key, { target: { value: 'sk-live' } })
|
|
|
fireEvent.click(screen.getByText(en.apply))
|
|
|
await screen.findByText(/shadowed by the read-only environment/)
|
|
|
+ expect(screen.queryByRole('status')).toBeNull()
|
|
|
})
|
|
|
|
|
|
it('locks the key input when the launch environment provides the credential', async () => {
|
|
|
@@ -898,34 +998,37 @@ describe('ModelsSection', () => {
|
|
|
fireEvent.click(screen.getAllByText(en.edit)[0] as HTMLElement)
|
|
|
const keys = await screen.findAllByLabelText<HTMLInputElement>(en.keyInput)
|
|
|
const editorKey = keys[keys.length - 1] as HTMLInputElement
|
|
|
- expect(editorKey.placeholder).toBe(en.keyPlaceholder)
|
|
|
+ expect(editorKey.placeholder).toBe(en.keyPlaceholderNative)
|
|
|
fireEvent.change(editorKey, { target: { value: 'sk-live' } })
|
|
|
fireEvent.click(screen.getAllByText(en.apply)[1] as HTMLElement)
|
|
|
await waitFor(() => { expect(set).toHaveBeenCalledTimes(1) })
|
|
|
})
|
|
|
|
|
|
it('requires confirmation before removing a user-added provider', async () => {
|
|
|
- const { replace, mutate } = await mountSection()
|
|
|
- fireEvent.click(screen.getAllByText(en.remove)[0] as HTMLElement)
|
|
|
- const dialog = screen.getByRole('dialog', { name: en.deleteTitle })
|
|
|
- expect(dialog.textContent).toContain(en.deleteDescription)
|
|
|
+ const { replace, 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))
|
|
|
expect(document.activeElement).toBe(within(dialog).getByRole('button', { name: en.cancel }))
|
|
|
+ expect(unset).not.toHaveBeenCalled()
|
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: en.cancel }))
|
|
|
- expect(screen.queryByRole('dialog', { name: en.deleteTitle })).toBeNull()
|
|
|
+ expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
|
|
|
|
- fireEvent.click(screen.getAllByText(en.remove)[0] as HTMLElement)
|
|
|
- fireEvent.click(within(screen.getByRole('dialog', { name: en.deleteTitle }))
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
|
|
+ fireEvent.click(within(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) }))
|
|
|
.getByRole('button', { name: en.close }))
|
|
|
- expect(screen.queryByRole('dialog', { name: en.deleteTitle })).toBeNull()
|
|
|
+ expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
|
|
|
|
- fireEvent.click(screen.getAllByText(en.remove)[0] as HTMLElement)
|
|
|
- fireEvent.click(within(screen.getByRole('dialog', { name: en.deleteTitle }))
|
|
|
- .getByRole('button', { name: en.deleteConfirm }))
|
|
|
+ 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(mutate).toHaveBeenCalledTimes(1) })
|
|
|
- expect(screen.queryByRole('dialog', { name: en.deleteTitle })).toBeNull()
|
|
|
+ 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',
|
|
|
@@ -939,20 +1042,22 @@ describe('ModelsSection', () => {
|
|
|
resolveRemoval = resolve
|
|
|
}))
|
|
|
await mountSection({ mutate })
|
|
|
- fireEvent.click(screen.getAllByText(en.remove)[0] as HTMLElement)
|
|
|
- const dialog = screen.getByRole('dialog', { name: en.deleteTitle })
|
|
|
- const confirm = within(dialog).getByRole<HTMLButtonElement>('button', { name: en.deleteConfirm })
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
|
|
+ const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
|
|
+ const confirm = within(dialog).getByRole<HTMLButtonElement>('button', { name: openaiCopy(en.deleteConfirm) })
|
|
|
fireEvent.click(confirm)
|
|
|
fireEvent.click(confirm)
|
|
|
- expect(mutate).toHaveBeenCalledOnce()
|
|
|
+ await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
|
|
expect(confirm.disabled).toBe(true)
|
|
|
expect(within(dialog).getByRole<HTMLButtonElement>('button', { name: en.cancel }).disabled).toBe(true)
|
|
|
- expect(within(dialog).getByRole('button', { name: en.deleting })).toBe(confirm)
|
|
|
+ expect(within(dialog).getByRole('button', { name: openaiCopy(en.deleting) })).toBe(confirm)
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: en.close }))
|
|
|
- expect(screen.getByRole('dialog', { name: en.deleteTitle })).toBe(dialog)
|
|
|
+ expect(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBe(dialog)
|
|
|
expect(mutate).toHaveBeenCalledOnce()
|
|
|
await act(async () => { resolveRemoval(ok(wireNamespaces()[2]!)) })
|
|
|
- await waitFor(() => { expect(screen.queryByRole('dialog', { name: en.deleteTitle })).toBeNull() })
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
it('renders the load failure with a retry control', async () => {
|
|
|
@@ -1057,15 +1162,58 @@ describe('ModelsSection', () => {
|
|
|
expect(controller.store.getSnapshot().rows).toBe(before)
|
|
|
})
|
|
|
|
|
|
- it('shows a failed removal on the page banner, including a non-Error rejection', async () => {
|
|
|
- // The whole click path: the row's Remove button, the transport rejecting
|
|
|
- // with a non-Error value, and the store surfacing it where a load failure
|
|
|
- // would appear — rather than the row silently staying put.
|
|
|
- await mountSection({ mutate: vi.fn(() => Promise.reject(new Error('the host refused'))) })
|
|
|
- fireEvent.click(screen.getAllByText(en.remove)[0] as HTMLElement)
|
|
|
- fireEvent.click(within(screen.getByRole('dialog', { name: en.deleteTitle }))
|
|
|
- .getByRole('button', { name: en.deleteConfirm }))
|
|
|
- await screen.findByText(`${en.loadFailed}: the host refused`)
|
|
|
+ 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]!))
|
|
|
+ const { unset } = await mountSection({ mutate })
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
|
|
+ const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
|
|
+ const confirm = within(dialog).getByRole('button', { name: openaiCopy(en.deleteConfirm) })
|
|
|
+ fireEvent.click(confirm)
|
|
|
+ await within(dialog).findByText('the host refused')
|
|
|
+ expect(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBe(dialog)
|
|
|
+ expect(unset).toHaveBeenCalledOnce()
|
|
|
+ expect(mutate).toHaveBeenCalledOnce()
|
|
|
+
|
|
|
+ fireEvent.click(confirm)
|
|
|
+ await waitFor(() => { expect(unset).toHaveBeenCalledTimes(2) })
|
|
|
+ await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(2) })
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('retains credentials that are not identified as page-managed', async () => {
|
|
|
+ const { unset, mutate } = await mountSection()
|
|
|
+ const target = { provider: 'zombie', displayName: 'zombie' }
|
|
|
+ fireEvent.click(screen.getByRole('button', { name: providerCopy(en.removeProvider, target) }))
|
|
|
+ const dialog = screen.getByRole('dialog', { name: providerCopy(en.deleteTitle, target) })
|
|
|
+ expect(dialog.textContent).toContain(providerCopy(en.deleteDescription, target))
|
|
|
+ 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'] }],
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ 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'))),
|
|
|
+ })
|
|
|
+ const failure = await removeProviderProfile(
|
|
|
+ face as unknown as Parameters<typeof removeProviderProfile>[0],
|
|
|
+ controller,
|
|
|
+ {
|
|
|
+ settingsNs: 'llm-pi-ai',
|
|
|
+ settingsPath: ['providers', 'openai'],
|
|
|
+ credentialRef: 'OPENAI_API_KEY',
|
|
|
+ },
|
|
|
+ )
|
|
|
+ expect(failure).toBe('credential is read-only')
|
|
|
+ expect(mutate).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it('reports a transport rejection instead of failing the removal silently', async () => {
|