feat: continue OAuth flow through HeroSMS phone verification

- 合并 dev 最新主线改动:同步当前认证页恢复、来源配置和侧栏能力的现有基线

- 本地补充修复:补齐 HeroSMS 手机验证与 sidepanel 初始化冲突,修正 Step 9 等待逻辑里的未声明变量,并同步结构/链路文档与回归测试

- 影响范围:OAuth 步骤 8/9、HeroSMS 配置、认证页内容脚本、侧栏初始化、项目文档与测试
This commit is contained in:
QLHazyCoder
2026-04-25 14:04:11 +08:00
73 changed files with 8442 additions and 462 deletions
+10
View File
@@ -81,6 +81,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
true
);
assert.equal(
isRecoverableStep9AuthFailure('提交回调失败:请更新CLI Proxy API或检查连接'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证失败:Bad Request'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
+165
View File
@@ -331,3 +331,168 @@ test('auto-run controller skips user_already_exists failures to the next round i
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller keeps retrying the same custom mail provider pool email until success', async () => {
const events = {
logs: [],
broadcasts: [],
accountRecords: [],
runCalls: 0,
};
let currentState = {
stepStatuses: {},
vpsUrl: 'https://example.com/vps',
vpsPassword: 'secret',
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: 'custom',
customMailProviderPool: ['first@example.com'],
emailGenerator: 'duck',
gmailBaseEmail: '',
mail2925BaseEmail: '',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
tabRegistry: {},
sourceLastUrls: {},
autoRunRoundSummaries: [],
};
const runtime = {
state: {
autoRunActive: false,
autoRunCurrentRun: 0,
autoRunTotalRuns: 1,
autoRunAttemptRun: 0,
autoRunSessionId: 0,
},
get() {
return { ...this.state };
},
set(updates = {}) {
this.state = { ...this.state, ...updates };
},
};
let sessionSeed = 0;
const controller = api.createAutoRunController({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
appendAccountRunRecord: async (status, _state, reason) => {
events.accountRecords.push({ status, reason });
return { status, reason };
},
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
AUTO_RUN_RETRY_DELAY_MS: 3000,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
broadcastAutoRunStatus: async (phase, payload = {}) => {
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
};
},
broadcastStopToContentScripts: async () => {},
cancelPendingCommands: () => {},
clearStopRequest: () => {},
createAutoRunSessionId: () => {
sessionSeed += 1;
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => error?.message || String(error || ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
getState: async () => ({
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
customMailProviderPool: [...(currentState.customMailProviderPool || [])],
}),
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
isRestartCurrentAttemptError: () => false,
isSignupUserAlreadyExistsFailure: (error) => /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(error?.message || String(error || '')),
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
persistAutoRunTimerPlan: async () => ({}),
resetState: async () => {
currentState = {
...currentState,
stepStatuses: {},
tabRegistry: {},
sourceLastUrls: {},
};
},
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls <= 2) {
throw new Error('步骤 3:页面异常,当前尝试失败。');
}
},
runtime,
setState: async (updates = {}) => {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
};
},
sleepWithStop: async () => {},
throwIfAutoRunSessionStopped: (sessionId) => {
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
throw new Error('流程已被用户停止。');
}
},
waitForRunningStepsToFinish: async () => currentState,
chrome: {
runtime: {
sendMessage() {
return Promise.resolve();
},
},
},
});
await controller.autoRunLoop(1, {
autoRunSkipFailures: true,
mode: 'restart',
});
assert.equal(events.runCalls, 3, 'custom mail provider pool should keep retrying the same round until success');
assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 2);
assert.ok(events.broadcasts.filter(({ phase }) => phase === 'retrying').every(({ currentRun }) => currentRun === 1));
assert.ok(events.logs.some(({ message }) => /继续使用当前邮箱/.test(message)));
assert.equal(events.logs.some(({ message }) => /达到 3 次重试上限继续下一轮/.test(message)), false);
assert.equal(events.accountRecords.length, 0);
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
+121
View File
@@ -330,3 +330,124 @@ return {
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), false);
});
test('auto-run skips steps 4/5 when step 2 has already marked registration chain as skipped', async () => {
const api = new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const chrome = {
tabs: {
update: async () => {},
},
runtime: {
sendMessage: async () => {},
},
};
let currentState = {
email: 'already@login.example',
password: 'Secret123!',
mailProvider: 'icloud',
stepStatuses: {
1: 'pending',
2: 'pending',
3: 'pending',
4: 'pending',
5: 'pending',
6: 'pending',
7: 'pending',
8: 'pending',
9: 'pending',
10: 'pending',
},
};
const events = {
steps: [],
logs: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function ensureAutoEmailReady() {
return currentState.email;
}
async function broadcastAutoRunStatus() {}
async function getState() {
return currentState;
}
async function setState(updates) {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
};
}
function isStopError(error) {
return (error?.message || String(error || '')) === '流程已被用户停止。';
}
function isStepDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === 2) {
currentState = {
...currentState,
stepStatuses: {
...currentState.stepStatuses,
3: 'skipped',
4: 'skipped',
5: 'skipped',
},
};
}
}
async function getTabId() {
return 1;
}
async function invalidateDownstreamAfterStepRestart() {}
function getLoginAuthStateLabel(state) {
return state || 'unknown';
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function getLoginAuthStateFromContent() {
return { state: 'password_page', url: 'https://auth.openai.com/log-in' };
}
${bundle}
return {
async run() {
await runAutoSequenceFromStep(1, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return { events, currentState };
},
};
`)();
const { events } = await api.run();
assert.deepStrictEqual(events.steps, [1, 2, 6, 7, 8, 9, 10]);
assert.equal(events.logs.some(({ message }) => /步骤 4 当前状态为 skipped/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /步骤 5 当前状态为 skipped/.test(message)), true);
});
@@ -50,6 +50,7 @@ function extractFunction(name) {
test('background account history settings are normalized independently from hotmail service mode', () => {
const bundle = [
extractFunction('normalizeCodex2ApiUrl'),
extractFunction('normalizeHotmailLocalBaseUrl'),
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
@@ -60,6 +61,7 @@ test('background account history settings are normalized independently from hotm
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts';
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_SUB2API_PROXY_NAME = '';
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
@@ -70,7 +72,7 @@ const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
mailProvider: '163',
};
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
@@ -118,4 +120,16 @@ return {
api.normalizePersistentSettingValue('sub2apiDefaultProxyName', ' proxy-a '),
'proxy-a'
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiUrl', 'localhost:8080/admin'),
'http://localhost:8080/admin/accounts'
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiUrl', 'https://codex-admin.example.com/'),
'https://codex-admin.example.com/admin/accounts'
);
assert.equal(
api.normalizePersistentSettingValue('codex2apiAdminKey', ' secret-key '),
'secret-key'
);
});
+95 -1
View File
@@ -71,6 +71,7 @@ test('executeStep reuses the active top-level auth chain instead of starting a d
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
@@ -106,6 +107,10 @@ function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
function isBrowserSwitchRequiredError() {
return false;
}
async function handleBrowserSwitchRequired() {}
function doesStepUseCompletionSignal() {
return false;
}
@@ -159,10 +164,99 @@ return {
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
test('executeStep stops flow when browser-switch-required error is raised', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
const events = {
logs: [],
statusCalls: [],
stopRequests: [],
appendRecords: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function setStepStatus(step, status) {
events.statusCalls.push({ step, status });
}
async function humanStepDelay() {}
async function getState() {
return {
flowStartTime: null,
stepStatuses: {},
};
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function appendManualAccountRunRecordIfNeeded(status, _state, reason) {
events.appendRecords.push({ status, reason });
}
function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
async function requestStop(options = {}) {
events.stopRequests.push(options);
}
function doesStepUseCompletionSignal() {
return false;
}
function isRetryableContentScriptTransportError() {
return false;
}
const stepRegistry = {
async executeStep() {
throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
},
};
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
return {
executeStep,
snapshot() {
return events;
},
};
`)();
await assert.rejects(
() => api.executeStep(10),
/流程已被用户停止。/
);
const events = api.snapshot();
assert.deepStrictEqual(events.stopRequests, [
{ logMessage: '请更换浏览器进行注册登录。' },
]);
assert.deepStrictEqual(events.statusCalls, [
{ step: 10, status: 'running' },
]);
assert.equal(
events.logs.some(({ message }) => /步骤 10 失败/.test(message)),
false,
'browser-switch-required error should stop the flow before generic failed logging'
);
});
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -0,0 +1,145 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('cloudflare temp email settings normalize and expose the random-subdomain toggle', () => {
const bundle = [
extractFunction('normalizeCloudflareTempEmailReceiveMailbox'),
extractFunction('getCloudflareTempEmailConfig'),
extractFunction('normalizePersistentSettingValue'),
extractFunction('buildPersistentSettingsPayload'),
].join('\n');
const api = new Function(`
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa',
autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163',
mail2925Mode: 'provide',
emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: false,
accountRunHistoryTextEnabled: false,
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; }
function normalizeEmailGenerator(value) { return String(value || '').trim().toLowerCase() || 'duck'; }
function normalizeIcloudHost(value) { const normalized = String(value || '').trim().toLowerCase(); return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; }
function normalizeAccountRunHistoryHelperBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailServiceMode(value) { return String(value || '').trim().toLowerCase() === 'remote' ? 'remote' : 'local'; }
function normalizeHotmailRemoteBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailLocalBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; }
function normalizeCloudflareTempEmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailAddress(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomains(value) {
const seen = new Set();
const domains = [];
for (const item of Array.isArray(value) ? value : []) {
const normalized = normalizeCloudflareTempEmailDomain(item);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
domains.push(normalized);
}
return domains;
}
function normalizeHotmailAccounts(value) { return Array.isArray(value) ? value : []; }
function normalizeMail2925Accounts(value) { return Array.isArray(value) ? value : []; }
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
${bundle}
return {
buildPersistentSettingsPayload,
getCloudflareTempEmailConfig,
normalizePersistentSettingValue,
};
`)();
assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailUseRandomSubdomain', 1), true);
const payload = api.buildPersistentSettingsPayload({
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
cloudflareTempEmailDomains: ['mail.example.com', 'alt.example.com'],
});
assert.equal(payload.cloudflareTempEmailUseRandomSubdomain, true);
assert.equal(payload.cloudflareTempEmailDomain, 'mail.example.com');
assert.deepEqual(payload.cloudflareTempEmailDomains, ['mail.example.com', 'alt.example.com']);
const config = api.getCloudflareTempEmailConfig({
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudflareTempEmailCustomAuth: 'custom-secret',
cloudflareTempEmailReceiveMailbox: 'Forward@Example.com',
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
cloudflareTempEmailDomains: ['mail.example.com'],
});
assert.deepEqual(config, {
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: 'custom-secret',
receiveMailbox: 'forward@example.com',
useRandomSubdomain: true,
domain: 'mail.example.com',
domains: ['mail.example.com'],
});
});
+3 -2
View File
@@ -423,6 +423,7 @@ test('contribution oauth manager starts session, opens auth url, submits callbac
assert.match(fetchCalls[0].url, /\/start$/);
assert.match(String(fetchCalls[0].options.body || ''), /"nickname":""/);
assert.match(String(fetchCalls[0].options.body || ''), /"qq":""/);
assert.match(String(fetchCalls[0].options.body || ''), /"email":"user@example\.com"/);
assert.match(fetchCalls[1].url, /\/status\?/);
const callbackState = await manager.handleCapturedCallback(
@@ -536,7 +537,7 @@ return { refreshOAuthUrlBeforeStep6 };
delete globalThis.LOG_PREFIX;
});
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API path explicitly when contributionMode=false', async () => {
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API/Codex2API path explicitly when contributionMode=false', async () => {
const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6');
const calls = [];
@@ -574,7 +575,7 @@ return { refreshOAuthUrlBeforeStep6 };
assert.equal(oauthUrl, 'https://panel.example.com/oauth');
assert.deepStrictEqual(calls, [
{ type: 'log', message: '步骤 7contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
{ type: 'log', message: '步骤 7contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
{ type: 'panel' },
{
type: 'step',
+124
View File
@@ -0,0 +1,124 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
extractFunction('normalizeEmailGenerator'),
extractFunction('normalizeCustomEmailPool'),
extractFunction('getCustomEmailPool'),
extractFunction('getCustomEmailPoolEmailForRun'),
extractFunction('getCustomMailProviderPool'),
extractFunction('getCustomMailProviderPoolEmailForRun'),
extractFunction('getEmailGeneratorLabel'),
].join('\n');
function createApi() {
return new Function(`
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
${bundle}
return {
normalizeEmailGenerator,
normalizeCustomEmailPool,
getCustomEmailPool,
getCustomEmailPoolEmailForRun,
getCustomMailProviderPool,
getCustomMailProviderPoolEmailForRun,
getEmailGeneratorLabel,
};
`)();
}
test('background recognizes custom email pool generator and label', () => {
const api = createApi();
assert.equal(api.normalizeEmailGenerator('custom-pool'), 'custom-pool');
assert.equal(api.getEmailGeneratorLabel('custom-pool'), '自定义邮箱池');
});
test('background normalizes custom email pool input and keeps order', () => {
const api = createApi();
assert.deepEqual(
api.normalizeCustomEmailPool(' Foo@Example.com \ninvalid\nbar@example.combaz@example.com '),
['foo@example.com', 'bar@example.com', 'baz@example.com']
);
});
test('background selects the matching email for the current auto-run round', () => {
const api = createApi();
const state = {
customEmailPool: ['first@example.com', 'second@example.com', 'third@example.com'],
};
assert.equal(api.getCustomEmailPoolEmailForRun(state, 1), 'first@example.com');
assert.equal(api.getCustomEmailPoolEmailForRun(state, 2), 'second@example.com');
assert.equal(api.getCustomEmailPoolEmailForRun(state, 4), '');
});
test('background selects the matching custom provider pool email for the current auto-run round', () => {
const api = createApi();
const state = {
customMailProviderPool: ['first@example.com', 'second@example.com', 'third@example.com'],
};
assert.deepEqual(api.getCustomMailProviderPool(state), [
'first@example.com',
'second@example.com',
'third@example.com',
]);
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 1), 'first@example.com');
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 3), 'third@example.com');
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 4), '');
});
+344 -7
View File
@@ -2,24 +2,25 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadGeneratedEmailHelpersApi() {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
return new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
}
test('background imports generated email helper module', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /importScripts\([\s\S]*'background\/generated-email-helpers\.js'/);
});
test('generated email helper module exposes a factory', () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const api = loadGeneratedEmailHelpersApi();
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
});
test('generated email helper falls back to normal generator when 2925 is in receive mode', async () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const api = loadGeneratedEmailHelpersApi();
const events = [];
const helpers = api.createGeneratedEmailHelpers({
@@ -76,3 +77,339 @@ test('generated email helper falls back to normal generator when 2925 is in rece
['email', 'duck@example.com'],
]);
});
test('generated email helper can read the requested address from custom email pool', async () => {
const api = loadGeneratedEmailHelpersApi();
const events = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build alias');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
CUSTOM_EMAIL_POOL_GENERATOR: 'custom-pool',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getCustomEmailPoolEmail: (state, targetRun) => state.customEmailPool?.[targetRun - 1] || '',
getState: async () => ({
customEmailPool: ['first@example.com', 'second@example.com'],
emailGenerator: 'custom-pool',
mailProvider: 'gmail',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate 2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not open duck tab');
},
setEmailState: async (email) => {
events.push(['email', email]);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
customEmailPool: ['first@example.com', 'second@example.com'],
emailGenerator: 'custom-pool',
mailProvider: 'gmail',
}, {
generator: 'custom-pool',
poolIndex: 1,
});
assert.equal(email, 'second@example.com');
assert.deepStrictEqual(events, [
['email', 'second@example.com'],
]);
});
test('generated email helper respects runtime generator overrides when deciding alias flow', async () => {
const api = loadGeneratedEmailHelpersApi();
const aliasStates = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: (state) => {
aliasStates.push({ ...state });
return 'base+tag@gmail.com';
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
CUSTOM_EMAIL_POOL_GENERATOR: 'custom-pool',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getCustomEmailPoolEmail: () => '',
getState: async () => ({
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: '',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: (stateOrProvider, mail2925Mode) => {
const provider = typeof stateOrProvider === 'string'
? stateOrProvider
: stateOrProvider?.mailProvider;
const generator = typeof stateOrProvider === 'string'
? ''
: stateOrProvider?.emailGenerator;
return String(provider || '').trim().toLowerCase() === 'gmail'
&& String(generator || '').trim().toLowerCase() !== 'custom-pool'
&& String(mail2925Mode || '').trim().toLowerCase() !== 'receive';
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
mailProvider: '163',
emailGenerator: 'duck',
}, {
generator: 'gmail-alias',
mailProvider: 'gmail',
gmailBaseEmail: 'base@gmail.com',
});
assert.equal(email, 'base+tag@gmail.com');
assert.deepEqual(savedEmails, ['base+tag@gmail.com']);
assert.equal(aliasStates.length, 1);
assert.equal(aliasStates[0].mailProvider, 'gmail');
assert.equal(aliasStates[0].emailGenerator, 'gmail-alias');
assert.equal(aliasStates[0].gmailBaseEmail, 'base@gmail.com');
});
test('generated email helper uses the regular temp email domain when random subdomain mode is disabled', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: 'user@mail.example.com' }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
useRandomSubdomain: false,
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
});
assert.equal(email, 'user@mail.example.com');
assert.deepEqual(savedEmails, ['user@mail.example.com']);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
assert.equal(requests[0].method, 'POST');
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: false,
name: requests[0].body.name,
domain: 'mail.example.com',
});
assert.match(requests[0].body.name, /^[a-z0-9]+$/);
});
test('generated email helper requests random subdomain creation while preserving the returned address', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: 'user@a1b2c3d4.example.com' }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
useRandomSubdomain: true,
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
localPart: 'user',
});
assert.equal(email, 'user@a1b2c3d4.example.com');
assert.deepEqual(savedEmails, ['user@a1b2c3d4.example.com']);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
assert.equal(requests[0].method, 'POST');
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: true,
name: 'user',
domain: 'mail.example.com',
});
});
test('generated email helper honors iCloud always-new fetch mode', async () => {
const api = loadGeneratedEmailHelpersApi();
const icloudOptions = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async (options) => {
icloudOptions.push(options);
return 'fresh@icloud.example.com';
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getState: async () => ({
emailGenerator: 'icloud',
icloudFetchMode: 'always_new',
mailProvider: 'gmail',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async () => {},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'icloud',
icloudFetchMode: 'always_new',
mailProvider: 'gmail',
}, {
generator: 'icloud',
});
assert.equal(email, 'fresh@icloud.example.com');
assert.deepEqual(icloudOptions, [{ generateNew: true }]);
});
+146
View File
@@ -291,6 +291,152 @@ return {
assert.equal(snapshot.buildCalls.length, 1);
});
test('pollLuckmailVerificationCode snapshots existing mails before accepting new LuckMail code', async () => {
const bundle = extractFunction('pollLuckmailVerificationCode');
const factory = new Function(`
let currentState = {
currentLuckmailPurchase: {
id: 7,
email_address: 'luck@example.com',
token: 'tok-luck',
},
currentLuckmailMailCursor: null,
};
const logs = [];
const cursorWrites = [];
let tokenCodeCalls = 0;
function getCurrentLuckmailPurchase(state) {
return state.currentLuckmailPurchase;
}
function createLuckmailClient() {
return {
user: {
async getTokenMails() {
if (tokenCodeCalls === 0) {
return {
mails: [
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
],
};
}
return {
mails: [
{ message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
],
};
},
async getTokenCode() {
tokenCodeCalls += 1;
return tokenCodeCalls === 1
? {
has_new_mail: true,
verification_code: '111111',
mail: { message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
}
: {
has_new_mail: true,
verification_code: '222222',
mail: { message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
};
},
async getTokenMailDetail(_token, messageId) {
return { message_id: messageId, received_at: '2026-04-14 13:32:05', verification_code: '222222' };
},
},
};
}
async function getState() {
return currentState;
}
async function setLuckmailMailCursorState(cursor) {
currentState = { ...currentState, currentLuckmailMailCursor: cursor };
cursorWrites.push(cursor);
}
function normalizeLuckmailMailCursor(cursor) {
return {
messageId: cursor?.messageId || cursor?.message_id || '',
receivedAt: cursor?.receivedAt || cursor?.received_at || '',
};
}
function normalizeLuckmailTimestamp(value) {
return Date.parse(String(value || '').replace(' ', 'T') + 'Z') || 0;
}
function buildLuckmailMailCursor(mail) {
return { messageId: mail.message_id || '', receivedAt: mail.received_at || '' };
}
function buildLuckmailBaselineCursor(mails) {
const latest = mails[0] || null;
return latest ? buildLuckmailMailCursor(latest) : null;
}
function isLuckmailMailNewerThanCursor(mail, cursor) {
if (!cursor?.messageId && !cursor?.receivedAt) return true;
if (mail.message_id === cursor.messageId) return false;
return normalizeLuckmailTimestamp(mail.received_at) > normalizeLuckmailTimestamp(cursor.receivedAt);
}
function pickLuckmailVerificationMail(mails, filters) {
const excludeCodes = new Set(filters.excludeCodes || []);
for (const mail of mails || []) {
if (!mail.verification_code || excludeCodes.has(mail.verification_code)) continue;
return { mail, code: mail.verification_code };
}
return null;
}
function normalizeLuckmailTokenCode(result) {
return result;
}
async function resolveLuckmailVerificationMail(client, token, filters, tokenCodeResult) {
if (tokenCodeResult?.mail) {
const inline = pickLuckmailVerificationMail([tokenCodeResult.mail], filters);
if (inline) return inline;
}
const mailList = await client.user.getTokenMails(token);
return pickLuckmailVerificationMail(mailList.mails, filters);
}
async function addLog(message, level) {
logs.push({ message, level });
}
function throwIfStopped() {}
function isStopError() {
return false;
}
async function sleepWithStop() {}
${bundle}
return {
pollLuckmailVerificationCode,
snapshot() {
return { currentState, cursorWrites, logs, tokenCodeCalls };
},
};
`);
const api = factory();
const result = await api.pollLuckmailVerificationCode(4, await api.snapshot().currentState, {
maxAttempts: 2,
intervalMs: 1000,
senderFilters: ['openai'],
subjectFilters: ['code'],
excludeCodes: [],
});
assert.equal(result.code, '222222');
const snapshot = api.snapshot();
assert.deepStrictEqual(snapshot.cursorWrites[0], {
messageId: 'old-mail',
receivedAt: '2026-04-14 13:31:15',
});
assert.deepStrictEqual(snapshot.cursorWrites.at(-1), {
messageId: 'new-mail',
receivedAt: '2026-04-14 13:32:05',
});
assert.equal(snapshot.logs.some((entry) => /已保存当前邮箱旧邮件快照/.test(entry.message)), true);
assert.equal(snapshot.tokenCodeCalls, 2);
});
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
const bundle = extractFunction('listLuckmailPurchasesByProject');
@@ -266,3 +266,314 @@ test('ensureMail2925MailboxSession logs in when login page is detected and accou
assert.equal(readyCalls, 1);
assert.equal(result.result.loggedIn, true);
});
test('ensureMail2925MailboxSession recovers after login-page navigation reload breaks the old content-script channel', async () => {
let currentState = {
autoRunning: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
let tabUrl = 'https://2925.com/login/';
const events = {
logs: [],
readyCalls: 0,
sendCalls: 0,
waitCompleteCalls: 0,
};
const manager = api.createMail2925SessionManager({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: tabUrl, status: tabUrl.includes('/#/mailList') ? 'complete' : 'loading' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {
events.readyCalls += 1;
},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => 9,
sendToContentScriptResilient: async () => {
events.sendCalls += 1;
if (events.sendCalls === 1) {
throw new Error('Could not establish connection. Receiving end does not exist.');
}
return { loggedIn: true, currentView: 'mailbox', mailboxEmail: 'acc1@2925.com' };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
waitForTabComplete: async () => {
events.waitCompleteCalls += 1;
tabUrl = 'https://2925.com/#/mailList';
return { id: 9, url: tabUrl, status: 'complete' };
},
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(events.sendCalls, 2);
assert.equal(events.readyCalls, 2);
assert.equal(events.waitCompleteCalls, 1);
assert.equal(result.result.loggedIn, true);
const combinedLogs = events.logs.map(({ message }) => message).join('\n');
assert.match(combinedLogs, /登录提交后页面发生跳转或重载/);
assert.match(combinedLogs, /登录跳转恢复后当前标签地址:https:\/\/2925\.com\/#\/mailList/);
assert.match(combinedLogs, /页面恢复完成,正在重新确认登录态/);
});
test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'target@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const openedUrls = [];
const sendPayloads = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: openedUrls.at(-1) || 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async (_source, url) => {
openedUrls.push(url);
return 9;
},
sendToMailContentScriptResilient: async (_mail, message) => {
sendPayloads.push(message.payload);
if (sendPayloads.length === 1) {
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
}
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'target@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
waitForTabUrlMatch: async () => ({ url: 'https://2925.com/login/' }),
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(result.result.loggedIn, true);
assert.deepStrictEqual(openedUrls, [
'https://2925.com/#/mailList',
'https://2925.com/login/',
]);
assert.equal(sendPayloads.length, 2);
assert.equal(sendPayloads[0].allowLoginWhenOnLoginPage, true);
assert.equal(sendPayloads[1].forceLogin, true);
});
test('ensureMail2925MailboxSession stops when mailbox page email mismatches and pool is off', async () => {
let currentState = {
autoRunning: true,
autoRunPhase: 'running',
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
const stopCalls = [];
let sendCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
requestStop: async (options = {}) => {
stopCalls.push(options);
},
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await assert.rejects(
() => manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(sendCalls, 1);
assert.equal(stopCalls.length, 1);
assert.match(stopCalls[0].logMessage, /与目标账号 target@2925\.com 不一致/);
});
test('ensureMail2925MailboxSession does not crash when mailbox page is reused but top email cannot be detected', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
let sendCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: '',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 1);
assert.equal(result.account, null);
assert.equal(result.result.usedExistingSession, true);
});
@@ -8,11 +8,13 @@ test('background mail2925 session uses /login/ as relogin entry url', () => {
assert.match(source, /const MAIL2925_LOGIN_URL = 'https:\/\/2925\.com\/login\/';/);
});
test('background mail2925 session keeps login message timeout above the 40-second mailbox wait', () => {
test('background mail2925 session keeps a long login response timeout and a separate page-recovery window', () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
assert.match(source, /timeoutMs:\s*50000,/);
assert.match(source, /responseTimeoutMs:\s*50000,/);
assert.match(source, /40 秒内未进入收件箱/);
assert.match(source, /const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000;/);
assert.match(source, /const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000;/);
assert.match(source, /const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000;/);
assert.match(source, /responseTimeoutMs:\s*MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS,/);
assert.match(source, /recoverMail2925LoginPageAfterTransportError/);
});
test('ensureMail2925MailboxSession waits 3 seconds before and after opening login page on force relogin', async () => {
@@ -294,3 +294,46 @@ test('handleMail2925LimitReachedError stops immediately when account pool is off
assert.equal(events.stopCalls.length, 1);
assert.equal(currentState.currentMail2925AccountId, 'current');
});
test('setCurrentMail2925Account persists currentMail2925AccountId for browser restart restore', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: null,
};
const persistedUpdates = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
setPersistentSettings: async (payload) => {
persistedUpdates.push(payload);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await manager.setCurrentMail2925Account('acc-1');
assert.equal(currentState.currentMail2925AccountId, 'acc-1');
assert.deepStrictEqual(persistedUpdates, [
{ currentMail2925AccountId: 'acc-1' },
]);
});
@@ -15,6 +15,8 @@ function createRouter(overrides = {}) {
notifyCompletions: [],
notifyErrors: [],
securityBlocks: [],
invalidations: [],
executedSteps: [],
};
const router = api.createMessageRouter({
@@ -41,7 +43,9 @@ function createRouter(overrides = {}) {
disableUsedLuckmailPurchases: async () => {},
doesStepUseCompletionSignal: () => false,
ensureManualInteractionAllowed: async () => ({}),
executeStep: async () => {},
executeStep: async (step) => {
events.executedSteps.push(step);
},
executeStepViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
@@ -55,6 +59,7 @@ function createRouter(overrides = {}) {
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
getTabId: overrides.getTabId || (async () => null),
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
handleCloudflareSecurityBlocked: overrides.handleCloudflareSecurityBlocked || (async (error) => {
@@ -63,13 +68,16 @@ function createRouter(overrides = {}) {
return message.replace(/^CF_SECURITY_BLOCKED::/, '') || message;
}),
importSettingsBundle: async () => {},
invalidateDownstreamAfterStepRestart: async () => {},
invalidateDownstreamAfterStepRestart: async (step, options) => {
events.invalidations.push({ step, options });
},
isCloudflareSecurityBlockedError: overrides.isCloudflareSecurityBlockedError || ((error) => /^CF_SECURITY_BLOCKED::/.test(typeof error === 'string' ? error : error?.message || '')),
isAutoRunLockedState: () => false,
isHotmailProvider: () => false,
isLocalhostOAuthCallbackUrl: () => true,
isLuckmailProvider: () => false,
isStopError: () => false,
isTabAlive: overrides.isTabAlive || (async () => false),
launchAutoRunTimerPlan: async () => {},
listIcloudAliases: async () => [],
listLuckmailPurchasesForManagement: async () => [],
@@ -143,6 +151,39 @@ test('message router does not overwrite a completed step 3 when step 2 is replay
assert.deepStrictEqual(events.stepStatuses, []);
});
test('message router skips steps 3/4/5 when step 2 detects already logged-in session', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 3: 'pending', 4: 'completed', 5: 'pending' } },
});
await router.handleStepData(2, {
email: 'user@example.com',
skipRegistrationFlow: true,
skippedPasswordStep: true,
});
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.stepStatuses, [
{ step: 3, status: 'skipped' },
{ step: 5, status: 'skipped' },
]);
assert.equal(events.logs[0]?.message, '步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。');
});
test('message router skips step 5 when step 4 reports already logged-in transition', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 5: 'pending' } },
});
await router.handleStepData(4, {
emailTimestamp: 123,
skipProfileStep: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 5, status: 'skipped' }]);
assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。');
});
test('message router finalizes step 3 before marking it completed', async () => {
const { router, events } = createRouter();
@@ -226,3 +267,22 @@ test('message router stops the flow and surfaces cloudflare security block error
error: '您已触发Cloudflare 安全防护系统',
});
});
test('message router blocks manual step 4 execution when signup page tab is missing', async () => {
const { router, events } = createRouter({
getTabId: async () => null,
isTabAlive: async () => false,
});
await assert.rejects(
() => router.handleMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step: 4 },
}, {}),
/手动执行步骤 4 前,请先执行步骤 1 或步骤 2/
);
assert.deepStrictEqual(events.invalidations, []);
assert.deepStrictEqual(events.executedSteps, []);
});
@@ -15,3 +15,23 @@ test('navigation utils module exposes a factory', () => {
assert.equal(typeof api?.createNavigationUtils, 'function');
});
test('navigation utils support codex2api mode and url normalization', () => {
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
const utils = api.createNavigationUtils({
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
DEFAULT_SUB2API_URL: 'https://sub.example.com/admin/accounts',
normalizeLocalCpaStep9Mode: (value) => value,
});
assert.equal(utils.normalizeCodex2ApiUrl('localhost:8080/admin'), 'http://localhost:8080/admin/accounts');
assert.equal(
utils.normalizeCodex2ApiUrl('https://codex-admin.example.com/'),
'https://codex-admin.example.com/admin/accounts'
);
assert.equal(utils.getPanelMode({ panelMode: 'codex2api' }), 'codex2api');
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
});
@@ -21,3 +21,53 @@ test('panel bridge requests oauth url with step 7 log label payload', () => {
assert.match(source, /logStep:\s*7/);
assert.doesNotMatch(source, /logStep:\s*6/);
});
test('panel bridge can request codex2api oauth url via protocol', async () => {
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options = {}) => {
assert.equal(url, 'http://localhost:8080/api/admin/oauth/generate-auth-url');
assert.equal(options.method, 'POST');
assert.equal(options.headers['X-Admin-Key'], 'admin-secret');
return {
ok: true,
json: async () => ({
auth_url: 'https://auth.openai.com/authorize?state=oauth-state',
session_id: 'session-123',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
const bridge = api.createPanelBridge({
addLog: async () => {},
chrome: {},
closeConflictingTabsForSource: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'codex2api',
normalizeCodex2ApiUrl: (value) => value ? `http://${value.replace(/^https?:\/\//, '')}`.replace(/\/admin$/, '/admin/accounts') : 'http://localhost:8080/admin/accounts',
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
waitForTabUrlFamily: async () => null,
DEFAULT_SUB2API_GROUP_NAME: 'codex',
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
});
const result = await bridge.requestOAuthUrlFromPanel({
panelMode: 'codex2api',
codex2apiUrl: 'localhost:8080/admin',
codex2apiAdminKey: 'admin-secret',
}, { logLabel: '步骤 7' });
assert.deepStrictEqual(result, {
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
codex2apiSessionId: 'session-123',
codex2apiOAuthState: 'oauth-state',
});
} finally {
globalThis.fetch = originalFetch;
}
});
@@ -0,0 +1,81 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
test('platform verify module supports codex2api protocol callback exchange', 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:8080/api/admin/oauth/exchange-code');
assert.equal(options.method, 'POST');
assert.equal(options.headers['X-Admin-Key'], 'admin-secret');
assert.deepStrictEqual(JSON.parse(options.body), {
session_id: 'session-123',
code: 'callback-code',
state: 'oauth-state',
});
return {
ok: true,
json: async () => ({
message: 'OAuth 账号 flow@example.com 添加成功',
id: 42,
email: 'flow@example.com',
plan_type: 'pro',
}),
};
};
try {
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const completed = [];
const logs = [];
const executor = api.createStep10Executor({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async (step, payload) => {
completed.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'codex2api',
getTabId: async () => 0,
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
isTabAlive: async () => false,
normalizeCodex2ApiUrl: () => 'http://localhost:8080/admin/accounts',
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
reuseOrCreateTab: async () => 0,
sendToContentScript: async () => ({}),
sendToContentScriptResilient: async () => ({}),
shouldBypassStep9ForLocalCpa: () => false,
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
});
await executor.executeStep10({
panelMode: 'codex2api',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
codex2apiUrl: 'http://localhost:8080/admin/accounts',
codex2apiAdminKey: 'admin-secret',
codex2apiSessionId: 'session-123',
codex2apiOAuthState: 'oauth-state',
});
assert.deepStrictEqual(logs, [
{ message: '步骤 10:正在向 Codex2API 提交回调并创建账号...', level: 'info' },
{ message: '步骤 10OAuth 账号 flow@example.com 添加成功', level: 'ok' },
]);
assert.deepStrictEqual(completed, [
{
step: 10,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
verifiedStatus: 'OAuth 账号 flow@example.com 添加成功',
},
},
]);
} finally {
globalThis.fetch = originalFetch;
}
});
@@ -84,6 +84,55 @@ test('step 2 keeps password flow when landing on password page', async () => {
]);
});
test('step 2 falls back to already-logged-in branch when auth entry recovery fails on chatgpt home', async () => {
const completedPayloads = [];
const logs = [];
const executor = step2Api.createStep2Executor({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
completeStepFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
ensureSignupAuthEntryPageReady: async () => {
throw new Error('当前页面没有可用的注册入口,也不在邮箱/密码页。URL: https://chatgpt.com/');
},
ensureSignupEntryPageReady: async () => ({ tabId: 13 }),
ensureSignupPostEmailPageReadyInTab: async () => ({
state: 'password_page',
url: 'https://auth.openai.com/create-account/password',
}),
getTabId: async () => 13,
isTabAlive: async () => true,
resolveSignupEmailForFlow: async () => 'user@example.com',
sendToContentScriptResilient: async () => ({ submitted: true }),
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep2({ email: 'user@example.com' });
assert.equal(completedPayloads.length, 1);
assert.deepStrictEqual(completedPayloads[0], {
step: 2,
payload: {
email: 'user@example.com',
nextSignupState: 'already_logged_in_home',
nextSignupUrl: 'https://chatgpt.com/',
skippedPasswordStep: true,
skipRegistrationFlow: true,
},
});
assert.ok(logs.some((item) => /已跳过注册链路/.test(item.message)));
});
test('signup flow helper recognizes email verification page as post-email landing page', async () => {
let ensureCalls = 0;
let passwordReadyChecks = 0;
@@ -175,8 +224,12 @@ test('signup flow helper reuses existing managed alias email when it is still co
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
let ensureCalls = 0;
const messages = [];
const logs = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
buildGeneratedAliasEmail: () => '',
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async (...args) => {
@@ -188,6 +241,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isRetryableContentScriptTransportError: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
@@ -206,6 +260,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
assert.deepStrictEqual(result, { ready: true, retried: 1 });
assert.equal(ensureCalls, 1);
assert.deepStrictEqual(logs, []);
assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, {
type: 'PREPARE_SIGNUP_VERIFICATION',
step: 3,
@@ -217,3 +272,45 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
},
});
});
test('signup flow helper rewrites retryable step 3 finalize transport timeout into a Chinese error', async () => {
const logs = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
buildGeneratedAliasEmail: () => '',
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isRetryableContentScriptTransportError: (error) => /did not respond in 45s/i.test(error?.message || String(error || '')),
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 31,
sendToContentScriptResilient: async () => {
throw new Error('Content script on signup-page did not respond in 45s. Try refreshing the tab and retry.');
},
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: ['content/utils.js', 'content/signup-page.js'],
waitForTabUrlMatch: async () => null,
});
await assert.rejects(
() => helpers.finalizeSignupPasswordSubmitInTab(31, 'Secret123!', 3),
/步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。/
);
assert.deepStrictEqual(logs, [
{
message: '步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。',
level: 'warn',
},
]);
});
@@ -119,3 +119,49 @@ test('step 4 does not request a fresh code first for Cloudflare temp mail', asyn
assert.equal(capturedOptions.requestFreshCodeFirst, false);
assert.equal(capturedOptions.resendIntervalMs, 25000);
});
test('step 4 checks iCloud session before polling iCloud mailbox', async () => {
let icloudChecks = 0;
let resolved = false;
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureIcloudMailSession: async () => {
icloudChecks += 1;
},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
source: 'icloud-mail',
url: 'https://www.icloud.com/mail/',
label: 'iCloud 邮箱',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
resolveVerificationStep: async () => {
resolved = true;
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => ({}),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
});
assert.equal(icloudChecks, 1);
assert.equal(resolved, true);
});
@@ -177,3 +177,88 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
},
]);
});
test('step 7 stops immediately when management secret is missing', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
refreshCalls: 0,
sendCalls: 0,
logs: [],
};
const executor = api.createStep7Executor({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => {
events.refreshCalls += 1;
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => {
events.sendCalls += 1;
return { step6Outcome: 'success' };
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
/管理密钥/
);
assert.equal(events.refreshCalls, 1);
assert.equal(events.sendCalls, 0);
assert.ok(events.logs.some(({ message }) => /管理密钥缺失或错误不再重试当前流程停止/.test(message)));
assert.ok(!events.logs.some(({ message }) => /准备重试/.test(message)));
});
test('step 7 stops immediately when management secret is invalid', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
refreshCalls: 0,
logs: [],
};
const executor = api.createStep7Executor({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => {
events.refreshCalls += 1;
throw new Error('Codex2API 请求失败(HTTP 401)。X-Admin-Key 无效或未授权。');
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => ({ step6Outcome: 'success' }),
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await assert.rejects(
() => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
/401|未授权|无效/
);
assert.equal(events.refreshCalls, 1);
assert.ok(events.logs.some(({ message }) => /管理密钥缺失或错误不再重试当前流程停止/.test(message)));
assert.ok(!events.logs.some(({ message }) => /准备重试/.test(message)));
});
+78 -4
View File
@@ -92,6 +92,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
let ensureOptions = null;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
@@ -108,8 +109,9 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureMail2925MailboxSession: async (options) => {
ensureCalls += 1;
ensureOptions = options;
},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
rerunStep7ForStep8Recovery: async () => {},
@@ -122,7 +124,15 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
url: 'https://2925.com',
navigateOnReuse: false,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
getState: async () => ({
email: 'user@example.com',
password: 'secret',
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-1',
mail2925Accounts: [
{ id: 'acc-1', email: 'pool-user@2925.com' },
],
}),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
@@ -148,16 +158,26 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-1',
mail2925Accounts: [
{ id: 'acc-1', email: 'pool-user@2925.com' },
],
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 0);
assert.equal(ensureCalls, 1);
assert.deepStrictEqual(ensureOptions, {
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: 'pool-user@2925.com',
actionLabel: 'Step 8: ensure 2925 mailbox session',
});
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
{ tabId: 2, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 300000);
assert.equal(capturedOptions.resendIntervalMs, 0);
@@ -276,3 +296,57 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone',
assert.equal(calls.rerunStep7, 0);
assert.ok(!calls.logs.some(({ message }) => /准备从步骤 7 重新开始/.test(message)));
});
test('step 8 checks iCloud session before polling iCloud mailbox', async () => {
let icloudChecks = 0;
let resolved = false;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureIcloudMailSession: async () => {
icloudChecks += 1;
},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: '' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
source: 'icloud-mail',
url: 'https://www.icloud.com/mail/',
label: 'iCloud 邮箱',
navigateOnReuse: true,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async () => {
resolved = true;
},
reuseOrCreateTab: async () => {},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(icloudChecks, 1);
assert.equal(resolved, true);
});
@@ -16,6 +16,41 @@ test('tab runtime module exposes a factory', () => {
assert.equal(typeof api?.createTabRuntime, 'function');
});
test('tab runtime caps per-attempt response timeout to the remaining resilient timeout budget', () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
const runtime = api.createTabRuntime({
LOG_PREFIX: '[test]',
addLog: async () => {},
chrome: {
tabs: {
get: async () => ({ id: 1, url: 'https://example.com', status: 'complete' }),
query: async () => [],
},
},
getSourceLabel: (source) => source || 'unknown',
getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }),
matchesSourceUrlFamily: () => false,
normalizeLocalCpaStep9Mode: () => 'submit',
parseUrlSafely: () => null,
registerTab: async () => {},
setState: async () => {},
shouldBypassStep9ForLocalCpa: () => false,
throwIfStopped: () => {},
});
assert.equal(
runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, undefined, 30000),
30000
);
assert.equal(
runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, 12000, 5000),
5000
);
});
test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {};
+663
View File
@@ -0,0 +1,663 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/mail-163.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('findMailItems falls back to visible aria-label mail rows when legacy selector is missing', () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('isVisibleNode'),
extractFunction('isLikelyMailItemNode'),
extractFunction('findMailItems'),
].join('\n');
const api = new Function(`
const mailRow = {
hidden: false,
textContent: 'Your temporary ChatGPT verification code 911113',
getAttribute(name) {
if (name === 'aria-label') return 'Your temporary ChatGPT verification code 911113 发件人 OpenAI';
return '';
},
getBoundingClientRect() {
return { width: 600, height: 48 };
},
matches() {
return false;
},
querySelector() {
return null;
},
};
const document = {
querySelectorAll(selector) {
if (selector === 'div[sign="letter"]') return [];
if (selector === '[role="option"][aria-label]') return [mailRow];
return [];
},
};
const window = {
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
${bundle}
return { findMailItems };
`)();
const rows = api.findMailItems();
assert.equal(rows.length, 1);
});
test('getMailTimestamp parses visible hh:mm text even when no title attribute exists', () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('parseMail163Timestamp'),
extractFunction('isLikelyMailTimestampText'),
extractFunction('collectMailTimestampCandidates'),
extractFunction('getMailTimestamp'),
].join('\n');
const timestamp = new Function(`
const item = {
getAttribute() {
return '';
},
querySelectorAll(selector) {
if (selector === '.e00, [title], [aria-label], time, [class*="time"], [class*="date"]') {
return [];
}
if (selector === 'span, div, td, strong, b') {
return [
{
textContent: '22:22',
getAttribute() {
return '';
},
},
];
}
return [];
},
};
${bundle}
return getMailTimestamp(item);
`)();
const now = new Date();
const expected = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 22, 22, 0, 0).getTime();
assert.equal(timestamp, expected);
});
test('readOpenedMailText prefers opened body content that contains the verification code', () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('collectOpenedMailTextCandidates'),
extractFunction('selectOpenedMailTextCandidate'),
extractFunction('readOpenedMailText'),
extractFunction('extractVerificationCode'),
].join('\n');
const text = new Function(`
const item = {
querySelectorAll() {
return [];
},
};
function getMailSubjectText() {
return 'Your temporary ChatGPT login code';
}
function getMailSenderText() {
return 'OpenAI';
}
const document = {
querySelectorAll(selector) {
if (selector === 'iframe') {
return [];
}
return [
{ innerText: 'Your temporary ChatGPT login code', textContent: 'Your temporary ChatGPT login code' },
{ innerText: 'Enter this temporary verification code to continue: 214203', textContent: 'Enter this temporary verification code to continue: 214203' },
];
},
body: {
innerText: 'fallback body',
textContent: 'fallback body',
},
};
${bundle}
return readOpenedMailText(item);
`)();
assert.match(text, /214203/);
});
test('openMailAndGetMessageText reads opened body text and returns to inbox', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('collectOpenedMailTextCandidates'),
extractFunction('selectOpenedMailTextCandidate'),
extractFunction('readOpenedMailText'),
extractFunction('returnToInbox'),
extractFunction('openMailAndGetMessageText'),
extractFunction('extractVerificationCode'),
].join('\n');
const api = new Function(`
let inInbox = true;
let clickCount = 0;
const mailItem = {
click() {
clickCount += 1;
inInbox = false;
},
};
const inboxLink = {
click() {
inInbox = true;
},
};
function getMailSubjectText() {
return 'Your temporary ChatGPT login code';
}
function getMailSenderText() {
return 'OpenAI';
}
const document = {
querySelector(selector) {
if (selector === '.nui-tree-item-text[title="收件箱"], [title="收件箱"]') return inboxLink;
return null;
},
querySelectorAll(selector) {
if (selector === 'iframe') {
return [];
}
return inInbox ? [] : [{ innerText: 'Enter this temporary verification code to continue: 214203', textContent: 'Enter this temporary verification code to continue: 214203' }];
},
body: {
innerText: '',
textContent: '',
},
};
function findMailItems() {
return inInbox ? [mailItem] : [];
}
async function sleep() {}
${bundle}
return {
openMailAndGetMessageText,
getClickCount: () => clickCount,
isInInbox: () => inInbox,
mailItem,
};
`)();
const text = await api.openMailAndGetMessageText(api.mailItem);
assert.match(text, /214203/);
assert.equal(api.getClickCount(), 1);
assert.equal(api.isInInbox(), true);
});
test('openMailAndGetMessageText ignores stale pre-open text that contains an old code', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('collectOpenedMailTextCandidates'),
extractFunction('selectOpenedMailTextCandidate'),
extractFunction('readOpenedMailText'),
extractFunction('returnToInbox'),
extractFunction('openMailAndGetMessageText'),
extractFunction('extractVerificationCode'),
].join('\n');
const api = new Function(`
let stage = 'before';
const oldText = 'OpenAI Your temporary ChatGPT login code. Ignore this old code 111111. '.repeat(10);
const newText = 'OpenAI Your temporary ChatGPT login code. Your new code is 222222.';
const mailItem = {
click() {
stage = 'after';
},
};
const inboxLink = {
click() {
stage = 'done';
},
};
function getMailSubjectText() {
return 'Your temporary ChatGPT login code';
}
function getMailSenderText() {
return 'OpenAI';
}
const document = {
querySelector(selector) {
if (selector === '.nui-tree-item-text[title="收件箱"], [title="收件箱"]') return inboxLink;
return null;
},
querySelectorAll(selector) {
if (selector === 'iframe') {
return [];
}
if (stage === 'before') {
return [{ innerText: oldText, textContent: oldText }];
}
if (stage === 'after') {
return [
{ innerText: oldText, textContent: oldText },
{ innerText: newText, textContent: newText },
];
}
return [];
},
body: {
innerText: '',
textContent: '',
},
};
function findMailItems() {
return stage === 'done' ? [mailItem] : [];
}
async function sleep() {}
${bundle}
return { openMailAndGetMessageText, mailItem };
`)();
const text = await api.openMailAndGetMessageText(api.mailItem);
assert.match(text, /222222/);
assert.doesNotMatch(text, /111111/);
});
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
let currentItems = [{ id: 'old-mail' }];
const seenCodes = new Set();
function findMailItems() {
return currentItems;
}
function getCurrentMailIds(items = []) {
return new Set((items.length ? items : currentItems).map((item) => item.id));
}
function getMailItemId(item) {
return item.id;
}
function getMailTimestamp() {
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
}
function getMailSenderText() {
return 'OpenAI';
}
function getMailSubjectText() {
return 'Your temporary ChatGPT verification code';
}
function getMailRowText() {
return 'Your temporary ChatGPT verification code 911113 发件人 OpenAI';
}
function extractVerificationCode() {
return '911113';
}
async function waitForElement() {
return { click() {} };
}
async function refreshInbox() {}
async function sleep() {}
function log() {}
function persistSeenCodes() {}
function scheduleEmailCleanup() {}
${bundle}
return { handlePollEmail };
`)();
await assert.rejects(
() => api.handlePollEmail(4, {
senderFilters: ['openai'],
subjectFilters: ['verification'],
maxAttempts: 1,
intervalMs: 1,
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
}),
/未在 163 邮箱中找到新的匹配邮件/
);
});
test('handlePollEmail accepts a new same-minute mail that appears after the snapshot', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const oldMail = {
id: 'old-mail',
getAttribute(name) {
if (name === 'aria-label') return 'Old verification mail 111111 发件人 OpenAI';
return '';
},
};
const newMail = {
id: 'new-mail',
getAttribute(name) {
if (name === 'aria-label') return 'Your temporary ChatGPT verification code 654321 发件人 OpenAI';
return '';
},
};
let refreshCount = 0;
let currentItems = [oldMail];
const seenCodes = new Set();
function findMailItems() {
return currentItems;
}
function getCurrentMailIds(items = []) {
return new Set((items.length ? items : currentItems).map((item) => item.id));
}
function getMailItemId(item) {
return item.id;
}
function getMailTimestamp() {
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
}
function getMailSenderText() {
return 'OpenAI';
}
function getMailSubjectText(item) {
return item.id === 'new-mail' ? 'Your temporary ChatGPT verification code' : 'Old verification mail';
}
function getMailRowText(item) {
return item.id === 'new-mail'
? 'Your temporary ChatGPT verification code 654321 发件人 OpenAI'
: 'Old verification mail 111111 发件人 OpenAI';
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function waitForElement() {
return { click() {} };
}
async function refreshInbox() {
refreshCount += 1;
if (refreshCount >= 1) {
currentItems = [oldMail, newMail];
}
}
async function sleep() {}
function log() {}
function persistSeenCodes() {}
function scheduleEmailCleanup() {}
${bundle}
return { handlePollEmail };
`)();
const result = await api.handlePollEmail(4, {
senderFilters: ['openai'],
subjectFilters: ['verification'],
maxAttempts: 2,
intervalMs: 1,
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
});
assert.equal(result.code, '654321');
assert.equal(result.mailId, 'new-mail');
});
test('handlePollEmail falls back to row text when the subject node is missing', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const matchingMail = {
id: 'mail-1',
getAttribute(name) {
if (name === 'aria-label') return 'OpenAI Your temporary ChatGPT verification code 123456';
return '';
},
};
const seenCodes = new Set();
function findMailItems() {
return [matchingMail];
}
function getCurrentMailIds() {
return new Set();
}
function getMailItemId(item) {
return item.id;
}
function getMailTimestamp() {
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
}
function getMailSenderText() {
return '';
}
function getMailSubjectText() {
return '';
}
function getMailRowText() {
return 'OpenAI Your temporary ChatGPT verification code 123456';
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function waitForElement() {
return { click() {} };
}
async function refreshInbox() {}
async function sleep() {}
function log() {}
function persistSeenCodes() {}
function scheduleEmailCleanup() {}
async function openMailAndGetMessageText() {
return '';
}
${bundle}
return { handlePollEmail };
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['openai'],
subjectFilters: ['verification'],
maxAttempts: 1,
intervalMs: 1,
filterAfterTimestamp: 0,
});
assert.equal(result.code, '123456');
assert.equal(result.mailId, 'mail-1');
});
test('handlePollEmail opens matching mail body when preview has no code', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const matchingMail = {
id: 'mail-body-1',
getAttribute(name) {
if (name === 'aria-label') return 'OpenAI Your temporary ChatGPT login code';
return '';
},
};
const seenCodes = new Set();
let openedCount = 0;
function findMailItems() {
return [matchingMail];
}
function getCurrentMailIds() {
return new Set();
}
function getMailItemId(item) {
return item.id;
}
function getMailTimestamp() {
return new Date(2026, 3, 22, 22, 49, 0, 0).getTime();
}
function getMailSenderText() {
return 'OpenAI';
}
function getMailSubjectText() {
return 'Your temporary ChatGPT login code';
}
function getMailRowText() {
return 'OpenAI Your temporary ChatGPT login code';
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function waitForElement() {
return { click() {} };
}
async function refreshInbox() {}
async function sleep() {}
function log() {}
function persistSeenCodes() {}
function scheduleEmailCleanup() {}
async function openMailAndGetMessageText() {
openedCount += 1;
return 'Enter this temporary verification code to continue: 214203';
}
${bundle}
return { handlePollEmail, getOpenedCount: () => openedCount };
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['openai'],
subjectFilters: ['verification', 'login'],
maxAttempts: 1,
intervalMs: 1,
filterAfterTimestamp: 0,
});
assert.equal(result.code, '214203');
assert.equal(result.mailId, 'mail-body-1');
assert.equal(api.getOpenedCount(), 1);
});
+63
View File
@@ -12,6 +12,69 @@ test('ensureMail2925Session waits 1 second after filling credentials before clic
assert.match(source, /fillInput\(passwordInput,\s*password\);[\s\S]*?await sleep\(200\);[\s\S]*?await sleep\(1000\);[\s\S]*?simulateClick\(loginButton\);/);
});
test('detectMail2925ViewState treats top mailbox email as mailbox view', () => {
const bundle = [
extractFunction('normalizeNodeText'),
extractFunction('isVisibleNode'),
extractFunction('isMailItemNode'),
extractFunction('resolveActionTarget'),
extractFunction('findMailItems'),
extractFunction('extractEmails'),
extractFunction('getMail2925DisplayedMailboxEmail'),
extractFunction('detectMail2925ViewState'),
].join('\n');
const api = new Function(`
const MAIL_ITEM_SELECTORS = ['.mail-item'];
const MAIL_ITEM_SELECTOR_GROUP = '.mail-item';
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [];
const MAIL2925_AGREEMENT_PATTERNS = [];
const document = {
querySelectorAll(selector) {
if (selector === '.mail-item') return [];
if (selector === 'body *') return [headerEmail, wrongHeader];
if (selector === '.right-header' || selector.includes('right-header')) return [headerEmail];
if (selector.includes('[class*="user"]')) return [];
return [];
},
body: {
innerText: 'QLHazycoder qlhazycoder@2925.com tm1.openai.com@foo.example',
textContent: 'QLHazycoder qlhazycoder@2925.com tm1.openai.com@foo.example',
},
};
const window = {
innerHeight: 900,
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
const headerEmail = {
hidden: false,
textContent: 'qlhazycoder@2925.com',
innerText: 'qlhazycoder@2925.com',
getBoundingClientRect() { return { top: 40, left: 400, width: 120, height: 20 }; },
closest() { return null; },
};
const wrongHeader = {
hidden: false,
textContent: 'tm1.openai.com@foo.example',
innerText: 'tm1.openai.com@foo.example',
getBoundingClientRect() { return { top: 48, left: 430, width: 150, height: 20 }; },
closest() { return null; },
};
function detectMail2925LimitMessage() { return ''; }
function findMail2925LoginPasswordInput() { return null; }
function findMail2925LoginEmailInput() { return null; }
function getPageTextSample() { return 'qlhazycoder@2925.com'; }
${bundle}
return { detectMail2925ViewState };
`)();
const state = api.detectMail2925ViewState();
assert.equal(state.view, 'mailbox');
assert.equal(state.mailboxEmail, 'qlhazycoder@2925.com');
});
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
@@ -48,7 +48,7 @@ function extractFunction(name) {
return sidepanelSource.slice(start, end);
}
function createApi({ refreshImpl } = {}) {
function createApi({ refreshImpl, confirmImpl, dismissImpl } = {}) {
const bundle = extractFunction('startAutoRunFromCurrentSettings');
return new Function(`
@@ -90,6 +90,14 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
}
async function maybeConfirmContributionUpdateBeforeAutoRun(snapshot) {
events.push({ type: 'confirm-check', snapshot });
${confirmImpl ? 'return (' + confirmImpl + ')(snapshot);' : 'return true;'}
}
function dismissContributionUpdateHint() {
events.push({ type: 'dismiss-hint' });
${dismissImpl ? '(' + dismissImpl + ')();' : ''}
}
${bundle}
return {
startAutoRunFromCurrentSettings,
@@ -108,9 +116,9 @@ test('startAutoRunFromCurrentSettings refreshes contribution content hint before
assert.equal(result, true);
assert.deepEqual(
api.getEvents().map((entry) => entry.type),
['refresh', 'send']
['refresh', 'confirm-check', 'send']
);
assert.equal(api.getEvents()[1].message.type, 'AUTO_RUN');
assert.equal(api.getEvents()[2].message.type, 'AUTO_RUN');
});
test('startAutoRunFromCurrentSettings continues auto run when contribution content refresh fails', async () => {
@@ -124,8 +132,26 @@ test('startAutoRunFromCurrentSettings continues auto run when contribution conte
assert.equal(result, true);
assert.deepEqual(
events.map((entry) => entry.type),
['refresh', 'warn', 'send']
['refresh', 'warn', 'confirm-check', 'send']
);
assert.match(String(events[1].args[0]), /Failed to refresh contribution content hint before auto run/);
assert.equal(events[2].message.type, 'AUTO_RUN');
assert.equal(events[3].message.type, 'AUTO_RUN');
});
test('startAutoRunFromCurrentSettings aborts when contribution update confirmation is declined', async () => {
const api = createApi({
refreshImpl: `async () => ({
promptVersion: 'questionnaire:2026-04-23T00:00:00Z',
items: [{ slug: 'questionnaire', isVisible: true }],
})`,
confirmImpl: 'async () => false',
});
const result = await api.startAutoRunFromCurrentSettings();
assert.equal(result, false);
assert.deepEqual(
api.getEvents().map((entry) => entry.type),
['refresh', 'confirm-check']
);
});
@@ -0,0 +1,269 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
function createRow(initialDisplay = 'none') {
return {
style: { display: initialDisplay },
};
}
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-cloudflare-temp-email-usage-guide"/);
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
assert.match(html, /id="input-temp-email-use-random-subdomain"/);
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
});
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;/);
assert.match(css, /\.modal-message a,\s*[\s\S]*\.modal-alert a/);
});
test('openCloudflareTempEmailUsageGuidePage opens the contribution portal home page', () => {
const bundle = extractFunction('openCloudflareTempEmailUsageGuidePage');
const api = new Function(`
const openedUrls = [];
function getContributionPortalUrl() { return 'https://apikey.qzz.io'; }
function openExternalUrl(url) { openedUrls.push(url); }
${bundle}
return {
openedUrls,
openCloudflareTempEmailUsageGuidePage,
};
`)();
api.openCloudflareTempEmailUsageGuidePage();
assert.deepEqual(api.openedUrls, ['https://apikey.qzz.io']);
});
test('openCloudflareTempEmailUsageGuidePage skips opening when the contribution portal URL is empty', () => {
const bundle = extractFunction('openCloudflareTempEmailUsageGuidePage');
const api = new Function(`
const openedUrls = [];
function getContributionPortalUrl() { return ''; }
function openExternalUrl(url) { openedUrls.push(url); }
${bundle}
return {
openedUrls,
openCloudflareTempEmailUsageGuidePage,
};
`)();
api.openCloudflareTempEmailUsageGuidePage();
assert.deepEqual(api.openedUrls, []);
});
test('openCloudflareTempEmailRepositoryPage opens the upstream repository', () => {
const bundle = extractFunction('openCloudflareTempEmailRepositoryPage');
const api = new Function(`
const calls = [];
const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email';
function openExternalUrl(url) { calls.push(url); }
${bundle}
return {
calls,
openCloudflareTempEmailRepositoryPage,
};
`)();
api.openCloudflareTempEmailRepositoryPage();
assert.deepEqual(api.calls, ['https://github.com/dreamhunter2333/cloudflare_temp_email']);
});
test('applyCloudflareTempEmailSettingsState restores the random subdomain toggle and temp domain list', () => {
const bundle = extractFunction('applyCloudflareTempEmailSettingsState');
const api = new Function(`
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const calls = {
domainOptions: [],
domainEditMode: [],
};
function renderCloudflareTempEmailDomainOptions(value) { calls.domainOptions.push(value); }
function setCloudflareTempEmailDomainEditMode(editing, options) { calls.domainEditMode.push({ editing, options }); }
${bundle}
return {
applyCloudflareTempEmailSettingsState,
calls,
inputTempEmailBaseUrl,
inputTempEmailAdminAuth,
inputTempEmailCustomAuth,
inputTempEmailReceiveMailbox,
inputTempEmailUseRandomSubdomain,
};
`)();
api.applyCloudflareTempEmailSettingsState({
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudflareTempEmailCustomAuth: 'custom-secret',
cloudflareTempEmailReceiveMailbox: 'relay@example.com',
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
});
assert.equal(api.inputTempEmailBaseUrl.value, 'https://temp.example.com');
assert.equal(api.inputTempEmailAdminAuth.value, 'admin-secret');
assert.equal(api.inputTempEmailCustomAuth.value, 'custom-secret');
assert.equal(api.inputTempEmailReceiveMailbox.value, 'relay@example.com');
assert.equal(api.inputTempEmailUseRandomSubdomain.checked, true);
assert.deepEqual(api.calls.domainOptions, ['mail.example.com']);
assert.deepEqual(api.calls.domainEditMode, [{ editing: false, options: { clearInput: true } }]);
});
test('updateMailProviderUI keeps the temp domain selector visible and updates the hint when random subdomain is enabled', () => {
const bundle = extractFunction('updateMailProviderUI');
const api = new Function(`
let latestState = {
cloudflareTempEmailDomains: ['mail.example.com'],
};
let cloudflareTempEmailDomainEditMode = false;
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const rowMail2925Mode = ${JSON.stringify(createRow('none'))};
const rowMail2925PoolSettings = ${JSON.stringify(createRow('none'))};
const rowEmailPrefix = ${JSON.stringify(createRow('none'))};
const rowInbucketHost = ${JSON.stringify(createRow('none'))};
const rowInbucketMailbox = ${JSON.stringify(createRow('none'))};
const rowEmailGenerator = ${JSON.stringify(createRow(''))};
const rowCfDomain = ${JSON.stringify(createRow('none'))};
const rowTempEmailBaseUrl = ${JSON.stringify(createRow('none'))};
const rowTempEmailAdminAuth = ${JSON.stringify(createRow('none'))};
const rowTempEmailCustomAuth = ${JSON.stringify(createRow('none'))};
const rowTempEmailReceiveMailbox = ${JSON.stringify(createRow('none'))};
const rowTempEmailRandomSubdomainToggle = ${JSON.stringify(createRow('none'))};
const rowTempEmailDomain = ${JSON.stringify(createRow('none'))};
const cloudflareTempEmailSection = ${JSON.stringify(createRow('none'))};
const hotmailSection = ${JSON.stringify(createRow('none'))};
const mail2925Section = ${JSON.stringify(createRow('none'))};
const luckmailSection = ${JSON.stringify(createRow('none'))};
const icloudSection = ${JSON.stringify(createRow('none'))};
const labelEmailPrefix = { textContent: '' };
const inputEmailPrefix = { placeholder: '', style: { display: '' }, readOnly: false };
const labelMail2925UseAccountPool = ${JSON.stringify(createRow('none'))};
const selectMail2925PoolAccount = { style: { display: 'none' }, disabled: false };
const btnFetchEmail = { hidden: false, disabled: false, textContent: '' };
const btnMailLogin = { disabled: false, textContent: '', title: '' };
const inputEmail = { readOnly: false, placeholder: '', value: '' };
const autoHintText = { textContent: '' };
const rowHotmailServiceMode = ${JSON.stringify(createRow('none'))};
const rowHotmailRemoteBaseUrl = ${JSON.stringify(createRow('none'))};
const rowHotmailLocalBaseUrl = ${JSON.stringify(createRow('none'))};
const inputMail2925UseAccountPool = { checked: false };
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email', disabled: false };
const inputTempEmailUseRandomSubdomain = { checked: false };
const calls = {
tempDomainEditMode: [],
};
function isLuckmailProvider() { return false; }
function isCustomMailProvider() { return false; }
function isIcloudMailProvider() { return false; }
function usesGeneratedAliasMailProvider() { return false; }
function getSelectedMail2925Mode() { return 'provide'; }
function getManagedAliasProviderUiCopy() { return null; }
function getCurrentRegistrationEmailUiCopy() {
return {
buttonLabel: '生成 Temp',
placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱',
label: 'Cloudflare Temp Email',
};
}
function updateMailLoginButtonState() {}
function getSelectedHotmailServiceMode() { return 'local'; }
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
function setCloudflareDomainEditMode() {}
function getCloudflareTempEmailDomainsFromState() { return { domains: ['mail.example.com'], activeDomain: 'mail.example.com' }; }
function setCloudflareTempEmailDomainEditMode(editing) { calls.tempDomainEditMode.push(editing); }
function queueIcloudAliasRefresh() {}
function hideIcloudLoginHelp() {}
function syncMail2925PoolAccountOptions() {}
function getMail2925Accounts() { return []; }
function renderHotmailAccounts() {}
function renderMail2925Accounts() {}
function renderLuckmailPurchases() {}
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
function isAutoRunLockedPhase() { return false; }
${bundle}
return {
updateMailProviderUI,
cloudflareTempEmailSection,
rowTempEmailRandomSubdomainToggle,
rowTempEmailDomain,
inputTempEmailUseRandomSubdomain,
autoHintText,
calls,
};
`)();
api.updateMailProviderUI();
assert.equal(api.cloudflareTempEmailSection.style.display, '');
assert.equal(api.rowTempEmailRandomSubdomainToggle.style.display, '');
assert.equal(api.rowTempEmailDomain.style.display, '');
api.inputTempEmailUseRandomSubdomain.checked = true;
api.updateMailProviderUI();
assert.equal(api.cloudflareTempEmailSection.style.display, '');
assert.equal(api.rowTempEmailDomain.style.display, '');
assert.match(api.autoHintText.textContent, /RANDOM_SUBDOMAIN_DOMAINS/);
});
+2 -2
View File
@@ -9,8 +9,8 @@ test('sidepanel html keeps a single contribution mode button in header', () => {
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.equal(matches.length, 1);
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开上传页"/);
assert.match(html, />贡献\/使用<\/button>/);
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开官网页"/);
assert.match(html, />贡献\/使用教程<\/button>/);
assert.match(html, /<\/header>\s*<div id="contribution-update-layer"/);
assert.match(html, /id="contribution-update-layer"/);
assert.match(html, /id="contribution-update-hint"/);
+14 -2
View File
@@ -135,6 +135,8 @@ const inputSub2ApiEmail = { value: 'user@example.com' };
const inputSub2ApiPassword = { value: 'sub-secret' };
const inputSub2ApiGroup = { value: ' codex ' };
const inputSub2ApiDefaultProxy = { value: ' proxy-a ' };
const inputCodex2ApiUrl = { value: 'http://localhost:8080/admin/accounts' };
const inputCodex2ApiAdminKey = { value: 'codex-admin-secret' };
const inputPassword = { value: 'Secret123!' };
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'duck' };
@@ -154,6 +156,7 @@ const inputTempEmailBaseUrl = { value: 'https://temp.example.com' };
const inputTempEmailAdminAuth = { value: 'admin-secret' };
const inputTempEmailCustomAuth = { value: 'custom-secret' };
const inputTempEmailReceiveMailbox = { value: 'relay@example.com' };
const inputTempEmailUseRandomSubdomain = { checked: true };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: true };
@@ -190,12 +193,16 @@ return {
assert.equal('customPassword' in contributionPayload, false);
assert.equal('accountRunHistoryTextEnabled' in contributionPayload, false);
assert.equal('accountRunHistoryHelperBaseUrl' in contributionPayload, false);
assert.equal(contributionPayload.cloudflareTempEmailUseRandomSubdomain, true);
api.setLatestState({ contributionMode: false });
const normalPayload = api.collectSettingsPayload();
assert.equal(normalPayload.customPassword, 'Secret123!');
assert.equal(normalPayload.accountRunHistoryTextEnabled, true);
assert.equal(normalPayload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(normalPayload.codex2apiUrl, 'http://localhost:8080/admin/accounts');
assert.equal(normalPayload.codex2apiAdminKey, 'codex-admin-secret');
assert.equal(normalPayload.cloudflareTempEmailUseRandomSubdomain, true);
});
test('contribution mode manager enters mode, starts main auto flow, polls contribution status, and exits cleanly', async () => {
@@ -264,6 +271,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
rowSub2ApiGroup: createElement(),
rowSub2ApiPassword: createElement(),
rowSub2ApiUrl: createElement(),
rowCodex2ApiUrl: createElement(),
rowCodex2ApiAdminKey: createElement(),
rowVpsPassword: createElement(),
rowVpsUrl: createElement(),
selectPanelMode: createElement({ value: 'sub2api' }),
@@ -393,7 +402,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
},
},
constants: {
contributionUploadUrl: 'https://apikey.qzz.io',
contributionPortalUrl: 'https://apikey.qzz.io',
contributionUploadUrl: 'https://apikey.qzz.io/upload',
pollIntervalMs: 2500,
},
});
@@ -414,6 +424,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
assert.equal(dom.contributionModeSummary.textContent.length > 0, true);
assert.equal(dom.btnContributionMode.classList.contains('is-active'), true);
assert.equal(dom.rowVpsUrl.classList.contains('is-contribution-hidden'), true);
assert.equal(dom.rowCodex2ApiUrl.classList.contains('is-contribution-hidden'), true);
assert.equal(dom.rowCodex2ApiAdminKey.classList.contains('is-contribution-hidden'), true);
assert.ok(closeConfigMenuCount >= 1);
assert.ok(closeAccountRecordsCount >= 1);
assert.ok(updatePanelModeCount >= 1);
@@ -441,7 +453,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03\uff0c\u7b49\u5f85 CPA \u786e\u8ba4');
dom.btnOpenContributionUpload.listeners.click();
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io', 'https://apikey.qzz.io']);
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io', 'https://apikey.qzz.io/upload']);
await dom.btnExitContributionMode.listeners.click();
manager.render();
@@ -0,0 +1,88 @@
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 extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => sidepanelSource.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < sidepanelSource.length; i += 1) {
const ch = sidepanelSource[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < sidepanelSource.length; end += 1) {
const ch = sidepanelSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
const helperBundle = [
extractFunction('getContributionUpdatePromptLines'),
extractFunction('getContributionUpdateHintMessage'),
].join('\n');
const api = new Function(`
${helperBundle}
return {
getContributionUpdatePromptLines,
getContributionUpdateHintMessage,
};
`)();
test('getContributionUpdateHintMessage numbers contribution updates when both content and questionnaire are visible', () => {
const message = api.getContributionUpdateHintMessage({
promptVersion: 'announcement:2026-04-23T00:00:00Z|questionnaire:2026-04-23T00:00:01Z',
items: [
{ slug: 'announcement', isVisible: true },
{ slug: 'questionnaire', isVisible: true },
],
});
assert.equal(
message,
'1. 公告 / 使用教程有更新了,可点上方“贡献/使用”查看。\n2. 有新的征求意见,请佬友共同参与选择。'
);
});
test('getContributionUpdateHintMessage returns questionnaire prompt alone when only questionnaire is updated', () => {
const message = api.getContributionUpdateHintMessage({
promptVersion: 'questionnaire:2026-04-23T00:00:01Z',
items: [
{ slug: 'questionnaire', isVisible: true },
],
});
assert.equal(message, '有新的征求意见,请佬友共同参与选择。');
});
+218
View File
@@ -0,0 +1,218 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('sidepanel html exposes custom email pool generator option and input row', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /option value="custom-pool">自定义邮箱池<\/option>/);
assert.match(html, /id="row-custom-email-pool"/);
assert.match(html, /id="input-custom-email-pool"/);
assert.match(html, /id="row-custom-mail-provider-pool"/);
assert.match(html, /id="input-custom-mail-provider-pool"/);
});
test('sidepanel locks run count to custom email pool size', () => {
const bundle = [
extractFunction('isCustomMailProvider'),
extractFunction('normalizeCustomEmailPoolEntries'),
extractFunction('getSelectedEmailGenerator'),
extractFunction('usesGeneratedAliasMailProvider'),
extractFunction('usesCustomEmailPoolGenerator'),
extractFunction('getCustomMailProviderPoolSize'),
extractFunction('usesCustomMailProviderPool'),
extractFunction('getLockedRunCountFromEmailPool'),
extractFunction('getCustomEmailPoolSize'),
extractFunction('getRunCountValue'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const selectMailProvider = { value: 'gmail' };
const selectEmailGenerator = { value: 'custom-pool' };
const inputCustomEmailPool = { value: 'first@example.com\\nsecond@example.com' };
const inputRunCount = { value: '99' };
function isLuckmailProvider() {
return false;
}
function isManagedAliasProvider() {
return false;
}
function getSelectedMail2925Mode() {
return 'provide';
}
function isManagedAliasProvider(provider) {
return String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER;
}
${bundle}
return {
getSelectedEmailGenerator,
usesGeneratedAliasMailProvider,
usesCustomEmailPoolGenerator,
getCustomEmailPoolSize,
getRunCountValue,
};
`)();
assert.equal(api.getSelectedEmailGenerator(), 'custom-pool');
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'provide', 'gmail-alias'), true);
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'provide', 'custom-pool'), false);
assert.equal(api.usesCustomEmailPoolGenerator(), true);
assert.equal(api.getCustomEmailPoolSize(), 2);
assert.equal(api.getRunCountValue(), 2);
});
test('sidepanel locks run count to custom mail provider pool size', () => {
const bundle = [
extractFunction('isCustomMailProvider'),
extractFunction('normalizeCustomEmailPoolEntries'),
extractFunction('getSelectedEmailGenerator'),
extractFunction('usesGeneratedAliasMailProvider'),
extractFunction('usesCustomEmailPoolGenerator'),
extractFunction('getCustomMailProviderPoolSize'),
extractFunction('usesCustomMailProviderPool'),
extractFunction('getLockedRunCountFromEmailPool'),
extractFunction('getCustomEmailPoolSize'),
extractFunction('getRunCountValue'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const selectMailProvider = { value: 'custom' };
const selectEmailGenerator = { value: 'duck' };
const inputCustomMailProviderPool = { value: 'first@example.com\\nsecond@example.com\\nthird@example.com' };
const inputCustomEmailPool = { value: '' };
const inputRunCount = { value: '99' };
function isLuckmailProvider() {
return false;
}
function isManagedAliasProvider() {
return false;
}
function getSelectedMail2925Mode() {
return 'provide';
}
function isManagedAliasProvider(provider) {
return String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER;
}
${bundle}
return {
usesCustomMailProviderPool,
getCustomMailProviderPoolSize,
getLockedRunCountFromEmailPool,
getRunCountValue,
};
`)();
assert.equal(api.usesCustomMailProviderPool(), true);
assert.equal(api.getCustomMailProviderPoolSize(), 3);
assert.equal(api.getLockedRunCountFromEmailPool(), 3);
assert.equal(api.getRunCountValue(), 3);
});
test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => {
const bundle = [
extractFunction('getCustomVerificationPromptCopy'),
extractFunction('openCustomVerificationConfirmDialog'),
].join('\n');
const api = new Function(`
let openActionModalPayload = null;
async function openActionModal(options) {
openActionModalPayload = options;
return options.buildResult('add_phone');
}
async function openConfirmModal() {
throw new Error('step 8 should use action modal');
}
${bundle}
return {
getCustomVerificationPromptCopy,
openCustomVerificationConfirmDialog,
getOpenActionModalPayload: () => openActionModalPayload,
};
`)();
const prompt = api.getCustomVerificationPromptCopy(8);
assert.equal(prompt.phoneActionLabel, '出现手机号验证');
const result = await api.openCustomVerificationConfirmDialog(8);
assert.deepEqual(result, {
confirmed: false,
addPhoneDetected: true,
});
const modalPayload = api.getOpenActionModalPayload();
assert.equal(modalPayload.actions.length, 3);
assert.equal(modalPayload.actions[1].id, 'add_phone');
assert.equal(modalPayload.actions[1].label, '出现手机号验证');
});
+17
View File
@@ -12,6 +12,23 @@ test('sidepanel loads icloud manager before sidepanel bootstrap', () => {
assert.ok(icloudManagerIndex < sidepanelIndex);
});
test('sidepanel source binds the icloud fetch mode control before using it', () => {
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
assert.match(source, /const selectIcloudFetchMode = document\.getElementById\('select-icloud-fetch-mode'\);/);
assert.match(source, /selectIcloudFetchMode\?\.addEventListener\('change'/);
});
test('update card highlights exporting config before upgrade', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(html, /<p class="update-card-reminder">一定请先导出配置,再执行更新<\/p>/);
assert.match(css, /\.update-card-reminder\s*\{/);
assert.match(css, /font-weight:\s*700;/);
assert.match(css, /color:\s*var\(--orange\);/);
});
test('icloud manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/icloud-manager.js', 'utf8');
const windowObject = {};
@@ -142,3 +142,89 @@ return {
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
assert.equal(api.getSaveCalls(), 1);
});
test('collectSettingsPayload persists currentMail2925AccountId for 2925 account pool restore', () => {
const bundle = [
extractFunction('collectSettingsPayload'),
].join('\n');
const api = new Function(`
let latestState = {
contributionMode: false,
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
const inputSub2ApiEmail = { value: '' };
const inputSub2ApiPassword = { value: '' };
const inputSub2ApiGroup = { value: '' };
const inputSub2ApiDefaultProxy = { value: '' };
const inputCodex2ApiUrl = { value: '' };
const inputCodex2ApiAdminKey = { value: '' };
const inputPassword = { value: '' };
const selectMailProvider = { value: '2925' };
const selectEmailGenerator = { value: 'duck' };
const checkboxAutoDeleteIcloud = { checked: false };
const selectIcloudHostPreference = { value: 'auto' };
const inputAccountRunHistoryTextEnabled = { checked: false };
const inputAccountRunHistoryHelperBaseUrl = { value: '' };
const inputMail2925UseAccountPool = { checked: true };
const inputInbucketHost = { value: '' };
const inputInbucketMailbox = { value: '' };
const inputHotmailRemoteBaseUrl = { value: '' };
const inputHotmailLocalBaseUrl = { value: '' };
const inputLuckmailApiKey = { value: '' };
const inputLuckmailBaseUrl = { value: '' };
const selectLuckmailEmailType = { value: 'ms_graph' };
const inputLuckmailDomain = { value: '' };
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
function getCloudflareDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
function getCloudflareTempEmailDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() {
return { gmailBaseEmail: '', mail2925BaseEmail: 'demo@2925.com', emailPrefix: '' };
}
function normalizeLuckmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeLuckmailEmailType(value) { return String(value || '').trim() || 'ms_graph'; }
function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${bundle}
return { collectSettingsPayload };
`)();
const payload = api.collectSettingsPayload();
assert.equal(payload.currentMail2925AccountId, 'acc-2');
assert.equal(payload.mail2925UseAccountPool, true);
});
+182
View File
@@ -0,0 +1,182 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('new user guide prompt is only eligible before the one-time dismissal is set', () => {
const bundle = [
extractFunction('isPromptDismissed'),
extractFunction('setPromptDismissed'),
extractFunction('isNewUserGuidePromptDismissed'),
extractFunction('setNewUserGuidePromptDismissed'),
extractFunction('shouldPromptNewUserGuide'),
].join('\n');
const api = new Function(`
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
const storage = new Map();
const localStorage = {
getItem(key) {
return storage.has(key) ? storage.get(key) : null;
},
setItem(key, value) {
storage.set(key, String(value));
},
removeItem(key) {
storage.delete(key);
},
};
const btnContributionMode = { disabled: false };
let latestState = { contributionMode: false };
${bundle}
return {
shouldPromptNewUserGuide,
setDismissed(value) {
setNewUserGuidePromptDismissed(value);
},
setButtonDisabled(value) {
btnContributionMode.disabled = Boolean(value);
},
setContributionMode(value) {
latestState = { contributionMode: Boolean(value) };
},
};
`)();
assert.equal(api.shouldPromptNewUserGuide(), true);
api.setDismissed(true);
assert.equal(api.shouldPromptNewUserGuide(), false);
api.setDismissed(false);
api.setButtonDisabled(true);
assert.equal(api.shouldPromptNewUserGuide(), false);
api.setButtonDisabled(false);
api.setContributionMode(true);
assert.equal(api.shouldPromptNewUserGuide(), false);
});
test('new user guide prompt persists dismissal before awaiting the user choice and opens the contribution page on confirm', async () => {
const bundle = [
extractFunction('isPromptDismissed'),
extractFunction('setPromptDismissed'),
extractFunction('isNewUserGuidePromptDismissed'),
extractFunction('setNewUserGuidePromptDismissed'),
extractFunction('shouldPromptNewUserGuide'),
extractFunction('getContributionPortalUrl'),
extractFunction('openNewUserGuidePrompt'),
extractFunction('maybeShowNewUserGuidePrompt'),
].join('\n');
const api = new Function(`
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
const storage = new Map();
const localStorage = {
getItem(key) {
return storage.has(key) ? storage.get(key) : null;
},
setItem(key, value) {
storage.set(key, String(value));
},
removeItem(key) {
storage.delete(key);
},
};
const btnContributionMode = { disabled: false };
const latestState = { contributionMode: false };
const contributionContentService = { portalUrl: 'https://apikey.qzz.io' };
const openedUrls = [];
let modalOptions = null;
let nextChoice = 'confirm';
function openExternalUrl(url) {
openedUrls.push(url);
}
function openActionModal(options) {
modalOptions = options;
return Promise.resolve(nextChoice);
}
${bundle}
return {
maybeShowNewUserGuidePrompt,
getDismissed() {
return localStorage.getItem(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY);
},
getOpenedUrls() {
return openedUrls.slice();
},
getModalOptions() {
return modalOptions;
},
setNextChoice(choice) {
nextChoice = choice;
},
};
`)();
const confirmed = await api.maybeShowNewUserGuidePrompt();
const modalOptions = api.getModalOptions();
assert.equal(confirmed, true);
assert.equal(api.getDismissed(), '1');
assert.deepStrictEqual(api.getOpenedUrls(), ['https://apikey.qzz.io']);
assert.equal(modalOptions.title, '新手引导');
assert.equal(modalOptions.alert.text, '本提示仅出现一次。');
assert.deepStrictEqual(
modalOptions.actions.map((item) => ({ id: item.id, label: item.label })),
[
{ id: null, label: '取消' },
{ id: 'confirm', label: '查看引导' },
]
);
api.setNextChoice(null);
const skipped = await api.maybeShowNewUserGuidePrompt();
assert.equal(skipped, false);
assert.deepStrictEqual(api.getOpenedUrls(), ['https://apikey.qzz.io']);
});
+314
View File
@@ -144,3 +144,317 @@ return {
]);
assert.match(result.bodyTextPreview, /Welcome to ChatGPT/);
});
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
const api = new Function(`
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
const location = { href: 'https://chatgpt.com/' };
const hiddenSection = {
tagName: 'DIV',
id: 'mobile-cta',
className: 'max-xs:hidden',
hidden: false,
parentElement: null,
hasAttribute() {
return false;
},
getAttribute(name) {
if (name === 'aria-hidden') return '';
return '';
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
_style: {
display: 'none',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const hiddenSignupButton = {
tagName: 'BUTTON',
textContent: 'Sign up for free',
disabled: false,
className: 'signup-button',
hidden: false,
parentElement: hiddenSection,
hasAttribute() {
return false;
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
getAttribute(name) {
if (name === 'type') return '';
if (name === 'aria-hidden') return '';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'ChatGPT',
readyState: 'complete',
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [hiddenSignupButton];
}
return [];
},
};
const window = {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getSignupEmailInput() {
return null;
}
function getSignupPasswordInput() {
return null;
}
function getPageTextSnapshot() {
return 'ChatGPT 登录';
}
${extractFunction('getSignupEntryDiagnostics')}
return {
run() {
return getSignupEntryDiagnostics();
},
};
`)();
const result = api.run();
assert.deepStrictEqual(result.viewport, {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
});
assert.deepStrictEqual(result.signupLikeActionCounts, {
total: 1,
visible: 0,
hidden: 1,
});
assert.equal(result.signupLikeActions[0]?.text, 'Sign up for free');
assert.equal(result.signupLikeActions[0]?.className, 'signup-button');
assert.equal(result.signupLikeActions[0]?.display, 'block');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.className, 'max-xs:hidden');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.display, 'none');
});
test('signup password diagnostics summarizes password inputs, submit button, and alternate code entry', () => {
const api = new Function(`
const location = { href: 'https://auth.openai.com/create-account/password' };
const form = { action: 'https://auth.openai.com/u/signup/password' };
const passwordInput = {
tagName: 'INPUT',
type: 'password',
name: 'new-password',
id: 'password-field',
value: 'SecretLength14',
className: 'password-input',
disabled: false,
form,
getBoundingClientRect() {
return { width: 320, height: 44 };
},
getAttribute(name) {
if (name === 'type') return 'password';
if (name === 'name') return 'new-password';
if (name === 'autocomplete') return 'new-password';
if (name === 'placeholder') return 'Password';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const submitButton = {
tagName: 'BUTTON',
textContent: 'Continue',
className: 'submit-btn',
disabled: false,
form,
getBoundingClientRect() {
return { width: 160, height: 40 };
},
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
if (name === 'data-dd-action-name') return 'Continue';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const oneTimeCodeButton = {
tagName: 'BUTTON',
textContent: 'Use a one-time code instead',
className: 'switch-btn',
disabled: false,
getBoundingClientRect() {
return { width: 220, height: 36 };
},
getAttribute(name) {
if (name === 'type') return 'button';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'Create your account',
readyState: 'complete',
querySelectorAll(selector) {
if (selector === 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]') {
return [passwordInput];
}
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [submitButton, oneTimeCodeButton];
}
return [];
},
};
const window = {
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getPageTextSnapshot() {
return 'Create your account Use a one-time code instead';
}
function getSignupPasswordInput() {
return passwordInput;
}
function getSignupPasswordSubmitButton() {
return submitButton;
}
function getSignupPasswordDisplayedEmail() {
return 'user@example.com';
}
function findOneTimeCodeLoginTrigger() {
return oneTimeCodeButton;
}
function getSignupPasswordTimeoutErrorPageState() {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
${extractFunction('getSignupPasswordDiagnostics')}
return {
run() {
return getSignupPasswordDiagnostics();
},
};
`)();
const result = api.run();
assert.equal(result.url, 'https://auth.openai.com/create-account/password');
assert.equal(result.displayedEmail, 'user@example.com');
assert.equal(result.hasVisiblePasswordInput, true);
assert.equal(result.passwordInputCount, 1);
assert.equal(result.visiblePasswordInputCount, 1);
assert.equal(result.passwordInputs[0]?.name, 'new-password');
assert.equal(result.passwordInputs[0]?.valueLength, 14);
assert.equal(result.submitButton?.text, 'Continue');
assert.equal(result.oneTimeCodeTrigger?.text, 'Use a one-time code instead');
assert.equal(result.retryPage, true);
assert.equal(result.retryEnabled, true);
assert.match(result.bodyTextPreview, /one-time code/);
});
+3
View File
@@ -115,6 +115,9 @@ function fillInput(el, value) {
filledValues.push(value);
}
async function sleep() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
+2 -1
View File
@@ -87,7 +87,8 @@ test('step6LoginFromPasswordPage switches to one-time-code login when password i
globalThis.log = (message, level = 'info') => {
logs.push({ message, level });
};
globalThis.step6SwitchToOneTimeCodeLogin = async (value) => {
globalThis.step6SwitchToOneTimeCodeLogin = async (payload, value) => {
assert.deepStrictEqual(payload, { email: 'user@example.com', password: '' });
assert.strictEqual(value, snapshot);
return { step6Outcome: 'success', via: 'switch_to_one_time_code_login' };
};
+143
View File
@@ -72,12 +72,18 @@ async function recoverCurrentAuthRetryPage() {
return { recovered: true };
}
function throwIfStopped() {}
async function sleep() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('waitForKnownLoginAuthState')}
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
return {
@@ -104,6 +110,141 @@ return {
assert.equal(result.message, '当前页面处于登录超时报错页。');
});
test('step 7 timeout recovery transition continues from password page after retry succeeds', async () => {
const api = new Function(`
const logs = [];
let recoverCalls = 0;
let currentState = 'login_timeout_error_page';
const location = {
href: 'https://auth.openai.com/log-in',
};
function inspectLoginAuthState() {
return {
state: currentState,
url: location.href,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
currentState = 'password_page';
return { recovered: true };
}
function throwIfStopped() {}
async function sleep() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('waitForKnownLoginAuthState')}
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
return {
async run() {
return createStep6LoginTimeoutRecoveryTransition(
'login_timeout_error_page',
{ state: 'login_timeout_error_page', url: location.href },
'当前页面处于登录超时报错页。',
{
via: 'login_timeout_initial_recovered',
}
);
},
snapshot() {
return { logs, recoverCalls };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.equal(snapshot.recoverCalls, 1);
assert.equal(result.action, 'password');
assert.equal(result.snapshot.state, 'password_page');
assert.equal(snapshot.logs.some(({ message }) => /密码页/.test(message)), true);
});
test('step 7 entry resumes password flow after retry page recovery reaches password page', async () => {
const api = new Function(`
const logs = [];
let recoverCalls = 0;
let currentState = 'login_timeout_error_page';
const location = {
href: 'https://auth.openai.com/log-in',
};
function inspectLoginAuthState() {
return {
state: currentState,
url: location.href,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
currentState = 'password_page';
return { recovered: true };
}
function throwIfStopped() {}
async function sleep() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
async function step6LoginFromPasswordPage(payload, snapshot) {
return { branch: 'password', payload, snapshot };
}
async function step6LoginFromEmailPage(payload, snapshot) {
return { branch: 'email', payload, snapshot };
}
async function finalizeStep6VerificationReady(options) {
return { branch: 'verification', options };
}
function throwForStep6FatalState() {}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('waitForKnownLoginAuthState')}
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
${extractFunction('step6_login')}
return {
async run() {
return step6_login({
email: 'user@example.com',
password: 'secret',
});
},
snapshot() {
return { logs, recoverCalls };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.equal(snapshot.recoverCalls, 1);
assert.equal(result.branch, 'password');
assert.equal(result.snapshot.state, 'password_page');
assert.equal(snapshot.logs.some(({ message }) => /密码页/.test(message)), true);
});
test('step 7 finalize converts verification page that falls into retry page into recoverable result', async () => {
const api = new Function(`
const logs = [];
@@ -150,6 +291,8 @@ function getLoginAuthStateLabel(snapshot) {
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('waitForKnownLoginAuthState')}
${extractFunction('createStep6LoginTimeoutRecoveryTransition')}
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
${extractFunction('finalizeStep6VerificationReady')}
+113 -19
View File
@@ -51,10 +51,8 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
test('step 8 click effect throws when retry page appears after clicking continue', async () => {
const api = new Function(`
let recoverCalls = 0;
const chrome = {
tabs: {
async get() {
@@ -69,10 +67,6 @@ const chrome = {
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function recoverAuthRetryPageOnTab() {
recoverCalls += 1;
return { recovered: true };
}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
@@ -89,20 +83,120 @@ return {
async run() {
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
},
snapshot() {
return { recoverCalls };
};
`)();
await assert.rejects(
() => api.run(),
/点击“继续”后页面进入认证页重试页/
);
});
test('step 8 ready check throws when consent page is already a retry page before clicking', async () => {
const api = new Function(`
const chrome = {
tabs: {
async get() {
return {
id: 88,
url: 'https://auth.openai.com/authorize',
};
},
},
};
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
retryPage: true,
addPhonePage: false,
consentReady: false,
};
}
${extractFunction('waitForStep8Ready')}
return {
async run() {
return waitForStep8Ready(88, 1000);
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
progressed: false,
reason: 'retry_page_recovered',
restartCurrentStep: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(snapshot.recoverCalls, 1);
await assert.rejects(
() => api.run(),
/当前认证页已进入重试页/
);
});
test('step 8 ready check completes phone verification flow before waiting for OAuth consent', async () => {
const api = new Function(`
let pollCount = 0;
const phoneVerificationCalls = [];
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
const phoneVerificationHelpers = {
async completePhoneVerificationFlow(tabId, pageState) {
phoneVerificationCalls.push({ tabId, pageState });
return {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
};
},
};
async function getStep8PageState() {
pollCount += 1;
if (pollCount === 1) {
return {
url: 'https://auth.openai.com/add-phone',
addPhonePage: true,
phoneVerificationPage: false,
consentReady: false,
};
}
return {
url: 'https://auth.openai.com/authorize',
addPhonePage: false,
phoneVerificationPage: false,
consentReady: true,
};
}
${extractFunction('waitForStep8Ready')}
return {
async run() {
return {
result: await waitForStep8Ready(88, 1000),
phoneVerificationCalls,
};
},
};
`)();
const { result, phoneVerificationCalls } = await api.run();
assert.deepStrictEqual(phoneVerificationCalls, [
{
tabId: 88,
pageState: {
url: 'https://auth.openai.com/add-phone',
addPhonePage: true,
phoneVerificationPage: false,
consentReady: false,
},
},
]);
assert.deepStrictEqual(result, {
url: 'https://auth.openai.com/authorize',
addPhonePage: false,
phoneVerificationPage: false,
consentReady: true,
});
});
+114 -5
View File
@@ -58,23 +58,36 @@ const bundle = [
extractFunction('summarizeStatusBadgeEntries'),
extractFunction('normalizeStep9StatusText'),
extractFunction('isOAuthCallbackTimeoutFailure'),
extractFunction('isStep10CallbackSubmittedStatus'),
extractFunction('isStep10CallbackFailureText'),
extractFunction('isStep10MainWaitingStatus'),
extractFunction('isStep10MainFailureText'),
extractFunction('isStep9FailureText'),
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
extractFunction('formatStep10StatusSummaryValue'),
extractFunction('isStep10BrowserSwitchRequiredConflict'),
extractFunction('getStep10BrowserSwitchRequiredMessage'),
extractFunction('buildStep9StatusDiagnostics'),
extractFunction('extractStep10FailureDetail'),
extractFunction('explainStep10Failure'),
].join('\n');
function createApi() {
return new Function(`
function isRecoverableStep9AuthFailure(text) {
return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
|| /oauth flow is not pending/i.test(String(text || '').trim());
const normalized = String(text || '').trim();
return /(?:认证失败|回调\\s*url\\s*提交失败|回调url提交失败|提交回调失败)\\s*[:]?/i.test(normalized)
|| /oauth flow is not pending|请更新\\s*cli\\s*proxy\\s*api\\s*或检查连接|bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(normalized);
}
${bundle}
return {
buildStep9StatusDiagnostics,
explainStep10Failure,
isStep10BrowserSwitchRequiredConflict,
getStep10BrowserSwitchRequiredMessage,
};
`)();
}
@@ -86,6 +99,7 @@ test('step 9 does not treat red success badges as exact success', () => {
visible: true,
text: '认证成功!',
className: 'status-badge text-danger',
location: 'main',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
@@ -104,23 +118,118 @@ test('step 9 keeps failure state dominant when success badge and error banner co
visible: true,
text: '认证成功!',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[
{
visible: true,
text: '回调 URL 提交失败: oauth flow is not pending',
className: 'alert alert-danger',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.hasFailureVisibleBadge, true);
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
assert.equal(diagnostics.failureSource, 'callback');
});
test('step 10 treats plain 认证成功 as success when no failure is visible', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.exactSuccessText, '认证成功');
});
test('step 10 recognizes callback accepted badge as in-progress signal', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '回调 URL 已提交,等待认证中...',
className: 'status-badge success',
location: 'callback',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '等待认证中...',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasCallbackSubmittedBadge, true);
assert.equal(diagnostics.callbackSubmittedText, '回调 URL 已提交,等待认证中...');
assert.equal(diagnostics.mainWaitingText, '等待认证中...');
assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
});
test('step 10 explains callback upgrade hint with user-friendly reason', () => {
const api = createApi();
const explanation = api.explainStep10Failure(
'回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
'callback'
);
assert.equal(explanation.code, 'callback_submit_api_unavailable');
assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
assert.match(explanation.userMessage, /回调提交阶段/);
});
test('step 10 requests browser switch when success badge and callback upgrade failure coexist', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge success',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(api.isStep10BrowserSwitchRequiredConflict(diagnostics), true);
assert.match(api.getStep10BrowserSwitchRequiredMessage(diagnostics), /更换浏览器后重新进行注册登录/);
});
+207
View File
@@ -348,6 +348,51 @@ test('verification flow completes step 8 and flags phone verification when add-p
]);
});
test('verification flow treats manual step 8 add-phone confirmation as the same fatal add-phone error', async () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {
throw new Error('should not complete step 8');
},
confirmCustomVerificationStepBypassRequest: async () => ({
confirmed: false,
addPhoneDetected: 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 () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {
throw new Error('should not mark step skipped when add-phone is chosen');
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await assert.rejects(
() => helpers.confirmCustomVerificationStepBypass(8),
/验证码提交后页面进入手机号页面/
);
});
test('verification flow caps mail polling timeout to the remaining oauth budget', async () => {
const mailPollCalls = [];
@@ -716,3 +761,165 @@ test('verification flow uses configured login resend count for step 8', async ()
assert.deepStrictEqual(resendSteps, [8, 8]);
assert.equal(pollCalls, 3);
});
test('verification flow waits during resend cooldown instead of tight-looping', async () => {
const sleepCalls = [];
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 () => ({}),
sendToMailContentScriptResilient: async (_mail, message) => {
if (message.type !== 'POLL_EMAIL') {
return {};
}
pollCalls += 1;
return pollCalls === 1
? {}
: { code: '654321', emailTimestamp: 123 };
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async (ms) => {
sleepCalls.push(ms);
},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.pollFreshVerificationCodeWithResendInterval(
4,
{
email: 'user@example.com',
lastSignupCode: null,
},
{ provider: 'qq', label: 'QQ 邮箱' },
{
maxResendRequests: 0,
resendIntervalMs: 25000,
lastResendAt: Date.now(),
}
);
assert.equal(result.code, '654321');
assert.equal(pollCalls, 2);
assert.ok(sleepCalls.length >= 1);
assert.ok(sleepCalls[0] >= 1000);
});
test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
const resilientCalls = [];
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 () => {
throw new Error('should not use non-resilient channel');
},
sendToContentScriptResilient: async (_source, message, options) => {
resilientCalls.push({ message, options });
return { success: true };
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(4, '654321');
assert.deepStrictEqual(result, { success: true });
assert.equal(resilientCalls.length, 1);
assert.equal(resilientCalls[0].message.type, 'FILL_CODE');
assert.equal(resilientCalls[0].message.payload.code, '654321');
assert.ok(resilientCalls[0].options.timeoutMs >= 30000);
});
test('verification flow treats retryable submit transport failure as success when step 4 already redirected to logged-in home', async () => {
const logs = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
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',
isRetryableContentScriptTransportError: (error) => /message channel is closed/i.test(String(error?.message || error || '')),
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 () => {
throw new Error('should not use non-resilient channel');
},
sendToContentScriptResilient: async () => {
throw new Error('The page keeping the extension port is moved into back/forward cache, so the message channel is closed.');
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(4, '654321');
assert.equal(result.success, true);
assert.equal(result.skipProfileStep, true);
assert.equal(result.assumed, true);
assert.equal(result.transportRecovered, true);
assert.equal(logs.some(({ message }) => /验证码提交后页面已切换到ChatGPT 已登录首页/.test(message)), true);
});