This commit is contained in:
QLHazyCoder
2026-04-24 10:29:43 +08:00
14 changed files with 648 additions and 44 deletions
+165
View File
@@ -331,3 +331,168 @@ test('auto-run controller skips user_already_exists failures to the next round i
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller keeps retrying the same custom mail provider pool email until success', async () => {
const events = {
logs: [],
broadcasts: [],
accountRecords: [],
runCalls: 0,
};
let currentState = {
stepStatuses: {},
vpsUrl: 'https://example.com/vps',
vpsPassword: 'secret',
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: 'custom',
customMailProviderPool: ['first@example.com'],
emailGenerator: 'duck',
gmailBaseEmail: '',
mail2925BaseEmail: '',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
tabRegistry: {},
sourceLastUrls: {},
autoRunRoundSummaries: [],
};
const runtime = {
state: {
autoRunActive: false,
autoRunCurrentRun: 0,
autoRunTotalRuns: 1,
autoRunAttemptRun: 0,
autoRunSessionId: 0,
},
get() {
return { ...this.state };
},
set(updates = {}) {
this.state = { ...this.state, ...updates };
},
};
let sessionSeed = 0;
const controller = api.createAutoRunController({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
appendAccountRunRecord: async (status, _state, reason) => {
events.accountRecords.push({ status, reason });
return { status, reason };
},
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
AUTO_RUN_RETRY_DELAY_MS: 3000,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
broadcastAutoRunStatus: async (phase, payload = {}) => {
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
};
},
broadcastStopToContentScripts: async () => {},
cancelPendingCommands: () => {},
clearStopRequest: () => {},
createAutoRunSessionId: () => {
sessionSeed += 1;
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => error?.message || String(error || ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
getState: async () => ({
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
customMailProviderPool: [...(currentState.customMailProviderPool || [])],
}),
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
isRestartCurrentAttemptError: () => false,
isSignupUserAlreadyExistsFailure: (error) => /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(error?.message || String(error || '')),
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
persistAutoRunTimerPlan: async () => ({}),
resetState: async () => {
currentState = {
...currentState,
stepStatuses: {},
tabRegistry: {},
sourceLastUrls: {},
};
},
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls <= 2) {
throw new Error('步骤 3:页面异常,当前尝试失败。');
}
},
runtime,
setState: async (updates = {}) => {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
};
},
sleepWithStop: async () => {},
throwIfAutoRunSessionStopped: (sessionId) => {
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
throw new Error('流程已被用户停止。');
}
},
waitForRunningStepsToFinish: async () => currentState,
chrome: {
runtime: {
sendMessage() {
return Promise.resolve();
},
},
},
});
await controller.autoRunLoop(1, {
autoRunSkipFailures: true,
mode: 'restart',
});
assert.equal(events.runCalls, 3, 'custom mail provider pool should keep retrying the same round until success');
assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 2);
assert.ok(events.broadcasts.filter(({ phase }) => phase === 'retrying').every(({ currentRun }) => currentRun === 1));
assert.ok(events.logs.some(({ message }) => /继续使用当前邮箱/.test(message)));
assert.equal(events.logs.some(({ message }) => /达到 3 次重试上限继续下一轮/.test(message)), false);
assert.equal(events.accountRecords.length, 0);
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
@@ -56,6 +56,8 @@ const bundle = [
extractFunction('normalizeCustomEmailPool'),
extractFunction('getCustomEmailPool'),
extractFunction('getCustomEmailPoolEmailForRun'),
extractFunction('getCustomMailProviderPool'),
extractFunction('getCustomMailProviderPoolEmailForRun'),
extractFunction('getEmailGeneratorLabel'),
].join('\n');
@@ -71,6 +73,8 @@ return {
normalizeCustomEmailPool,
getCustomEmailPool,
getCustomEmailPoolEmailForRun,
getCustomMailProviderPool,
getCustomMailProviderPoolEmailForRun,
getEmailGeneratorLabel,
};
`)();
@@ -102,3 +106,19 @@ test('background selects the matching email for the current auto-run round', ()
assert.equal(api.getCustomEmailPoolEmailForRun(state, 2), 'second@example.com');
assert.equal(api.getCustomEmailPoolEmailForRun(state, 4), '');
});
test('background selects the matching custom provider pool email for the current auto-run round', () => {
const api = createApi();
const state = {
customMailProviderPool: ['first@example.com', 'second@example.com', 'third@example.com'],
};
assert.deepEqual(api.getCustomMailProviderPool(state), [
'first@example.com',
'second@example.com',
'third@example.com',
]);
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 1), 'first@example.com');
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 3), 'third@example.com');
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 4), '');
});
+103
View File
@@ -57,6 +57,8 @@ test('sidepanel html exposes custom email pool generator option and input row',
assert.match(html, /option value="custom-pool">自定义邮箱池<\/option>/);
assert.match(html, /id="row-custom-email-pool"/);
assert.match(html, /id="input-custom-email-pool"/);
assert.match(html, /id="row-custom-mail-provider-pool"/);
assert.match(html, /id="input-custom-mail-provider-pool"/);
});
test('sidepanel locks run count to custom email pool size', () => {
@@ -66,6 +68,9 @@ test('sidepanel locks run count to custom email pool size', () => {
extractFunction('getSelectedEmailGenerator'),
extractFunction('usesGeneratedAliasMailProvider'),
extractFunction('usesCustomEmailPoolGenerator'),
extractFunction('getCustomMailProviderPoolSize'),
extractFunction('usesCustomMailProviderPool'),
extractFunction('getLockedRunCountFromEmailPool'),
extractFunction('getCustomEmailPoolSize'),
extractFunction('getRunCountValue'),
].join('\n');
@@ -113,3 +118,101 @@ return {
assert.equal(api.getCustomEmailPoolSize(), 2);
assert.equal(api.getRunCountValue(), 2);
});
test('sidepanel locks run count to custom mail provider pool size', () => {
const bundle = [
extractFunction('isCustomMailProvider'),
extractFunction('normalizeCustomEmailPoolEntries'),
extractFunction('getSelectedEmailGenerator'),
extractFunction('usesGeneratedAliasMailProvider'),
extractFunction('usesCustomEmailPoolGenerator'),
extractFunction('getCustomMailProviderPoolSize'),
extractFunction('usesCustomMailProviderPool'),
extractFunction('getLockedRunCountFromEmailPool'),
extractFunction('getCustomEmailPoolSize'),
extractFunction('getRunCountValue'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const selectMailProvider = { value: 'custom' };
const selectEmailGenerator = { value: 'duck' };
const inputCustomMailProviderPool = { value: 'first@example.com\\nsecond@example.com\\nthird@example.com' };
const inputCustomEmailPool = { value: '' };
const inputRunCount = { value: '99' };
function isLuckmailProvider() {
return false;
}
function isManagedAliasProvider() {
return false;
}
function getSelectedMail2925Mode() {
return 'provide';
}
function isManagedAliasProvider(provider) {
return String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER;
}
${bundle}
return {
usesCustomMailProviderPool,
getCustomMailProviderPoolSize,
getLockedRunCountFromEmailPool,
getRunCountValue,
};
`)();
assert.equal(api.usesCustomMailProviderPool(), true);
assert.equal(api.getCustomMailProviderPoolSize(), 3);
assert.equal(api.getLockedRunCountFromEmailPool(), 3);
assert.equal(api.getRunCountValue(), 3);
});
test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => {
const bundle = [
extractFunction('getCustomVerificationPromptCopy'),
extractFunction('openCustomVerificationConfirmDialog'),
].join('\n');
const api = new Function(`
let openActionModalPayload = null;
async function openActionModal(options) {
openActionModalPayload = options;
return options.buildResult('add_phone');
}
async function openConfirmModal() {
throw new Error('step 8 should use action modal');
}
${bundle}
return {
getCustomVerificationPromptCopy,
openCustomVerificationConfirmDialog,
getOpenActionModalPayload: () => openActionModalPayload,
};
`)();
const prompt = api.getCustomVerificationPromptCopy(8);
assert.equal(prompt.phoneActionLabel, '出现手机号验证');
const result = await api.openCustomVerificationConfirmDialog(8);
assert.deepEqual(result, {
confirmed: false,
addPhoneDetected: true,
});
const modalPayload = api.getOpenActionModalPayload();
assert.equal(modalPayload.actions.length, 3);
assert.equal(modalPayload.actions[1].id, 'add_phone');
assert.equal(modalPayload.actions[1].label, '出现手机号验证');
});
+45
View File
@@ -345,6 +345,51 @@ test('verification flow treats add-phone after login code submit as fatal instea
]);
});
test('verification flow treats manual step 8 add-phone confirmation as the same fatal add-phone error', async () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {
throw new Error('should not complete step 8');
},
confirmCustomVerificationStepBypassRequest: async () => ({
confirmed: false,
addPhoneDetected: true,
}),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {
throw new Error('should not mark step skipped when add-phone is chosen');
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await assert.rejects(
() => helpers.confirmCustomVerificationStepBypass(8),
/验证码提交后页面进入手机号页面/
);
});
test('verification flow caps mail polling timeout to the remaining oauth budget', async () => {
const mailPollCalls = [];