Add re-auth y/N confirmation to minimal TUI login flows
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const {
|
||||
mockLoadStoredAnthropicAuth,
|
||||
mockLoadStoredAnthropicAuthToken,
|
||||
mockStoreAnthropicAuth,
|
||||
} = vi.hoisted(() => ({
|
||||
mockLoadStoredAnthropicAuth: vi.fn(),
|
||||
mockLoadStoredAnthropicAuthToken: vi.fn(),
|
||||
mockStoreAnthropicAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
const { mockCreateInterface } = vi.hoisted(() => ({
|
||||
mockCreateInterface: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../auth/index.js', () => ({
|
||||
loadStoredAnthropicAuth: mockLoadStoredAnthropicAuth,
|
||||
loadStoredAnthropicAuthToken: mockLoadStoredAnthropicAuthToken,
|
||||
loadStoredOpenAIApiKey: vi.fn(),
|
||||
loadStoredOpenAIAuth: vi.fn(),
|
||||
loadStoredZaiAuth: vi.fn(),
|
||||
loginGitHub: vi.fn(),
|
||||
loginOpenAI: vi.fn(),
|
||||
storeAnthropicAuth: mockStoreAnthropicAuth,
|
||||
storeAnthropicAuthToken: vi.fn(),
|
||||
storeOpenAIApiKey: vi.fn(),
|
||||
storeZaiAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:readline', () => ({
|
||||
createInterface: mockCreateInterface,
|
||||
emitKeypressEvents: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('MinimalTui login re-auth confirmation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoadStoredAnthropicAuth.mockReset();
|
||||
mockLoadStoredAnthropicAuthToken.mockReset();
|
||||
mockStoreAnthropicAuth.mockReset();
|
||||
mockCreateInterface.mockReset();
|
||||
});
|
||||
|
||||
it('cancels anthropic API-key re-auth when user answers no', async () => {
|
||||
mockLoadStoredAnthropicAuth.mockReturnValue({ api_key: 'existing-key', created_at: '2026-02-16T00:00:00.000Z' });
|
||||
mockLoadStoredAnthropicAuthToken.mockReturnValue(null);
|
||||
|
||||
const { MinimalTui } = await import('./minimal.js');
|
||||
|
||||
const mockSession = {
|
||||
id: 'test',
|
||||
getHistory: () => [],
|
||||
addMessage: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
replaceHistory: vi.fn(),
|
||||
};
|
||||
|
||||
const tui = new MinimalTui({
|
||||
session: mockSession as any,
|
||||
modelClient: {} as any,
|
||||
modelRouter: {} as any,
|
||||
systemPrompt: 'test',
|
||||
});
|
||||
|
||||
(tui as any).rl = { pause: vi.fn(), resume: vi.fn() };
|
||||
const promptMock = vi.spyOn(tui as any, 'prompt')
|
||||
.mockResolvedValueOnce('') // default -> API key path
|
||||
.mockResolvedValueOnce('n'); // confirmation
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await (tui as any).handleLoginCommand('anthropic');
|
||||
|
||||
expect(promptMock).toHaveBeenCalled();
|
||||
expect(mockStoreAnthropicAuth).not.toHaveBeenCalled();
|
||||
expect(consoleLog).toHaveBeenCalledWith(expect.stringContaining('Cancelled.'));
|
||||
|
||||
consoleLog.mockRestore();
|
||||
});
|
||||
|
||||
it('overwrites anthropic API key when user answers yes', async () => {
|
||||
mockLoadStoredAnthropicAuth.mockReturnValue({ api_key: 'existing-key', created_at: '2026-02-16T00:00:00.000Z' });
|
||||
mockLoadStoredAnthropicAuthToken.mockReturnValue(null);
|
||||
mockCreateInterface.mockImplementation(() => ({
|
||||
question: (_q: string, cb: (ans: string) => void) => cb('new-anthropic-key'),
|
||||
close: () => undefined,
|
||||
}));
|
||||
|
||||
const { MinimalTui } = await import('./minimal.js');
|
||||
|
||||
const mockSession = {
|
||||
id: 'test',
|
||||
getHistory: () => [],
|
||||
addMessage: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
replaceHistory: vi.fn(),
|
||||
};
|
||||
|
||||
const tui = new MinimalTui({
|
||||
session: mockSession as any,
|
||||
modelClient: {} as any,
|
||||
modelRouter: {} as any,
|
||||
systemPrompt: 'test',
|
||||
});
|
||||
|
||||
const pause = vi.fn();
|
||||
const resume = vi.fn();
|
||||
(tui as any).rl = { pause, resume };
|
||||
vi.spyOn(tui as any, 'prompt')
|
||||
.mockResolvedValueOnce('') // default -> API key path
|
||||
.mockResolvedValueOnce('y'); // confirmation
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
await (tui as any).handleLoginCommand('anthropic');
|
||||
|
||||
expect(mockStoreAnthropicAuth).toHaveBeenCalledWith('new-anthropic-key');
|
||||
expect(pause).toHaveBeenCalled();
|
||||
expect(resume).toHaveBeenCalled();
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
|
||||
consoleLog.mockRestore();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -437,6 +437,12 @@ export class MinimalTui {
|
||||
|
||||
private async handleLoginCommand(provider?: string): Promise<void> {
|
||||
const target = provider ?? 'github';
|
||||
const confirmReplace = async (): Promise<boolean> => {
|
||||
const answer = (await this.prompt(
|
||||
`${colors.orange}Re-authenticate and replace it?${colors.reset} ${colors.gray}(y/N)${colors.reset} `,
|
||||
)).trim().toLowerCase();
|
||||
return answer === 'y' || answer === 'yes';
|
||||
};
|
||||
|
||||
const promptHidden = async (question: string): Promise<string> => {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
||||
@@ -495,8 +501,10 @@ export class MinimalTui {
|
||||
const existing = loadStoredOpenAIApiKey();
|
||||
if (existing) {
|
||||
console.log(`${colors.gray}OpenAI API key already exists.${colors.reset}`);
|
||||
console.log(`${colors.gray}Delete ~/.config/flynn/auth.json openai.api_key entry to re-authenticate.${colors.reset}\n`);
|
||||
return;
|
||||
if (!await confirmReplace()) {
|
||||
console.log(`${colors.gray}Cancelled.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${colors.gray}OpenAI uses API keys for standard API access.${colors.reset}`);
|
||||
@@ -523,8 +531,10 @@ export class MinimalTui {
|
||||
const existing = loadStoredOpenAIAuth();
|
||||
if (existing) {
|
||||
console.log(`${colors.gray}OpenAI OAuth token already exists.${colors.reset}`);
|
||||
console.log(`${colors.gray}Delete ~/.config/flynn/auth.json openai.oauth entry (or legacy openai entry) to re-authenticate.${colors.reset}\n`);
|
||||
return;
|
||||
if (!await confirmReplace()) {
|
||||
console.log(`${colors.gray}Cancelled.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${colors.gray}Starting OpenAI OAuth device flow...${colors.reset}`);
|
||||
@@ -560,8 +570,10 @@ export class MinimalTui {
|
||||
if (choice === '2') {
|
||||
if (hasToken) {
|
||||
console.log(`${colors.gray}Anthropic auth token already exists.${colors.reset}`);
|
||||
console.log(`${colors.gray}Delete ~/.config/flynn/auth.json anthropic.auth_token entry to re-authenticate.${colors.reset}\n`);
|
||||
return;
|
||||
if (!await confirmReplace()) {
|
||||
console.log(`${colors.gray}Cancelled.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${colors.gray}Anthropic supports token-style auth (provider-specific).${colors.reset}`);
|
||||
@@ -586,8 +598,10 @@ export class MinimalTui {
|
||||
// 1) API key (default)
|
||||
if (hasApiKey) {
|
||||
console.log(`${colors.gray}Anthropic API key already exists.${colors.reset}`);
|
||||
console.log(`${colors.gray}Delete ~/.config/flynn/auth.json anthropic.api_key entry to re-authenticate.${colors.reset}\n`);
|
||||
return;
|
||||
if (!await confirmReplace()) {
|
||||
console.log(`${colors.gray}Cancelled.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${colors.gray}Anthropic uses API keys for authentication.${colors.reset}`);
|
||||
@@ -614,8 +628,10 @@ export class MinimalTui {
|
||||
const existing = loadStoredZaiAuth();
|
||||
if (existing) {
|
||||
console.log(`${colors.gray}Z.AI credential already exists.${colors.reset}`);
|
||||
console.log(`${colors.gray}Delete ~/.config/flynn/auth.json zai/zhipuai entry to re-authenticate.${colors.reset}\n`);
|
||||
return;
|
||||
if (!await confirmReplace()) {
|
||||
console.log(`${colors.gray}Cancelled.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${colors.gray}Z.AI uses API keys (HTTP Bearer), not an OAuth device flow.${colors.reset}`);
|
||||
|
||||
Reference in New Issue
Block a user