Route third-party first-run setup into the provider wizard (#261)
The login picker previously sent third-party users to a dead-end info screen that only mentioned env vars. This change reuses the existing provider wizard from the login flow so first-run setup can continue without requiring slash command access first. Constraint: The existing provider setup logic must remain the single source of truth Rejected: Build a separate third-party auth wizard in ConsoleOAuthFlow | would duplicate provider setup behavior and drift over time Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep third-party onboarding routed through ProviderWizard unless the provider command flow is intentionally redesigned Tested: bun test src/components/ConsoleOAuthFlow.test.tsx src/commands/provider/provider.test.tsx Tested: tsc --noEmit via project diagnostics Not-tested: Live gh-authenticated push and PR creation path Co-authored-by: anandh8x <test@example.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -903,7 +903,11 @@ function resolveCodexCredentials(processEnv: NodeJS.ProcessEnv):
|
||||
}
|
||||
}
|
||||
|
||||
function ProviderWizard({ onDone }: { onDone: LocalJSXCommandOnDone }): React.ReactNode {
|
||||
export function ProviderWizard({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: LocalJSXCommandOnDone
|
||||
}): React.ReactNode {
|
||||
const defaults = getProviderWizardDefaults()
|
||||
const [step, setStep] = React.useState<Step>({ name: 'choose' })
|
||||
|
||||
|
||||
117
src/components/ConsoleOAuthFlow.test.tsx
Normal file
117
src/components/ConsoleOAuthFlow.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { expect, test } from 'bun:test'
|
||||
import React from 'react'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
import { AppStateProvider } from '../state/AppState.js'
|
||||
import { createRoot } from '../ink.js'
|
||||
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
|
||||
import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.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
|
||||
}
|
||||
|
||||
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 renderFrame(node: React.ReactNode): Promise<string> {
|
||||
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>
|
||||
<KeybindingSetup>{node}</KeybindingSetup>
|
||||
</AppStateProvider>,
|
||||
)
|
||||
|
||||
await Bun.sleep(50)
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
await Bun.sleep(25)
|
||||
|
||||
return stripAnsi(extractLastFrame(getOutput()))
|
||||
}
|
||||
|
||||
test('login picker shows the third-party platform option', async () => {
|
||||
const output = await renderFrame(<ConsoleOAuthFlow onDone={() => {}} />)
|
||||
|
||||
expect(output).toContain('Select login method:')
|
||||
expect(output).toContain('3rd-party platform')
|
||||
})
|
||||
|
||||
test('third-party provider branch opens the provider wizard', async () => {
|
||||
const output = await renderFrame(
|
||||
<ConsoleOAuthFlow
|
||||
initialStatus={{ state: 'platform_setup' }}
|
||||
onDone={() => {}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(output).toContain('Set up a provider profile')
|
||||
expect(output).toContain('OpenAI-compatible')
|
||||
expect(output).toContain('Ollama')
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user