mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
Preserve the complete credential storage, migration, IPC, Settings and rate-limit refresh change alongside standalone GLM plans, current-main database diagnostics and the reviewed unknown-backend environment correction. Keep native discovery cancellation third and selected environment fourth. Original-topic-commit:7903f1cddbOriginal-topic-commit:588117b5cfOriginal-topic-commit:dedd4f8c86Original-topic-commit:6645dae104Original-topic-commit: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-from: a25b80c02c1af7830b0e6a65e72d965b3ad98276 Restacked-onto:b032867021Co-authored-by: kespineira <kespineira@users.noreply.github.com> Co-authored-by: kevimux <kevimux@users.noreply.github.com> Reported-by: pullfrog Reviewed-full-source: 849fe093073f4c1606bd65d79a0c725d955d0d1f Native-helper-source:80dbe23237Reviewed-full-current-source: 36acb57d44adb3d378c0289c8c15f7da0fda214c
591 lines
22 KiB
TypeScript
591 lines
22 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type * as WslTranscriptFsAccess from '../native-chat/wsl-transcript-fs-access'
|
|
import { WslTranscriptFsError } from '../native-chat/wsl-transcript-fs-error'
|
|
import Database from '../sqlite/sync-database'
|
|
import { detectOpenCodeCredentialBackend } from '../opencode/opencode-credential-backend'
|
|
import {
|
|
listOpenCodeDatabases,
|
|
listOpenCodeDatabasesInDirectory
|
|
} from '../opencode-usage/opencode-database-discovery'
|
|
import {
|
|
getOpenCodeAuthFilePath,
|
|
readOpenCodeAuthFileGoKey,
|
|
resolveOpenCodeGoApiKey
|
|
} from './opencode-go-api-key-source'
|
|
|
|
vi.mock('../opencode/opencode-credential-backend', () => ({
|
|
detectOpenCodeCredentialBackend: vi.fn()
|
|
}))
|
|
|
|
// Placeholder values only — a real key must never reach a fixture.
|
|
const SETTINGS_KEY = 'settings-placeholder-key'
|
|
const ENVIRONMENT_KEY = 'environment-placeholder-key'
|
|
const AUTH_FILE_KEY = 'auth-file-placeholder-key'
|
|
const DATABASE_KEY = 'database-placeholder-key'
|
|
|
|
const ENVIRONMENT_KEYS = [
|
|
'XDG_DATA_HOME',
|
|
'OPENCODE_API_KEY',
|
|
'OPENCODE_DB',
|
|
'OPENCODE_AUTH_CONTENT'
|
|
] as const
|
|
|
|
// Lets a test fail the data-directory listing with an error the host filesystem cannot portably produce.
|
|
const readdirFailure = vi.hoisted((): { error: unknown } => ({ error: null }))
|
|
vi.mock('../native-chat/wsl-transcript-fs-access', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof WslTranscriptFsAccess>()
|
|
return {
|
|
...actual,
|
|
wslGatedReaddir: (...args: Parameters<typeof actual.wslGatedReaddir>) =>
|
|
readdirFailure.error ? Promise.reject(readdirFailure.error) : actual.wslGatedReaddir(...args)
|
|
}
|
|
})
|
|
|
|
describe('resolveOpenCodeGoApiKey', () => {
|
|
let dataHome: string
|
|
let originalEnvironment: Partial<Record<(typeof ENVIRONMENT_KEYS)[number], string>>
|
|
|
|
function writeAuthFile(contents: unknown): void {
|
|
mkdirSync(join(dataHome, 'opencode'), { recursive: true })
|
|
writeFileSync(join(dataHome, 'opencode', 'auth.json'), JSON.stringify(contents))
|
|
}
|
|
|
|
function writeCredentialDatabase(rows: { value: string; active: number; created: number }[]): {
|
|
path: string
|
|
} {
|
|
const path = join(dataHome, 'opencode-credentials.db')
|
|
const database = new Database(path)
|
|
database.exec(
|
|
'CREATE TABLE credential (id TEXT PRIMARY KEY, integration_id TEXT, label TEXT, ' +
|
|
'value TEXT, active INTEGER, time_created INTEGER)'
|
|
)
|
|
rows.forEach((row, index) => {
|
|
database
|
|
.prepare(
|
|
'INSERT INTO credential (id, integration_id, label, value, active, time_created) ' +
|
|
"VALUES (?, 'opencode-go', 'API key', ?, ?, ?)"
|
|
)
|
|
.run(`cred_${index}`, row.value, row.active, row.created)
|
|
})
|
|
database.close()
|
|
return { path }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockReset().mockResolvedValue('v2')
|
|
originalEnvironment = Object.fromEntries(ENVIRONMENT_KEYS.map((key) => [key, process.env[key]]))
|
|
dataHome = mkdtempSync(join(tmpdir(), 'orca-opencode-go-key-'))
|
|
process.env.XDG_DATA_HOME = dataHome
|
|
delete process.env.OPENCODE_API_KEY
|
|
delete process.env.OPENCODE_AUTH_CONTENT
|
|
// Keeps the credential-database tier from touching the developer's own store.
|
|
process.env.OPENCODE_DB = ':memory:'
|
|
})
|
|
|
|
afterEach(() => {
|
|
readdirFailure.error = null
|
|
for (const key of ENVIRONMENT_KEYS) {
|
|
const value = originalEnvironment[key]
|
|
if (value === undefined) {
|
|
delete process.env[key]
|
|
} else {
|
|
process.env[key] = value
|
|
}
|
|
}
|
|
rmSync(dataHome, { recursive: true, force: true })
|
|
})
|
|
|
|
it.each([undefined, 'selected.db'] as const)(
|
|
'uses the fourth-argument environment for discovery (%s)',
|
|
async (override) => {
|
|
const directory = join(dataHome, 'opencode')
|
|
mkdirSync(directory)
|
|
const path = join(directory, override ?? 'opencode.db')
|
|
writeFileSync(path, '')
|
|
const onFsError = vi.fn()
|
|
|
|
await expect(
|
|
listOpenCodeDatabases(undefined, onFsError, undefined, {
|
|
XDG_DATA_HOME: dataHome,
|
|
OPENCODE_DB: override
|
|
})
|
|
).resolves.toEqual([path])
|
|
expect(onFsError).not.toHaveBeenCalled()
|
|
expect(process.env.OPENCODE_DB).toBe(':memory:')
|
|
}
|
|
)
|
|
|
|
it('keeps filesystem error reporting in the second argument', async () => {
|
|
const error = Object.assign(new Error('discovery denied'), { code: 'EACCES' })
|
|
readdirFailure.error = error
|
|
const onFsError = vi.fn()
|
|
|
|
await expect(
|
|
listOpenCodeDatabases(undefined, onFsError, undefined, { XDG_DATA_HOME: dataHome })
|
|
).resolves.toEqual([])
|
|
expect(onFsError).toHaveBeenCalledWith(join(dataHome, 'opencode'), error)
|
|
})
|
|
|
|
it.each([undefined, 'missing.db'] as const)(
|
|
'preserves third-argument cancellation before reporting discovery errors (%s)',
|
|
async (override) => {
|
|
readdirFailure.error = new Error('discovery stopped')
|
|
const reason = new Error('caller cancelled')
|
|
const controller = new AbortController()
|
|
controller.abort(reason)
|
|
const onFsError = vi.fn()
|
|
|
|
await expect(
|
|
listOpenCodeDatabases(undefined, onFsError, controller.signal, {
|
|
XDG_DATA_HOME: dataHome,
|
|
OPENCODE_DB: override
|
|
})
|
|
).rejects.toBe(reason)
|
|
expect(onFsError).not.toHaveBeenCalled()
|
|
}
|
|
)
|
|
|
|
it('preserves the native directory helper cancellation and error argument positions', async () => {
|
|
readdirFailure.error = new Error('directory read stopped')
|
|
const reason = new Error('caller cancelled')
|
|
const controller = new AbortController()
|
|
controller.abort(reason)
|
|
const onFsError = vi.fn()
|
|
|
|
await expect(
|
|
listOpenCodeDatabasesInDirectory(dataHome, undefined, controller.signal, onFsError)
|
|
).rejects.toBe(reason)
|
|
expect(onFsError).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('reads auth.json from XDG_DATA_HOME, which OpenCode uses on every platform', () => {
|
|
expect(getOpenCodeAuthFilePath({ XDG_DATA_HOME: '/data' })).toBe('/data/opencode/auth.json')
|
|
// OpenCode's global-roots.ts falls back to os.homedir() + .local/share even on Windows.
|
|
expect(getOpenCodeAuthFilePath({}, '/home/person')).toBe(
|
|
join('/home/person', '.local', 'share', 'opencode', 'auth.json')
|
|
)
|
|
})
|
|
|
|
it('prefers the settings override over every other tier', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
|
|
await expect(
|
|
resolveOpenCodeGoApiKey({ settingsOverride: ` ${SETTINGS_KEY} ` })
|
|
).resolves.toEqual({ status: 'found', key: SETTINGS_KEY, tier: 'settings' })
|
|
})
|
|
|
|
it('prefers the key OpenCode saved on /connect over OPENCODE_API_KEY, as OpenCode does', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ settingsOverride: ' ' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: AUTH_FILE_KEY,
|
|
tier: 'opencode-auth-file'
|
|
})
|
|
})
|
|
|
|
it('falls back to OPENCODE_API_KEY when OpenCode stored no key', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
writeAuthFile({ anthropic: { type: 'api', key: 'not-the-go-key' } })
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
})
|
|
|
|
it('uses the key OpenCode 1.x saved on /connect', async () => {
|
|
writeAuthFile({
|
|
anthropic: { type: 'oauth', refresh: 'r', access: 'a', expires: 1 },
|
|
'opencode-go': { type: 'api', key: AUTH_FILE_KEY }
|
|
})
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend: 'v1' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: AUTH_FILE_KEY,
|
|
tier: 'opencode-auth-file'
|
|
})
|
|
})
|
|
|
|
it('falls back to the OpenCode 2 credential table when auth.json has no entry', async () => {
|
|
writeAuthFile({ anthropic: { type: 'api', key: 'not-the-go-key' } })
|
|
const { path } = writeCredentialDatabase([
|
|
{
|
|
value: JSON.stringify({ type: 'key', key: 'stale-placeholder-key' }),
|
|
active: 0,
|
|
created: 2
|
|
},
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: DATABASE_KEY,
|
|
tier: 'opencode-credential-database'
|
|
})
|
|
})
|
|
|
|
it('does not fall back to OPENCODE_API_KEY when the credential database is unreadable', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
const path = join(dataHome, 'opencode-unreadable.db')
|
|
writeFileSync(path, 'not a sqlite database')
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'credential-database-unreadable'
|
|
})
|
|
})
|
|
|
|
it('does not fall back to OPENCODE_API_KEY when the data directory cannot be listed', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
delete process.env.OPENCODE_DB
|
|
readdirFailure.error = Object.assign(new Error('permission denied'), { code: 'EACCES' })
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'credential-database-unreadable'
|
|
})
|
|
})
|
|
|
|
it('still falls back to OPENCODE_API_KEY when the data directory does not exist', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
delete process.env.OPENCODE_DB
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
})
|
|
|
|
it('still falls back to OPENCODE_API_KEY when the WSL gate refuses the listing', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
delete process.env.OPENCODE_DB
|
|
readdirFailure.error = new WslTranscriptFsError('timeout', 'slow')
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
})
|
|
|
|
it('reports missing for an unreadable database when no env key could be misused', async () => {
|
|
const path = join(dataHome, 'opencode-unreadable.db')
|
|
writeFileSync(path, 'not a sqlite database')
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({ status: 'missing' })
|
|
})
|
|
|
|
it('still falls back to OPENCODE_API_KEY when a readable database holds no Go key', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
const { path } = writeCredentialDatabase([])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
})
|
|
|
|
it('prefers a database key over OPENCODE_API_KEY', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
const { path } = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: DATABASE_KEY,
|
|
tier: 'opencode-credential-database'
|
|
})
|
|
})
|
|
|
|
it('prefers the credential table over a stale auth.json, since OpenCode 2 stops writing the file', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
const { path } = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: DATABASE_KEY,
|
|
tier: 'opencode-credential-database'
|
|
})
|
|
})
|
|
|
|
it('uses v1 auth.json despite a conflicting populated credential table', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
const { path } = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend: 'v1' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: AUTH_FILE_KEY,
|
|
tier: 'opencode-auth-file'
|
|
})
|
|
})
|
|
|
|
it('does not use a v2 table-only credential for v1 execution', async () => {
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
const { path } = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend: 'v1' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
})
|
|
|
|
it('applies the installed v1 backend even when the table has a key', async () => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue('v1')
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
process.env.OPENCODE_DB = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
]).path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({
|
|
status: 'found',
|
|
key: AUTH_FILE_KEY,
|
|
tier: 'opencode-auth-file'
|
|
})
|
|
})
|
|
|
|
it('uses v1 inline authentication ahead of a conflicting auth file and table', async () => {
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
process.env.OPENCODE_DB = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
]).path
|
|
process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({
|
|
'opencode-go': { type: 'api', key: 'inline-placeholder-key' }
|
|
})
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend: 'v1' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: 'inline-placeholder-key',
|
|
tier: 'opencode-auth-content'
|
|
})
|
|
})
|
|
|
|
it('does not fall back to the auth file when valid v1 inline auth omits Go', async () => {
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
process.env.OPENCODE_AUTH_CONTENT = '{}'
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend: 'v1' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
})
|
|
|
|
it('uses the v1 auth file after malformed inline authentication', async () => {
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
process.env.OPENCODE_AUTH_CONTENT = '{invalid'
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend: 'v1' })).resolves.toEqual({
|
|
status: 'found',
|
|
key: AUTH_FILE_KEY,
|
|
tier: 'opencode-auth-file'
|
|
})
|
|
})
|
|
|
|
it('withholds stored credentials when the execution backend cannot be determined', async () => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue(null)
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
process.env.OPENCODE_DB = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
]).path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({ status: 'missing' })
|
|
})
|
|
|
|
it('uses the provided environment key without reading stores when the backend is unknown', async () => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue(null)
|
|
const environment: NodeJS.ProcessEnv = { OPENCODE_API_KEY: ` ${ENVIRONMENT_KEY} ` }
|
|
const readStoreContext = vi.fn(() => {
|
|
throw new Error('Unknown backend must not read version-specific stores')
|
|
})
|
|
for (const name of ['OPENCODE_AUTH_CONTENT', 'XDG_DATA_HOME', 'OPENCODE_DB']) {
|
|
Object.defineProperty(environment, name, { get: readStoreContext })
|
|
}
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ environment, cwd: dataHome })).resolves.toEqual({
|
|
status: 'found',
|
|
key: ENVIRONMENT_KEY,
|
|
tier: 'environment'
|
|
})
|
|
expect(vi.mocked(detectOpenCodeCredentialBackend).mock.calls[0]?.[0]).toBe(environment)
|
|
expect(vi.mocked(detectOpenCodeCredentialBackend).mock.calls[0]?.[1]).toBe(dataHome)
|
|
expect(readStoreContext).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it.each([undefined, '', ' \t\n '])(
|
|
'does not borrow the host key when an unknown selected backend has environment key %j',
|
|
async (key) => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue(null)
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
const environment: NodeJS.ProcessEnv = { OPENCODE_API_KEY: key }
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ environment })).resolves.toEqual({ status: 'missing' })
|
|
expect(detectOpenCodeCredentialBackend).toHaveBeenCalledWith(environment, undefined)
|
|
}
|
|
)
|
|
|
|
it('keeps host and selected environment keys isolated when the backend is unknown', async () => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockResolvedValue(null)
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
const selected = { OPENCODE_API_KEY: 'selected-environment-placeholder' }
|
|
const inherited = { ...process.env }
|
|
|
|
const first = await resolveOpenCodeGoApiKey({})
|
|
const managed = await resolveOpenCodeGoApiKey({ environment: selected })
|
|
const restored = await resolveOpenCodeGoApiKey({})
|
|
|
|
expect(first).toEqual({ status: 'found', key: ENVIRONMENT_KEY, tier: 'environment' })
|
|
expect(managed).toEqual({
|
|
status: 'found',
|
|
key: 'selected-environment-placeholder',
|
|
tier: 'environment'
|
|
})
|
|
expect(restored).toEqual(first)
|
|
expect(process.env).toEqual(inherited)
|
|
expect(selected).toEqual({ OPENCODE_API_KEY: 'selected-environment-placeholder' })
|
|
})
|
|
|
|
it.each(['v1', 'v2'] as const)(
|
|
'uses caller backend authority for %s without probing',
|
|
async (backend) => {
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockRejectedValue(new Error('Unexpected probe'))
|
|
process.env.OPENCODE_API_KEY = ENVIRONMENT_KEY
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
process.env.OPENCODE_DB = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
]).path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ backend })).resolves.toEqual(
|
|
backend === 'v1'
|
|
? { status: 'found', key: AUTH_FILE_KEY, tier: 'opencode-auth-file' }
|
|
: { status: 'found', key: DATABASE_KEY, tier: 'opencode-credential-database' }
|
|
)
|
|
expect(detectOpenCodeCredentialBackend).not.toHaveBeenCalled()
|
|
}
|
|
)
|
|
|
|
it('returns the manual override before reading execution context or probing a CLI', async () => {
|
|
const input = { settingsOverride: SETTINGS_KEY }
|
|
Object.defineProperty(input, 'environment', {
|
|
get: () => {
|
|
throw new Error('Selected profile metadata is unreadable')
|
|
}
|
|
})
|
|
vi.mocked(detectOpenCodeCredentialBackend).mockRejectedValue(new Error('Unexpected probe'))
|
|
|
|
await expect(resolveOpenCodeGoApiKey(input)).resolves.toEqual({
|
|
status: 'found',
|
|
key: SETTINGS_KEY,
|
|
tier: 'settings'
|
|
})
|
|
expect(detectOpenCodeCredentialBackend).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('propagates unreadable selected context instead of reading host credentials', async () => {
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
const input = {}
|
|
Object.defineProperty(input, 'environment', {
|
|
get: () => {
|
|
throw new Error('Selected profile metadata is unreadable')
|
|
}
|
|
})
|
|
|
|
await expect(resolveOpenCodeGoApiKey(input)).rejects.toThrow(
|
|
'Selected profile metadata is unreadable'
|
|
)
|
|
})
|
|
|
|
it.each(['v1', 'v2'] as const)(
|
|
'keeps System and selected data roots isolated for %s',
|
|
async (backend) => {
|
|
writeAuthFile({ 'opencode-go': { type: 'api', key: AUTH_FILE_KEY } })
|
|
const system = { XDG_DATA_HOME: dataHome, OPENCODE_DB: ':memory:' }
|
|
const selectedDataHome = join(dataHome, 'selected')
|
|
mkdirSync(join(selectedDataHome, 'opencode'), { recursive: true })
|
|
writeFileSync(
|
|
join(selectedDataHome, 'opencode', 'auth.json'),
|
|
JSON.stringify({
|
|
'opencode-go': { type: 'api', key: 'selected-auth-placeholder' }
|
|
})
|
|
)
|
|
const { path } = writeCredentialDatabase([
|
|
{
|
|
value: JSON.stringify({ type: 'key', key: 'selected-table-placeholder' }),
|
|
active: 1,
|
|
created: 1
|
|
}
|
|
])
|
|
const selected = { XDG_DATA_HOME: selectedDataHome, OPENCODE_DB: path }
|
|
const inherited = { ...process.env }
|
|
|
|
const first = await resolveOpenCodeGoApiKey({ environment: system, backend })
|
|
const managed = await resolveOpenCodeGoApiKey({ environment: selected, backend })
|
|
const restored = await resolveOpenCodeGoApiKey({ environment: system, backend })
|
|
|
|
expect(first).toEqual({ status: 'found', key: AUTH_FILE_KEY, tier: 'opencode-auth-file' })
|
|
expect(managed).toEqual(
|
|
backend === 'v1'
|
|
? { status: 'found', key: 'selected-auth-placeholder', tier: 'opencode-auth-file' }
|
|
: {
|
|
status: 'found',
|
|
key: 'selected-table-placeholder',
|
|
tier: 'opencode-credential-database'
|
|
}
|
|
)
|
|
expect(restored).toEqual(first)
|
|
expect(process.env).toEqual(inherited)
|
|
}
|
|
)
|
|
|
|
it('keeps the settings override above the credential table', async () => {
|
|
const { path } = writeCredentialDatabase([
|
|
{ value: JSON.stringify({ type: 'key', key: DATABASE_KEY }), active: 1, created: 1 }
|
|
])
|
|
process.env.OPENCODE_DB = path
|
|
|
|
await expect(resolveOpenCodeGoApiKey({ settingsOverride: SETTINGS_KEY })).resolves.toEqual({
|
|
status: 'found',
|
|
key: SETTINGS_KEY,
|
|
tier: 'settings'
|
|
})
|
|
})
|
|
|
|
it('reports missing when no tier holds a key', async () => {
|
|
writeAuthFile({ 'opencode-go': { type: 'oauth', refresh: 'r', access: 'a', expires: 1 } })
|
|
|
|
await expect(resolveOpenCodeGoApiKey({})).resolves.toEqual({ status: 'missing' })
|
|
})
|
|
|
|
it('treats a malformed auth file as "no key" rather than a failure', () => {
|
|
mkdirSync(join(dataHome, 'opencode'), { recursive: true })
|
|
writeFileSync(join(dataHome, 'opencode', 'auth.json'), '{not json')
|
|
|
|
expect(readOpenCodeAuthFileGoKey(process.env)).toBeNull()
|
|
})
|
|
})
|