feat(flow): stabilize step7-9, expand HeroSMS, and update usage tutorial

This commit is contained in:
daniellee2015
2026-04-29 00:51:05 +08:00
parent 9e26b16f20
commit 4c091a3d32
21 changed files with 5265 additions and 1021 deletions
+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'
+13
View File
@@ -573,6 +573,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 = {',
@@ -618,6 +620,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',",
@@ -659,6 +662,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 () => {
File diff suppressed because it is too large Load Diff
@@ -61,9 +61,6 @@ function createRow(initialDisplay = 'none') {
test('sidepanel html places cloudflare temp email controls in a standalone section', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="cloudflare-temp-email-section"/);
assert.match(html, /id="btn-toggle-cloudflare-temp-email-section"/);
assert.match(html, /aria-controls="cloudflare-temp-email-section-body"/);
assert.match(html, /id="cloudflare-temp-email-section-body" class="section-collapse-body" hidden/);
assert.match(html, /id="btn-cloudflare-temp-email-usage-guide"/);
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
@@ -71,16 +68,6 @@ test('sidepanel html places cloudflare temp email controls in a standalone secti
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
});
test('sidepanel persists cloudflare temp email section collapse state', () => {
assert.match(source, /CLOUDFLARE_TEMP_EMAIL_SECTION_EXPANDED_STORAGE_KEY = 'multipage-cloudflare-temp-email-section-expanded'/);
assert.match(source, /let cloudflareTempEmailSectionExpanded = false/);
assert.match(source, /function updateCloudflareTempEmailSectionExpandedUI\(\)/);
assert.match(source, /cloudflareTempEmailSectionBody\.hidden = !expanded/);
assert.match(source, /btnToggleCloudflareTempEmailSection\.setAttribute\('aria-expanded', String\(expanded\)\)/);
assert.match(source, /btnToggleCloudflareTempEmailSection\?\.addEventListener\('click', \(\) => \{\s*toggleCloudflareTempEmailSectionExpanded\(\)/);
assert.match(source, /initCloudflareTempEmailSectionExpandedState\(\)/);
});
test('sidepanel modal message preserves line breaks and supports inline links', () => {
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(css, /\.modal-message\s*\{[\s\S]*white-space:\s*pre-line;/);
@@ -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(); }
-16
View File
@@ -2,8 +2,6 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
@@ -62,25 +60,11 @@ test('sidepanel loads hotmail manager before sidepanel bootstrap', () => {
test('sidepanel html contains collapsible hotmail form controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="btn-toggle-hotmail-section"/);
assert.match(html, /aria-controls="hotmail-section-body"/);
assert.match(html, /id="hotmail-section-body" class="section-collapse-body" hidden/);
assert.match(html, /id="btn-toggle-hotmail-form"/);
assert.match(html, /id="hotmail-form-shell"/);
assert.match(html, /id="btn-import-hotmail-accounts"[^>]*>批量导入</);
});
test('sidepanel keeps hotmail account pool behind a persisted section collapse', () => {
assert.match(sidepanelSource, /HOTMAIL_SECTION_EXPANDED_STORAGE_KEY = 'multipage-hotmail-section-expanded'/);
assert.match(sidepanelSource, /let hotmailSectionExpanded = false/);
assert.match(sidepanelSource, /function updateHotmailSectionExpandedUI\(\)/);
assert.match(sidepanelSource, /hotmailSectionBody\.hidden = !expanded/);
assert.match(sidepanelSource, /btnToggleHotmailSection\.setAttribute\('aria-expanded', String\(expanded\)\)/);
assert.match(sidepanelSource, /btnToggleHotmailSection\?\.addEventListener\('click', \(\) => \{\s*toggleHotmailSectionExpanded\(\)/);
assert.match(sidepanelSource, /btnToggleHotmailForm\?\.addEventListener\('click', \(\) => \{\s*setHotmailSectionExpanded\(true\)/);
assert.match(sidepanelSource, /initHotmailSectionExpandedState\(\)/);
});
test('hotmail manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {
+68 -1
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: '' }; }
@@ -322,9 +380,13 @@ 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() {}
@@ -348,7 +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 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: '' };
}
@@ -7,7 +7,6 @@ const {
} = require('../mail-provider-utils');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const ipProxyPanelSource = fs.readFileSync('sidepanel/ip-proxy-panel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
@@ -57,78 +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="ip-proxy-section"/);
assert.match(html, /id="row-ip-proxy-enabled"/);
assert.match(html, /id="input-ip-proxy-enabled"/);
assert.match(html, /id="row-hero-sms-platform"/);
assert.match(html, /id="row-hero-sms-country"/);
assert.match(html, /id="row-hero-sms-max-price"/);
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('sidepanel renders IP proxy as a standalone card after sms verification without proxy status chrome', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const phoneToggleIndex = html.indexOf('id="row-phone-verification-enabled"');
const ipProxySectionIndex = html.indexOf('id="ip-proxy-section"');
const ipProxyToggleIndex = html.indexOf('id="row-ip-proxy-enabled"');
const cloudflareSectionIndex = html.indexOf('id="cloudflare-temp-email-section"');
assert.match(html, /id="ip-proxy-section" class="data-card ip-proxy-card"/);
assert.match(html, /id="btn-toggle-ip-proxy-section"/);
assert.match(html, /aria-controls="row-ip-proxy-fold"/);
assert.match(html, />展开设置<\/button>/);
assert.ok(phoneToggleIndex >= 0);
assert.ok(ipProxySectionIndex > phoneToggleIndex);
assert.ok(ipProxyToggleIndex > phoneToggleIndex);
assert.ok(cloudflareSectionIndex > ipProxySectionIndex);
assert.doesNotMatch(html, /id="ip-proxy-enabled-status"/);
assert.doesNotMatch(html, /id="row-ip-proxy-runtime-status"/);
});
test('IP proxy standalone card supports persisted collapse control', () => {
assert.match(ipProxyPanelSource, /IP_PROXY_SECTION_EXPANDED_STORAGE_KEY = 'multipage-ip-proxy-section-expanded'/);
assert.match(ipProxyPanelSource, /let ipProxySectionExpanded = false/);
assert.match(ipProxyPanelSource, /const showSettings = enabled && ipProxySectionExpanded/);
assert.match(ipProxyPanelSource, /rowIpProxyFold\.style\.display = showSettings \? '' : 'none'/);
assert.match(ipProxyPanelSource, /btnToggleIpProxySection\.setAttribute\('aria-expanded', String\(showSettings\)\)/);
assert.match(sidepanelSource, /btnToggleIpProxySection\?\.addEventListener\('click', \(\) => \{\s*if \(typeof toggleIpProxySectionExpanded === 'function'\)/);
assert.match(sidepanelSource, /initIpProxySectionExpandedState\(\)/);
});
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 rowHeroSmsMaxPrice = { 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,
rowHeroSmsMaxPrice,
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.rowHeroSmsMaxPrice.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.rowHeroSmsMaxPrice.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', () => {
@@ -179,9 +225,35 @@ const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
const inputHeroSmsMaxPrice = { value: '0.08' };
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 = {
@@ -206,10 +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('normalizeHeroSmsMaxPriceValue')}
${extractFunction('getSelectedHeroSmsCountryOption')}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }];
}
${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload };
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
@@ -220,7 +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.heroSmsMaxPrice, '0.08');
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' }]);
});
+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);
});