mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
pi-ai credentials carry optional members as explicit undefined (a github.com Copilot grant holds enterpriseUrl: undefined), and the store bridge committed the object verbatim, so the credential store's strict validator refused the write and sign-in failed after the provider had already authorized it. toRecord now drops explicitly-undefined members and renders undefined array entries null, exactly as JSON.stringify would; everything else passes through untouched so genuinely unstorable values still fail loud at the store.
214 lines
8.9 KiB
TypeScript
214 lines
8.9 KiB
TypeScript
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
|
|
import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
import { authContextFrom, credentialStoreFrom, recordKeyFor } from '../src/auth.ts'
|
|
|
|
const CODEX = recordKeyFor('openai-codex')
|
|
|
|
const dirs: string[] = []
|
|
|
|
/** A context whose credential records live in a throwaway `$DSH_HOME`. */
|
|
async function stored(): Promise<Context> {
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-auth-'))
|
|
dirs.push(dir)
|
|
const ctx = new Context()
|
|
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
|
|
return ctx
|
|
}
|
|
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs()
|
|
await Promise.all(dirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
|
})
|
|
|
|
describe('pi-ai credential store over harness records', () => {
|
|
it('reads nothing for a provider with no record', async () => {
|
|
const store = credentialStoreFrom(await stored())
|
|
|
|
await expect(store.read('openai-codex')).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('round-trips an api-key credential field by field', async () => {
|
|
const ctx = await stored()
|
|
const store = credentialStoreFrom(ctx)
|
|
|
|
await store.modify('cloudflare', () =>
|
|
Promise.resolve({ type: 'api_key', key: 'sk-live', env: { ACCOUNT_ID: 'acct-1' } }))
|
|
|
|
await expect(store.read('cloudflare'))
|
|
.resolves.toEqual({ type: 'api_key', key: 'sk-live', env: { ACCOUNT_ID: 'acct-1' } })
|
|
await expect(ctx.credentials.readRecord(recordKeyFor('cloudflare')))
|
|
.resolves.toEqual({ kind: 'api-key', key: 'sk-live', env: { ACCOUNT_ID: 'acct-1' } })
|
|
})
|
|
|
|
it('stores an api-key credential carrying neither a key nor env', async () => {
|
|
const store = credentialStoreFrom(await stored())
|
|
|
|
await store.modify('bedrock', () => Promise.resolve({ type: 'api_key' }))
|
|
|
|
await expect(store.read('bedrock')).resolves.toEqual({ type: 'api_key' })
|
|
})
|
|
|
|
it('keeps an OAuth credential verbatim, refresh fields and all', async () => {
|
|
const ctx = await stored()
|
|
const store = credentialStoreFrom(ctx)
|
|
const granted = { type: 'oauth' as const, access: 'at', refresh: 'rt', expires: 42, accountId: 'acc' }
|
|
|
|
await store.modify('openai-codex', () => Promise.resolve(granted))
|
|
|
|
await expect(store.read('openai-codex')).resolves.toEqual(granted)
|
|
await expect(ctx.credentials.readRecord(CODEX)).resolves.toEqual({ kind: 'grant', payload: granted })
|
|
})
|
|
|
|
it('stores the JSON image of a grant, dropping explicitly-undefined members', async () => {
|
|
const ctx = await stored()
|
|
const store = credentialStoreFrom(ctx)
|
|
// The github.com Copilot shape: pi-ai sets optional members to explicit
|
|
// undefined, which the strict record validator refuses verbatim.
|
|
const granted = {
|
|
type: 'oauth' as const,
|
|
access: 'at',
|
|
refresh: 'rt',
|
|
expires: 42,
|
|
enterpriseUrl: undefined,
|
|
nested: { keep: 'x', drop: undefined },
|
|
list: ['a', undefined, 'b'],
|
|
}
|
|
|
|
await store.modify('github-copilot', () => Promise.resolve(granted))
|
|
|
|
await expect(ctx.credentials.readRecord(recordKeyFor('github-copilot'))).resolves.toEqual({
|
|
kind: 'grant',
|
|
payload: {
|
|
type: 'oauth',
|
|
access: 'at',
|
|
refresh: 'rt',
|
|
expires: 42,
|
|
nested: { keep: 'x' },
|
|
list: ['a', null, 'b'],
|
|
},
|
|
})
|
|
})
|
|
|
|
it('passes a genuinely unstorable grant value through to the store\'s loud refusal', async () => {
|
|
const ctx = await stored()
|
|
const store = credentialStoreFrom(ctx)
|
|
// A foreign-prototype member is not the undefined idiom: the image leaves
|
|
// it untouched and the record validator still refuses the write.
|
|
const granted = { type: 'oauth' as const, access: 'at', refresh: 'rt', expires: 42, issued: new Date(0) }
|
|
|
|
await expect(store.modify('github-copilot', () => Promise.resolve(granted)))
|
|
.rejects.toThrow(/JSON cannot represent/)
|
|
})
|
|
|
|
it('shows the mutation the current credential and leaves it alone when declined', async () => {
|
|
const store = credentialStoreFrom(await stored())
|
|
await store.modify('openai-codex', () =>
|
|
Promise.resolve({ type: 'oauth', access: 'first', refresh: 'r', expires: 1 }))
|
|
const seen: unknown[] = []
|
|
|
|
const unchanged = await store.modify('openai-codex', (current) => {
|
|
seen.push(current)
|
|
return Promise.resolve(undefined)
|
|
})
|
|
|
|
expect(seen).toEqual([{ type: 'oauth', access: 'first', refresh: 'r', expires: 1 }])
|
|
expect(unchanged).toEqual({ type: 'oauth', access: 'first', refresh: 'r', expires: 1 })
|
|
})
|
|
|
|
it('lists only the records this adapter family owns', async () => {
|
|
const ctx = await stored()
|
|
const store = credentialStoreFrom(ctx)
|
|
await store.modify('openai-codex', () =>
|
|
Promise.resolve({ type: 'oauth', access: 'at', refresh: 'rt', expires: 1 }))
|
|
await store.modify('cloudflare', () => Promise.resolve({ type: 'api_key', key: 'k' }))
|
|
// Another plugin's record for a provider name this one also serves: its
|
|
// payload is written in a format pi-ai never agreed to.
|
|
await ctx.credentials.modifyRecord(credentialKey('llm-kimi', 'openai-codex'), () =>
|
|
Promise.resolve({ kind: 'grant', payload: { theirs: true } }))
|
|
|
|
await expect(store.list()).resolves.toEqual([
|
|
{ providerId: 'openai-codex', type: 'oauth' },
|
|
{ providerId: 'cloudflare', type: 'api_key' },
|
|
])
|
|
})
|
|
|
|
it('forgets a credential on delete, and stays quiet when there was none', async () => {
|
|
const store = credentialStoreFrom(await stored())
|
|
await store.modify('openai-codex', () =>
|
|
Promise.resolve({ type: 'oauth', access: 'at', refresh: 'rt', expires: 1 }))
|
|
|
|
await store.delete('openai-codex')
|
|
await store.delete('openai-codex')
|
|
|
|
await expect(store.read('openai-codex')).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('reads empty but refuses to write without a credentials service', async () => {
|
|
const store = credentialStoreFrom(new Context())
|
|
|
|
await expect(store.read('openai-codex')).resolves.toBeUndefined()
|
|
await expect(store.list()).resolves.toEqual([])
|
|
await expect(store.modify('openai-codex', () => Promise.resolve({ type: 'api_key', key: 'k' })))
|
|
.rejects.toThrow(/mounts no credentials service/)
|
|
await expect(store.delete('openai-codex')).rejects.toThrow(/mounts no credentials service/)
|
|
})
|
|
|
|
it('treats a provider id outside the record grammar as holding nothing', async () => {
|
|
const store = credentialStoreFrom(await stored())
|
|
|
|
// A hand-declared route key is an arbitrary settings dict key, and pi-ai
|
|
// reads it during auth resolution: the answer is "not signed in", never a
|
|
// thrown address error…
|
|
await expect(store.read('My_Proxy')).resolves.toBeUndefined()
|
|
// …nothing can ever be stored under it, so a logout has nothing to remove…
|
|
await expect(store.delete('My_Proxy')).resolves.toBeUndefined()
|
|
// …while a write that cannot land must refuse rather than report success.
|
|
await expect(store.modify('My_Proxy', () => Promise.resolve({ type: 'api_key', key: 'k' })))
|
|
.rejects.toThrow(/cannot address a stored credential record/)
|
|
})
|
|
})
|
|
|
|
describe('pi-ai ambient auth context', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('PI_AUTH_AMBIENT', 'from-environment')
|
|
})
|
|
|
|
it('answers an environment name from the credential seam first', async () => {
|
|
const ctx = await stored()
|
|
await ctx.credentials.set(credentialRef('PI_AUTH_SEAM'), 'from-seam')
|
|
|
|
await expect(authContextFrom(ctx).env('PI_AUTH_SEAM')).resolves.toBe('from-seam')
|
|
})
|
|
|
|
it('falls back to the launch environment when nothing is stored', async () => {
|
|
await expect(authContextFrom(await stored()).env('PI_AUTH_AMBIENT')).resolves.toBe('from-environment')
|
|
})
|
|
|
|
it('answers "not set" for a name no reference could ever address', async () => {
|
|
// pi-ai asks about provider-declared names; one outside the reference
|
|
// grammar has no reference to miss, and must not throw.
|
|
await expect(authContextFrom(await stored()).env('not a var')).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('answers about the host filesystem, expanding a leading ~', async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-home-'))
|
|
dirs.push(dir)
|
|
await writeFile(join(dir, 'creds'), 'x')
|
|
// Both spellings of "home": os.homedir() reads HOME on POSIX and
|
|
// USERPROFILE on Windows, and the expansion under test goes through it.
|
|
vi.stubEnv('HOME', dir)
|
|
vi.stubEnv('USERPROFILE', dir)
|
|
const context = authContextFrom(await stored())
|
|
|
|
await expect(context.fileExists('~/creds')).resolves.toBe(true)
|
|
await expect(context.fileExists('~/missing')).resolves.toBe(false)
|
|
await expect(context.fileExists(join(dir, 'creds'))).resolves.toBe(true)
|
|
await expect(context.fileExists('~')).resolves.toBe(true)
|
|
})
|
|
})
|