feat: support custom email pool in the merged dev registration flow

- 吸收 dev 最新 sidepanel 与收码链路改动,保留 Cloudflare Temp Email 独立设置区、贡献模式自动前提醒和 2925 当前账号恢复接线

- 修复生成邮箱辅助层对运行态参数的判断,避免 Gmail 别名与自定义邮箱池误用旧的已保存状态

- 补充自定义邮箱池相关测试与文档,更新 README、项目结构说明和完整链路说明
This commit is contained in:
QLHazyCoder
2026-04-23 20:40:50 +08:00
51 changed files with 4479 additions and 230 deletions
@@ -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'
);
});
@@ -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',
+235 -10
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({
@@ -78,9 +79,7 @@ test('generated email helper falls back to normal generator when 2925 is in rece
});
test('generated email helper can read the requested address from custom email pool', 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({
@@ -136,3 +135,229 @@ test('generated email helper can read the requested address from custom email po
['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',
});
});
@@ -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 () => [],
@@ -226,3 +234,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;
}
});
@@ -175,8 +175,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 +192,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 +211,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 +223,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',
},
]);
});
@@ -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)));
});
+24 -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);
@@ -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,297 @@
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('buildCloudflareTempEmailUsageGuideModalConfig returns a modal payload with inline links for generator mode', () => {
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
const api = new Function(`
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
const modalConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
assert.equal(typeof modalConfig.title, 'string');
assert.equal(typeof modalConfig.messageHtml, 'string');
assert.equal(typeof modalConfig.alert?.text, 'string');
assert.equal(modalConfig.title.length > 0, true);
assert.equal(modalConfig.messageHtml.length > 0, true);
assert.equal(modalConfig.alert.text.length > 0, true);
assert.equal(modalConfig.messageHtml.includes('<a '), true);
assert.equal(modalConfig.messageHtml.includes('Issue #942'), true);
assert.equal(modalConfig.messageHtml.includes('LINUX DO 教程'), true);
});
test('buildCloudflareTempEmailUsageGuideModalConfig returns a distinct alert for provider mode', () => {
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
const api = new Function(`
const selectMailProvider = { value: 'cloudflare-temp-email' };
const selectEmailGenerator = { value: 'duck' };
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
const providerConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
const generatorApi = new Function(`
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
const generatorConfig = generatorApi.buildCloudflareTempEmailUsageGuideModalConfig();
assert.equal(typeof providerConfig.alert?.text, 'string');
assert.equal(typeof providerConfig.messageHtml, 'string');
assert.equal(providerConfig.alert.text.length > 0, true);
assert.equal(providerConfig.messageHtml.length > 0, true);
assert.notEqual(providerConfig.alert.text, generatorConfig.alert.text);
});
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/);
});
+11
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' }),
@@ -414,6 +423,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);
@@ -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, '有新的征求意见,请佬友共同参与选择。');
});
@@ -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']);
});
+1 -1
View File
@@ -147,7 +147,7 @@ return {
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 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',
+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')}