diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index f3deb95..193adff 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -1459,7 +1459,7 @@ function isAuthContentScriptUnreachableError(error) { const message = String(error?.message || error || '').trim(); - return /Receiving end does not exist|Could not establish connection|Frame with ID \d+ is showing error page/i.test(message); + return /Receiving end does not exist|Could not establish connection|Frame with ID \d+ is showing error page|等待认证页状态检查超时/i.test(message); } function buildPhoneRestartStep7Error(phoneNumber = '') { @@ -4011,29 +4011,46 @@ async function readPhonePageState(tabId, timeoutMs = 10000) { const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; - await ensureStep8SignupPageReady(tabId, { - timeoutMs, - logMessage: '步骤 9:等待认证页脚本恢复后继续手机号验证。', - visibleStep, - logStepKey: 'phone-verification', - }); - const result = await sendToContentScriptResilient('signup-page', { - type: 'STEP8_GET_STATE', - source: 'background', - payload: { visibleStep }, - }, { - timeoutMs, - responseTimeoutMs: timeoutMs, - retryDelayMs: 600, - logMessage: '步骤 9:认证页正在切换,等待后重新检查手机号验证状态...', - logStep: visibleStep, - logStepKey: 'phone-verification', + const deadlineMs = Math.max(1, Math.floor(Number(timeoutMs) || 0)); + let timeoutId = null; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`步骤 ${visibleStep}:等待认证页状态检查超时。`)); + }, deadlineMs); }); + const readPromise = (async () => { + await ensureStep8SignupPageReady(tabId, { + timeoutMs: deadlineMs, + logMessage: '步骤 9:等待认证页脚本恢复后继续手机号验证。', + visibleStep, + logStepKey: 'phone-verification', + }); + const result = await sendToContentScriptResilient('signup-page', { + type: 'STEP8_GET_STATE', + source: 'background', + payload: { visibleStep }, + }, { + timeoutMs: deadlineMs, + responseTimeoutMs: deadlineMs, + retryDelayMs: 600, + logMessage: '步骤 9:认证页正在切换,等待后重新检查手机号验证状态...', + logStep: visibleStep, + logStepKey: 'phone-verification', + }); - if (result?.error) { - throw new Error(result.error); + if (result?.error) { + throw new Error(result.error); + } + return result || {}; + })(); + + try { + return await Promise.race([readPromise, timeoutPromise]); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } } - return result || {}; } function resolveCountryCandidatesForProvider(state = {}, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) { @@ -5388,13 +5405,14 @@ return withPhoneVerificationLogContext({ step: 4, stepKey: 'fetch-signup-code' }, async () => { let state = options?.state || await getState(); const activation = normalizeActivation(options?.activation || state?.signupPhoneActivation); + const pageStateCheckTimeoutMs = Math.max(1, Math.floor(Number(options?.pageStateCheckTimeoutMs) || 5000)); if (!activation) { throw new Error('步骤 4:未找到当前注册手机号激活记录,请重新执行步骤 2。'); } const assertSignupPhoneStillApplicable = async (phaseLabel) => { try { - const pageState = await readPhonePageState(tabId, 5000); + const pageState = await readPhonePageState(tabId, pageStateCheckTimeoutMs); if (isSignupEmailVerificationPageState(pageState)) { throw buildSignupPhoneStaleEmailVerificationError(pageState); } diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 671f29c..3533d17 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -568,6 +568,105 @@ test('signup phone helper fails stale email-verification that appears during SMS assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE', 'STEP8_GET_STATE']); }); +test('signup phone helper does not let a hung page-state probe stall HeroSMS polling', async () => { + let smsPollCount = 0; + let pageReadyCalls = 0; + const statusActions = []; + const contentMessages = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsReuseEnabled: false, + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 1, + signupPhoneNumber: '66959916439', + signupPhoneVerificationPurpose: 'signup', + signupPhoneActivation: { + activationId: 'signup-123', + phoneNumber: '66959916439', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 0, + maxUses: 3, + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => { + pageReadyCalls += 1; + if (pageReadyCalls >= 2) { + return new Promise(() => {}); + } + }, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + if (action === 'getStatus') { + smsPollCount += 1; + return { + ok: true, + text: async () => 'STATUS_WAIT_CODE', + }; + } + if (action === 'setStatus') { + statusActions.push(parsedUrl.searchParams.get('status')); + return { + ok: true, + text: async () => 'ACCESS_READY', + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (fallback) => fallback, + getState: async () => currentState, + sendToContentScriptResilient: async (_source, message) => { + contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + return { + emailVerificationPage: false, + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + throw new Error('SMS timeout should fail before submitting a code'); + } + return {}; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + let caughtError = null; + try { + await Promise.race([ + helpers.completeSignupPhoneVerificationFlow(77, { + state: currentState, + pageStateCheckTimeoutMs: 1, + }), + new Promise((_, reject) => setTimeout( + () => reject(new Error('hung waiting for signup phone page-state probe')), + 50 + )), + ]); + } catch (error) { + caughtError = error; + } + + assert.equal(smsPollCount, 1, 'HeroSMS polling should continue after the first waiting status'); + assert.equal(pageReadyCalls, 2, 'should attempt the page-state probe during SMS polling'); + assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE']); + assert.deepStrictEqual(statusActions, ['8']); + assert.ok(caughtError, 'expected SMS timeout rather than a stalled page-state probe'); + assert.doesNotMatch(caughtError.message, /hung waiting for signup phone page-state probe/); + assert.match(caughtError.message, /等待手机验证码超时/); +}); + test('signup phone helper fails stale email-verification on 5sim RECEIVED without code during SMS polling', async () => { const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8'); const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({}); @@ -5432,7 +5531,7 @@ test('phone verification helper replaces number immediately when phone-verificat assert.equal(messages.includes('RETURN_TO_ADD_PHONE'), true); }); -test('phone verification helper directly navigates back to add-phone when replace-number recovery loses the auth page', async () => { +test('phone verification helper directly navigates back to add-phone when replace-number recovery page-state probe hangs', async () => { const requests = []; const messages = []; const navigationCalls = []; @@ -5458,13 +5557,24 @@ test('phone verification helper directly navigates back to add-phone when replac ]; let numberIndex = 0; const realDateNow = Date.now; + const realSetTimeout = global.setTimeout; let fakeNow = 0; Date.now = () => fakeNow; + global.setTimeout = (callback, ms, ...args) => realSetTimeout( + callback, + Number(ms) >= 12000 ? 1 : ms, + ...args + ); try { const helpers = api.createPhoneVerificationHelpers({ addLog: async () => {}, - ensureStep8SignupPageReady: async () => {}, + ensureStep8SignupPageReady: async () => { + if (!addPhoneRouteReady) { + return new Promise(() => {}); + } + return undefined; + }, fetchImpl: async (url) => { const parsedUrl = new URL(url); requests.push(parsedUrl); @@ -5539,7 +5649,7 @@ test('phone verification helper directly navigates back to add-phone when replac } if (message.type === 'RETURN_TO_ADD_PHONE') { addPhoneRouteReady = false; - throw new Error('Could not establish connection. Receiving end does not exist.'); + return {}; } if (message.type === 'STEP8_GET_STATE') { if (!addPhoneRouteReady) { @@ -5586,6 +5696,7 @@ test('phone verification helper directly navigates back to add-phone when replac assert.equal(messages.includes('RETURN_TO_ADD_PHONE'), true); } finally { Date.now = realDateNow; + global.setTimeout = realSetTimeout; } });