import { PassThrough } from 'node:stream' import { expect, test } from 'bun:test' import React from 'react' import stripAnsi from 'strip-ansi' import { createRoot, render, useApp } from '../../ink.js' import { AppStateProvider } from '../../state/AppState.js' import { buildCurrentProviderSummary, buildProfileSaveMessage, getProviderWizardDefaults, TextEntryDialog, } from './provider.js' const SYNC_START = '\x1B[?2026h' const SYNC_END = '\x1B[?2026l' 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 } async function renderFinalFrame(node: React.ReactNode): Promise { let output = '' const { stdout, stdin, getOutput } = createTestStreams() const instance = await render(node, { stdout: stdout as unknown as NodeJS.WriteStream, stdin: stdin as unknown as NodeJS.ReadStream, patchConsole: false, }) await instance.waitUntilExit() return stripAnsi(extractLastFrame(getOutput())) } 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, } } function StepChangeHarness(): React.ReactNode { const { exit } = useApp() const [step, setStep] = React.useState<'api' | 'model'>('api') React.useLayoutEffect(() => { if (step === 'api') { setStep('model') return } const timer = setTimeout(exit, 0) return () => clearTimeout(timer) }, [exit, step]) return ( {}} onCancel={() => {}} /> ) } test('TextEntryDialog resets its input state when initialValue changes', async () => { const output = await renderFinalFrame() expect(output).toContain('Model step') expect(output).toContain('fresh-model-name') expect(output).not.toContain('stale-secret-key') }) test('wizard step remount prevents a typed API key from leaking into the next field', async () => { 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( {}} onCancel={() => {}} /> , ) await Bun.sleep(25) stdin.write('sk-secret-12345678') await Bun.sleep(25) root.render( {}} onCancel={() => {}} /> , ) await Bun.sleep(25) root.unmount() stdin.end() stdout.end() await Bun.sleep(25) const output = stripAnsi(extractLastFrame(getOutput())) expect(output).toContain('Model step') expect(output).not.toContain('sk-secret-12345678') }) test('buildProfileSaveMessage maps provider fields without echoing secrets', () => { const message = buildProfileSaveMessage( 'openai', { OPENAI_API_KEY: 'sk-secret-12345678', OPENAI_MODEL: 'gpt-4o', OPENAI_BASE_URL: 'https://api.openai.com/v1', }, 'D:/codings/Opensource/openclaude/.openclaude-profile.json', ) expect(message).toContain('Saved OpenAI-compatible profile.') expect(message).toContain('Model: gpt-4o') expect(message).toContain('Endpoint: https://api.openai.com/v1') expect(message).toContain('Credentials: configured') expect(message).not.toContain('sk-secret-12345678') }) test('buildCurrentProviderSummary redacts poisoned model and endpoint values', () => { const summary = buildCurrentProviderSummary({ processEnv: { CLAUDE_CODE_USE_OPENAI: '1', OPENAI_API_KEY: 'sk-secret-12345678', OPENAI_MODEL: 'sk-secret-12345678', OPENAI_BASE_URL: 'sk-secret-12345678', }, persisted: null, }) expect(summary.providerLabel).toBe('OpenAI-compatible') expect(summary.modelLabel).toBe('sk-...5678') expect(summary.endpointLabel).toBe('sk-...5678') }) test('getProviderWizardDefaults ignores poisoned current provider values', () => { const defaults = getProviderWizardDefaults({ OPENAI_API_KEY: 'sk-secret-12345678', OPENAI_MODEL: 'sk-secret-12345678', OPENAI_BASE_URL: 'sk-secret-12345678', GEMINI_API_KEY: 'AIzaSecret12345678', GEMINI_MODEL: 'AIzaSecret12345678', }) expect(defaults.openAIModel).toBe('gpt-4o') expect(defaults.openAIBaseUrl).toBe('https://api.openai.com/v1') expect(defaults.geminiModel).toBe('gemini-2.0-flash') })