fix: improve iCloud polling and add-phone handoff

This commit is contained in:
root
2026-05-08 23:19:18 -04:00
parent 19a884d6c4
commit 6f67a00980
8 changed files with 534 additions and 39 deletions
+2
View File
@@ -10913,9 +10913,11 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
getLoginAuthStateLabel,
getOAuthFlowStepTimeoutMs,
getState,
getTabId,
isAddPhoneAuthFailure,
isStep6RecoverableResult,
isStep6SuccessResult,
phoneVerificationHelpers,
refreshOAuthUrlBeforeStep6,
reuseOrCreateTab,
sendToContentScriptResilient,
+54 -1
View File
@@ -18,6 +18,8 @@
},
isStep6RecoverableResult,
isStep6SuccessResult,
getTabId,
phoneVerificationHelpers = null,
refreshOAuthUrlBeforeStep6,
reuseOrCreateTab,
sendToContentScriptResilient,
@@ -74,6 +76,49 @@
return normalizeStep7IdentifierType(fallbackType) || 'email';
}
function extractAddPhoneUrl(error) {
const message = String(typeof error === 'string' ? error : error?.message || '');
const match = message.match(/https:\/\/auth\.openai\.com\/add-phone(?:[^\s]*)?/i);
return match ? match[0] : 'https://auth.openai.com/add-phone';
}
async function completeStep7AddPhoneHandoff(state = {}, err, completionStep) {
if (!state?.phoneVerificationEnabled) {
throw new Error(
`步骤 ${completionStep}:登录提交后页面进入手机号页面,必须先启用接码/phone verification 后才能继续。URL: ${extractAddPhoneUrl(err)}`
);
}
if (typeof phoneVerificationHelpers?.completePhoneVerificationFlow !== 'function') {
throw new Error(`步骤 ${completionStep}:手机号验证流程不可用,接码模块尚未初始化。`);
}
if (typeof getTabId !== 'function') {
throw new Error(`步骤 ${completionStep}:无法定位认证页面标签页,不能继续手机号验证。`);
}
const signupTabId = await getTabId('signup-page');
if (!Number.isInteger(signupTabId)) {
throw new Error(`步骤 ${completionStep}:认证页面标签页已关闭,无法继续手机号验证。`);
}
const pageState = {
addPhonePage: true,
phoneVerificationPage: false,
state: 'add_phone_page',
url: extractAddPhoneUrl(err),
};
await phoneVerificationHelpers.completePhoneVerificationFlow(signupTabId, pageState, {
step: completionStep,
visibleStep: completionStep,
});
await completeStepFromBackground(completionStep, {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
phoneVerification: true,
loginPhoneVerification: true,
});
}
async function executeStep7(state) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
const completionStep = visibleStep > 0 ? visibleStep : 7;
@@ -218,7 +263,15 @@
} catch (err) {
throwIfStopped(err);
if (isAddPhoneAuthFailure(err)) {
throw err;
const latestAddPhoneState = typeof getState === 'function'
? await getState().catch(() => state)
: state;
await completeStep7AddPhoneHandoff(
{ ...(state || {}), ...(latestAddPhoneState || {}) },
err,
completionStep
);
return;
}
if (isManagementSecretConfigError(err)) {
await addLog(
+43 -31
View File
@@ -1,8 +1,8 @@
(function attachBackgroundVerificationFlow(root, factory) {
root.MultiPageBackgroundVerificationFlow = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
const ICLOUD_MAIL_POLL_RESPONSE_TIMEOUT_CAP_MS = 18000;
const ICLOUD_MAIL_POLL_TOTAL_TIMEOUT_CAP_MS = 22000;
const ICLOUD_MAIL_POLL_MIN_ATTEMPTS = 5;
const ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS = 25000;
function createVerificationFlowHelpers(deps = {}) {
const {
@@ -78,30 +78,54 @@
return step === 4 ? '注册' : '登录';
}
function capIcloudMailPollingTimeouts(mail, timedPoll) {
function isIcloudMail(mail) {
return mail?.source === 'icloud-mail' || mail?.provider === 'icloud';
}
function normalizeIcloudMailPollPayload(mail, payload = {}) {
if (!isIcloudMail(mail)) {
return payload;
}
const currentAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
if (currentAttempts >= ICLOUD_MAIL_POLL_MIN_ATTEMPTS) {
return payload;
}
return {
...payload,
maxAttempts: ICLOUD_MAIL_POLL_MIN_ATTEMPTS,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS);
}
function resolveMailPollingTimeouts(mail, timedPoll) {
const payload = normalizeIcloudMailPollPayload(mail, timedPoll?.payload || {});
const defaultResponseTimeoutMs = Math.max(1000, Number(timedPoll?.responseTimeoutMs) || 30000);
const defaultTimeoutMs = Math.max(defaultResponseTimeoutMs, Number(timedPoll?.timeoutMs) || defaultResponseTimeoutMs);
if (mail?.source !== 'icloud-mail') {
if (!isIcloudMail(mail)) {
return {
payload,
responseTimeoutMs: defaultResponseTimeoutMs,
timeoutMs: defaultTimeoutMs,
capped: false,
};
}
const cappedResponseTimeoutMs = Math.max(
5000,
Math.min(defaultResponseTimeoutMs, ICLOUD_MAIL_POLL_RESPONSE_TIMEOUT_CAP_MS)
);
const cappedTimeoutMs = Math.max(
cappedResponseTimeoutMs,
Math.min(defaultTimeoutMs, ICLOUD_MAIL_POLL_TOTAL_TIMEOUT_CAP_MS)
const derivedResponseTimeoutMs = Math.max(
defaultResponseTimeoutMs,
getMailPollingResponseTimeoutMs(payload)
);
const derivedTimeoutMs = Math.max(defaultTimeoutMs, derivedResponseTimeoutMs);
return {
responseTimeoutMs: cappedResponseTimeoutMs,
timeoutMs: cappedTimeoutMs,
capped: cappedResponseTimeoutMs < defaultResponseTimeoutMs || cappedTimeoutMs < defaultTimeoutMs,
payload,
responseTimeoutMs: derivedResponseTimeoutMs,
timeoutMs: derivedTimeoutMs,
};
}
@@ -707,20 +731,14 @@
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const timeoutWindow = capIcloudMailPollingTimeouts(mail, timedPoll);
if (timeoutWindow.capped) {
await addLog(
`步骤 ${step}:iCloud 邮箱轮询已启用快速超时保护(${Math.ceil(timeoutWindow.timeoutMs / 1000)} 秒),避免页面无响应导致长时间卡住。`,
'info'
);
}
const timeoutWindow = resolveMailPollingTimeouts(mail, timedPoll);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: timedPoll.payload,
payload: timeoutWindow.payload,
},
{
timeoutMs: timeoutWindow.timeoutMs,
@@ -982,20 +1000,14 @@
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const timeoutWindow = capIcloudMailPollingTimeouts(mail, timedPoll);
if (timeoutWindow.capped) {
await addLog(
`步骤 ${step}:iCloud 邮箱轮询已启用快速超时保护(${Math.ceil(timeoutWindow.timeoutMs / 1000)} 秒),避免页面无响应导致长时间卡住。`,
'info'
);
}
const timeoutWindow = resolveMailPollingTimeouts(mail, timedPoll);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: timedPoll.payload,
payload: timeoutWindow.payload,
},
{
timeoutMs: timeoutWindow.timeoutMs,
@@ -0,0 +1,12 @@
# iCloud Polling And Add Phone Handoff Drift Adjudication Ledger
Immutable spec path: `/root/projects/resister-codex/codex-oauth-automation-extension-upstream-base-vu8.0/codex-oauth-automation-extension/.worktrees/fix-icloud-polling-addphone-handoff/docs/superpowers/specs/2026-05-08-icloud-polling-addphone-fix-design.md`
Immutable plan path: `/root/projects/resister-codex/codex-oauth-automation-extension-upstream-base-vu8.0/codex-oauth-automation-extension/.worktrees/fix-icloud-polling-addphone-handoff/docs/superpowers/plans/2026-05-08-icloud-polling-addphone-fix-plan.md`
Controller-only mutation rule: only the P9/controller may create, append, supersede, close, summarize, or otherwise mutate this ledger. Subagents may read it, cite `DRIFT_ID`, report evidence, and request supersession, but may not edit it directly.
## Active Adjudications Summary
| DRIFT_ID | STATUS | SCOPE | P9_FINAL_ADJUDICATION | EFFECTIVE_EXECUTION_CONTRACT |
|---|---|---|---|---|
@@ -0,0 +1,85 @@
# iCloud Polling And Add Phone Handoff Fix Plan
## Goal
Implement the fix described in `docs/superpowers/specs/2026-05-08-icloud-polling-addphone-fix-design.md` on the Ultra8.0 operation-delay branch.
## Architecture
- Keep operation-delay excluded from polling.
- Adjust iCloud polling in the verification flow and/or iCloud polling payload construction so normal no-code polling uses a larger iCloud-specific attempt/time window.
- Reuse the existing shared phone verification flow when Step 7 lands on `/add-phone` and phone verification is enabled.
- Preserve fatal behavior when phone verification is disabled.
## Implementation Granularity
- Implementer granularity: TaskGroup.
- Reviewer granularity: TaskGroup.
- One checkpoint commit after both spec and code reviewers approve.
## TaskGroup 1: iCloud polling and Step 7 add-phone handoff
### Task 1: Expand iCloud polling window without using operation-delay
Files likely involved:
- `background/verification-flow.js`
- `content/icloud-mail.js`
- `tests/verification-flow-polling.test.js`
- `tests/icloud-mail-content.test.js`
- Existing operation-delay exclusion tests as regression coverage.
Required behavior:
- Add failing tests proving iCloud Step 4 no-code polling is not configured as one attempt under normal conditions.
- Add failing tests proving iCloud response timeout is large enough to cover the configured attempt count and interval.
- Implement the minimum production change that gives iCloud at least 5 configured polling attempts and a response timeout derived from the iCloud polling window.
- Do not wrap iCloud `POLL_EMAIL` handlers with `performOperationWithDelay`.
- Keep provider-specific fast-failure for actual transport non-response.
Suggested verification:
```bash
node --test tests/verification-flow-polling.test.js tests/icloud-mail-content.test.js tests/mail-polling-operation-delay-exclusion.test.js
```
### Task 2: Continue Step 7 direct add-phone through shared phone verification when enabled
Files likely involved:
- `content/signup-page.js`
- `background/steps/oauth-login.js`
- `background.js`
- `tests/background-step6-retry-limit.test.js`
- `tests/auto-run-step6-restart.test.js`
- `tests/step7-phone-login-entry.test.js`
- `tests/step8-retry-page-recovery.test.js`
- `tests/phone-verification-flow.test.js`
Required behavior:
- Add failing tests for Step 7 password submit landing on `/add-phone` with phone verification enabled.
- Add or preserve tests for Step 7 password submit landing on `/add-phone` with phone verification disabled.
- Add or preserve tests proving phone-login entry pages are not misclassified as add-phone.
- Reuse existing shared phone verification helpers; do not implement new phone/SMS submission logic in Step 7.
- Keep existing fatal propagation for shared phone verification failures.
Suggested verification:
```bash
node --test tests/background-step6-retry-limit.test.js tests/auto-run-step6-restart.test.js tests/step7-phone-login-entry.test.js tests/step8-retry-page-recovery.test.js tests/phone-verification-flow.test.js
```
## Final Verification
Run these before completion:
```bash
node --test tests/verification-flow-polling.test.js tests/icloud-mail-content.test.js tests/mail-polling-operation-delay-exclusion.test.js tests/operation-delay-injection.test.js tests/content-operation-delay.test.js
node --test tests/background-step6-retry-limit.test.js tests/auto-run-step6-restart.test.js tests/step7-phone-login-entry.test.js tests/step8-retry-page-recovery.test.js tests/phone-verification-flow.test.js
npm test
git diff --check
git status --short --branch --untracked-files=all
```
If `npm test` creates `scripts/__pycache__/`, remove it before final status.
@@ -0,0 +1,60 @@
# iCloud Polling And Add Phone Handoff Fix Spec
## Goal
Fix two Ultra8.0 runtime failures on top of the migrated operation-delay branch:
- iCloud verification email polling must wait longer and perform more than one check before declaring the code missing.
- Step 7 password login that lands directly on `https://auth.openai.com/add-phone` must continue into the existing phone verification flow when phone verification is enabled, instead of failing immediately.
## Non-Goals
- Do not add operation-delay waits to email polling, SMS polling, backend retries, or background timers.
- Do not change non-iCloud provider polling cadence unless required by shared test harness setup.
- Do not duplicate phone verification logic inside Step 7.
- Do not treat ordinary phone-login entry pages as add-phone pages.
- Do not continue add-phone when phone verification is disabled or when the existing phone verification flow reports a fatal error.
## iCloud Polling Behavior
The current failure log shows Step 4 iCloud polling using fast timeout protection with a 22 second response timeout and a `最多 1 次` polling payload, then failing after roughly 3 seconds. This is too short for the user's active iCloud-only workflow.
For iCloud verification-code polling:
- Step 4 signup-code polling and Step 8 login-code polling must not be reduced to a single attempt under normal no-code conditions.
- The iCloud polling payload must use an iCloud-specific minimum attempt floor of at least 5 attempts.
- The iCloud polling interval must allow delayed mail arrival and may be longer than the current 3 second effective no-code window.
- The tab-message response timeout for iCloud polling must be large enough for the configured iCloud polling attempts and intervals to complete, plus a small transport margin.
- Transport-unresponsive protection must remain: if the iCloud content script itself does not respond, the flow should fail or recover through the existing transport-error path instead of hanging indefinitely.
- Logs must accurately describe the actual iCloud attempt count and wait window.
- Operation-delay remains excluded from all email polling paths.
## Add Phone Handoff Behavior
When Step 7 password submission transitions directly to an add-phone page:
- If phone verification is enabled, Step 7 must not throw the current fatal `提交密码后页面直接进入手机号页面,未经过登录验证码页` error.
- The flow must hand off to the existing shared phone verification implementation used by later steps.
- After successful phone verification, the automation should continue through the existing OAuth/login flow instead of stopping the run.
- If phone verification is disabled, the flow must keep a clear fatal error telling the user that add-phone requires phone verification to be enabled.
- Existing fatal conditions from the shared phone verification flow remain fatal, including unavailable phone numbers, provider failures, phone already exists, and user stop.
- Existing phone-login entry detection must remain separate from add-phone detection.
## Acceptance Criteria
- Reproducing the reported iCloud Step 4 path no longer emits `开始轮询 iCloud 邮箱(最多 1 次)` for normal iCloud no-code polling.
- iCloud Step 4 no-code polling performs at least 5 configured attempts before the missing-code failure path.
- iCloud polling response timeout is derived from the configured iCloud attempt count and interval rather than being capped to a too-short single-attempt window.
- Mail polling operation-delay exclusion tests still pass.
- Existing non-iCloud provider tests continue to pass.
- Step 7 password-submit to `/add-phone` with phone verification enabled continues into the shared phone verification flow.
- Step 7 password-submit to `/add-phone` with phone verification disabled still fails clearly.
- Step 7 phone-login entry pages are still treated as login phone input, not add-phone.
- Relevant targeted tests and full `npm test` pass.
## Verification Targets
- `node --test tests/verification-flow-polling.test.js tests/icloud-mail-content.test.js`
- `node --test tests/background-step6-retry-limit.test.js tests/auto-run-step6-restart.test.js tests/step7-phone-login-entry.test.js tests/step8-retry-page-recovery.test.js tests/phone-verification-flow.test.js`
- `node --test tests/mail-polling-operation-delay-exclusion.test.js tests/operation-delay-injection.test.js tests/content-operation-delay.test.js`
- `npm test`
+168
View File
@@ -188,6 +188,174 @@ test('step 7 exits internal retry loop immediately when add-phone is detected',
);
});
test('step 7 hands direct add-phone to shared phone verification when enabled', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
refreshCalls: 0,
phoneCalls: [],
completions: [],
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
events.completions.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({
email: 'user@example.com',
password: 'secret',
phoneVerificationEnabled: true,
}),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 91 : 0),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
phoneVerificationHelpers: {
completePhoneVerificationFlow: async (tabId, pageState, options) => {
events.phoneCalls.push({ tabId, pageState, options });
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize/resume' };
},
},
refreshOAuthUrlBeforeStep6: async () => {
events.refreshCalls += 1;
return `https://oauth.example/${events.refreshCalls}`;
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => {
throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7({
email: 'user@example.com',
password: 'secret',
phoneVerificationEnabled: true,
});
assert.equal(events.refreshCalls, 1);
assert.deepStrictEqual(events.phoneCalls, [
{
tabId: 91,
pageState: {
addPhonePage: true,
phoneVerificationPage: false,
state: 'add_phone_page',
url: 'https://auth.openai.com/add-phone',
},
options: {
step: 7,
visibleStep: 7,
},
},
]);
assert.deepStrictEqual(events.completions, [
{
step: 7,
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
phoneVerification: true,
loginPhoneVerification: true,
},
},
]);
});
test('step 7 direct add-phone stays fatal when phone verification is disabled', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
phoneCalls: 0,
completions: 0,
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {
events.completions += 1;
},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: false }),
getTabId: async () => 91,
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
phoneVerificationHelpers: {
completePhoneVerificationFlow: async () => {
events.phoneCalls += 1;
return {};
},
},
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => {
throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: false }),
/手机号页面.*接码|phone verification/i
);
assert.equal(events.phoneCalls, 0);
assert.equal(events.completions, 0);
});
test('step 7 propagates fatal errors from shared add-phone verification', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
phoneCalls: 0,
completions: 0,
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {
events.completions += 1;
},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: true }),
getTabId: async () => 91,
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
phoneVerificationHelpers: {
completePhoneVerificationFlow: async () => {
events.phoneCalls += 1;
throw new Error('步骤 9:没有可用手机号。');
},
},
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => {
throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: true }),
/没有可用手机号/
);
assert.equal(events.phoneCalls, 1);
assert.equal(events.completions, 0);
});
test('step 7 starts a new oauth timeout window for each refreshed oauth url', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
+110 -7
View File
@@ -6,6 +6,43 @@ const source = fs.readFileSync('background/verification-flow.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
function createVerificationFlowTestHelpers(overrides = {}) {
return api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
remove: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER: 'cloudmail',
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 () => ({}),
pollCloudMailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
...overrides,
});
}
test('verification flow keeps 2925 polling cadence in the default payload', () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
@@ -43,6 +80,68 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
assert.equal(step8Payload.intervalMs, 15000);
});
test('verification flow keeps iCloud step 4 polling at least five attempts under a short remaining budget', async () => {
const pollRequests = [];
const helpers = createVerificationFlowTestHelpers({
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
pollRequests.push({ payload: message.payload, options });
return {};
},
});
await assert.rejects(
helpers.pollFreshVerificationCode(
4,
{ email: 'user@example.com', mailProvider: 'icloud', lastSignupCode: null },
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
{
filterAfterTimestamp: 123456,
getRemainingTimeMs: async () => 3000,
maxResendRequests: 0,
}
),
/邮箱轮询结束|无法获取/
);
assert.equal(pollRequests.length, 1);
const [{ payload, options }] = pollRequests;
assert.equal(payload.maxAttempts >= 5, true);
assert.equal(payload.intervalMs >= 3000, true);
assert.equal(options.responseTimeoutMs >= (payload.maxAttempts * payload.intervalMs) + 10000, true);
});
test('verification flow keeps iCloud step 8 polling at least five attempts before no-code failure', async () => {
const pollRequests = [];
const helpers = createVerificationFlowTestHelpers({
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
pollRequests.push({ payload: message.payload, options });
return {};
},
});
await assert.rejects(
helpers.pollFreshVerificationCodeWithResendInterval(
8,
{ email: 'user@example.com', mailProvider: 'icloud', lastLoginCode: null },
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
{
filterAfterTimestamp: 123456,
getRemainingTimeMs: async () => 3000,
maxResendRequests: 0,
resendIntervalMs: 25000,
}
),
/空轮询循环|停止当前链路/
);
assert.equal(pollRequests.length >= 1, true);
assert.equal(pollRequests.every(({ payload }) => payload.maxAttempts >= 5), true);
assert.equal(
pollRequests.every(({ payload, options }) => options.responseTimeoutMs >= (payload.maxAttempts * payload.intervalMs) + 10000),
true
);
});
test('verification flow only enables 2925 target email matching in receive mode', () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
@@ -1599,8 +1698,8 @@ test('verification flow stops iCloud poll-only loop after repeated no-code round
assert.equal(resendRequests, 0);
});
test('verification flow caps iCloud polling response timeout to avoid long silent stalls', async () => {
const pollTimeouts = [];
test('verification flow derives iCloud polling response timeout from the configured polling window', async () => {
const pollRequests = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
@@ -1630,8 +1729,9 @@ test('verification flow caps iCloud polling response timeout to avoid long silen
}
return {};
},
sendToMailContentScriptResilient: async (_mail, _message, options = {}) => {
pollTimeouts.push({
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
pollRequests.push({
payload: message.payload,
timeoutMs: Number(options.timeoutMs) || 0,
responseTimeoutMs: Number(options.responseTimeoutMs) || 0,
});
@@ -1659,10 +1759,13 @@ test('verification flow caps iCloud polling response timeout to avoid long silen
/空轮询循环|停止当前链路/
);
assert.equal(pollTimeouts.length > 0, true);
assert.equal(pollTimeouts.every(({ timeoutMs }) => timeoutMs > 0 && timeoutMs <= 22000), true);
assert.equal(pollRequests.length > 0, true);
assert.equal(
pollTimeouts.every(({ responseTimeoutMs }) => responseTimeoutMs > 0 && responseTimeoutMs <= 18000),
pollRequests.every(({ payload, responseTimeoutMs }) => responseTimeoutMs >= (payload.maxAttempts * payload.intervalMs) + 10000),
true
);
assert.equal(
pollRequests.every(({ timeoutMs, responseTimeoutMs }) => timeoutMs >= responseTimeoutMs),
true
);
});