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 defaults = getProviderWizardDefaults()
|
||||||
const [step, setStep] = React.useState<Step>({ name: 'choose' })
|
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