* ui: show locked API key environment reference * fix(ui-settings-models): skip unused pi-ai credential reference * fix(ui-settings-models): show active provider auth source * test(llm): cover provider auth check failures * docs(ui-settings-models): clarify active provider authentication * test(web): update model key copy snapshots * fix(ui-settings-models): clarify native authentication source hint * fix: address PR 5483 review feedback * docs(llm): state empty auth lookup behavior * fix: refresh auth hints with credential snapshots * fix: require credential refresh revision * test: cover auth status projection * fix: refresh catalog and browser expectations after rebase * fix: keep auth status projection internal and explicit * fix: preserve credential source checks with signed-in account rows * test: pin Models authentication instead of inheriting host keys * fix: prioritize stored credentials across providers * fix: use one environment fallback hint * docs: refresh credential contract * docs: refresh config catalog * test: align provider hint verification * fix: use provider-native auth hints and endpoints * fix: cover oauth-capable provider auth hints * fix: show canonical endpoint defaults * fix: avoid misleading endpoint fallback * fix: label catalog endpoint defaults * fix: shorten auth hints with hover details * fix: clarify native auth resolution * test: align DeepSeek endpoint hint * fix: pair help button corner shape * fix: address credential precedence review Record the stored-credential precedence in a superseding Agent Note and an upgrade guide, and align the credentials subsystem page, package READMEs, and JSDoc with the order file > launch env > $cwd/.env > $DSH_HOME/.env. Report each pi-ai route's catalog endpoint and native-auth environment names through the provider directory, check the names against the installed pi-ai resolvers, and remove the llm/checkAuth and credential describe operations that no Models page code calls. Restore the route-derived key reference and the home expansion that reads the home directory only for a leading ~. * fix: clarify model authentication guidance * fix: explain native provider OAuth requirements * fix: keep authentication guidance method neutral * fix: show credential reference source in model editor * fix: use environment authentication in headless profile * fix: validate headless authentication and current provider metadata * test: retain authentication guidance coverage with account catalog updates * test: retain writable Models credentials with the pinned environment
139 lines
6.2 KiB
TypeScript
139 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
|
|
import { remoteErrorOf, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
|
|
import CredentialsController from '../src/credentials.ts'
|
|
import { MemoryCredentials } from '../../../credentials/credentials/tests/memory.ts'
|
|
|
|
/** A store whose `describe` carries more than the view declares, as a foreign provider might. */
|
|
class LeakyCredentials extends MemoryCredentials {
|
|
override describe(): Promise<CredentialInfo> {
|
|
return Promise.resolve(
|
|
{ configured: true, source: 'memory', writable: true, value: 'sk-leaked' } as CredentialInfo,
|
|
)
|
|
}
|
|
}
|
|
|
|
/** A store whose write rejects with a bare string, the way some client libraries do. */
|
|
class LiteralRejectingCredentials extends MemoryCredentials {
|
|
override async set(): Promise<void> {
|
|
throw 'the store refused'
|
|
}
|
|
}
|
|
|
|
/** A store whose provider-owned policy rejects an otherwise valid write. */
|
|
class RejectingCredentials extends MemoryCredentials {
|
|
override set(): Promise<void> {
|
|
return Promise.reject(new Error('the stored document no longer parses'))
|
|
}
|
|
}
|
|
|
|
async function boot(
|
|
seed: Record<string, string> = {},
|
|
provider: typeof MemoryCredentials = MemoryCredentials,
|
|
): Promise<CredentialsController> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(provider, seed)
|
|
await ctx.plugin(CredentialsController)
|
|
return ctx.credentialsController
|
|
}
|
|
|
|
describe('the credentials Remote namespace a configuration surface calls', () => {
|
|
it('publishes the credentials namespace from its own service key', async () => {
|
|
const controller = await boot()
|
|
const binding = controller.typertRemote
|
|
expect(binding.serviceKey).toBe('credentialsController')
|
|
expect(binding.namespace).toBe('credentials')
|
|
expect(remoteMethods(controller)).toEqual([
|
|
{ method: 'describe', invocation: { kind: 'direct' } },
|
|
{ method: 'set', invocation: { kind: 'direct' } },
|
|
{ method: 'unset', invocation: { kind: 'direct' } },
|
|
])
|
|
})
|
|
|
|
it('reports the actionable configuration error while no credential provider is mounted', async () => {
|
|
const ctx = new Context()
|
|
await ctx.plugin(CredentialsController)
|
|
for (const call of [
|
|
() => ctx.credentialsController.describe(['DEEPSEEK_API_KEY']),
|
|
() => ctx.credentialsController.set('DEEPSEEK_API_KEY', 'sk-live'),
|
|
() => ctx.credentialsController.unset('DEEPSEEK_API_KEY'),
|
|
]) {
|
|
const failure = await call().catch((error: unknown) => error)
|
|
expect(remoteErrorOf(failure)).toMatchObject({
|
|
code: 'gateway/internal',
|
|
message: 'credentials service is absent: this deployment does not mount a credential provider (e.g. @deepseek-ai/dsh-credentials-local) in its composition',
|
|
details: {},
|
|
})
|
|
}
|
|
})
|
|
|
|
it('describes a batch of references as one map, values excluded', async () => {
|
|
const controller = await boot({ DEEPSEEK_API_KEY: 'sk-seeded' })
|
|
const described = await controller.describe(['DEEPSEEK_API_KEY', 'OPENAI_API_KEY'])
|
|
expect(described).toEqual({
|
|
DEEPSEEK_API_KEY: { configured: true, source: 'memory', writable: true },
|
|
OPENAI_API_KEY: { configured: false, writable: true },
|
|
})
|
|
expect(JSON.stringify(described)).not.toContain('sk-seeded')
|
|
})
|
|
|
|
it('reports an invalid reference as bad-request', async () => {
|
|
const controller = await boot()
|
|
for (const call of [
|
|
() => controller.describe(['DEEPSEEK_API_KEY', 'not a var']),
|
|
() => controller.set('not a var', 'sk-live'),
|
|
() => controller.unset('not a var'),
|
|
]) {
|
|
const failure = await call().catch((error: unknown) => error)
|
|
expect(remoteErrorOf(failure)).toMatchObject({ code: 'gateway/bad-request' })
|
|
}
|
|
})
|
|
|
|
it('answers the largest batch it accepts and reports one reference more as bad-request', async () => {
|
|
const controller = await boot()
|
|
const accepted = Array.from({ length: 64 }, (_unused, index) => `REF_${String(index)}`)
|
|
expect(Object.keys(await controller.describe(accepted))).toHaveLength(64)
|
|
const failure = await controller.describe([...accepted, 'REF_64']).catch((error: unknown) => error)
|
|
expect(remoteErrorOf(failure)).toMatchObject({ code: 'gateway/bad-request' })
|
|
})
|
|
|
|
it('answers only the fields the view declares, whatever a provider returns', async () => {
|
|
const controller = await boot({}, LeakyCredentials)
|
|
const described = await controller.describe(['DEEPSEEK_API_KEY'])
|
|
expect(described.DEEPSEEK_API_KEY).toEqual({ configured: true, source: 'memory', writable: true })
|
|
expect(JSON.stringify(described)).not.toContain('sk-leaked')
|
|
})
|
|
|
|
it('stores and removes through the same references the batch describes', async () => {
|
|
const controller = await boot()
|
|
await controller.set('DEEPSEEK_API_KEY', 'sk-live')
|
|
expect(await controller.describe(['DEEPSEEK_API_KEY']))
|
|
.toEqual({ DEEPSEEK_API_KEY: { configured: true, source: 'memory', writable: true } })
|
|
await controller.unset('DEEPSEEK_API_KEY')
|
|
expect(await controller.describe(['DEEPSEEK_API_KEY']))
|
|
.toEqual({ DEEPSEEK_API_KEY: { configured: false, writable: true } })
|
|
})
|
|
|
|
it('reports a refused write as credential/rejected naming only the reference', async () => {
|
|
const controller = await boot({}, RejectingCredentials)
|
|
const failure = await controller.set('DEEPSEEK_API_KEY', 'sk-live').catch((error: unknown) => error)
|
|
const { code, message, details } = remoteErrorOf(failure) ?? {}
|
|
expect(code).toBe('credential/rejected')
|
|
expect(message).toContain('no longer parses')
|
|
expect(details).toEqual({ ref: 'DEEPSEEK_API_KEY' })
|
|
})
|
|
|
|
it('reports an empty value as bad-request', async () => {
|
|
const controller = await boot()
|
|
const failure = await controller.set('DEEPSEEK_API_KEY', '').catch((error: unknown) => error)
|
|
expect(remoteErrorOf(failure)).toMatchObject({ code: 'gateway/bad-request' })
|
|
})
|
|
|
|
it('stringifies a refusal that is not an Error', async () => {
|
|
const controller = await boot({}, LiteralRejectingCredentials)
|
|
const failure = await controller.set('DEEPSEEK_API_KEY', 'sk-live').catch((error: unknown) => error)
|
|
expect(remoteErrorOf(failure)?.message).toBe('the store refused')
|
|
})
|
|
})
|