import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { describe, expect, it } from 'vitest' import { ClaudeLoginManager } from '../src/services/claude-login' // 模拟 `claude auth login` 的非交互行为:输出网址 → 读授权码 → 无效则提示并继续等待,有效则成功退出 const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-claude-')) const fake = path.join(dir, 'claude') fs.writeFileSync(fake, `#!/usr/bin/env node process.stdout.write('Opening browser to sign in…\\nIf the browser didn\\'t open, visit: https://claude.com/cai/oauth/authorize?code=true&state=abc\\nPaste code here if prompted > ') let buf = '' process.stdin.on('data', d => { buf += d const lines = buf.split('\\n'); buf = lines.pop() for (const l of lines) { if (l === 'good-code') { console.log('Login successful.'); process.exit(0) } else console.error('Invalid code. Please make sure the full code was copied.') } }) `, { mode: 0o755 }) describe('ClaudeLoginManager', () => { const audits: string[] = [] const mgr = new ClaudeLoginManager(() => fake, s => audits.push(s)) it('启动后返回登录网址;无效授权码可以重试;有效授权码完成登录', async () => { const start = await mgr.start({}) expect(start.url).toBe('https://claude.com/cai/oauth/authorize?code=true&state=abc') await expect(mgr.start({})).rejects.toThrow(/已有一个登录流程/) const bad = await mgr.submitCode(start.id, 'wrong') expect(bad).toEqual({ ok: false, retry: true, message: expect.stringContaining('Invalid code') }) const good = await mgr.submitCode(start.id, 'good-code') expect(good.ok).toBe(true) expect(mgr.current()).toBeNull() // 审计中不包含授权码 expect(audits.join('\n')).not.toMatch(/good-code|wrong/) }) it('取消后流程结束,旧 ID 不能再提交', async () => { const start = await mgr.start({ sso: true, email: 'a@b.com' }) mgr.cancel(start.id) expect(mgr.current()).toBeNull() await expect(mgr.submitCode(start.id, 'good-code')).rejects.toThrow(/不存在/) }) it('拒绝格式不正确的输入', async () => { await expect(mgr.start({ email: 'not-an-email' })).rejects.toThrow(/邮箱/) const start = await mgr.start({}) await expect(mgr.submitCode(start.id, 'has space')).rejects.toThrow(/格式/) mgr.cancel(start.id) }) })