fix: prevent hung phone page-state probes from stalling SMS polling

This commit is contained in:
imxiaozhan
2026-05-09 22:08:47 -04:00
parent 6bd00dbd5b
commit b7b6deed5e
2 changed files with 154 additions and 25 deletions
+40 -22
View File
@@ -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);
}
+114 -3
View File
@@ -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;
}
});