fix: improve iCloud polling and add-phone handoff
This commit is contained in:
@@ -10913,9 +10913,11 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
|||||||
getLoginAuthStateLabel,
|
getLoginAuthStateLabel,
|
||||||
getOAuthFlowStepTimeoutMs,
|
getOAuthFlowStepTimeoutMs,
|
||||||
getState,
|
getState,
|
||||||
|
getTabId,
|
||||||
isAddPhoneAuthFailure,
|
isAddPhoneAuthFailure,
|
||||||
isStep6RecoverableResult,
|
isStep6RecoverableResult,
|
||||||
isStep6SuccessResult,
|
isStep6SuccessResult,
|
||||||
|
phoneVerificationHelpers,
|
||||||
refreshOAuthUrlBeforeStep6,
|
refreshOAuthUrlBeforeStep6,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
},
|
},
|
||||||
isStep6RecoverableResult,
|
isStep6RecoverableResult,
|
||||||
isStep6SuccessResult,
|
isStep6SuccessResult,
|
||||||
|
getTabId,
|
||||||
|
phoneVerificationHelpers = null,
|
||||||
refreshOAuthUrlBeforeStep6,
|
refreshOAuthUrlBeforeStep6,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
@@ -74,6 +76,49 @@
|
|||||||
return normalizeStep7IdentifierType(fallbackType) || 'email';
|
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) {
|
async function executeStep7(state) {
|
||||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||||
@@ -218,7 +263,15 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throwIfStopped(err);
|
throwIfStopped(err);
|
||||||
if (isAddPhoneAuthFailure(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)) {
|
if (isManagementSecretConfigError(err)) {
|
||||||
await addLog(
|
await addLog(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
(function attachBackgroundVerificationFlow(root, factory) {
|
(function attachBackgroundVerificationFlow(root, factory) {
|
||||||
root.MultiPageBackgroundVerificationFlow = factory();
|
root.MultiPageBackgroundVerificationFlow = factory();
|
||||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
|
||||||
const ICLOUD_MAIL_POLL_RESPONSE_TIMEOUT_CAP_MS = 18000;
|
const ICLOUD_MAIL_POLL_MIN_ATTEMPTS = 5;
|
||||||
const ICLOUD_MAIL_POLL_TOTAL_TIMEOUT_CAP_MS = 22000;
|
const ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS = 25000;
|
||||||
|
|
||||||
function createVerificationFlowHelpers(deps = {}) {
|
function createVerificationFlowHelpers(deps = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -78,30 +78,54 @@
|
|||||||
return step === 4 ? '注册' : '登录';
|
return step === 4 ? '注册' : '登录';
|
||||||
}
|
}
|
||||||
|
|
||||||
function capIcloudMailPollingTimeouts(mail, timedPoll) {
|
function isIcloudMail(mail) {
|
||||||
const defaultResponseTimeoutMs = Math.max(1000, Number(timedPoll?.responseTimeoutMs) || 30000);
|
return mail?.source === 'icloud-mail' || mail?.provider === 'icloud';
|
||||||
const defaultTimeoutMs = Math.max(defaultResponseTimeoutMs, Number(timedPoll?.timeoutMs) || defaultResponseTimeoutMs);
|
}
|
||||||
if (mail?.source !== 'icloud-mail') {
|
|
||||||
|
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 {
|
return {
|
||||||
responseTimeoutMs: defaultResponseTimeoutMs,
|
...payload,
|
||||||
timeoutMs: defaultTimeoutMs,
|
maxAttempts: ICLOUD_MAIL_POLL_MIN_ATTEMPTS,
|
||||||
capped: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cappedResponseTimeoutMs = Math.max(
|
function getMailPollingResponseTimeoutMs(payload = {}) {
|
||||||
5000,
|
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
|
||||||
Math.min(defaultResponseTimeoutMs, ICLOUD_MAIL_POLL_RESPONSE_TIMEOUT_CAP_MS)
|
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
|
||||||
);
|
return Math.max(45000, maxAttempts * intervalMs + ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS);
|
||||||
const cappedTimeoutMs = Math.max(
|
}
|
||||||
cappedResponseTimeoutMs,
|
|
||||||
Math.min(defaultTimeoutMs, ICLOUD_MAIL_POLL_TOTAL_TIMEOUT_CAP_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 (!isIcloudMail(mail)) {
|
||||||
|
return {
|
||||||
|
payload,
|
||||||
|
responseTimeoutMs: defaultResponseTimeoutMs,
|
||||||
|
timeoutMs: defaultTimeoutMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const derivedResponseTimeoutMs = Math.max(
|
||||||
|
defaultResponseTimeoutMs,
|
||||||
|
getMailPollingResponseTimeoutMs(payload)
|
||||||
);
|
);
|
||||||
|
const derivedTimeoutMs = Math.max(defaultTimeoutMs, derivedResponseTimeoutMs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseTimeoutMs: cappedResponseTimeoutMs,
|
payload,
|
||||||
timeoutMs: cappedTimeoutMs,
|
responseTimeoutMs: derivedResponseTimeoutMs,
|
||||||
capped: cappedResponseTimeoutMs < defaultResponseTimeoutMs || cappedTimeoutMs < defaultTimeoutMs,
|
timeoutMs: derivedTimeoutMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,20 +731,14 @@
|
|||||||
pollOverrides,
|
pollOverrides,
|
||||||
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
|
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
|
||||||
);
|
);
|
||||||
const timeoutWindow = capIcloudMailPollingTimeouts(mail, timedPoll);
|
const timeoutWindow = resolveMailPollingTimeouts(mail, timedPoll);
|
||||||
if (timeoutWindow.capped) {
|
|
||||||
await addLog(
|
|
||||||
`步骤 ${step}:iCloud 邮箱轮询已启用快速超时保护(${Math.ceil(timeoutWindow.timeoutMs / 1000)} 秒),避免页面无响应导致长时间卡住。`,
|
|
||||||
'info'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await sendToMailContentScriptResilient(
|
const result = await sendToMailContentScriptResilient(
|
||||||
mail,
|
mail,
|
||||||
{
|
{
|
||||||
type: 'POLL_EMAIL',
|
type: 'POLL_EMAIL',
|
||||||
step,
|
step,
|
||||||
source: 'background',
|
source: 'background',
|
||||||
payload: timedPoll.payload,
|
payload: timeoutWindow.payload,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeoutMs: timeoutWindow.timeoutMs,
|
timeoutMs: timeoutWindow.timeoutMs,
|
||||||
@@ -982,20 +1000,14 @@
|
|||||||
pollOverrides,
|
pollOverrides,
|
||||||
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
|
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
|
||||||
);
|
);
|
||||||
const timeoutWindow = capIcloudMailPollingTimeouts(mail, timedPoll);
|
const timeoutWindow = resolveMailPollingTimeouts(mail, timedPoll);
|
||||||
if (timeoutWindow.capped) {
|
|
||||||
await addLog(
|
|
||||||
`步骤 ${step}:iCloud 邮箱轮询已启用快速超时保护(${Math.ceil(timeoutWindow.timeoutMs / 1000)} 秒),避免页面无响应导致长时间卡住。`,
|
|
||||||
'info'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await sendToMailContentScriptResilient(
|
const result = await sendToMailContentScriptResilient(
|
||||||
mail,
|
mail,
|
||||||
{
|
{
|
||||||
type: 'POLL_EMAIL',
|
type: 'POLL_EMAIL',
|
||||||
step,
|
step,
|
||||||
source: 'background',
|
source: 'background',
|
||||||
payload: timedPoll.payload,
|
payload: timeoutWindow.payload,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeoutMs: timeoutWindow.timeoutMs,
|
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 () => {
|
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 source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
|
|||||||
@@ -6,6 +6,43 @@ const source = fs.readFileSync('background/verification-flow.js', 'utf8');
|
|||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(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', () => {
|
test('verification flow keeps 2925 polling cadence in the default payload', () => {
|
||||||
const helpers = api.createVerificationFlowHelpers({
|
const helpers = api.createVerificationFlowHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
@@ -43,6 +80,68 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
|
|||||||
assert.equal(step8Payload.intervalMs, 15000);
|
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', () => {
|
test('verification flow only enables 2925 target email matching in receive mode', () => {
|
||||||
const helpers = api.createVerificationFlowHelpers({
|
const helpers = api.createVerificationFlowHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
@@ -1599,8 +1698,8 @@ test('verification flow stops iCloud poll-only loop after repeated no-code round
|
|||||||
assert.equal(resendRequests, 0);
|
assert.equal(resendRequests, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('verification flow caps iCloud polling response timeout to avoid long silent stalls', async () => {
|
test('verification flow derives iCloud polling response timeout from the configured polling window', async () => {
|
||||||
const pollTimeouts = [];
|
const pollRequests = [];
|
||||||
|
|
||||||
const helpers = api.createVerificationFlowHelpers({
|
const helpers = api.createVerificationFlowHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
@@ -1630,8 +1729,9 @@ test('verification flow caps iCloud polling response timeout to avoid long silen
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
sendToMailContentScriptResilient: async (_mail, _message, options = {}) => {
|
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
|
||||||
pollTimeouts.push({
|
pollRequests.push({
|
||||||
|
payload: message.payload,
|
||||||
timeoutMs: Number(options.timeoutMs) || 0,
|
timeoutMs: Number(options.timeoutMs) || 0,
|
||||||
responseTimeoutMs: Number(options.responseTimeoutMs) || 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(pollRequests.length > 0, true);
|
||||||
assert.equal(pollTimeouts.every(({ timeoutMs }) => timeoutMs > 0 && timeoutMs <= 22000), true);
|
|
||||||
assert.equal(
|
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
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user