'use strict'; const assert = require('assert'); const { _private } = require('../src/services/chatgpt_service'); const { classifyFailure } = require('../src/utils/error_classifier'); function missingLocator() { return { count: async () => 0, nth() { return this; }, isVisible: async () => false, }; } function textPage(url, text) { return { url: () => url, locator: selector => selector === 'body' ? { innerText: async () => text } : missingLocator(), }; } function collectionLocator(items) { return { count: async () => items.length, nth: index => items[index] || missingLocator(), }; } function chooserElement(text, onClick, attributes = {}) { let visible = true; return { count: async () => 1, nth() { return this; }, isVisible: async () => visible, innerText: async () => text, getAttribute: async name => attributes[name] || '', scrollIntoViewIfNeeded: async () => {}, click: async () => { const shouldHide = await onClick(); if (shouldHide !== false) visible = false; }, }; } function accountChooserPage({ choices, otherAccount, text, url = 'https://chatgpt.com/' }) { return { url: () => url, waitForTimeout: async () => {}, locator: selector => { if (selector === 'body') return { innerText: async () => text }; if (selector === 'button, [role="button"], a[href], [tabindex="0"]') { return collectionLocator(choices); } if (selector.includes('Log in to another account')) { return otherAccount || missingLocator(); } return missingLocator(); }, }; } (async () => { const deleted = await _private.detectOpenAITerminalPage(textPage( 'https://auth.openai.com/error', 'Your account has been deactivated.', )); assert.strictEqual(deleted.code, 'CHATGPT_ACCOUNT_DEACTIVATED'); assert.strictEqual(deleted.retryable, false); assert.strictEqual(deleted.restartBrowser, false); const deletedRealCopy = await _private.detectOpenAITerminalPage(textPage( 'https://auth.openai.com/error', 'Authentication Error. You do not have an account because it has been deleted or deactivated.', )); assert.strictEqual(deletedRealCopy.code, 'CHATGPT_ACCOUNT_DEACTIVATED'); assert.strictEqual(deletedRealCopy.retryable, false); const oops = await _private.detectOpenAITerminalPage(textPage( 'https://auth.openai.com/error', 'Oops! Something went wrong while signing you in.', )); assert.strictEqual(oops.code, 'CHATGPT_OPENAI_OOPS'); assert.strictEqual(oops.restartBrowser, true); assert.strictEqual( await _private.detectOpenAITerminalPage(textPage( 'https://example.com/', 'Oops! Something went wrong while signing you in.', )), null, '只能在 OpenAI 官方域名上识别终止页', ); let terminalRaceUrl = 'https://auth.openai.com/error'; const terminalRacePage = { url: () => terminalRaceUrl, locator: selector => selector === 'body' ? { innerText: async () => { terminalRaceUrl = 'https://accounts.google.com/signin'; return 'Oops! Something went wrong while signing you in.'; }, } : missingLocator(), }; assert.strictEqual( await _private.detectOpenAITerminalPage(terminalRacePage), null, '读取正文期间已跳到 Google 时不得使用旧 OpenAI 路由误判', ); const welcomeCases = [ { name: '旧版登录入口', text: 'Welcome back. Log in or sign up', selector: 'button:text-is("Log in or sign up")', visible: true, expected: true, }, { name: '真实新版 Google 入口', text: 'Welcome back. Continue with Google Continue with Apple Continue with Microsoft', selector: 'button:text-is("Continue with Google")', visible: true, expected: true, }, { name: 'Google 入口不可见', text: 'Welcome back. Continue with Google', selector: 'button:text-is("Continue with Google")', visible: false, expected: false, }, ]; for (const testCase of welcomeCases) { const action = { count: async () => 1, nth() { return this; }, isVisible: async () => testCase.visible, }; const welcomePage = { url: () => 'https://auth.openai.com/log-in', locator: selector => { if (selector === 'body') return { innerText: async () => testCase.text }; if (selector === testCase.selector) return action; return missingLocator(); }, }; assert.strictEqual(Boolean(await _private.findWelcomeAuthEntry(welcomePage)), testCase.expected, testCase.name); } assert.strictEqual( await _private.findWelcomeAuthEntry(textPage('https://auth.openai.com/log-in', 'Welcome back')), null, '没有 Log in or sign up 操作时不应误判', ); let targetAccountClicks = 0; let removeAccountClicks = 0; let targetOtherAccountClicks = 0; const removeTarget = chooserElement( 'Target User target.user+label@googlemail.com', async () => { removeAccountClicks++; }, { 'aria-label': 'Remove account' }, ); const matchingTarget = chooserElement( 'Target User target.user+label@googlemail.com', async () => { targetAccountClicks++; }, ); const targetOtherAccount = chooserElement( 'Log in to another account', async () => { targetOtherAccountClicks++; }, ); const matchingChooser = accountChooserPage({ choices: [removeTarget, matchingTarget], otherAccount: targetOtherAccount, text: 'Welcome back Choose an account to continue Target User target.user+label@googlemail.com Log in to another account', }); const matchingChooserResult = await _private.handleChatGPTAccountChooser( matchingChooser, 'targetuser@gmail.com', () => {}, { transitionTimeoutMs: 0 }, ); assert.strictEqual(matchingChooserResult.action, 'target_account'); assert.strictEqual(targetAccountClicks, 1, '邮箱匹配时应点击保存的目标账号卡片'); assert.strictEqual(removeAccountClicks, 0, '不得误点账号卡片中的移除按钮'); assert.strictEqual(targetOtherAccountClicks, 0, '找到目标账号后不得切换其他账号'); let mismatchedAccountClicks = 0; let mismatchedOtherAccountClicks = 0; const mismatchedAccount = chooserElement( 'Another User another@example.com', async () => { mismatchedAccountClicks++; }, ); const mismatchedOtherAccount = chooserElement( 'Log in to another account', async () => { mismatchedOtherAccountClicks++; }, ); const mismatchedChooserResult = await _private.handleChatGPTAccountChooser( accountChooserPage({ choices: [mismatchedAccount], otherAccount: mismatchedOtherAccount, text: 'Welcome back Choose an account to continue Another User another@example.com Log in to another account', }), 'target@example.com', () => {}, { transitionTimeoutMs: 0 }, ); assert.strictEqual(mismatchedChooserResult.action, 'other_account'); assert.strictEqual(mismatchedAccountClicks, 0, '邮箱不匹配时不得点击已保存的其他账号'); assert.strictEqual(mismatchedOtherAccountClicks, 1, '邮箱不匹配时应点击登录其他账号'); let nonChooserClicks = 0; const nonChooserResult = await _private.handleChatGPTAccountChooser( accountChooserPage({ choices: [chooserElement('target@example.com', async () => { nonChooserClicks++; })], text: 'Welcome back Continue with Google', }), 'target@example.com', () => {}, { transitionTimeoutMs: 0 }, ); assert.strictEqual(nonChooserResult.found, false, '普通 Welcome back 页面不应当作账号选择弹窗'); assert.strictEqual(nonChooserClicks, 0); let stuckChooserClicks = 0; const stuckTarget = chooserElement( 'Target target@example.com', async () => { stuckChooserClicks++; return false; }, ); const stuckChooserResult = await _private.waitForChatGPTResult( accountChooserPage({ choices: [stuckTarget], text: 'Welcome back Choose an account to continue Target target@example.com', }), { email: 'target@example.com' }, 23, () => {}, { accountChooserTransitionTimeoutMs: 0 }, ); assert.strictEqual(stuckChooserResult.code, 'CHATGPT_ACCOUNT_CHOOSER_STUCK'); assert.strictEqual(stuckChooserClicks, 3, '账号选择弹窗最多只点击三次'); let ageConfirmClicks = 0; const confirmButton = { count: async () => 1, nth() { return this; }, isVisible: async () => true, click: async () => { ageConfirmClicks++; }, }; const dialog = { isVisible: async () => true, innerText: async () => 'Please confirm your age is correct', locator: selector => selector === 'button:text-is("OK")' ? confirmButton : missingLocator(), }; const ageDialogPage = { url: () => 'https://auth.openai.com/create-account', waitForLoadState: async () => {}, waitForTimeout: async () => {}, locator: selector => { if (selector === '[role="dialog"]') { return { count: async () => 1, nth: () => dialog }; } if (selector === 'body') return { innerText: async () => 'Please confirm your age is correct OK' }; return missingLocator(); }, }; assert.strictEqual(await _private.acceptAgeConfirmationDialog(ageDialogPage, () => {}), true); assert.strictEqual(ageConfirmClicks, 1); const ageDialogResult = await _private.fillAgeOnboarding( ageDialogPage, { email: 'target@example.com' }, 23, () => {}, { stabilizeDelay: 0 }, ); assert.strictEqual(ageDialogResult.ok, true); assert.strictEqual(ageDialogResult.confirmed, true); assert.strictEqual(ageConfirmClicks, 2); let realAgeConfirmClicks = 0; const realAgeConfirmButton = { count: async () => 1, nth() { return this; }, isVisible: async () => true, click: async () => { realAgeConfirmClicks++; }, }; const realAgeDialogPage = { url: () => 'https://auth.openai.com/create-account', locator: selector => { if (selector === 'body') { return { innerText: async () => "You're setting your birthday to Aug 20, 2003. OK Cancel" }; } if (selector === 'button:text-is("OK")') return realAgeConfirmButton; return missingLocator(); }, }; assert.strictEqual( await _private.acceptAgeConfirmationDialog(realAgeDialogPage, () => {}), true, '应兼容没有标准 dialog 属性的真实生日确认弹框', ); assert.strictEqual(realAgeConfirmClicks, 1); const blankSnapshot = { available: true, readyState: 'complete', bodyTextLength: 0, bodyHtmlLength: 0, bodyElementCount: 0, visibleContentCount: 0, resourceCount: 1, resourceTransferSize: 100, navigationResponseEnd: 20, }; assert.strictEqual(_private.isPureBlankSnapshot(blankSnapshot), true); assert.strictEqual(_private.isPureBlankSnapshot({ ...blankSnapshot, readyState: 'loading' }), false); assert.strictEqual(_private.isPureBlankSnapshot({ ...blankSnapshot, visibleContentCount: 1 }), false); let blankState = _private.advanceStableBlankTracker(null, 'https://auth.openai.com/x', blankSnapshot, 1000); assert.strictEqual(blankState.stable, false); blankState = _private.advanceStableBlankTracker(blankState.tracker, 'https://auth.openai.com/x', blankSnapshot, 1000); blankState = _private.advanceStableBlankTracker(blankState.tracker, 'https://auth.openai.com/x', blankSnapshot, 1000); blankState = _private.advanceStableBlankTracker(blankState.tracker, 'https://auth.openai.com/x', blankSnapshot, 1000); assert.strictEqual(blankState.stable, true, '空白页必须稳定持续达到阈值才分类'); let blankReloads = 0; const blankPage = { url: () => 'https://chatgpt.com/', title: async () => '', evaluate: async () => blankSnapshot, waitForTimeout: async () => {}, reload: async () => { blankReloads++; }, goto: async () => { throw new Error('刷新成功后不应改用 goto'); }, locator: selector => selector === 'body' ? { innerText: async () => '' } : missingLocator(), }; const blankResult = await _private.waitForChatGPTResult( blankPage, { email: 'target@example.com' }, 23, () => {}, ); assert.strictEqual(blankResult.code, 'CHATGPT_BLANK_PAGE'); assert.strictEqual(blankReloads, 1, '稳定空白页只允许自动刷新一次'); const cfState = { body: 'ChatGPT home', waits: 0, probeRequests: 0, probeClosed: false }; const probePage = { goto: async () => { cfState.probeRequests++; }, locator: selector => { assert.strictEqual(selector, 'body'); return { innerText: async () => '{}' }; }, close: async () => { cfState.probeClosed = true; }, }; const cfMainPage = { url: () => 'https://chatgpt.com/', context: () => ({ newPage: async () => probePage }), waitForTimeout: async () => { cfState.waits++; cfState.body = 'Performing security verification Verify you are human'; }, locator: selector => selector === 'body' ? { innerText: async () => cfState.body } : missingLocator(), }; const interrupted = await _private.readChatGPTSessionInProbePage(cfMainPage, () => {}, { maxAttempts: 2, retryIntervalMs: 1000, expectedHome: true, }); assert.strictEqual(_private.isSessionProbeInterruption(interrupted, 'cloudflare'), true); assert.strictEqual(cfState.probeRequests, 1, 'CF 重现后不应继续请求 Session'); assert.strictEqual(cfState.probeClosed, true); const probeResponseCases = [ { name: 'Cloudflare HTML', status: 403, contentType: 'text/html; charset=UTF-8', body: 'Performing security verification Cloudflare cf-ray', code: 'CHATGPT_SESSION_CF_HTML', }, { name: 'HTTP 403', status: 403, contentType: 'application/json', body: '{}', code: 'CHATGPT_SESSION_HTTP_403' }, { name: 'HTTP 429', status: 429, contentType: 'application/json', body: '{}', code: 'CHATGPT_SESSION_HTTP_429' }, { name: 'HTTP 503', status: 503, contentType: 'text/plain', body: 'Service Unavailable', code: 'CHATGPT_SESSION_HTTP_5XX' }, ]; for (const testCase of probeResponseCases) { let closed = false; const mainPage = { context: () => ({ newPage: async () => ({ goto: async () => ({ status: () => testCase.status, headers: async () => ({ 'content-type': testCase.contentType }), }), locator: () => ({ innerText: async () => testCase.body }), close: async () => { closed = true; }, }), }), waitForTimeout: async () => {}, }; const result = await _private.readChatGPTSessionInProbePage(mainPage, () => {}, { maxAttempts: 1 }); assert.strictEqual(result.code, testCase.code, testCase.name); assert.strictEqual(_private.isSessionProbeFailure(result), true, testCase.name); assert.strictEqual(closed, true, `${testCase.name} 临时页应关闭`); } const routeChangeState = { url: 'https://chatgpt.com/', probeRequests: 0, probeBodyReads: 0, }; const routeChangeMainPage = { url: () => routeChangeState.url, context: () => ({ newPage: async () => ({ goto: async () => { routeChangeState.probeRequests++; routeChangeState.url = 'https://auth.openai.com/error'; }, locator: () => ({ innerText: async () => { routeChangeState.probeBodyReads++; return '{}'; }, }), close: async () => {}, }), }), locator: selector => selector === 'body' ? { innerText: async () => "You're all set" } : missingLocator(), waitForTimeout: async () => {}, }; const routeChanged = await _private.readChatGPTSessionInProbePage(routeChangeMainPage, () => {}, { maxAttempts: 2, }); assert.strictEqual(_private.isSessionProbeInterruption(routeChanged, 'route_changed'), true); assert.strictEqual(routeChangeState.probeRequests, 1); assert.strictEqual(routeChangeState.probeBodyReads, 0, '主页面换路由后应立即退出,不再读取旧 Session 响应'); let oauthStarts = 0; let oauthUrl = 'https://auth.openai.com/log-in'; const oauthPage = { url: () => oauthUrl, title: async () => '', waitForTimeout: async () => {}, goto: async url => { assert.match(url, /chatgpt\.com\/auth\/login_with/); oauthStarts++; oauthUrl = 'https://accounts.google.com/signin/rejected'; }, locator: selector => { if (selector === 'body') return { innerText: async () => 'Welcome back. Continue with Google' }; if (oauthUrl.includes('auth.openai.com') && selector === 'button:text-is("Continue with Google")') { return { count: async () => 1, nth() { return this; }, isVisible: async () => true }; } return missingLocator(); }, }; const oauthResult = await _private.waitForChatGPTResult( oauthPage, { email: 'target@example.com' }, 23, () => {}, ); assert.strictEqual(oauthStarts, 1); assert.strictEqual(oauthResult.code, 'SIGNIN_REJECTED'); const classifiedDeleted = classifyFailure({ message: 'OpenAI账号已删除或停用' }); assert.strictEqual(classifiedDeleted.code, 'CHATGPT_ACCOUNT_DEACTIVATED'); assert.strictEqual(classifiedDeleted.restartBrowser, false); assert.strictEqual(classifiedDeleted.label, '账号已删除/停用'); assert.strictEqual( classifyFailure({ message: 'You do not have an account because it has been deleted or deactivated.' }).code, 'CHATGPT_ACCOUNT_DEACTIVATED', ); assert.strictEqual( classifyFailure({ message: 'OpenAI登录页面出现 Oops 错误' }).code, 'CHATGPT_OPENAI_OOPS', ); assert.strictEqual(classifyFailure({ message: 'OpenAI页面持续空白' }).code, 'CHATGPT_BLANK_PAGE'); console.log('chatgpt_page_states.test.js: PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });