feat(flow): unify sms multi-provider fallback and stabilize proxy/auth retries

This commit is contained in:
daniellee2015
2026-05-03 01:20:10 +08:00
parent 99313843c1
commit 2437e6e67d
37 changed files with 12254 additions and 1142 deletions
+37 -2
View File
@@ -42,7 +42,7 @@ function createRecoveryApi(state) {
isActionEnabled: (element) => Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true',
isVisibleElement: () => true,
log: () => {},
routeErrorPattern: /405\s+method\s+not\s+allowed|route\s+error.*405/i,
routeErrorPattern: /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/email-verification/i,
simulateClick: () => {
state.clickCount += 1;
if (typeof state.onClick === 'function') {
@@ -103,6 +103,42 @@ test('auth page recovery detects route error retry page on email verification ro
assert.equal(snapshot.routeErrorMatched, true);
});
test('auth page recovery detects email route missing-action retry page text', () => {
const state = {
clickCount: 0,
pageText: "Error: You made a POST request to \"/email-verification\" but did not provide an `action` for route \"EMAIL_VERIFICATION\"",
pathname: '/email-verification',
retryVisible: true,
title: '',
};
const api = createRecoveryApi(state);
const snapshot = api.getAuthTimeoutErrorPageState({
pathPatterns: [/\/email-verification(?:[/?#]|$)/i],
});
assert.equal(Boolean(snapshot), true);
assert.equal(snapshot.routeErrorMatched, true);
});
test('auth page recovery detects failed-to-fetch retry page on email verification route', () => {
const state = {
clickCount: 0,
pageText: 'Oops, an error occurred! Failed to fetch',
pathname: '/email-verification',
retryVisible: true,
title: 'Oops, an error occurred!',
};
const api = createRecoveryApi(state);
const snapshot = api.getAuthTimeoutErrorPageState({
pathPatterns: [/\/email-verification(?:[/?#]|$)/i],
});
assert.equal(Boolean(snapshot), true);
assert.equal(snapshot.fetchFailedMatched, true);
});
test('auth page recovery clicks retry and waits until page recovers', async () => {
const state = {
clickCount: 0,
@@ -225,4 +261,3 @@ test('auth page recovery throws signup user already exists error without clickin
assert.equal(state.clickCount, 0);
});
+321
View File
@@ -169,6 +169,327 @@ test('auto-run controller skips add-phone failures to the next round instead of
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller treats phone-number supply exhaustion as round-fatal and skips same-round retries', async () => {
const events = {
logs: [],
broadcasts: [],
accountRecords: [],
runCalls: 0,
};
let currentState = {
stepStatuses: {},
vpsUrl: 'https://example.com/vps',
vpsPassword: 'secret',
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: '',
mail2925BaseEmail: '',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
tabRegistry: {},
sourceLastUrls: {},
autoRunRoundSummaries: [],
};
const runtime = {
state: {
autoRunActive: false,
autoRunCurrentRun: 0,
autoRunTotalRuns: 1,
autoRunAttemptRun: 0,
autoRunSessionId: 0,
},
get() {
return { ...this.state };
},
set(updates = {}) {
this.state = { ...this.state, ...updates };
},
};
let sessionSeed = 0;
const controller = api.createAutoRunController({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
appendAccountRunRecord: async (status, _state, reason) => {
events.accountRecords.push({ status, reason });
return { status, reason };
},
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
AUTO_RUN_RETRY_DELAY_MS: 3000,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
broadcastAutoRunStatus: async (phase, payload = {}) => {
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
};
},
broadcastStopToContentScripts: async () => {},
cancelPendingCommands: () => {},
clearStopRequest: () => {},
createAutoRunSessionId: () => {
sessionSeed += 1;
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => error?.message || String(error || ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
getState: async () => ({
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
}),
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: () => false,
isRestartCurrentAttemptError: () => false,
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
persistAutoRunTimerPlan: async () => ({}),
resetState: async () => {
currentState = {
...currentState,
stepStatuses: {},
tabRegistry: {},
sourceLastUrls: {},
};
},
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls === 1) {
throw new Error('HeroSMS no numbers available across 1 country candidate(s): Thailand: NO_NUMBERS.');
}
},
runtime,
setState: async (updates = {}) => {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
};
},
sleepWithStop: async () => {},
throwIfAutoRunSessionStopped: (sessionId) => {
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
throw new Error('流程已被用户停止。');
}
},
waitForRunningStepsToFinish: async () => currentState,
chrome: {
runtime: {
sendMessage() {
return Promise.resolve();
},
},
},
});
await controller.autoRunLoop(2, {
autoRunSkipFailures: true,
mode: 'restart',
});
assert.equal(events.runCalls, 2, 'number supply failure should fail current round and continue next round');
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'number supply failure should not enter same-round retrying phase');
assert.equal(events.accountRecords.length, 1, 'number supply failure should persist one failed round record');
assert.equal(events.accountRecords[0].status, 'failed');
assert.match(events.accountRecords[0].reason, /NO_NUMBERS/i);
assert.ok(events.logs.some(({ message }) => /接码号池/.test(message)));
assert.equal(events.broadcasts.some(({ phase }) => phase === 'stopped'), false);
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller keeps same-round retrying for step9 local replacement exhaustion errors', async () => {
const events = {
logs: [],
broadcasts: [],
accountRecords: [],
runCalls: 0,
};
let currentState = {
stepStatuses: {},
vpsUrl: 'https://example.com/vps',
vpsPassword: 'secret',
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: '',
mail2925BaseEmail: '',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
tabRegistry: {},
sourceLastUrls: {},
autoRunRoundSummaries: [],
};
const runtime = {
state: {
autoRunActive: false,
autoRunCurrentRun: 0,
autoRunTotalRuns: 1,
autoRunAttemptRun: 0,
autoRunSessionId: 0,
},
get() {
return { ...this.state };
},
set(updates = {}) {
this.state = { ...this.state, ...updates };
},
};
let sessionSeed = 0;
const controller = api.createAutoRunController({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
appendAccountRunRecord: async (status, _state, reason) => {
events.accountRecords.push({ status, reason });
return { status, reason };
},
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
AUTO_RUN_RETRY_DELAY_MS: 3000,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
broadcastAutoRunStatus: async (phase, payload = {}) => {
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
};
},
broadcastStopToContentScripts: async () => {},
cancelPendingCommands: () => {},
clearStopRequest: () => {},
createAutoRunSessionId: () => {
sessionSeed += 1;
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => error?.message || String(error || ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
getState: async () => ({
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
}),
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: () => false,
isRestartCurrentAttemptError: () => false,
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
persistAutoRunTimerPlan: async () => ({}),
resetState: async () => {
currentState = {
...currentState,
stepStatuses: {},
tabRegistry: {},
sourceLastUrls: {},
};
},
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls === 1) {
throw new Error('Step 9: phone verification did not succeed after 3 number replacements. Last reason: sms_timeout_after_2_windows.');
}
},
runtime,
setState: async (updates = {}) => {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
};
},
sleepWithStop: async () => {},
throwIfAutoRunSessionStopped: (sessionId) => {
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
throw new Error('流程已被用户停止。');
}
},
waitForRunningStepsToFinish: async () => currentState,
chrome: {
runtime: {
sendMessage() {
return Promise.resolve();
},
},
},
});
await controller.autoRunLoop(1, {
autoRunSkipFailures: true,
mode: 'restart',
});
assert.equal(events.runCalls, 2, 'step9 local replacement exhaustion should retry in same round before success');
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), true, 'should enter retrying phase for same-round retry');
assert.equal(events.logs.some(({ message }) => /接码号池/.test(message)), false, 'should not be misclassified as global phone supply exhaustion');
assert.equal(events.accountRecords.length, 0, 'eventual same-round success should not persist failed round record');
});
test('auto-run controller skips user_already_exists failures to the next round instead of retrying the same round', async () => {
const events = {
logs: [],
+51
View File
@@ -67,6 +67,7 @@ function createHarness(options = {}) {
failureBudget = 1,
failureMessage = '认证失败: Request failed with status code 502',
authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
customState = {},
} = options;
return new Function(`
@@ -97,8 +98,30 @@ async function getState() {
return {
stepStatuses: { 3: 'completed' },
mailProvider: '163',
...${JSON.stringify(customState)},
};
}
function getStepIdsForState() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
function getStepDefinitionForState(step) {
const map = {
1: { key: 'open-signup' },
2: { key: 'prepare-email' },
3: { key: 'fill-password' },
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'auth-login' },
8: { key: 'auth-email-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
return map[Number(step)] || null;
}
function getStepExecutionKeyForState(step, state = {}) {
return String(getStepDefinitionForState(step, state)?.key || '').trim();
}
function isStopError(error) {
return (error?.message || String(error || '')) === '流程已被用户停止。';
}
@@ -246,3 +269,31 @@ test('auto-run stop errors after step 7 are rethrown immediately instead of rest
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run restarts from confirm-oauth step after transient step10 token_exchange_user_error', async () => {
const harness = createHarness({
failureStep: 10,
failureBudget: 1,
failureMessage: 'token exchange failed: status 400, body: { "error": { "message": "Invalid request. Please try again later.", "type": "invalid_request_error", "param": null, "code": "token_exchange_user_error" } }',
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
customState: {
panelMode: 'sub2api',
stepStatuses: { 3: 'completed' },
stepsVersion: 'ultra2.0',
visibleStep: 10,
contributionMode: false,
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [7, 8, 9, 10, 9, 10]);
assert.equal(events.invalidations.length, 1);
assert.deepStrictEqual(events.invalidations[0], {
step: 8,
options: {
logLabel: '步骤 10 报错后准备回到步骤 9 重试(第 1 次重开)',
},
});
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
});
@@ -54,6 +54,12 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeHotmailLocalBaseUrl'),
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeFiveSimCountryCode'),
extractFunction('normalizeFiveSimCountryOrder'),
extractFunction('normalizeNexSmsCountryId'),
extractFunction('normalizeNexSmsCountryOrder'),
extractFunction('normalizePhoneVerificationReplacementLimit'),
extractFunction('normalizePhoneCodeWaitSeconds'),
extractFunction('normalizePhoneCodeTimeoutWindows'),
@@ -136,6 +142,7 @@ return {
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
assert.equal(api.normalizePersistentSettingValue('heroSmsCountryId', 0), 0);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
@@ -172,4 +179,16 @@ return {
api.normalizePersistentSettingValue('codex2apiAdminKey', ' secret-key '),
'secret-key'
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('phoneSmsProviderOrder', []),
[]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('fiveSimCountryOrder', []),
[]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('nexSmsCountryOrder', []),
[]
);
});
+94
View File
@@ -392,3 +392,97 @@ return {
}]);
assert.match(events.logs[0].message, /授权后链总超时已关闭/);
});
test('executeStep retries fetch-network errors for step 4 with cooldown and bounded attempts', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
const STEP_FETCH_NETWORK_RETRY_POLICIES = new Map([[4, { maxAttempts: 3, cooldownMs: 1 }]]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
const events = {
logs: [],
statusCalls: [],
registryCalls: [],
sleepCalls: [],
};
let runCount = 0;
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function setStepStatus(step, status) {
events.statusCalls.push({ step, status });
}
async function humanStepDelay() {}
async function getState() {
return {
flowStartTime: null,
stepStatuses: {},
};
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function appendManualAccountRunRecordIfNeeded() {}
function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
function doesStepUseCompletionSignal() {
return false;
}
async function sleepWithStop(ms) {
events.sleepCalls.push(ms);
}
const stepRegistry = {
getStepDefinition(step) {
return { id: step, key: 'fetch-signup-code' };
},
async executeStep(step) {
events.registryCalls.push(step);
runCount += 1;
if (runCount < 3) {
throw new TypeError('Failed to fetch');
}
},
};
function getStepRegistryForState() {
return stepRegistry;
}
function getStepDefinitionForState(step) {
return { id: step, key: 'fetch-signup-code' };
}
${extractFunction('isStopError')}
${extractFunction('isRetryableContentScriptTransportError')}
${extractFunction('isStepFetchNetworkRetryableError')}
${extractFunction('getStepFetchNetworkRetryPolicy')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
return {
executeStep,
snapshot() {
return events;
},
};
`)();
await api.executeStep(4);
const events = api.snapshot();
assert.deepStrictEqual(events.registryCalls, [4, 4, 4]);
assert.deepStrictEqual(events.sleepCalls, [1, 1]);
assert.equal(
events.logs.filter(({ message }) => message.includes('[NETWORK_FETCH_RETRY]')).length >= 3,
true
);
});
+2 -2
View File
@@ -19,8 +19,8 @@ test('icloud login helper distinguishes auth-required errors from transient cont
assert.match(
source,
/if \(isIcloudTransientContextError\(err\)\) \{[\s\S]*iCloud 别名加载受网络\/上下文波动影响,请稍后重试。/m,
'withIcloudLoginHelp should surface transient-context copy instead of forcing login prompt'
/if \(isIcloudTransientContextError\(err\)\) \{[\s\S]*safeActionLabel[\s\S]*iCloud\$\{safeActionLabel\}受网络\/上下文波动影响,请稍后重试。/m,
'withIcloudLoginHelp should surface action-scoped transient-context copy instead of forcing login prompt'
);
assert.match(
+28
View File
@@ -30,6 +30,7 @@ ${providerSource}
const transformIpProxyAccountEntryByProvider = self.transformIpProxyAccountEntryByProvider;
${coreSource}
return {
applyExitRegionExpectation,
buildIpProxyPacScript,
getAccountModeProxyPoolFromState,
normalizeIpProxyAccountList,
@@ -133,3 +134,30 @@ test('IP proxy auto-switch threshold is clamped to the supported range', () => {
assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '25' }), 25);
assert.equal(api.resolveIpProxyAutoSwitchThreshold({ ipProxyPoolTargetCount: '9999' }), 500);
});
test('711 proxy region mismatch with missing auth challenge keeps routing as warning instead of hard failure', () => {
const api = loadIpProxyCore();
const status = api.applyExitRegionExpectation({
applied: true,
reason: 'applied',
provider: '711proxy',
hasAuth: true,
username: 'USER047152-zone-custom-region-US',
entrySource: 'fixed_account',
exitIp: '1.2.3.4',
exitRegion: 'BR',
authDiagnostics: 'auth(challenge=0,provided=0,isProxy=n/a,status=0,host=unknown)',
error: '',
warning: '',
}, 'US');
assert.equal(status.applied, true);
assert.equal(status.reason, 'applied_with_warning');
assert.equal(status.error, '');
assert.match(
String(status.warning || ''),
/地区校验未通过且未触发代理鉴权挑战,疑似匿名链路;先保留代理接管并给出强告警/
);
assert.match(String(status.warning || ''), /期望 US,实际 BR/);
});
@@ -79,3 +79,120 @@ test('platform verify module supports codex2api protocol callback exchange', asy
globalThis.fetch = originalFetch;
}
});
test('platform verify retries transient SUB2API oauth/token exchange errors before failing', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const logs = [];
const attempts = [];
let callCount = 0;
const executor = api.createStep10Executor({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
},
},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'sub2api',
getTabId: async () => 12,
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
isTabAlive: async () => true,
normalizeCodex2ApiUrl: (value) => value,
normalizeSub2ApiUrl: () => 'https://sub2api.example.com/admin/accounts',
rememberSourceLastUrl: async () => {},
reuseOrCreateTab: async () => 12,
sendToContentScript: async (_source, message) => {
attempts.push(message.type);
callCount += 1;
if (callCount === 1) {
return {
error: 'request failed: Post "https://auth.openai.com/oauth/token": unexpected EOF',
};
}
return { ok: true };
},
sendToContentScriptResilient: async () => ({}),
shouldBypassStep9ForLocalCpa: () => false,
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
});
await executor.executeStep10({
panelMode: 'sub2api',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub2api.example.com/admin/accounts',
sub2apiEmail: 'flow@example.com',
sub2apiPassword: 'secret',
sub2apiSessionId: 'session-123',
sub2apiOAuthState: 'oauth-state',
});
assert.equal(callCount, 2);
assert.deepStrictEqual(attempts, ['EXECUTE_STEP', 'EXECUTE_STEP']);
assert.equal(
logs.some((entry) => /临时网络波动/.test(entry.message) && entry.level === 'warn'),
true
);
});
test('platform verify retries transient SUB2API token_exchange_user_error before succeeding', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const logs = [];
let callCount = 0;
const executor = api.createStep10Executor({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
},
},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'sub2api',
getTabId: async () => 12,
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
isTabAlive: async () => true,
normalizeCodex2ApiUrl: (value) => value,
normalizeSub2ApiUrl: () => 'https://sub2api.example.com/admin/accounts',
rememberSourceLastUrl: async () => {},
reuseOrCreateTab: async () => 12,
sendToContentScript: async () => {
callCount += 1;
if (callCount === 1) {
return {
error: 'token exchange failed: status 400, body: { "error": { "message": "Invalid request. Please try again later.", "type": "invalid_request_error", "param": null, "code": "token_exchange_user_error" } }',
};
}
return { ok: true };
},
sendToContentScriptResilient: async () => ({}),
shouldBypassStep9ForLocalCpa: () => false,
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
});
await executor.executeStep10({
panelMode: 'sub2api',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub2api.example.com/admin/accounts',
sub2apiEmail: 'flow@example.com',
sub2apiPassword: 'secret',
sub2apiSessionId: 'session-123',
sub2apiOAuthState: 'oauth-state',
});
assert.equal(callCount, 2);
assert.equal(
logs.some((entry) => /临时网络波动/.test(entry.message) && entry.level === 'warn'),
true
);
});
@@ -45,7 +45,9 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
@@ -100,7 +102,9 @@ test('step 4 does not request a fresh code first for Cloudflare temp mail', asyn
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
@@ -151,7 +155,9 @@ test('step 4 checks iCloud session before polling iCloud mailbox', async () => {
resolved = true;
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
@@ -197,10 +203,15 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
resolveCalls += 1;
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => ({
alreadyVerified: true,
skipProfileStep: true,
}),
sendToContentScriptResilient: async () => ({
alreadyVerified: true,
skipProfileStep: true,
}),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
@@ -219,3 +230,73 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
]);
assert.equal(resolveCalls, 0);
});
test('step 4 prepare retries transport by recovering retry page without replaying full prepare loop', async () => {
let sendToContentScriptCalls = 0;
let recoverCalls = 0;
let resolveCalls = 0;
const logs = [];
const executor = api.createStep4Executor({
addLog: async (message, level) => {
logs.push({ message, level: level || 'info' });
},
chrome: {
tabs: {
update: async () => {},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
provider: '163',
label: '163 邮箱',
source: 'mail-163',
url: 'https://mail.163.com',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
resolveVerificationStep: async () => {
resolveCalls += 1;
},
reuseOrCreateTab: async () => {},
sendToContentScript: async (_source, message) => {
if (message.type !== 'PREPARE_SIGNUP_VERIFICATION') {
return {};
}
sendToContentScriptCalls += 1;
if (sendToContentScriptCalls === 1) {
throw new Error('Content script on signup-page did not respond in 30s. Try refreshing the tab and retry.');
}
return { ready: true };
},
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'RECOVER_AUTH_RETRY_PAGE') {
recoverCalls += 1;
return { recovered: true };
}
return {};
},
isRetryableContentScriptTransportError: (error) => /did not respond in \d+s/i.test(String(error?.message || error)),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
});
assert.equal(sendToContentScriptCalls, 2);
assert.equal(recoverCalls, 1);
assert.equal(resolveCalls, 1);
assert.equal(
logs.some((entry) => /正在确认注册验证码页面是否就绪/.test(entry.message)),
true
);
});
+101
View File
@@ -193,3 +193,104 @@ return {
assert.equal(api.isThreadItemSelected(staleItem, api.buildItemSignature(selectedItem)), true);
assert.equal(api.isThreadItemSelected(staleItem, api.buildItemSignature(staleItem)), false);
});
test('icloud poll session baseline is reused across calls and enables fallback after carry-over attempts', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
function normalizeThreadEntry(entry) {
return {
signature: String(entry.signature || ''),
sender: String(entry.sender || ''),
subject: String(entry.subject || ''),
preview: String(entry.preview || ''),
timestamp: String(entry.timestamp || ''),
ariaLabel: String(entry.ariaLabel || ''),
};
}
let currentThreadData = [];
function setThreadData(next) {
currentThreadData = Array.isArray(next) ? next.map(normalizeThreadEntry) : [];
}
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return {
handlePollEmail,
setThreadData,
};
`)();
api.setThreadData([
{ signature: 'old-1', sender: 'OpenAI', subject: '旧邮件', preview: 'no code', timestamp: '10:00', ariaLabel: 'old-1' },
]);
await assert.rejects(
() => api.handlePollEmail(8, {
senderFilters: ['openai'],
subjectFilters: ['code'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: [],
sessionKey: 's-1',
}),
/仍未在 iCloud 邮箱中找到新的匹配邮件/
);
api.setThreadData([
{ signature: 'old-1', sender: 'OpenAI', subject: '旧邮件', preview: 'no code', timestamp: '10:00', ariaLabel: 'old-1' },
{ signature: 'old-2', sender: 'OpenAI', subject: '旧邮件2', preview: 'no code', timestamp: '10:01', ariaLabel: 'old-2' },
{ signature: 'old-3', sender: 'OpenAI', subject: '旧邮件3', preview: 'no code', timestamp: '10:02', ariaLabel: 'old-3' },
{ signature: 'old-4', sender: 'OpenAI', subject: '旧邮件4', preview: 'no code', timestamp: '10:03', ariaLabel: 'old-4' },
{ signature: 'old-code', sender: 'OpenAI', subject: 'ChatGPT Log-in Code', preview: 'enter this code 556677', timestamp: '10:04', ariaLabel: 'old-code' },
]);
const result = await api.handlePollEmail(8, {
senderFilters: ['openai'],
subjectFilters: ['code', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: [],
sessionKey: 's-1',
});
assert.equal(result.code, '556677');
});
+256 -7
View File
@@ -6,19 +6,27 @@ const source = fs.readFileSync('content/phone-auth.js', 'utf8');
const globalScope = { navigator: { language: 'zh-CN' } };
const api = new Function('self', `${source}; return self.MultiPagePhoneAuth;`)(globalScope);
function createFakeAddPhoneDom() {
function createFakeAddPhoneDom(config = {}) {
const selectEvents = [];
const hiddenInputEvents = [];
let submitClicked = false;
const options = [
{ value: 'US', textContent: '美国' },
{ value: 'TH', textContent: '泰国' },
];
const options = (
Array.isArray(config.options) && config.options.length
? config.options
: [
{ value: 'US', textContent: '美国', buttonText: '美国 (+1)' },
{ value: 'TH', textContent: '泰国', buttonText: '泰国 (+66)' },
]
).map((entry) => ({
value: String(entry?.value || '').trim(),
textContent: String(entry?.textContent || '').trim(),
buttonText: String(entry?.buttonText || entry?.textContent || '').trim(),
}));
const select = {
options,
selectedIndex: 0,
selectedIndex: Math.max(0, Math.min(options.length - 1, Number(config.selectedIndex) || 0)),
dispatchEvent(event) {
selectEvents.push(event?.type || '');
return true;
@@ -58,7 +66,7 @@ function createFakeAddPhoneDom() {
const selectValueNode = {
get textContent() {
return select.value === 'TH' ? '泰国 (+66)' : '美国 (+1)';
return options[select.selectedIndex]?.buttonText || '';
},
};
@@ -199,3 +207,244 @@ test('phone auth matches english HeroSMS country labels against localized add-ph
Intl.DisplayNames = OriginalDisplayNames;
}
});
test('phone auth keeps explicit international number and auto-selects country by dial code when label lookup fails', async () => {
const originalDocument = global.document;
const originalEvent = global.Event;
const originalLocation = global.location;
const OriginalDisplayNames = Intl.DisplayNames;
const dom = createFakeAddPhoneDom({
options: [
{ value: 'CO', textContent: 'Colombia (+57)', buttonText: 'Colombia (+57)' },
{ value: 'GB', textContent: 'United Kingdom (+44)', buttonText: 'United Kingdom (+44)' },
],
selectedIndex: 0,
});
let phoneVerificationReady = false;
global.document = dom.document;
global.Event = class Event {
constructor(type) {
this.type = type;
}
};
global.location = { href: 'https://auth.openai.com/add-phone' };
Intl.DisplayNames = class DisplayNames {
of(regionCode) {
return regionCode;
}
};
try {
const helpers = api.createPhoneAuthHelpers({
fillInput: (element, value) => {
element.value = value;
},
getActionText: () => '',
getPageTextSnapshot: () => '',
getVerificationErrorText: () => '',
humanPause: async () => {},
isActionEnabled: () => true,
isAddPhonePageReady: () => true,
isConsentReady: () => false,
isPhoneVerificationPageReady: () => phoneVerificationReady,
isVisibleElement: () => true,
simulateClick: (element) => {
element.click?.();
phoneVerificationReady = true;
global.location.href = 'https://auth.openai.com/phone-verification';
},
sleep: async () => {},
throwIfStopped: () => {},
waitForElement: async () => null,
});
const result = await helpers.submitPhoneNumber({
countryLabel: 'Country #16',
phoneNumber: '+447999221823',
});
assert.equal(dom.select.value, 'GB');
assert.equal(dom.phoneInput.value, '7999221823');
assert.equal(dom.hiddenPhoneInput.value, '+447999221823');
assert.equal(dom.wasSubmitClicked(), true);
assert.deepStrictEqual(result, {
phoneVerificationPage: true,
displayedPhone: '',
url: 'https://auth.openai.com/phone-verification',
});
} finally {
global.document = originalDocument;
global.Event = originalEvent;
global.location = originalLocation;
Intl.DisplayNames = OriginalDisplayNames;
}
});
test('phone auth can auto-select country by dial code even when number has no plus prefix', async () => {
const originalDocument = global.document;
const originalEvent = global.Event;
const originalLocation = global.location;
const OriginalDisplayNames = Intl.DisplayNames;
const dom = createFakeAddPhoneDom({
options: [
{ value: 'CO', textContent: 'Colombia (+57)', buttonText: 'Colombia (+57)' },
{ value: 'GB', textContent: 'United Kingdom (+44)', buttonText: 'United Kingdom (+44)' },
],
selectedIndex: 0,
});
let phoneVerificationReady = false;
global.document = dom.document;
global.Event = class Event {
constructor(type) {
this.type = type;
}
};
global.location = { href: 'https://auth.openai.com/add-phone' };
Intl.DisplayNames = class DisplayNames {
of(regionCode) {
return regionCode;
}
};
try {
const helpers = api.createPhoneAuthHelpers({
fillInput: (element, value) => {
element.value = value;
},
getActionText: () => '',
getPageTextSnapshot: () => '',
getVerificationErrorText: () => '',
humanPause: async () => {},
isActionEnabled: () => true,
isAddPhonePageReady: () => true,
isConsentReady: () => false,
isPhoneVerificationPageReady: () => phoneVerificationReady,
isVisibleElement: () => true,
simulateClick: (element) => {
element.click?.();
phoneVerificationReady = true;
global.location.href = 'https://auth.openai.com/phone-verification';
},
sleep: async () => {},
throwIfStopped: () => {},
waitForElement: async () => null,
});
const result = await helpers.submitPhoneNumber({
countryLabel: 'Country #16',
phoneNumber: '447999221823',
});
assert.equal(dom.select.value, 'GB');
assert.equal(dom.phoneInput.value, '7999221823');
assert.equal(dom.hiddenPhoneInput.value, '+447999221823');
assert.equal(dom.wasSubmitClicked(), true);
assert.deepStrictEqual(result, {
phoneVerificationPage: true,
displayedPhone: '',
url: 'https://auth.openai.com/phone-verification',
});
} finally {
global.document = originalDocument;
global.Event = originalEvent;
global.location = originalLocation;
Intl.DisplayNames = OriginalDisplayNames;
}
});
test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of endless Try-again loop', async () => {
const originalDocument = global.document;
const originalLocation = global.location;
const originalWindow = global.window;
let retryClicks = 0;
const fakeRetryButton = {
getAttribute(name) {
if (name === 'data-dd-action-name') return 'Try again';
return '';
},
click() {
retryClicks += 1;
},
textContent: 'Try again',
};
const fakeResendButton = {
getAttribute(name) {
if (name === 'value') return 'resend';
return '';
},
textContent: 'Resend text message',
};
const fakePhoneForm = {
querySelectorAll(selector) {
if (selector === 'button, input[type="submit"], input[type="button"]') {
return [fakeResendButton, fakeRetryButton];
}
return [];
},
};
global.document = {
title: 'Route Error',
querySelector(selector) {
if (selector === 'button[data-dd-action-name="Try again"]') {
return fakeRetryButton;
}
if (selector === 'form[action*="/phone-verification" i]') {
return fakePhoneForm;
}
return null;
},
querySelectorAll(selector) {
if (selector === 'button, [role="button"], input[type="submit"], input[type="button"]') {
return [fakeRetryButton, fakeResendButton];
}
return [];
},
};
global.location = {
href: 'https://auth.openai.com/phone-verification',
pathname: '/phone-verification',
};
global.window = global;
const helpers = api.createPhoneAuthHelpers({
fillInput: () => {},
getActionText: (element) => String(element?.textContent || ''),
getPageTextSnapshot: () => (
'Route Error (405 Method Not Allowed): You made a POST request to "/phone-verification" but did not provide an action.'
),
getVerificationErrorText: () => '',
humanPause: async () => {},
isActionEnabled: () => true,
isAddPhonePageReady: () => false,
isConsentReady: () => false,
isPhoneVerificationPageReady: () => true,
isVisibleElement: () => true,
simulateClick: (element) => {
element?.click?.();
},
sleep: async () => {},
throwIfStopped: () => {},
waitForElement: async () => null,
});
try {
await assert.rejects(
() => helpers.resendPhoneVerificationCode(4000),
/PHONE_ROUTE_405_RECOVERY_FAILED::/i
);
assert.ok(
retryClicks > 0 && retryClicks <= 6,
`expected bounded retry clicks (1..6), got ${retryClicks}`
);
} finally {
global.document = originalDocument;
global.location = originalLocation;
global.window = originalWindow;
}
});
File diff suppressed because it is too large Load Diff
@@ -59,6 +59,13 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
assert.match(html, /id="btn-toggle-phone-verification-section"/);
assert.match(html, /id="row-phone-verification-fold"/);
assert.match(html, /id="input-phone-verification-enabled"/);
assert.match(html, /id="row-phone-sms-provider"/);
assert.match(html, /id="select-phone-sms-provider"/);
assert.match(html, /id="row-phone-sms-provider-order"/);
assert.match(html, /id="select-phone-sms-provider-order"[^>]*multiple/);
assert.match(html, /id="btn-phone-sms-provider-order-menu"/);
assert.match(html, /id="row-phone-sms-provider-order-actions"/);
assert.match(html, /id="btn-phone-sms-provider-order-reset"/);
assert.match(html, /id="row-hero-sms-platform"/);
assert.match(html, /id="row-hero-sms-country"/);
assert.match(html, /id="row-hero-sms-country-fallback"/);
@@ -69,32 +76,83 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
assert.match(html, /id="row-hero-sms-api-key"/);
assert.match(html, /id="row-hero-sms-max-price"/);
assert.match(html, /id="row-hero-sms-current-number"/);
assert.match(html, /id="row-hero-sms-current-countdown"/);
assert.match(html, /id="row-hero-sms-price-tiers"/);
assert.match(html, /id="row-hero-sms-current-code"/);
assert.match(html, /id="row-hero-sms-preferred-activation"/);
assert.match(html, /id="select-hero-sms-preferred-activation"/);
assert.match(html, /id="row-phone-replacement-limit"/);
assert.match(html, /id="row-phone-verification-resend-count"/);
assert.match(html, /id="row-phone-code-wait-seconds"/);
assert.match(html, /id="row-phone-code-timeout-windows"/);
assert.match(html, /id="row-phone-code-poll-interval-seconds"/);
assert.match(html, /id="row-phone-code-poll-max-rounds"/);
assert.match(html, /id="row-oauth-flow-timeout"/);
assert.match(html, /id="input-oauth-flow-timeout-enabled"/);
assert.match(html, /只取消 Step 7 后链总预算/);
assert.match(html, /id="row-five-sim-api-key"/);
assert.match(html, /id="input-five-sim-api-key"/);
assert.match(html, /id="row-five-sim-country"/);
assert.match(html, /id="select-five-sim-country"[^>]*multiple/);
assert.match(html, /id="row-five-sim-country-fallback"/);
assert.match(html, /id="row-five-sim-operator"/);
assert.match(html, /id="input-five-sim-operator"/);
assert.match(html, /id="row-five-sim-product"/);
assert.match(html, /id="input-five-sim-product"/);
assert.match(html, /<option value="nexsms">/);
assert.match(html, /id="row-nex-sms-api-key"/);
assert.match(html, /id="input-nex-sms-api-key"/);
assert.match(html, /id="row-nex-sms-country"/);
assert.match(html, /id="select-nex-sms-country"[^>]*multiple/);
assert.match(html, /id="row-nex-sms-country-fallback"/);
assert.match(html, /id="row-nex-sms-service-code"/);
assert.match(html, /id="input-nex-sms-service-code"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
const api = new Function(`
const phoneVerificationSectionExpanded = true;
let latestState = {};
const inputPhoneVerificationEnabled = { checked: false };
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
const rowPhoneVerificationFold = { style: { display: 'none' } };
const rowPhoneSmsProvider = { style: { display: 'none' } };
const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
const selectPhoneSmsProvider = { value: 'hero-sms' };
const btnTogglePhoneVerificationSection = {
disabled: false,
textContent: '',
title: '',
setAttribute: () => {},
};
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms'];
const phoneSmsProviderOrderSelection = [];
function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = DEFAULT_PHONE_SMS_PROVIDER_ORDER) {
const source = Array.isArray(value) ? value : [];
const normalized = [...source];
if (normalized.length) {
return normalized.slice(0, 3);
}
if (!Array.isArray(fallbackOrder) || !fallbackOrder.length) {
return [];
}
const fallbackNormalized = [];
for (const provider of fallbackOrder) {
if (!fallbackNormalized.includes(provider)) {
fallbackNormalized.push(provider);
}
}
return fallbackNormalized.slice(0, 3);
}
function resolveNormalizedProviderOrderForRuntime(state = {}) {
const rawOrder = Array.isArray(state?.phoneSmsProviderOrder) ? state.phoneSmsProviderOrder : [];
const normalizedOrder = normalizePhoneSmsProviderOrderValue(rawOrder, []);
if (normalizedOrder.length) {
return normalizedOrder;
}
const fallbackProvider = String(state?.phoneSmsProvider || selectPhoneSmsProvider?.value || 'hero-sms').trim().toLowerCase() || 'hero-sms';
return [fallbackProvider];
}
function updatePhoneSmsProviderOrderSummary() {}
const rowHeroSmsPlatform = { style: { display: 'none' } };
const rowHeroSmsCountry = { style: { display: 'none' } };
const rowHeroSmsCountryFallback = { style: { display: 'none' } };
@@ -102,20 +160,36 @@ const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
const rowHeroSmsCurrentCode = { style: { display: 'none' } };
const rowHeroSmsPreferredActivation = { style: { display: 'none' } };
const rowPhoneVerificationResendCount = { style: { display: 'none' } };
const rowPhoneReplacementLimit = { style: { display: 'none' } };
const rowPhoneCodeWaitSeconds = { style: { display: 'none' } };
const rowPhoneCodeTimeoutWindows = { style: { display: 'none' } };
const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
const rowFiveSimApiKey = { style: { display: 'none' } };
const rowFiveSimCountry = { style: { display: 'none' } };
const rowFiveSimCountryFallback = { style: { display: 'none' } };
const rowFiveSimOperator = { style: { display: 'none' } };
const rowFiveSimProduct = { style: { display: 'none' } };
const rowNexSmsApiKey = { style: { display: 'none' } };
const rowNexSmsCountry = { style: { display: 'none' } };
const rowNexSmsCountryFallback = { style: { display: 'none' } };
const rowNexSmsServiceCode = { style: { display: 'none' } };
${extractFunction('updatePhoneVerificationSettingsUI')}
return {
setLatestState: (state) => { latestState = state || {}; },
rowPhoneVerificationEnabled,
rowPhoneVerificationFold,
rowPhoneSmsProvider,
rowPhoneSmsProviderOrder,
rowPhoneSmsProviderOrderActions,
selectPhoneSmsProvider,
btnTogglePhoneVerificationSection,
inputPhoneVerificationEnabled,
rowHeroSmsPlatform,
@@ -125,14 +199,25 @@ return {
rowHeroSmsApiKey,
rowHeroSmsMaxPrice,
rowHeroSmsCurrentNumber,
rowHeroSmsCurrentCountdown,
rowHeroSmsPriceTiers,
rowHeroSmsCurrentCode,
rowHeroSmsPreferredActivation,
rowPhoneVerificationResendCount,
rowPhoneReplacementLimit,
rowPhoneCodeWaitSeconds,
rowPhoneCodeTimeoutWindows,
rowPhoneCodePollIntervalSeconds,
rowPhoneCodePollMaxRounds,
rowFiveSimApiKey,
rowFiveSimCountry,
rowFiveSimCountryFallback,
rowFiveSimOperator,
rowFiveSimProduct,
rowNexSmsApiKey,
rowNexSmsCountry,
rowNexSmsCountryFallback,
rowNexSmsServiceCode,
updatePhoneVerificationSettingsUI,
};
`)();
@@ -140,6 +225,9 @@ return {
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
assert.equal(api.rowPhoneSmsProvider.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, 'none');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, 'none');
assert.equal(api.btnTogglePhoneVerificationSection.disabled, true);
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置');
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
@@ -149,18 +237,32 @@ return {
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentNumber.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, 'none');
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentCode.style.display, 'none');
assert.equal(api.rowHeroSmsPreferredActivation.style.display, 'none');
assert.equal(api.rowPhoneVerificationResendCount.style.display, 'none');
assert.equal(api.rowPhoneReplacementLimit.style.display, 'none');
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, 'none');
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, 'none');
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, 'none');
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, 'none');
assert.equal(api.rowFiveSimApiKey.style.display, 'none');
assert.equal(api.rowFiveSimCountry.style.display, 'none');
assert.equal(api.rowFiveSimCountryFallback.style.display, 'none');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
assert.equal(api.rowFiveSimProduct.style.display, 'none');
assert.equal(api.rowNexSmsApiKey.style.display, 'none');
assert.equal(api.rowNexSmsCountry.style.display, 'none');
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationFold.style.display, '');
assert.equal(api.rowPhoneSmsProvider.style.display, '');
assert.equal(api.rowPhoneSmsProviderOrder.style.display, '');
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, '');
assert.equal(api.btnTogglePhoneVerificationSection.disabled, false);
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '收起设置');
assert.equal(api.rowHeroSmsPlatform.style.display, '');
@@ -170,14 +272,51 @@ return {
assert.equal(api.rowHeroSmsApiKey.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, '');
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentCode.style.display, '');
assert.equal(api.rowHeroSmsPreferredActivation.style.display, '');
assert.equal(api.rowPhoneVerificationResendCount.style.display, '');
assert.equal(api.rowPhoneReplacementLimit.style.display, '');
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, '');
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, '');
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, '');
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, '');
assert.equal(api.rowFiveSimApiKey.style.display, 'none');
assert.equal(api.rowFiveSimCountry.style.display, 'none');
assert.equal(api.rowFiveSimCountryFallback.style.display, 'none');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
assert.equal(api.rowFiveSimProduct.style.display, 'none');
assert.equal(api.rowNexSmsApiKey.style.display, 'none');
assert.equal(api.rowNexSmsCountry.style.display, 'none');
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
api.selectPhoneSmsProvider.value = '5sim';
api.setLatestState({ phoneSmsProviderOrder: ['5sim'] });
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
assert.equal(api.rowFiveSimApiKey.style.display, '');
assert.equal(api.rowFiveSimCountry.style.display, '');
assert.equal(api.rowFiveSimCountryFallback.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, '');
assert.equal(api.rowFiveSimProduct.style.display, '');
assert.equal(api.rowNexSmsApiKey.style.display, 'none');
assert.equal(api.rowNexSmsCountry.style.display, 'none');
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
api.selectPhoneSmsProvider.value = 'nexsms';
api.setLatestState({ phoneSmsProviderOrder: ['nexsms'] });
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowFiveSimApiKey.style.display, 'none');
assert.equal(api.rowFiveSimCountry.style.display, 'none');
assert.equal(api.rowNexSmsApiKey.style.display, '');
assert.equal(api.rowNexSmsCountry.style.display, '');
assert.equal(api.rowNexSmsCountryFallback.style.display, '');
assert.equal(api.rowNexSmsServiceCode.style.display, '');
});
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
@@ -186,6 +325,7 @@ let latestState = {
contributionMode: false,
mail2925UseAccountPool: false,
currentMail2925AccountId: '',
fiveSimCountryOrder: ['thailand', 'england'],
};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
@@ -225,12 +365,26 @@ const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: false };
const inputPhoneVerificationEnabled = { checked: true };
const selectPhoneSmsProvider = { value: 'hero-sms' };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
const inputFiveSimApiKey = { value: 'five-sim-key' };
const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' };
function getSelectedPhonePreferredActivation() {
return {
provider: 'hero-sms',
activationId: 'demo-activation',
phoneNumber: '66958889999',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
};
}
const inputHeroSmsMaxPrice = { value: '0.12' };
const inputPhoneReplacementLimit = { value: '5' };
const inputPhoneCodeWaitSeconds = { value: '75' };
@@ -303,13 +457,26 @@ return { collectSettingsPayload };
const payload = api.collectSettingsPayload();
assert.equal(payload.phoneVerificationEnabled, true);
assert.equal(payload.oauthFlowTimeoutEnabled, false);
assert.equal(payload.phoneSmsProvider, 'hero-sms');
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
assert.equal(payload.fiveSimApiKey, 'five-sim-key');
assert.deepStrictEqual(payload.fiveSimCountryOrder, ['thailand', 'england']);
assert.equal(payload.fiveSimOperator, 'any');
assert.equal(payload.fiveSimProduct, 'openai');
assert.equal(payload.heroSmsReuseEnabled, true);
assert.equal(payload.heroSmsAcquirePriority, 'price');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.deepStrictEqual(payload.phonePreferredActivation, {
provider: 'hero-sms',
activationId: 'demo-activation',
phoneNumber: '66958889999',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
});
assert.equal(payload.phoneVerificationReplacementLimit, 5);
assert.equal(payload.phoneCodeWaitSeconds, 75);
assert.equal(payload.phoneCodeTimeoutWindows, 3);
+71
View File
@@ -189,3 +189,74 @@ test('step 8 reruns step 7 when auth page enters login timeout retry state', asy
},
]);
});
test('step 8 escalates to rerun step 7 after too many local retry_without_step7 recoveries', async () => {
const calls = {
rerunStep7: 0,
ensureReady: 0,
logs: [],
};
const executor = step8Api.createStep8Executor({
addLog: async (message, level) => {
calls.logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => {
calls.ensureReady += 1;
return { state: 'verification_page' };
},
rerunStep7ForStep8Recovery: async () => {
calls.rerunStep7 += 1;
throw new Error('RERUN_MARKER');
},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
provider: 'qq',
label: 'QQ mail',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret', oauthUrl: 'https://oauth.example/latest' }),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async () => {
throw new Error('Content script on icloud-mail did not respond in 1s. Try refreshing the tab and retry.');
},
reuseOrCreateTab: async () => {},
setState: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async () => {},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
}),
/RERUN_MARKER/
);
assert.equal(calls.rerunStep7, 1);
assert.equal(calls.ensureReady >= 4, true);
assert.equal(
calls.logs.some(({ message }) => /连续重试 \d+ 改为回到步骤 7/.test(message)),
true
);
});
+6
View File
@@ -51,4 +51,10 @@ assert.strictEqual(
'真实业务错误不应被误判为可重试传输错误'
);
assert.strictEqual(
api.isRetryableContentScriptTransportError(new Error('TypeError: Failed to fetch')),
true,
'Failed to fetch 应进入可重试传输错误分支'
);
console.log('step8 state timeout retry tests passed');
+257 -1
View File
@@ -601,6 +601,69 @@ test('verification flow caps mail polling timeout to the remaining oauth budget'
assert.equal(mailPollCalls[0].payload.maxAttempts, 2);
});
test('verification flow keeps mail polling response timeout above minimum floor', async () => {
const mailPollCalls = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
return {};
}
return {};
},
sendToMailContentScriptResilient: async (_mail, message, options) => {
mailPollCalls.push({
payload: message.payload,
options,
});
return { code: '654321', emailTimestamp: 123 };
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
lastLoginCode: null,
},
{ provider: 'qq', label: 'QQ 邮箱' },
{
getRemainingTimeMs: async () => 1200,
resendIntervalMs: 0,
}
);
assert.ok(mailPollCalls.length >= 1);
assert.equal(mailPollCalls[0].options.timeoutMs, 5000);
assert.equal(mailPollCalls[0].options.responseTimeoutMs, 5000);
});
test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even when oauth budget is smaller', async () => {
const mailPollCalls = [];
@@ -1155,7 +1218,7 @@ test('verification flow notifies onResendRequestedAt when resend is triggered',
{ provider: 'qq', label: 'QQ 邮箱' },
{
maxResendRequests: 1,
resendIntervalMs: 25000,
resendIntervalMs: 0,
onResendRequestedAt: async (requestedAt) => {
resendRequestedAtCalls.push(Number(requestedAt) || 0);
},
@@ -1270,3 +1333,196 @@ test('verification flow treats retryable submit transport failure as success whe
assert.equal(result.transportRecovered, true);
assert.equal(logs.some(({ message }) => /验证码提交后页面已切换到ChatGPT 已登录首页/.test(message)), true);
});
test('verification flow avoids resend storms when iCloud polling keeps hitting transport errors', async () => {
let resendRequests = 0;
let pollAttempts = 0;
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
resendRequests += 1;
return {};
}
return {};
},
sendToMailContentScriptResilient: async () => {
pollAttempts += 1;
throw new Error('Content script on icloud-mail did not respond in 1s. Try refreshing the tab and retry.');
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await assert.rejects(
helpers.pollFreshVerificationCodeWithResendInterval(
8,
{ email: 'user@example.com', lastLoginCode: null },
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
{
intervalMs: 1000,
maxAttempts: 1,
resendIntervalMs: 25000,
maxResendRequests: 2,
}
),
/页面通信异常连续/
);
assert.equal(pollAttempts >= 6, true);
assert.equal(resendRequests, 0);
});
test('verification flow stops iCloud poll-only loop after repeated no-code rounds before resend', async () => {
let resendRequests = 0;
let pollCalls = 0;
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
resendRequests += 1;
}
return {};
},
sendToMailContentScriptResilient: async () => {
pollCalls += 1;
return {};
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await assert.rejects(
helpers.pollFreshVerificationCodeWithResendInterval(
8,
{ email: 'user@example.com', lastLoginCode: null },
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
{
intervalMs: 1000,
maxAttempts: 1,
resendIntervalMs: 25000,
maxResendRequests: 2,
}
),
/空轮询循环|停止当前链路/
);
assert.equal(pollCalls >= 4, true);
assert.equal(resendRequests, 0);
});
test('verification flow caps iCloud polling response timeout to avoid long silent stalls', async () => {
const pollTimeouts = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
return {};
}
return {};
},
sendToMailContentScriptResilient: async (_mail, _message, options = {}) => {
pollTimeouts.push({
timeoutMs: Number(options.timeoutMs) || 0,
responseTimeoutMs: Number(options.responseTimeoutMs) || 0,
});
return {};
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await assert.rejects(
helpers.pollFreshVerificationCodeWithResendInterval(
4,
{ email: 'user@example.com', lastSignupCode: null },
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
{
intervalMs: 3000,
maxAttempts: 5,
resendIntervalMs: 25000,
maxResendRequests: 1,
}
),
/空轮询循环|停止当前链路/
);
assert.equal(pollTimeouts.length > 0, true);
assert.equal(pollTimeouts.every(({ timeoutMs }) => timeoutMs > 0 && timeoutMs <= 22000), true);
assert.equal(
pollTimeouts.every(({ responseTimeoutMs }) => responseTimeoutMs > 0 && responseTimeoutMs <= 18000),
true
);
});