fix: sync LuckMail PR with dev

This commit is contained in:
QLHazyCoder
2026-04-29 22:12:28 +08:00
52 changed files with 8013 additions and 909 deletions
@@ -114,6 +114,12 @@ let currentState = {
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 1,
maxUses: 3,
},
tabRegistry: {},
sourceLastUrls: {},
};
@@ -329,6 +335,16 @@ return {
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
assert.deepStrictEqual(
snapshot.currentState.reusablePhoneActivation,
{
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 1,
maxUses: 3,
},
'reusable phone activation should survive fresh-attempt reset'
);
console.log('auto-run fresh attempt reset tests passed');
})().catch((error) => {
+7 -5
View File
@@ -215,18 +215,20 @@ test('auto-run stops restarting on generic phone-page failure messages even with
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run restarts from step 7 when phone verification cannot receive SMS after resend', async () => {
test('auto-run does not restart step 7 when phone verification exhausted replacement attempts in add-phone flow', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 1,
failureMessage: 'Phone verification could not receive an SMS after resend. Restart step 7 with a new number. Current number: 66959916439.',
failureMessage: 'Step 9: phone verification did not succeed after 3 number replacements. Last reason: sms_timeout_after_resend.',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
});
const events = await harness.run();
const result = await harness.runAndCaptureError();
assert.equal(events.invalidations.length, 1);
assert.deepStrictEqual(events.steps, [7, 8, 9, 7, 8, 9, 10]);
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
@@ -54,6 +54,13 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeHotmailLocalBaseUrl'),
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePhoneVerificationReplacementLimit'),
extractFunction('normalizePhoneCodeWaitSeconds'),
extractFunction('normalizePhoneCodeTimeoutWindows'),
extractFunction('normalizePhoneCodePollIntervalSeconds'),
extractFunction('normalizePhoneCodePollMaxRounds'),
extractFunction('normalizeHeroSmsMaxPrice'),
extractFunction('normalizeHeroSmsCountryFallback'),
extractFunction('normalizePersistentSettingValue'),
].join('\n');
@@ -63,11 +70,28 @@ const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_U
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
const PHONE_CODE_POLL_ROUNDS_MIN = 1;
const PHONE_CODE_POLL_ROUNDS_MAX = 120;
const DEFAULT_PHONE_CODE_POLL_ROUNDS = 4;
const DEFAULT_SUB2API_PROXY_NAME = '';
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
const VERIFICATION_RESEND_COUNT_MIN = 0;
const VERIFICATION_RESEND_COUNT_MAX = 20;
const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
mailProvider: '163',
@@ -101,6 +125,18 @@ return {
assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '-1'), 1);
assert.equal(api.normalizePersistentSettingValue('phoneCodeWaitSeconds', '75'), 75);
assert.equal(api.normalizePersistentSettingValue('phoneCodeTimeoutWindows', '3'), 3);
assert.equal(api.normalizePersistentSettingValue('phoneCodePollIntervalSeconds', '6'), 6);
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
assert.deepStrictEqual(
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
);
assert.equal(
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
'http://127.0.0.1:17373'
@@ -0,0 +1,39 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /cpaOAuthState:\s*null/);
assert.match(source, /cpaManagementOrigin:\s*null/);
assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/);
assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/);
});
test('message router step-1 handler stores cpa oauth runtime keys', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
const updates = [];
const router = api.createMessageRouter({
broadcastDataUpdate: () => {},
setState: async (payload) => {
updates.push(payload);
},
});
await router.handleStepData(1, {
cpaOAuthState: 'oauth-state-1',
cpaManagementOrigin: 'http://localhost:8317',
});
assert.deepStrictEqual(updates, [
{
cpaOAuthState: 'oauth-state-1',
cpaManagementOrigin: 'http://localhost:8317',
},
]);
});
+14
View File
@@ -625,6 +625,8 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
" luckmailPreserveTagName: '保留',",
" currentLuckmailPurchase: { token: 'stale' },",
" currentLuckmailMailCursor: { messageId: 'stale' },",
' currentPhoneActivation: null,',
' reusablePhoneActivation: null,',
' email: null,',
'};',
'const CONTRIBUTION_RUNTIME_DEFAULTS = {',
@@ -670,6 +672,7 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
" accounts: [{ email: 'saved@example.com' }],",
" tabRegistry: { foo: { tabId: 1 } },",
" sourceLastUrls: { foo: 'https://example.com' },",
" reusablePhoneActivation: { activationId: 'rx-001', phoneNumber: '66951112222', provider: 'hero-sms', serviceCode: 'dr', countryId: 52, successfulUses: 1, maxUses: 3 },",
" luckmailApiKey: 'sk-session',",
" luckmailBaseUrl: 'https://demo.example.com/',",
" luckmailEmailType: 'ms_imap',",
@@ -711,6 +714,16 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
assert.deepStrictEqual(snapshot.storedPayload.reusablePhoneActivation, {
activationId: 'rx-001',
phoneNumber: '66951112222',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
});
assert.equal(snapshot.storedPayload.currentPhoneActivation, null);
});
test('handleStepData step 10 marks current LuckMail purchase as used and clears runtime state', async () => {
@@ -765,6 +778,7 @@ function broadcastDataUpdate() {}
function isLocalhostOAuthCallbackUrl() {
return true;
}
async function finalizePhoneActivationAfterSuccessfulFlow() {}
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
${bundle}
@@ -0,0 +1,105 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
const appendCalls = [];
const router = api.createMessageRouter({
addLog: async () => {},
appendAccountRunRecord: async (...args) => {
appendCalls.push(args);
},
batchUpdateLuckmailPurchases: async () => {},
buildLocalhostCleanupPrefix: () => '',
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: () => ({}),
broadcastDataUpdate: () => {},
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {},
clearLuckmailRuntimeState: async () => {},
clearStopRequest: () => {},
closeLocalhostCallbackTabs: async () => {},
closeTabsByUrlPrefix: async () => {},
deleteHotmailAccount: async () => {},
deleteHotmailAccounts: async () => {},
deleteIcloudAlias: async () => {},
deleteUsedIcloudAliases: async () => {},
disableUsedLuckmailPurchases: async () => {},
doesStepUseCompletionSignal: () => false,
ensureManualInteractionAllowed: async () => ({}),
executeStep: async () => {},
executeStepViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
finalizeStep3Completion: async () => {},
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
findHotmailAccount: async () => null,
flushCommand: async () => {},
getCurrentLuckmailPurchase: () => null,
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
getState: async () => ({ plusModeEnabled: true, stepStatuses: { 13: 'pending' } }),
getLastStepIdForState: () => 13,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
getTabId: async () => null,
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
handleCloudflareSecurityBlocked: async () => '',
importSettingsBundle: async () => {},
invalidateDownstreamAfterStepRestart: async () => {},
isCloudflareSecurityBlockedError: () => false,
isAutoRunLockedState: () => false,
isHotmailProvider: () => false,
isLocalhostOAuthCallbackUrl: () => true,
isLuckmailProvider: () => false,
isStopError: () => false,
isTabAlive: async () => false,
launchAutoRunTimerPlan: async () => {},
listIcloudAliases: async () => [],
listLuckmailPurchasesForManagement: async () => [],
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyStepComplete: () => {},
notifyStepError: () => {},
patchHotmailAccount: async () => {},
patchMail2925Account: async () => {},
registerTab: async () => {},
requestStop: async () => {},
resetState: async () => {},
resumeAutoRun: async () => {},
scheduleAutoRun: async () => {},
selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {},
setCurrentMail2925Account: async () => {},
setContributionMode: async () => {},
setEmailState: async () => {},
setEmailStateSilently: async () => {},
setIcloudAliasPreservedState: async () => {},
setIcloudAliasUsedState: async () => {},
setLuckmailPurchaseDisabledState: async () => {},
setLuckmailPurchasePreservedState: async () => {},
setLuckmailPurchaseUsedState: async () => {},
setPersistentSettings: async () => {},
setState: async () => {},
setStepStatus: async () => {},
skipAutoRunCountdown: async () => false,
skipStep: async () => {},
startAutoRunLoop: async () => {},
syncHotmailAccounts: async () => {},
testHotmailAccountMailAccess: async () => {},
upsertHotmailAccount: async () => {},
verifyHotmailAccount: async () => {},
});
await router.handleMessage({ type: 'STEP_COMPLETE', step: 13, payload: {} }, {});
assert.equal(appendCalls.length, 1);
assert.equal(appendCalls[0][0], 'success');
});
@@ -12,6 +12,7 @@ function createRouter(overrides = {}) {
stepStatuses: [],
emailStates: [],
finalizePayloads: [],
phoneFinalizations: [],
notifyCompletions: [],
notifyErrors: [],
securityBlocks: [],
@@ -49,10 +50,13 @@ function createRouter(overrides = {}) {
executeStepViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
events.phoneFinalizations.push(state);
}),
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload);
}),
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
findHotmailAccount: async () => null,
flushCommand: async () => {},
getCurrentLuckmailPurchase: () => null,
@@ -61,6 +65,7 @@ function createRouter(overrides = {}) {
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
getStepDefinitionForState: overrides.getStepDefinitionForState,
getStepIdsForState: overrides.getStepIdsForState,
getLastStepIdForState: overrides.getLastStepIdForState,
getTabId: overrides.getTabId || (async () => null),
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
@@ -262,6 +267,68 @@ test('message router finalizes step 3 before marking it completed', async () =>
assert.deepStrictEqual(response, { ok: true });
});
test('message router finalizes pending phone activation on platform verify success', async () => {
const state = {
stepStatuses: { 10: 'pending' },
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
};
const { router, events } = createRouter({
state,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
});
await router.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
});
assert.deepStrictEqual(events.phoneFinalizations, [state]);
});
test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
const state = {
stepStatuses: { 10: 'pending' },
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
};
const { router, events } = createRouter({
state,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
finalizeIcloudAliasAfterSuccessfulFlow: async () => {
throw new Error('icloud finalize failed');
},
});
await assert.rejects(
() => router.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
}),
/icloud finalize failed/
);
assert.deepStrictEqual(events.phoneFinalizations, []);
});
test('message router marks step 3 failed when post-submit finalize fails', async () => {
const { router, events } = createRouter({
finalizeStep3Completion: async () => {
@@ -71,3 +71,48 @@ test('panel bridge can request codex2api oauth url via protocol', async () => {
globalThis.fetch = originalFetch;
}
});
test('panel bridge can request cpa oauth url via management api', async () => {
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
assert.equal(url, 'http://localhost:8317/v0/management/codex-auth-url');
assert.equal(options.method, 'GET');
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
return {
ok: true,
json: async () => ({
status: 'ok',
url: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
state: 'cpa-oauth-state',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
const bridge = api.createPanelBridge({
addLog: async () => {},
getPanelMode: () => 'cpa',
normalizeCodex2ApiUrl: (value) => value,
normalizeSub2ApiUrl: (value) => value,
DEFAULT_SUB2API_GROUP_NAME: 'codex',
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
});
const result = await bridge.requestOAuthUrlFromPanel({
panelMode: 'cpa',
vpsUrl: 'http://localhost:8317/admin/oauth',
vpsPassword: 'cpa-key',
}, { logLabel: '步骤 7' });
assert.deepStrictEqual(result, {
oauthUrl: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
cpaOAuthState: 'cpa-oauth-state',
cpaManagementOrigin: 'http://localhost:8317',
});
} finally {
globalThis.fetch = originalFetch;
}
});
@@ -0,0 +1,91 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background imports paypal account store module', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /background\/paypal-account-store\.js/);
assert.match(source, /paypal-utils\.js/);
});
test('paypal account store module exposes a factory', () => {
const source = fs.readFileSync('background/paypal-account-store.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundPayPalAccountStore;`)(globalScope);
assert.equal(typeof api?.createPayPalAccountStore, 'function');
});
test('paypal account store selects account and keeps legacy paypal credentials in sync', async () => {
const source = fs.readFileSync('background/paypal-account-store.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundPayPalAccountStore;`)(globalScope);
let latestState = {
paypalAccounts: [],
currentPayPalAccountId: '',
paypalEmail: '',
paypalPassword: '',
};
const broadcasts = [];
const store = api.createPayPalAccountStore({
broadcastDataUpdate(payload) {
broadcasts.push(payload);
},
findPayPalAccount(accounts, accountId) {
return (Array.isArray(accounts) ? accounts : []).find((account) => account.id === accountId) || null;
},
getState: async () => latestState,
normalizePayPalAccount(account = {}) {
return {
id: String(account.id || 'generated'),
email: String(account.email || '').trim().toLowerCase(),
password: String(account.password || ''),
createdAt: Number(account.createdAt) || 1,
updatedAt: Number(account.updatedAt) || 1,
lastUsedAt: Number(account.lastUsedAt) || 0,
};
},
normalizePayPalAccounts(accounts) {
return Array.isArray(accounts) ? accounts.slice() : [];
},
setPersistentSettings: async (updates) => {
latestState = { ...latestState, ...updates };
},
setState: async (updates) => {
latestState = { ...latestState, ...updates };
},
upsertPayPalAccountInList(accounts, nextAccount) {
const list = Array.isArray(accounts) ? accounts.slice() : [];
const existingIndex = list.findIndex((account) => account.id === nextAccount.id);
if (existingIndex >= 0) {
list[existingIndex] = nextAccount;
return list;
}
list.push(nextAccount);
return list;
},
});
const account = await store.upsertPayPalAccount({
id: 'pp-1',
email: 'User@Example.com',
password: 'secret',
});
assert.equal(account.email, 'user@example.com');
const selected = await store.setCurrentPayPalAccount('pp-1');
assert.equal(selected.id, 'pp-1');
assert.equal(latestState.currentPayPalAccountId, 'pp-1');
assert.equal(latestState.paypalEmail, 'user@example.com');
assert.equal(latestState.paypalPassword, 'secret');
assert.deepStrictEqual(
broadcasts.at(-1),
{
currentPayPalAccountId: 'pp-1',
paypalEmail: 'user@example.com',
paypalPassword: 'secret',
}
);
});
@@ -0,0 +1,239 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
function createDeps(overrides = {}) {
const logs = [];
const completed = [];
const uiCalls = [];
const deps = {
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
},
},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async (step, payload) => {
completed.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'cpa',
getTabId: async () => 0,
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
isTabAlive: async () => false,
normalizeCodex2ApiUrl: (value) => value,
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
reuseOrCreateTab: async () => 91,
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async (source, message, options) => {
uiCalls.push({ source, message, options });
return {};
},
shouldBypassStep9ForLocalCpa: () => false,
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
...overrides,
};
return { deps, logs, completed, uiCalls };
}
test('platform verify module submits CPA callback via management API first', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const originalFetch = globalThis.fetch;
let uiCalled = false;
globalThis.fetch = async (url, options = {}) => {
assert.equal(url, 'http://localhost:8317/v0/management/oauth-callback');
assert.equal(options.method, 'POST');
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
assert.deepStrictEqual(JSON.parse(options.body), {
provider: 'codex',
redirect_url: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
});
return {
ok: true,
json: async () => ({
message: 'CPA API 回调提交成功',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps, logs, completed } = createDeps({
sendToContentScriptResilient: async () => {
uiCalled = true;
return {};
},
});
const executor = api.createStep10Executor(deps);
await executor.executeStep10({
panelMode: 'cpa',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
vpsUrl: 'http://localhost:8317/admin/oauth',
vpsPassword: 'cpa-key',
});
assert.equal(uiCalled, false);
assert.deepStrictEqual(completed, [
{
step: 10,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
verifiedStatus: 'CPA API 回调提交成功',
},
},
]);
assert.deepStrictEqual(logs, [
{ message: '步骤 10:正在通过 CPA 管理接口提交回调地址...', level: 'info' },
{ message: '步骤 10CPA API 回调提交成功', level: 'ok' },
]);
} finally {
globalThis.fetch = originalFetch;
}
});
test('platform verify module prefers cpaManagementOrigin when provided', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
assert.equal(url, 'http://localhost:9999/v0/management/oauth-callback');
assert.equal(options.method, 'POST');
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
return {
ok: true,
json: async () => ({
message: 'CPA API 回调提交成功',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps, completed } = createDeps();
const executor = api.createStep10Executor(deps);
await executor.executeStep10({
panelMode: 'cpa',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
cpaManagementOrigin: 'http://localhost:9999',
vpsUrl: 'http://localhost:8317/admin/oauth',
vpsPassword: 'cpa-key',
});
assert.equal(completed.length, 1);
} finally {
globalThis.fetch = originalFetch;
}
});
test('platform verify module fails fast when CPA API submit fails', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => ({
ok: false,
status: 500,
json: async () => ({ message: 'failed to persist oauth callback' }),
});
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps, logs, completed, uiCalls } = createDeps();
const executor = api.createStep10Executor(deps);
await assert.rejects(
() => executor.executeStep10({
panelMode: 'cpa',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
vpsUrl: 'http://localhost:8317/admin/oauth',
vpsPassword: 'cpa-key',
}),
/failed to persist oauth callback/
);
assert.equal(uiCalls.length, 0);
assert.equal(completed.length, 0);
assert.equal(logs[0].message, '步骤 10:正在通过 CPA 管理接口提交回调地址...');
assert.match(logs[1].message, /步骤 10CPA 接口提交失败:failed to persist oauth callback/);
assert.equal(logs[1].level, 'error');
} finally {
globalThis.fetch = originalFetch;
}
});
test('platform verify module requires management key for CPA API-only flow', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const originalFetch = globalThis.fetch;
let fetchCalled = false;
globalThis.fetch = async () => {
fetchCalled = true;
return {
ok: true,
json: async () => ({}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps, logs, completed, uiCalls } = createDeps();
const executor = api.createStep10Executor(deps);
await assert.rejects(
() => executor.executeStep10({
panelMode: 'cpa',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
vpsUrl: 'http://localhost:8317/admin/oauth',
vpsPassword: ' ',
}),
/尚未配置 CPA 管理密钥/
);
assert.equal(fetchCalled, false);
assert.equal(uiCalls.length, 0);
assert.equal(completed.length, 0);
assert.equal(logs.length, 0);
} finally {
globalThis.fetch = originalFetch;
}
});
test('platform verify module rejects callback when cpa oauth state mismatches', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const originalFetch = globalThis.fetch;
let fetchCalled = false;
globalThis.fetch = async () => {
fetchCalled = true;
return {
ok: true,
json: async () => ({ message: 'should not happen' }),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps } = createDeps();
const executor = api.createStep10Executor(deps);
await assert.rejects(
() => executor.executeStep10({
panelMode: 'cpa',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=callback-state',
cpaOAuthState: 'expected-state',
vpsUrl: 'http://localhost:8317/admin/oauth',
vpsPassword: 'cpa-key',
}),
/CPA 回调 state 与当前授权会话不匹配/
);
assert.equal(fetchCalled, false);
} finally {
globalThis.fetch = originalFetch;
}
});
+72
View File
@@ -310,6 +310,78 @@ test('step 8 retries in-place when polling fails but auth page still stays on ve
assert.equal(events.ensureCalls >= 3, true);
});
test('step 8 keeps resend cooldown timestamp across in-place retries to avoid repeated resend storms', async () => {
const events = {
resolveCalls: 0,
resolveLastResendAts: [],
sleepMs: [],
};
const realDateNow = Date.now;
Date.now = () => 230000;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'user@example.com' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 9000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 9000),
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret', loginVerificationRequestedAt: null }),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: (error) => /页面通信异常|did not respond/i.test(String(error?.message || error || '')),
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async (_step, _state, _mail, options) => {
events.resolveCalls += 1;
events.resolveLastResendAts.push(Number(options?.lastResendAt) || 0);
if (events.resolveCalls === 1) {
await options.onResendRequestedAt(222000);
throw new Error('步骤 8:页面通信异常 did not respond in 1s');
}
},
reuseOrCreateTab: async () => {},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async (ms) => {
events.sleepMs.push(ms);
},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
try {
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
} finally {
Date.now = realDateNow;
}
assert.equal(events.resolveCalls, 2);
assert.deepStrictEqual(events.resolveLastResendAts, [0, 222000]);
assert.equal(events.sleepMs.length >= 1, true);
assert.equal(events.sleepMs[0], 3000);
});
test('step 8 completes when polling fails but recovery probe shows oauth consent page', async () => {
const events = {
ensureCalls: 0,
+103 -1
View File
@@ -18,6 +18,107 @@ hotmail_helper = load_hotmail_helper()
class HotmailHelperLoggingTest(unittest.TestCase):
def test_select_latest_code_can_use_full_body_when_preview_is_truncated(self):
css_prefix = (
'Your temporary ChatGPT verification code '
'@font-face { font-family: "Söhne"; src: url(https://cdn.openai.com/common/fonts/soehne/soehne-buch.woff2) format("woff2"); } '
'.ExternalClass { width: 100%; } '
'#bodyTable { width: 560px; } '
'body { min-width: 100% !important; } '
) * 8
full_body = (
css_prefix
+ 'Enter this temporary verification code to continue: 272964 '
+ 'Please ignore this email if this was not you.'
)
message = {
"id": "imap-1",
"mailbox": "INBOX",
"subject": "Your temporary ChatGPT verification code",
"from": {
"emailAddress": {
"address": "otp@tm1.openai.com",
"name": "OpenAI",
}
},
"bodyPreview": full_body[:500],
"body": {
"content": full_body,
},
"receivedTimestamp": 200,
}
result = hotmail_helper.select_latest_code(
[message],
['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
[],
0,
)
self.assertEqual(result["code"], "272964")
self.assertEqual(result["message"]["id"], "imap-1")
def test_log_openai_messages_logs_full_body_when_available(self):
messages = [{
"mailbox": "INBOX",
"subject": "Your verification code",
"from": {
"emailAddress": {
"address": "account-security@openai.com",
"name": "OpenAI",
}
},
"bodyPreview": "Use 123456 to continue.",
"body": {
"content": "Hello there\nUse 123456 to continue.",
},
}]
output = io.StringIO()
with redirect_stdout(output):
hotmail_helper.log_openai_messages(messages, transport="imap")
rendered = output.getvalue()
self.assertIn(
"[HotmailHelper] openai mail received transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
rendered,
)
self.assertIn(
"[HotmailHelper] openai mail full body start transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
rendered,
)
self.assertIn("Hello there\nUse 123456 to continue.", rendered)
self.assertIn("[HotmailHelper] openai mail full body end", rendered)
def test_log_openai_messages_falls_back_to_preview_without_full_body(self):
messages = [{
"mailbox": "Junk",
"subject": "Verify your sign in",
"from": {
"emailAddress": {
"address": "noreply@tm.openai.com",
"name": "ChatGPT",
}
},
"bodyPreview": "Use 654321 to continue.",
}]
output = io.StringIO()
with redirect_stdout(output):
hotmail_helper.log_openai_messages(messages, transport="graph")
rendered = output.getvalue()
self.assertIn(
"[HotmailHelper] openai mail received transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in",
rendered,
)
self.assertIn(
"[HotmailHelper] openai mail preview transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in preview=Use 654321 to continue.",
rendered,
)
self.assertNotIn("openai mail full body start", rendered)
def test_refresh_access_token_logs_invalid_grant_and_direct_connection_refused_separately(self):
failures = [
{
@@ -38,7 +139,8 @@ class HotmailHelperLoggingTest(unittest.TestCase):
},
]
with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures):
with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures), \
mock.patch.object(hotmail_helper, "get_proxy_debug_context", return_value="direct"):
output = io.StringIO()
with redirect_stdout(output):
with self.assertRaises(RuntimeError):
+26
View File
@@ -107,6 +107,32 @@ test('PayPal approve keeps original combined email and password login path', asy
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
});
test('PayPal approve prefers the selected paypal pool account over legacy fields', async () => {
const { executor, events } = createExecutor({
pageStates: [
{ needsLogin: true, hasEmailInput: true, hasPasswordInput: true, loginPhase: 'login_combined' },
{ needsLogin: false, approveReady: true },
{ needsLogin: false, approveReady: true },
],
submitResults: [
{ submitted: true, phase: 'password_submitted', awaiting: 'redirect_or_approval' },
],
});
await executor.executePayPalApprove({
paypalEmail: '',
paypalPassword: '',
currentPayPalAccountId: 'pp-1',
paypalAccounts: [
{ id: 'pp-1', email: 'pool@example.com', password: 'pool-secret' },
],
});
assert.deepStrictEqual(events.submittedPayloads, [
{ email: 'pool@example.com', password: 'pool-secret' },
]);
});
test('PayPal approve discovers an already open unregistered PayPal tab', async () => {
const { executor, events } = createExecutor({
pageStates: [
+105
View File
@@ -145,6 +145,57 @@ return {
`)(document, window);
}
function createSubmitApi(overrides = {}) {
const bindings = {
waitForDocumentComplete: async () => {},
normalizeText: (text = '') => String(text || '').replace(/\s+/g, ' ').trim(),
findPasswordInput: () => null,
findEmailInput: () => null,
findEmailNextButton: () => null,
isEnabledControl: () => true,
findPasswordLoginButton: () => null,
fillInput: () => {},
simulateClick: () => {},
waitUntil: async (predicate) => predicate(),
findLoginNextButton: () => null,
sleep: async () => {},
...overrides,
};
return new Function(
'waitForDocumentComplete',
'normalizeText',
'findPasswordInput',
'findEmailInput',
'findEmailNextButton',
'isEnabledControl',
'findPasswordLoginButton',
'fillInput',
'simulateClick',
'waitUntil',
'findLoginNextButton',
'sleep',
`
${extractFunction('refillPayPalEmailInput')}
${extractFunction('submitPayPalLogin')}
return { refillPayPalEmailInput, submitPayPalLogin };
`
)(
bindings.waitForDocumentComplete,
bindings.normalizeText,
bindings.findPasswordInput,
bindings.findEmailInput,
bindings.findEmailNextButton,
bindings.isEnabledControl,
bindings.findPasswordLoginButton,
bindings.fillInput,
bindings.simulateClick,
bindings.waitUntil,
bindings.findLoginNextButton,
bindings.sleep
);
}
test('PayPal email page ignores hidden pre-rendered password input', () => {
const hiddenPanel = createElement({ attrs: { 'aria-hidden': 'true' } });
const emailInput = createElement({
@@ -203,3 +254,57 @@ test('PayPal combined login page still sees visible password input', () => {
assert.equal(api.findPasswordLoginButton(), loginButton);
assert.equal(api.getPayPalLoginPhase(emailInput, passwordInput), 'login_combined');
});
test('PayPal email submit refills a prefilled email before clicking next', async () => {
const emailInput = createElement({
tag: 'input',
type: 'text',
id: 'login_email',
name: 'login_email',
value: 'user@example.com',
placeholder: 'Email',
});
const nextButton = createElement({
tag: 'button',
id: 'btnNext',
text: 'Next',
});
const fillValues = [];
const clicked = [];
let focusCount = 0;
let blurCount = 0;
emailInput.focus = () => {
focusCount += 1;
};
emailInput.blur = () => {
blurCount += 1;
};
const api = createSubmitApi({
findEmailInput: () => emailInput,
findEmailNextButton: () => nextButton,
fillInput: (element, value) => {
fillValues.push(value);
element.value = value;
},
simulateClick: (element) => {
clicked.push(element);
},
});
const result = await api.submitPayPalLogin({
email: 'user@example.com',
password: 'secret',
});
assert.deepEqual(fillValues, ['', 'user@example.com']);
assert.equal(focusCount, 1);
assert.equal(blurCount, 1);
assert.deepEqual(clicked, [nextButton]);
assert.deepEqual(result, {
submitted: false,
phase: 'email_submitted',
awaiting: 'password_page',
});
});
File diff suppressed because it is too large Load Diff
@@ -222,6 +222,7 @@ const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '10' };
const inputVerificationResendCount = { value: '6' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
function getCloudflareDomainsFromState() { return { domains: ['example.com'], activeDomain: 'example.com' }; }
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
+69
View File
@@ -130,6 +130,64 @@ const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const inputHeroSmsApiKey = { value: '' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'country' };
const inputHeroSmsMaxPrice = { value: '' };
const inputPhoneReplacementLimit = { value: '3' };
const inputPhoneCodeWaitSeconds = { value: '60' };
const inputPhoneCodeTimeoutWindows = { value: '2' };
const inputPhoneCodePollIntervalSeconds = { value: '5' };
const inputPhoneCodePollMaxRounds = { value: '4' };
const selectHeroSmsCountry = { value: '52', selectedIndex: 0, options: [{ value: '52', textContent: 'Thailand' }] };
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
function normalizeHeroSmsReuseEnabledValue(value) { return value === undefined || value === null ? true : Boolean(value); }
function normalizeHeroSmsAcquirePriority(value = '') { return String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country'; }
function normalizeHeroSmsCountryId(value) { return Math.max(1, Math.floor(Number(value) || 52)); }
function normalizeHeroSmsCountryLabel(value = '') { return String(value || '').trim() || 'Thailand'; }
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function normalizePhoneCodeWaitSecondsValue(value, fallback = 60) {
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function normalizePhoneCodeTimeoutWindowsValue(value, fallback = 2) {
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function normalizePhoneCodePollIntervalSecondsValue(value, fallback = 5) {
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function normalizePhoneCodePollMaxRoundsValue(value, fallback = 12) {
const parsed = Number.parseInt(String(value ?? '').trim(), 10);
return Number.isFinite(parsed) ? parsed : fallback;
}
function getSelectedHeroSmsCountryOption() { return { id: 52, label: 'Thailand' }; }
function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }]; }
function getPayPalAccounts() { return []; }
function getCurrentPayPalAccount() { return null; }
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
function getCloudflareTempEmailDomainsFromState() { return { domains: [], activeDomain: '' }; }
@@ -321,9 +379,14 @@ const inputVerificationResendCount = { value: '' };
const inputPhoneVerificationEnabled = { checked: false };
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
const inputHeroSmsApiKey = { value: '' };
const inputHeroSmsMaxPrice = { value: '' };
const inputPhoneReplacementLimit = { value: '' };
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
const inputRunCount = { value: '' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
function syncLatestState(state) { latestState = { ...latestState, ...state }; }
function syncAutoRunState() {}
function syncPasswordField() {}
@@ -347,6 +410,12 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.min(PHONE_REPLACEMENT_LIMIT_MAX, Math.floor(numeric)));
}
function normalizeHeroSmsCountryId() { return 52; }
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
function updateHeroSmsPlatformDisplay() {}
@@ -201,6 +201,7 @@ const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
function getCloudflareDomainsFromState() {
return { domains: [], activeDomain: '' };
}
@@ -0,0 +1,39 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('sidepanel password inputs expose visibility toggles', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const passwordInputIds = Array.from(
html.matchAll(/<input\b[^>]*type="password"[^>]*id="([^"]+)"/g),
(match) => match[1]
);
const legacyToggleIds = new Map([
['input-vps-url', 'btn-toggle-vps-url'],
['input-vps-password', 'btn-toggle-vps-password'],
['input-ip-proxy-username', 'btn-toggle-ip-proxy-username'],
['input-ip-proxy-password', 'btn-toggle-ip-proxy-password'],
['input-ip-proxy-api-url', 'btn-toggle-ip-proxy-api-url'],
['input-password', 'btn-toggle-password'],
]);
assert.ok(passwordInputIds.length > 0);
for (const inputId of passwordInputIds) {
const hasDataToggle = html.includes(`data-password-toggle="${inputId}"`);
const legacyToggleId = legacyToggleIds.get(inputId);
const hasLegacyToggle = legacyToggleId ? html.includes(`id="${legacyToggleId}"`) : false;
assert.equal(
hasDataToggle || hasLegacyToggle,
true,
`${inputId} should have a visibility toggle button`
);
}
});
test('shared form dialog adds visibility toggles for password fields', () => {
const source = fs.readFileSync('sidepanel/form-dialog.js', 'utf8');
assert.match(source, /field\.type === 'password'[\s\S]*data-input-with-icon/);
assert.match(source, /syncPasswordToggleButton\(toggleButton,\s*input,\s*labels\)/);
assert.match(source, /input\.type = input\.type === 'password' \? 'text' : 'password'/);
});
+133
View File
@@ -0,0 +1,133 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('sidepanel loads reusable form dialog and paypal manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const formDialogIndex = html.indexOf('<script src="form-dialog.js"></script>');
const managerIndex = html.indexOf('<script src="paypal-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(formDialogIndex, -1);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(formDialogIndex < managerIndex);
assert.ok(managerIndex < sidepanelIndex);
});
test('sidepanel html contains paypal select and add button controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="row-paypal-account"/);
assert.match(html, /id="select-paypal-account"/);
assert.match(html, /id="btn-add-paypal-account"/);
assert.match(html, /id="shared-form-modal"/);
});
test('paypal manager saves a paypal account and selects it immediately', async () => {
const source = fs.readFileSync('sidepanel/paypal-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelPayPalManager;`)(windowObject);
let latestState = {
paypalAccounts: [],
currentPayPalAccountId: null,
paypalEmail: '',
paypalPassword: '',
};
const events = [];
const clickHandlers = {};
const changeHandlers = {};
const selectNode = {
innerHTML: '',
value: '',
disabled: false,
addEventListener(type, handler) {
changeHandlers[type] = handler;
},
};
const addButton = {
disabled: false,
addEventListener(type, handler) {
clickHandlers[type] = handler;
},
};
const manager = api.createPayPalManager({
state: {
getLatestState: () => latestState,
syncLatestState(updates) {
latestState = { ...latestState, ...updates };
},
},
dom: {
btnAddPayPalAccount: addButton,
selectPayPalAccount: selectNode,
},
helpers: {
escapeHtml: (value) => String(value || ''),
getPayPalAccounts: (state) => Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [],
openFormDialog: async () => ({ email: 'user@example.com', password: 'secret' }),
showToast(message, tone) {
events.push({ type: 'toast', message, tone });
},
},
runtime: {
sendMessage: async (message) => {
events.push({ type: 'message', message });
if (message.type === 'UPSERT_PAYPAL_ACCOUNT') {
return {
ok: true,
account: {
id: 'pp-1',
email: 'user@example.com',
password: 'secret',
},
};
}
if (message.type === 'SELECT_PAYPAL_ACCOUNT') {
return {
ok: true,
account: {
id: 'pp-1',
email: 'user@example.com',
password: 'secret',
},
};
}
throw new Error(`unexpected message ${message.type}`);
},
},
paypalUtils: {
upsertPayPalAccountInList(accounts, nextAccount) {
const list = Array.isArray(accounts) ? accounts.slice() : [];
const existingIndex = list.findIndex((account) => account.id === nextAccount.id);
if (existingIndex >= 0) {
list[existingIndex] = nextAccount;
return list;
}
list.push(nextAccount);
return list;
},
},
});
manager.bindPayPalEvents();
manager.renderPayPalAccounts();
assert.match(selectNode.innerHTML, /请先添加 PayPal 账号/);
clickHandlers.click();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepStrictEqual(
events.filter((event) => event.type === 'message').map((event) => event.message.type),
['UPSERT_PAYPAL_ACCOUNT', 'SELECT_PAYPAL_ACCOUNT']
);
assert.equal(latestState.currentPayPalAccountId, 'pp-1');
assert.equal(latestState.paypalEmail, 'user@example.com');
assert.equal(latestState.paypalPassword, 'secret');
assert.equal(selectNode.value, 'pp-1');
assert.equal(selectNode.disabled, false);
assert.match(events.at(-1)?.message || '', /已保存 PayPal 账号/);
});
@@ -56,41 +56,125 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="row-phone-verification-enabled"/);
assert.match(html, /id="btn-toggle-phone-verification-section"/);
assert.match(html, /id="row-phone-verification-fold"/);
assert.match(html, /id="input-phone-verification-enabled"/);
assert.match(html, /id="row-hero-sms-platform"/);
assert.match(html, /id="row-hero-sms-country"/);
assert.match(html, /id="row-hero-sms-country-fallback"/);
assert.match(html, /id="row-hero-sms-acquire-priority"/);
assert.match(html, /id="select-hero-sms-acquire-priority"/);
assert.match(html, /id="select-hero-sms-country"[^>]*multiple/);
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
assert.match(html, /id="row-hero-sms-api-key"/);
assert.match(html, /id="row-hero-sms-max-price"/);
assert.match(html, /id="row-hero-sms-current-number"/);
assert.match(html, /id="row-hero-sms-price-tiers"/);
assert.match(html, /id="row-hero-sms-current-code"/);
assert.match(html, /id="row-phone-replacement-limit"/);
assert.match(html, /id="row-phone-verification-resend-count"/);
assert.match(html, /id="row-phone-code-wait-seconds"/);
assert.match(html, /id="row-phone-code-timeout-windows"/);
assert.match(html, /id="row-phone-code-poll-interval-seconds"/);
assert.match(html, /id="row-phone-code-poll-max-rounds"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
const api = new Function(`
const phoneVerificationSectionExpanded = true;
const inputPhoneVerificationEnabled = { checked: false };
const rowPhoneVerificationEnabled = { style: { display: 'none' } };
const rowPhoneVerificationFold = { style: { display: 'none' } };
const btnTogglePhoneVerificationSection = {
disabled: false,
textContent: '',
title: '',
setAttribute: () => {},
};
const rowHeroSmsPlatform = { style: { display: 'none' } };
const rowHeroSmsCountry = { style: { display: 'none' } };
const rowHeroSmsCountryFallback = { style: { display: 'none' } };
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
const rowHeroSmsCurrentCode = { style: { display: 'none' } };
const rowPhoneVerificationResendCount = { style: { display: 'none' } };
const rowPhoneReplacementLimit = { style: { display: 'none' } };
const rowPhoneCodeWaitSeconds = { style: { display: 'none' } };
const rowPhoneCodeTimeoutWindows = { style: { display: 'none' } };
const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
${extractFunction('updatePhoneVerificationSettingsUI')}
return {
rowPhoneVerificationEnabled,
rowPhoneVerificationFold,
btnTogglePhoneVerificationSection,
inputPhoneVerificationEnabled,
rowHeroSmsPlatform,
rowHeroSmsCountry,
rowHeroSmsCountryFallback,
rowHeroSmsAcquirePriority,
rowHeroSmsApiKey,
rowHeroSmsMaxPrice,
rowHeroSmsCurrentNumber,
rowHeroSmsPriceTiers,
rowHeroSmsCurrentCode,
rowPhoneVerificationResendCount,
rowPhoneReplacementLimit,
rowPhoneCodeWaitSeconds,
rowPhoneCodeTimeoutWindows,
rowPhoneCodePollIntervalSeconds,
rowPhoneCodePollMaxRounds,
updatePhoneVerificationSettingsUI,
};
`)();
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationEnabled.style.display, '');
assert.equal(api.rowPhoneVerificationFold.style.display, 'none');
assert.equal(api.btnTogglePhoneVerificationSection.disabled, true);
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置');
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none');
assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none');
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentNumber.style.display, 'none');
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentCode.style.display, 'none');
assert.equal(api.rowPhoneVerificationResendCount.style.display, 'none');
assert.equal(api.rowPhoneReplacementLimit.style.display, 'none');
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, 'none');
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, 'none');
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, 'none');
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowPhoneVerificationFold.style.display, '');
assert.equal(api.btnTogglePhoneVerificationSection.disabled, false);
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '收起设置');
assert.equal(api.rowHeroSmsPlatform.style.display, '');
assert.equal(api.rowHeroSmsCountry.style.display, '');
assert.equal(api.rowHeroSmsCountryFallback.style.display, '');
assert.equal(api.rowHeroSmsAcquirePriority.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentCode.style.display, '');
assert.equal(api.rowPhoneVerificationResendCount.style.display, '');
assert.equal(api.rowPhoneReplacementLimit.style.display, '');
assert.equal(api.rowPhoneCodeWaitSeconds.style.display, '');
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, '');
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, '');
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, '');
});
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
@@ -141,8 +225,35 @@ const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' };
const inputHeroSmsMaxPrice = { value: '0.12' };
const inputPhoneReplacementLimit = { value: '5' };
const inputPhoneCodeWaitSeconds = { value: '75' };
const inputPhoneCodeTimeoutWindows = { value: '3' };
const inputPhoneCodePollIntervalSeconds = { value: '6' };
const inputPhoneCodePollMaxRounds = { value: '18' };
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const selectHeroSmsCountry = {
@@ -167,9 +278,20 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${extractFunction('normalizeHeroSmsMaxPriceValue')}
${extractFunction('normalizePhoneVerificationReplacementLimit')}
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
${extractFunction('normalizePhoneCodeTimeoutWindowsValue')}
${extractFunction('normalizePhoneCodePollIntervalSecondsValue')}
${extractFunction('normalizePhoneCodePollMaxRoundsValue')}
${extractFunction('normalizeHeroSmsReuseEnabledValue')}
${extractFunction('normalizeHeroSmsAcquirePriority')}
${extractFunction('normalizeHeroSmsCountryId')}
${extractFunction('normalizeHeroSmsCountryLabel')}
${extractFunction('getSelectedHeroSmsCountryOption')}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }];
}
${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload };
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
@@ -180,6 +302,15 @@ return { collectSettingsPayload };
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
assert.equal(payload.heroSmsReuseEnabled, true);
assert.equal(payload.heroSmsAcquirePriority, 'price');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.equal(payload.phoneVerificationReplacementLimit, 5);
assert.equal(payload.phoneCodeWaitSeconds, 75);
assert.equal(payload.phoneCodeTimeoutWindows, 3);
assert.equal(payload.phoneCodePollIntervalSeconds, 6);
assert.equal(payload.phoneCodePollMaxRounds, 18);
assert.equal(payload.heroSmsCountryId, 52);
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
});
+3 -2
View File
@@ -68,6 +68,7 @@ test('sidepanel html loads shared step definitions before sidepanel bootstrap',
test('sidepanel html exposes Plus mode and PayPal settings', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="input-plus-mode-enabled"/);
assert.match(html, /id="input-paypal-email"/);
assert.match(html, /id="input-paypal-password"/);
assert.match(html, /id="select-paypal-account"/);
assert.match(html, /id="btn-add-paypal-account"/);
assert.match(html, /id="shared-form-modal"/);
});
@@ -114,6 +114,7 @@ function broadcastDataUpdate() {}
async function addLog(message) {
logMessages.push(message);
}
async function finalizePhoneActivationAfterSuccessfulFlow() {}
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
function matchesSourceUrlFamily() {
return false;
+231
View File
@@ -0,0 +1,231 @@
const assert = require('assert');
const fs = require('fs');
const step9ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
const api = new Function('step9ModuleSource', `
const self = {};
let webNavListener = null;
let webNavCommittedListener = null;
let step8TabUpdatedListener = null;
let step8PendingReject = null;
let cleanupCalls = 0;
let timeoutCalls = 0;
let recoveryCalls = 0;
let completePayload = null;
const logs = [];
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
const chrome = {
webNavigation: {
onBeforeNavigate: {
addListener(listener) {
webNavListener = listener;
setTimeout(() => {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 0);
},
removeListener() {},
},
onCommitted: {
addListener(listener) {
webNavCommittedListener = listener;
},
removeListener() {},
},
},
tabs: {
onUpdated: {
addListener(listener) {
step8TabUpdatedListener = listener;
},
removeListener() {},
},
async update() {},
},
};
function cleanupStep8NavigationListeners() {
cleanupCalls += 1;
webNavListener = null;
webNavCommittedListener = null;
step8TabUpdatedListener = null;
}
async function addLog(message) {
logs.push(message);
}
function throwIfStep8SettledOrStopped() {}
async function getTabId() {
return 123;
}
async function isTabAlive() {
return true;
}
async function ensureStep8SignupPageReady() {}
async function waitForStep8Ready() {
return {
consentReady: true,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function triggerStep8ContentStrategy() {
return { success: true };
}
async function waitForStep8ClickEffect() {
return {
progressed: true,
reason: 'url_changed',
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
if (options.actionLabel === 'OAuth localhost 回调') {
timeoutCalls += 1;
if (timeoutCalls === 1) {
throw new Error('步骤 9:从拿到 OAuth 登录地址开始,5 分钟内未完成OAuth localhost 回调,结束当前链路,准备从步骤 7 重新开始。');
}
}
return defaultTimeoutMs;
}
async function recoverOAuthLocalhostTimeout(details = {}) {
recoveryCalls += 1;
return {
...(details.state || {}),
oauthUrl: 'https://auth.openai.com/recovered-oauth',
};
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
if (
Number(signupTabId) === Number(details?.tabId)
&& String(details?.url || '').includes('http://localhost:1455/auth/callback')
) {
return details.url;
}
return '';
}
function getStep8CallbackUrlFromTabUpdate() {
return '';
}
function getStep8EffectLabel() {
return 'URL 已变化';
}
async function prepareStep8DebuggerClick() {
return { rect: { centerX: 10, centerY: 10 } };
}
async function clickWithDebugger() {}
async function reloadStep8ConsentPage() {}
async function reuseOrCreateTab() { return 123; }
async function sleepWithStop() {}
function setWebNavListener(listener) { webNavListener = listener; }
function getWebNavListener() { return webNavListener; }
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject(handler) { step8PendingReject = handler; }
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 200;
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
const STEP8_MAX_ROUNDS = 2;
const STEP8_STRATEGIES = [
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
{ mode: 'debugger', label: 'debugger click' },
];
${step9ModuleSource}
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
recoverOAuthLocalhostTimeout,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
STEP8_READY_WAIT_TIMEOUT_MS,
STEP8_STRATEGIES,
throwIfStep8SettledOrStopped,
triggerStep8ContentStrategy,
waitForStep8ClickEffect,
waitForStep8Ready,
});
return {
executeStep9: executor.executeStep9,
snapshot() {
return {
cleanupCalls,
timeoutCalls,
recoveryCalls,
completePayload,
hasPendingReject: Boolean(step8PendingReject),
logs,
};
},
};
`)(step9ModuleSource);
(async () => {
await api.executeStep9({
oauthUrl: 'https://auth.openai.com/original-oauth',
visibleStep: 9,
});
const snapshot = api.snapshot();
assert.strictEqual(snapshot.timeoutCalls, 2, 'step9 should retry timeout budget check after recovery');
assert.strictEqual(snapshot.recoveryCalls, 1, 'step9 should call timeout recovery hook exactly once');
assert.strictEqual(snapshot.cleanupCalls >= 1, true, 'step9 should cleanup navigation listeners');
assert.strictEqual(snapshot.hasPendingReject, false, 'step9 should clear pending reject after completion');
assert.deepStrictEqual(snapshot.completePayload, {
step: 9,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
});
console.log('step9 timeout recovery tests passed');
})().catch((error) => {
console.error(error);
process.exit(1);
});
+71
View File
@@ -1099,6 +1099,77 @@ test('verification flow clicks resend before waiting for the next LuckMail /code
]);
});
test('verification flow notifies onResendRequestedAt when resend is triggered', async () => {
const resendRequestedAtCalls = [];
const stateUpdates = [];
let pollCalls = 0;
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
return { resent: true };
}
return {};
},
sendToMailContentScriptResilient: async (_mail, message) => {
if (message.type !== 'POLL_EMAIL') {
return {};
}
pollCalls += 1;
return pollCalls === 1
? {}
: { code: '654321', emailTimestamp: 123 };
},
setState: async (payload) => {
stateUpdates.push(payload);
},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
lastLoginCode: null,
},
{ provider: 'qq', label: 'QQ 邮箱' },
{
maxResendRequests: 1,
resendIntervalMs: 25000,
onResendRequestedAt: async (requestedAt) => {
resendRequestedAtCalls.push(Number(requestedAt) || 0);
},
}
);
assert.equal(resendRequestedAtCalls.length >= 1, true);
assert.equal(resendRequestedAtCalls[0] > 0, true);
assert.equal(
stateUpdates.some((payload) => Number(payload?.loginVerificationRequestedAt) > 0),
true
);
});
test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
const resilientCalls = [];