feat(flow): unify sms multi-provider fallback and stabilize proxy/auth retries
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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', []),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user