fix: avoid sync github credential reads in provider manager (#428)
* fix: avoid sync github credential reads in provider manager * test: stabilize provider manager async credential test * fix: avoid first-frame github provider false negative --------- Co-authored-by: KRATOS <84986124+gnanam1990@users.noreply.github.com>
This commit is contained in:
305
src/components/ProviderManager.test.tsx
Normal file
305
src/components/ProviderManager.test.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import { PassThrough } from 'node:stream'
|
||||||
|
|
||||||
|
import { afterEach, expect, mock, test } from 'bun:test'
|
||||||
|
import React from 'react'
|
||||||
|
import stripAnsi from 'strip-ansi'
|
||||||
|
|
||||||
|
import { createRoot } from '../ink.js'
|
||||||
|
import { AppStateProvider } from '../state/AppState.js'
|
||||||
|
|
||||||
|
const SYNC_START = '\x1B[?2026h'
|
||||||
|
const SYNC_END = '\x1B[?2026l'
|
||||||
|
|
||||||
|
const ORIGINAL_ENV = {
|
||||||
|
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
|
||||||
|
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
||||||
|
GH_TOKEN: process.env.GH_TOKEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLastFrame(output: string): string {
|
||||||
|
let lastFrame: string | null = null
|
||||||
|
let cursor = 0
|
||||||
|
|
||||||
|
while (cursor < output.length) {
|
||||||
|
const start = output.indexOf(SYNC_START, cursor)
|
||||||
|
if (start === -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentStart = start + SYNC_START.length
|
||||||
|
const end = output.indexOf(SYNC_END, contentStart)
|
||||||
|
if (end === -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = output.slice(contentStart, end)
|
||||||
|
if (frame.trim().length > 0) {
|
||||||
|
lastFrame = frame
|
||||||
|
}
|
||||||
|
cursor = end + SYNC_END.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastFrame ?? output
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestStreams(): {
|
||||||
|
stdout: PassThrough
|
||||||
|
stdin: PassThrough & {
|
||||||
|
isTTY: boolean
|
||||||
|
setRawMode: (mode: boolean) => void
|
||||||
|
ref: () => void
|
||||||
|
unref: () => void
|
||||||
|
}
|
||||||
|
getOutput: () => string
|
||||||
|
} {
|
||||||
|
let output = ''
|
||||||
|
const stdout = new PassThrough()
|
||||||
|
const stdin = new PassThrough() as PassThrough & {
|
||||||
|
isTTY: boolean
|
||||||
|
setRawMode: (mode: boolean) => void
|
||||||
|
ref: () => void
|
||||||
|
unref: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin.isTTY = true
|
||||||
|
stdin.setRawMode = () => {}
|
||||||
|
stdin.ref = () => {}
|
||||||
|
stdin.unref = () => {}
|
||||||
|
;(stdout as unknown as { columns: number }).columns = 120
|
||||||
|
stdout.on('data', chunk => {
|
||||||
|
output += chunk.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
stdout,
|
||||||
|
stdin,
|
||||||
|
getOutput: () => output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForCondition(
|
||||||
|
predicate: () => boolean,
|
||||||
|
options?: { timeoutMs?: number; intervalMs?: number },
|
||||||
|
): Promise<void> {
|
||||||
|
const timeoutMs = options?.timeoutMs ?? 2000
|
||||||
|
const intervalMs = options?.intervalMs ?? 10
|
||||||
|
const startedAt = Date.now()
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
if (predicate()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await Bun.sleep(intervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Timed out waiting for ProviderManager test condition')
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeferred<T>(): {
|
||||||
|
promise: Promise<T>
|
||||||
|
resolve: (value: T) => void
|
||||||
|
} {
|
||||||
|
let resolve!: (value: T) => void
|
||||||
|
const promise = new Promise<T>(r => {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
return { promise, resolve }
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockProviderProfilesModule(): void {
|
||||||
|
mock.module('../utils/providerProfiles.js', () => ({
|
||||||
|
addProviderProfile: () => null,
|
||||||
|
applyActiveProviderProfileFromConfig: () => {},
|
||||||
|
deleteProviderProfile: () => ({ removed: false, activeProfileId: null }),
|
||||||
|
getActiveProviderProfile: () => null,
|
||||||
|
getProviderPresetDefaults: () => ({
|
||||||
|
provider: 'openai',
|
||||||
|
name: 'Mock provider',
|
||||||
|
baseUrl: 'http://localhost:11434/v1',
|
||||||
|
model: 'mock-model',
|
||||||
|
apiKey: '',
|
||||||
|
}),
|
||||||
|
getProviderProfiles: () => [],
|
||||||
|
setActiveProviderProfile: () => null,
|
||||||
|
updateProviderProfile: () => null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockProviderManagerDependencies(
|
||||||
|
syncRead: () => string | undefined,
|
||||||
|
asyncRead: () => Promise<string | undefined>,
|
||||||
|
): void {
|
||||||
|
mockProviderProfilesModule()
|
||||||
|
|
||||||
|
mock.module('../utils/githubModelsCredentials.js', () => ({
|
||||||
|
clearGithubModelsToken: () => ({ success: true }),
|
||||||
|
GITHUB_MODELS_HYDRATED_ENV_MARKER: 'CLAUDE_CODE_GITHUB_TOKEN_HYDRATED',
|
||||||
|
hydrateGithubModelsTokenFromSecureStorage: () => {},
|
||||||
|
readGithubModelsToken: syncRead,
|
||||||
|
readGithubModelsTokenAsync: asyncRead,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../utils/settings/settings.js', () => ({
|
||||||
|
updateSettingsForSource: () => ({ error: null }),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForFrameOutput(
|
||||||
|
getOutput: () => string,
|
||||||
|
predicate: (output: string) => boolean,
|
||||||
|
timeoutMs = 2500,
|
||||||
|
): Promise<string> {
|
||||||
|
let output = ''
|
||||||
|
|
||||||
|
await waitForCondition(() => {
|
||||||
|
output = stripAnsi(extractLastFrame(getOutput()))
|
||||||
|
return predicate(output)
|
||||||
|
}, { timeoutMs })
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountProviderManager(
|
||||||
|
ProviderManager: React.ComponentType<{
|
||||||
|
mode: 'first-run' | 'manage'
|
||||||
|
onDone: () => void
|
||||||
|
}>,
|
||||||
|
): Promise<{
|
||||||
|
getOutput: () => string
|
||||||
|
dispose: () => Promise<void>
|
||||||
|
}> {
|
||||||
|
const { stdout, stdin, getOutput } = createTestStreams()
|
||||||
|
const root = await createRoot({
|
||||||
|
stdout: stdout as unknown as NodeJS.WriteStream,
|
||||||
|
stdin: stdin as unknown as NodeJS.ReadStream,
|
||||||
|
patchConsole: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<AppStateProvider>
|
||||||
|
<ProviderManager
|
||||||
|
mode="manage"
|
||||||
|
onDone={() => {}}
|
||||||
|
/>
|
||||||
|
</AppStateProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getOutput,
|
||||||
|
dispose: async () => {
|
||||||
|
root.unmount()
|
||||||
|
stdin.end()
|
||||||
|
stdout.end()
|
||||||
|
await Bun.sleep(0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderProviderManagerFrame(
|
||||||
|
ProviderManager: React.ComponentType<{
|
||||||
|
mode: 'first-run' | 'manage'
|
||||||
|
onDone: () => void
|
||||||
|
}>,
|
||||||
|
options?: {
|
||||||
|
waitForOutput?: (output: string) => boolean
|
||||||
|
timeoutMs?: number
|
||||||
|
},
|
||||||
|
): Promise<string> {
|
||||||
|
const mounted = await mountProviderManager(ProviderManager)
|
||||||
|
const output = await waitForFrameOutput(
|
||||||
|
mounted.getOutput,
|
||||||
|
frame => {
|
||||||
|
if (!options?.waitForOutput) {
|
||||||
|
return frame.includes('Provider manager')
|
||||||
|
}
|
||||||
|
return options.waitForOutput(frame)
|
||||||
|
},
|
||||||
|
options?.timeoutMs ?? 2500,
|
||||||
|
)
|
||||||
|
|
||||||
|
await mounted.dispose()
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore()
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key as keyof typeof ORIGINAL_ENV]
|
||||||
|
} else {
|
||||||
|
process.env[key as keyof typeof ORIGINAL_ENV] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ProviderManager resolves GitHub virtual provider from async storage without sync reads in render flow', async () => {
|
||||||
|
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||||
|
delete process.env.GITHUB_TOKEN
|
||||||
|
delete process.env.GH_TOKEN
|
||||||
|
|
||||||
|
const syncRead = mock(() => {
|
||||||
|
throw new Error('sync credential read should not run in ProviderManager render flow')
|
||||||
|
})
|
||||||
|
const asyncRead = mock(async () => 'stored-token')
|
||||||
|
|
||||||
|
mockProviderManagerDependencies(syncRead, asyncRead)
|
||||||
|
|
||||||
|
const nonce = `${Date.now()}-${Math.random()}`
|
||||||
|
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
|
||||||
|
const output = await renderProviderManagerFrame(ProviderManager, {
|
||||||
|
waitForOutput: frame =>
|
||||||
|
frame.includes('Provider manager') &&
|
||||||
|
frame.includes('GitHub Models') &&
|
||||||
|
frame.includes('token stored'),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(output).toContain('Provider manager')
|
||||||
|
expect(output).toContain('GitHub Models')
|
||||||
|
expect(output).toContain('token stored')
|
||||||
|
expect(output).not.toContain('No provider profiles configured yet.')
|
||||||
|
|
||||||
|
expect(syncRead).not.toHaveBeenCalled()
|
||||||
|
expect(asyncRead).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ProviderManager avoids first-frame false negative while stored-token lookup is pending', async () => {
|
||||||
|
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||||
|
delete process.env.GITHUB_TOKEN
|
||||||
|
delete process.env.GH_TOKEN
|
||||||
|
|
||||||
|
const syncRead = mock(() => {
|
||||||
|
throw new Error('sync credential read should not run in ProviderManager render flow')
|
||||||
|
})
|
||||||
|
const deferredStoredToken = createDeferred<string | undefined>()
|
||||||
|
const asyncRead = mock(async () => deferredStoredToken.promise)
|
||||||
|
|
||||||
|
mockProviderManagerDependencies(syncRead, asyncRead)
|
||||||
|
|
||||||
|
const nonce = `${Date.now()}-${Math.random()}`
|
||||||
|
const { ProviderManager } = await import(`./ProviderManager.js?ts=${nonce}`)
|
||||||
|
const mounted = await mountProviderManager(ProviderManager)
|
||||||
|
|
||||||
|
const firstFrame = await waitForFrameOutput(
|
||||||
|
mounted.getOutput,
|
||||||
|
frame => frame.includes('Provider manager'),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(firstFrame).toContain('Checking GitHub Models credentials...')
|
||||||
|
expect(firstFrame).not.toContain('No provider profiles configured yet.')
|
||||||
|
|
||||||
|
deferredStoredToken.resolve('stored-token')
|
||||||
|
|
||||||
|
const resolvedFrame = await waitForFrameOutput(
|
||||||
|
mounted.getOutput,
|
||||||
|
frame => frame.includes('GitHub Models') && frame.includes('token stored'),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resolvedFrame).toContain('GitHub Models')
|
||||||
|
expect(resolvedFrame).toContain('token stored')
|
||||||
|
|
||||||
|
await mounted.dispose()
|
||||||
|
|
||||||
|
expect(syncRead).not.toHaveBeenCalled()
|
||||||
|
expect(asyncRead).toHaveBeenCalled()
|
||||||
|
})
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
GITHUB_MODELS_HYDRATED_ENV_MARKER,
|
GITHUB_MODELS_HYDRATED_ENV_MARKER,
|
||||||
hydrateGithubModelsTokenFromSecureStorage,
|
hydrateGithubModelsTokenFromSecureStorage,
|
||||||
readGithubModelsToken,
|
readGithubModelsToken,
|
||||||
|
readGithubModelsTokenAsync,
|
||||||
} from '../utils/githubModelsCredentials.js'
|
} from '../utils/githubModelsCredentials.js'
|
||||||
import { isEnvTruthy } from '../utils/envUtils.js'
|
import { isEnvTruthy } from '../utils/envUtils.js'
|
||||||
import { updateSettingsForSource } from '../utils/settings/settings.js'
|
import { updateSettingsForSource } from '../utils/settings/settings.js'
|
||||||
@@ -118,25 +119,38 @@ function profileSummary(profile: ProviderProfile, isActive: boolean): string {
|
|||||||
return `${providerKind} · ${profile.baseUrl} · ${profile.model} · ${keyInfo}${activeSuffix}`
|
return `${providerKind} · ${profile.baseUrl} · ${profile.model} · ${keyInfo}${activeSuffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGithubCredentialSource(
|
function getGithubCredentialSourceFromEnv(
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
): GithubCredentialSource {
|
): GithubCredentialSource {
|
||||||
if (readGithubModelsToken()?.trim()) {
|
|
||||||
return 'stored'
|
|
||||||
}
|
|
||||||
if (processEnv.GITHUB_TOKEN?.trim() || processEnv.GH_TOKEN?.trim()) {
|
if (processEnv.GITHUB_TOKEN?.trim() || processEnv.GH_TOKEN?.trim()) {
|
||||||
return 'env'
|
return 'env'
|
||||||
}
|
}
|
||||||
return 'none'
|
return 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveGithubCredentialSource(
|
||||||
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
|
): Promise<GithubCredentialSource> {
|
||||||
|
const envSource = getGithubCredentialSourceFromEnv(processEnv)
|
||||||
|
if (envSource !== 'none') {
|
||||||
|
return envSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await readGithubModelsTokenAsync()) {
|
||||||
|
return 'stored'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
|
||||||
function isGithubProviderAvailable(
|
function isGithubProviderAvailable(
|
||||||
|
credentialSource: GithubCredentialSource,
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
|
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return getGithubCredentialSource(processEnv) !== 'none'
|
return credentialSource !== 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGithubProviderModel(
|
function getGithubProviderModel(
|
||||||
@@ -164,19 +178,24 @@ function getGithubProviderSummary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
||||||
|
const initialGithubCredentialSource = getGithubCredentialSourceFromEnv()
|
||||||
|
const initialIsGithubActive = isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
|
||||||
|
const initialHasGithubCredential = initialGithubCredentialSource !== 'none'
|
||||||
|
|
||||||
const [profiles, setProfiles] = React.useState(() => getProviderProfiles())
|
const [profiles, setProfiles] = React.useState(() => getProviderProfiles())
|
||||||
const [activeProfileId, setActiveProfileId] = React.useState(
|
const [activeProfileId, setActiveProfileId] = React.useState(
|
||||||
() => getActiveProviderProfile()?.id,
|
() => getActiveProviderProfile()?.id,
|
||||||
)
|
)
|
||||||
const [githubProviderAvailable, setGithubProviderAvailable] = React.useState(() =>
|
const [githubProviderAvailable, setGithubProviderAvailable] = React.useState(
|
||||||
isGithubProviderAvailable(),
|
() => isGithubProviderAvailable(initialGithubCredentialSource),
|
||||||
)
|
)
|
||||||
const [githubCredentialSource, setGithubCredentialSource] = React.useState<GithubCredentialSource>(
|
const [githubCredentialSource, setGithubCredentialSource] = React.useState<GithubCredentialSource>(
|
||||||
() => getGithubCredentialSource(),
|
() => initialGithubCredentialSource,
|
||||||
)
|
|
||||||
const [isGithubActive, setIsGithubActive] = React.useState(() =>
|
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB),
|
|
||||||
)
|
)
|
||||||
|
const [isGithubActive, setIsGithubActive] = React.useState(() => initialIsGithubActive)
|
||||||
|
const [isGithubCredentialSourceResolved, setIsGithubCredentialSourceResolved] =
|
||||||
|
React.useState(() => initialHasGithubCredential || initialIsGithubActive)
|
||||||
|
const githubRefreshEpochRef = React.useRef(0)
|
||||||
const [screen, setScreen] = React.useState<Screen>(
|
const [screen, setScreen] = React.useState<Screen>(
|
||||||
mode === 'first-run' ? 'select-preset' : 'menu',
|
mode === 'first-run' ? 'select-preset' : 'menu',
|
||||||
)
|
)
|
||||||
@@ -196,13 +215,48 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
|||||||
const currentStepKey = currentStep.key
|
const currentStepKey = currentStep.key
|
||||||
const currentValue = draft[currentStepKey]
|
const currentValue = draft[currentStepKey]
|
||||||
|
|
||||||
|
const refreshGithubProviderState = React.useCallback((): void => {
|
||||||
|
const envCredentialSource = getGithubCredentialSourceFromEnv()
|
||||||
|
const githubActive = isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
|
||||||
|
const canResolveFromEnv = githubActive || envCredentialSource !== 'none'
|
||||||
|
|
||||||
|
if (canResolveFromEnv) {
|
||||||
|
githubRefreshEpochRef.current += 1
|
||||||
|
setGithubCredentialSource(envCredentialSource)
|
||||||
|
setGithubProviderAvailable(isGithubProviderAvailable(envCredentialSource))
|
||||||
|
setIsGithubActive(githubActive)
|
||||||
|
setIsGithubCredentialSourceResolved(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGithubCredentialSourceResolved(false)
|
||||||
|
const refreshEpoch = ++githubRefreshEpochRef.current
|
||||||
|
void (async () => {
|
||||||
|
const credentialSource = await resolveGithubCredentialSource()
|
||||||
|
if (refreshEpoch !== githubRefreshEpochRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setGithubCredentialSource(credentialSource)
|
||||||
|
setGithubProviderAvailable(isGithubProviderAvailable(credentialSource))
|
||||||
|
setIsGithubActive(isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB))
|
||||||
|
setIsGithubCredentialSourceResolved(true)
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
refreshGithubProviderState()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
githubRefreshEpochRef.current += 1
|
||||||
|
}
|
||||||
|
}, [refreshGithubProviderState])
|
||||||
|
|
||||||
function refreshProfiles(): void {
|
function refreshProfiles(): void {
|
||||||
const nextProfiles = getProviderProfiles()
|
const nextProfiles = getProviderProfiles()
|
||||||
setProfiles(nextProfiles)
|
setProfiles(nextProfiles)
|
||||||
setActiveProfileId(getActiveProviderProfile()?.id)
|
setActiveProfileId(getActiveProviderProfile()?.id)
|
||||||
setGithubProviderAvailable(isGithubProviderAvailable())
|
refreshGithubProviderState()
|
||||||
setGithubCredentialSource(getGithubCredentialSource())
|
|
||||||
setIsGithubActive(isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearStartupProviderOverrideFromUserSettings(): string | null {
|
function clearStartupProviderOverrideFromUserSettings(): string | null {
|
||||||
@@ -640,7 +694,11 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
|
|||||||
{statusMessage && <Text>{statusMessage}</Text>}
|
{statusMessage && <Text>{statusMessage}</Text>}
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{profiles.length === 0 && !githubProviderAvailable ? (
|
{profiles.length === 0 && !githubProviderAvailable ? (
|
||||||
<Text dimColor>No provider profiles configured yet.</Text>
|
isGithubCredentialSourceResolved ? (
|
||||||
|
<Text dimColor>No provider profiles configured yet.</Text>
|
||||||
|
) : (
|
||||||
|
<Text dimColor>Checking GitHub Models credentials...</Text>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{profiles.map(profile => (
|
{profiles.map(profile => (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe('hydrateGithubModelsTokenFromSecureStorage', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const { hydrateGithubModelsTokenFromSecureStorage } = await import(
|
const { hydrateGithubModelsTokenFromSecureStorage } = await import(
|
||||||
'./githubModelsCredentials.js'
|
'./githubModelsCredentials.js?hydrate=sets-token'
|
||||||
)
|
)
|
||||||
hydrateGithubModelsTokenFromSecureStorage()
|
hydrateGithubModelsTokenFromSecureStorage()
|
||||||
expect(process.env.GITHUB_TOKEN).toBe('stored-secret')
|
expect(process.env.GITHUB_TOKEN).toBe('stored-secret')
|
||||||
@@ -62,7 +62,7 @@ describe('hydrateGithubModelsTokenFromSecureStorage', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const { hydrateGithubModelsTokenFromSecureStorage } = await import(
|
const { hydrateGithubModelsTokenFromSecureStorage } = await import(
|
||||||
'./githubModelsCredentials.js'
|
'./githubModelsCredentials.js?hydrate=preserve-existing'
|
||||||
)
|
)
|
||||||
hydrateGithubModelsTokenFromSecureStorage()
|
hydrateGithubModelsTokenFromSecureStorage()
|
||||||
expect(process.env.GITHUB_TOKEN).toBe('already')
|
expect(process.env.GITHUB_TOKEN).toBe('already')
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
|
||||||
import {
|
|
||||||
clearGithubModelsToken,
|
|
||||||
readGithubModelsToken,
|
|
||||||
saveGithubModelsToken,
|
|
||||||
} from './githubModelsCredentials.js'
|
|
||||||
|
|
||||||
describe('readGithubModelsToken', () => {
|
describe('readGithubModelsToken', () => {
|
||||||
test('returns undefined in bare mode', () => {
|
test('returns undefined in bare mode', async () => {
|
||||||
|
const { readGithubModelsToken } = await import(
|
||||||
|
'./githubModelsCredentials.js?read-bare-mode'
|
||||||
|
)
|
||||||
|
|
||||||
const prev = process.env.CLAUDE_CODE_SIMPLE
|
const prev = process.env.CLAUDE_CODE_SIMPLE
|
||||||
process.env.CLAUDE_CODE_SIMPLE = '1'
|
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||||
expect(readGithubModelsToken()).toBeUndefined()
|
expect(readGithubModelsToken()).toBeUndefined()
|
||||||
@@ -20,7 +18,11 @@ describe('readGithubModelsToken', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('saveGithubModelsToken / clearGithubModelsToken', () => {
|
describe('saveGithubModelsToken / clearGithubModelsToken', () => {
|
||||||
test('save returns failure in bare mode', () => {
|
test('save returns failure in bare mode', async () => {
|
||||||
|
const { saveGithubModelsToken } = await import(
|
||||||
|
'./githubModelsCredentials.js?save-bare-mode'
|
||||||
|
)
|
||||||
|
|
||||||
const prev = process.env.CLAUDE_CODE_SIMPLE
|
const prev = process.env.CLAUDE_CODE_SIMPLE
|
||||||
process.env.CLAUDE_CODE_SIMPLE = '1'
|
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||||
const r = saveGithubModelsToken('abc')
|
const r = saveGithubModelsToken('abc')
|
||||||
@@ -33,7 +35,11 @@ describe('saveGithubModelsToken / clearGithubModelsToken', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('clear succeeds in bare mode', () => {
|
test('clear succeeds in bare mode', async () => {
|
||||||
|
const { clearGithubModelsToken } = await import(
|
||||||
|
'./githubModelsCredentials.js?clear-bare-mode'
|
||||||
|
)
|
||||||
|
|
||||||
const prev = process.env.CLAUDE_CODE_SIMPLE
|
const prev = process.env.CLAUDE_CODE_SIMPLE
|
||||||
process.env.CLAUDE_CODE_SIMPLE = '1'
|
process.env.CLAUDE_CODE_SIMPLE = '1'
|
||||||
expect(clearGithubModelsToken().success).toBe(true)
|
expect(clearGithubModelsToken().success).toBe(true)
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ export function readGithubModelsToken(): string | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readGithubModelsTokenAsync(): Promise<string | undefined> {
|
||||||
|
if (isBareMode()) return undefined
|
||||||
|
try {
|
||||||
|
const data = (await getSecureStorage().readAsync()) as
|
||||||
|
| ({ githubModels?: GithubModelsCredentialBlob } & Record<string, unknown>)
|
||||||
|
| null
|
||||||
|
const t = data?.githubModels?.accessToken?.trim()
|
||||||
|
return t || undefined
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If GitHub Models mode is on and no token is in the environment, copy the
|
* If GitHub Models mode is on and no token is in the environment, copy the
|
||||||
* stored token into process.env so the OpenAI shim and validation see it.
|
* stored token into process.env so the OpenAI shim and validation see it.
|
||||||
|
|||||||
Reference in New Issue
Block a user