fix: improve iCloud polling and add-phone handoff
This commit is contained in:
@@ -10913,9 +10913,11 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
||||
getLoginAuthStateLabel,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getState,
|
||||
getTabId,
|
||||
isAddPhoneAuthFailure,
|
||||
isStep6RecoverableResult,
|
||||
isStep6SuccessResult,
|
||||
phoneVerificationHelpers,
|
||||
refreshOAuthUrlBeforeStep6,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user