Refactor multi-flow mail capability boundaries

This commit is contained in:
QLHazyCoder
2026-05-13 06:35:43 +08:00
parent a85ed0ce89
commit d93cdfff9e
33 changed files with 2035 additions and 198 deletions
+106
View File
@@ -145,3 +145,109 @@ return {
assert.equal(snapshot.autoRunTotalRuns, 1);
assert.equal(snapshot.autoRunAttemptRun, 0);
});
test('launchAutoRunTimerPlan cancels an invalid scheduled start before restarting auto-run', async () => {
const api = new Function(`
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
let autoRunTimerLaunching = false;
let autoRunActive = false;
let autoRunCurrentRun = 0;
let autoRunTotalRuns = 1;
let autoRunAttemptRun = 0;
let autoRunSessionId = 0;
const state = {
activeFlowId: 'site-a',
panelMode: 'cpa',
signupMethod: 'phone',
autoRunDelayEnabled: false,
autoRunTimerPlan: {
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
fireAt: Date.now() + 60_000,
totalRuns: 2,
autoRunSkipFailures: false,
autoRunSessionId: 0,
countdownTitle: '已计划自动运行',
countdownNote: '等待启动',
},
};
let startCalls = 0;
let clearStopCalls = 0;
let clearAlarmCalls = 0;
const broadcasts = [];
const logs = [];
async function getState() {
return { ...state };
}
function getPendingAutoRunTimerPlan() {
return state.autoRunTimerPlan;
}
async function clearAutoRunTimerAlarm() {
clearAlarmCalls += 1;
}
async function broadcastAutoRunStatus(phase, statusPayload, statePayload) {
broadcasts.push({ phase, statusPayload, statePayload });
}
async function addLog(message, level) {
logs.push({ message, level });
}
async function setAutoRunDelayEnabledState() {}
function serializeAutoRunRoundSummaries(totalRuns, summaries = []) {
return Array.isArray(summaries) ? summaries : [];
}
function clearStopRequest() {
clearStopCalls += 1;
}
function startAutoRunLoop() {
startCalls += 1;
}
function validateAutoRunStartState() {
return {
ok: false,
errors: [{ message: '当前 flow 不支持手机号注册。' }],
};
}
${helperBundle}
return {
launchAutoRunTimerPlan,
snapshot() {
return {
startCalls,
clearStopCalls,
clearAlarmCalls,
broadcasts,
logs,
autoRunCurrentRun,
autoRunTotalRuns,
autoRunAttemptRun,
};
},
};
`)();
const started = await api.launchAutoRunTimerPlan('alarm');
const snapshot = api.snapshot();
assert.equal(started, false);
assert.equal(snapshot.startCalls, 0);
assert.equal(snapshot.clearStopCalls, 0);
assert.equal(snapshot.clearAlarmCalls, 1);
assert.equal(snapshot.broadcasts.length, 1);
assert.equal(snapshot.broadcasts[0].phase, 'idle');
assert.match(snapshot.logs[0].message, /自动运行计划已取消:当前 flow 不支持手机号注册。/);
assert.equal(snapshot.logs[0].level, 'error');
assert.equal(snapshot.autoRunCurrentRun, 0);
assert.equal(snapshot.autoRunTotalRuns, 1);
assert.equal(snapshot.autoRunAttemptRun, 0);
});
@@ -351,6 +351,65 @@ test('message router re-syncs contribution mode before AUTO_RUN when sidepanel p
]);
});
test('message router blocks AUTO_RUN and SCHEDULE_AUTO_RUN when shared auto-run validation fails', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
const calls = [];
const router = api.createMessageRouter({
clearStopRequest: () => {},
getPendingAutoRunTimerPlan: () => null,
getState: async () => ({
activeFlowId: 'site-a',
panelMode: 'cpa',
signupMethod: 'phone',
stepStatuses: {},
}),
normalizeRunCount: (value) => Number(value) || 1,
scheduleAutoRun: async () => {
calls.push({ type: 'scheduleAutoRun' });
return { ok: true };
},
setState: async (updates) => {
calls.push({ type: 'setState', updates });
},
startAutoRunLoop: () => {
calls.push({ type: 'startAutoRunLoop' });
},
validateAutoRunStart: () => ({
ok: false,
errors: [{ message: '当前 flow 不支持手机号注册。' }],
}),
});
await assert.rejects(
() => router.handleMessage({
type: 'AUTO_RUN',
payload: {
totalRuns: 2,
autoRunSkipFailures: true,
mode: 'restart',
},
}),
/当前 flow 不支持手机号注册。/
);
await assert.rejects(
() => router.handleMessage({
type: 'SCHEDULE_AUTO_RUN',
payload: {
totalRuns: 2,
delayMinutes: 5,
autoRunSkipFailures: false,
},
}),
/当前 flow 不支持手机号注册。/
);
assert.deepStrictEqual(calls, []);
});
test('account run history snapshot sync is disabled in contribution mode', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
@@ -0,0 +1,28 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background forwards mail rule metadata to Hotmail helper and shared picker paths', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(
source,
/requiredKeywords:\s*pollPayload\.requiredKeywords\s*\|\|\s*\[\]/,
'Hotmail helper 请求体应转发 requiredKeywords'
);
assert.match(
source,
/codePatterns:\s*pollPayload\.codePatterns\s*\|\|\s*\[\]/,
'Hotmail helper 请求体应转发 codePatterns'
);
assert.match(
source,
/pickVerificationMessageWithTimeFallback\(fetchResult\.messages,\s*\{[\s\S]*requiredKeywords:\s*pollPayload\.requiredKeywords\s*\|\|\s*\[\],[\s\S]*codePatterns:\s*pollPayload\.codePatterns\s*\|\|\s*\[\]/,
'Hotmail API 轮询应把 rule metadata 传给共享验证码筛选器'
);
assert.match(
source,
/pickVerificationMessageWithTimeFallback\(messages,\s*\{[\s\S]*requiredKeywords:\s*pollPayload\.requiredKeywords\s*\|\|\s*\[\],[\s\S]*codePatterns:\s*pollPayload\.codePatterns\s*\|\|\s*\[\]/,
'Cloudflare Temp Email 轮询也应复用同一套 rule metadata'
);
});
@@ -42,10 +42,26 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
ruleId: 'openai-signup-code',
step: 4,
artifactType: 'code',
codePatterns: [
{
source: '(?:chatgpt\\s+log-?in\\s+code|enter\\s+this\\s+code)[^0-9]{0,24}(\\d{6})',
flags: 'i',
},
{
source: 'your\\s+chatgpt\\s+code\\s+is\\s+(\\d{6})',
flags: 'i',
},
{
source: '(?:verification\\s+code|temporary\\s+verification\\s+code|your\\s+chatgpt\\s+code|code(?:\\s+is)?)[^0-9]{0,16}(\\d{6})',
flags: 'i',
},
],
filterAfterTimestamp: 0,
requiredKeywords: ['openai', 'chatgpt', 'verify', 'verification', 'confirm', '验证码', '代码'],
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
targetEmail: 'user@example.com',
targetEmailHints: ['user@example.com', 'user=example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 15,
intervalMs: 15000,
@@ -64,10 +80,26 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
ruleId: 'openai-login-code',
step: 8,
artifactType: 'code',
codePatterns: [
{
source: '(?:chatgpt\\s+log-?in\\s+code|enter\\s+this\\s+code)[^0-9]{0,24}(\\d{6})',
flags: 'i',
},
{
source: 'your\\s+chatgpt\\s+code\\s+is\\s+(\\d{6})',
flags: 'i',
},
{
source: '(?:verification\\s+code|temporary\\s+verification\\s+code|your\\s+chatgpt\\s+code|code(?:\\s+is)?)[^0-9]{0,16}(\\d{6})',
flags: 'i',
},
],
filterAfterTimestamp: 456,
requiredKeywords: ['openai', 'chatgpt', 'verify', 'verification', 'confirm', '验证码', '代码', 'login'],
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
targetEmail: 'login@example.com',
targetEmailHints: ['login@example.com', 'login=example.com'],
mail2925MatchTargetEmail: false,
maxAttempts: 5,
intervalMs: 3000,
@@ -198,3 +198,67 @@ test('SAVE_SETTING re-resolves signup method when panel mode changes', async ()
assert.equal(state.panelMode, 'cpa');
assert.equal(state.signupMethod, 'email');
});
test('SAVE_SETTING applies shared mode-switch normalization before persisting incompatible capability flags', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
const persistedPayloads = [];
let state = {
activeFlowId: 'site-a',
signupMethod: 'email',
phoneVerificationEnabled: false,
plusModeEnabled: false,
panelMode: 'cpa',
};
const router = api.createMessageRouter({
addLog: async () => {},
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: (input = {}) => ({
plusModeEnabled: Boolean(input.plusModeEnabled),
phoneVerificationEnabled: Boolean(input.phoneVerificationEnabled),
signupMethod: String(input.signupMethod || 'email'),
}),
broadcastDataUpdate: () => {},
getState: async () => ({ ...state }),
resolveSignupMethod: (nextState = {}) => (
Boolean(nextState.phoneVerificationEnabled) && Boolean(nextState.plusModeEnabled) ? 'phone' : 'email'
),
setPersistentSettings: async (updates) => {
persistedPayloads.push({ ...updates });
},
setState: async (updates) => {
state = { ...state, ...updates };
},
validateModeSwitch: () => ({
ok: false,
errors: [{ code: 'plus_mode_unsupported', message: '当前 flow 不支持 Plus 模式。' }],
normalizedUpdates: {
plusModeEnabled: false,
phoneVerificationEnabled: false,
signupMethod: 'email',
},
}),
});
const response = await router.handleMessage({
type: 'SAVE_SETTING',
payload: {
plusModeEnabled: true,
phoneVerificationEnabled: true,
signupMethod: 'phone',
},
});
assert.equal(response.ok, true);
assert.equal(state.plusModeEnabled, false);
assert.equal(state.phoneVerificationEnabled, false);
assert.equal(state.signupMethod, 'email');
assert.deepEqual(persistedPayloads[0], {
plusModeEnabled: false,
phoneVerificationEnabled: false,
signupMethod: 'email',
});
assert.equal(response.modeValidation?.errors?.[0]?.code, 'plus_mode_unsupported');
});
@@ -0,0 +1,132 @@
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;
if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) signatureEnded = true;
}
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('importSettingsBundle normalizes unsupported capability flags before persisting imported settings', async () => {
const api = new Function(`
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
const DEFAULT_REGISTRATION_EMAIL_STATE = { emailHistory: [] };
let persistedUpdates = null;
let stateUpdates = null;
let broadcastPayload = null;
let currentState = {
activeFlowId: 'site-a',
panelMode: 'sub2api',
signupMethod: 'phone',
plusModeEnabled: false,
phoneVerificationEnabled: false,
stepStatuses: {},
};
async function ensureManualInteractionAllowed() {
return currentState;
}
function buildPersistentSettingsPayload(settings = {}) {
return { ...settings };
}
function validateModeSwitchState() {
return {
ok: false,
errors: [{ code: 'panel_mode_unsupported', message: '当前 flow 不支持 SUB2API 面板模式。' }],
normalizedUpdates: {
panelMode: 'cpa',
plusModeEnabled: false,
phoneVerificationEnabled: false,
signupMethod: 'email',
},
};
}
function resolveSignupMethod(state = {}) {
return String(state?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
async function setPersistentSettings(updates) {
persistedUpdates = { ...updates };
}
async function setState(updates) {
stateUpdates = { ...updates };
currentState = { ...currentState, ...updates };
}
function broadcastDataUpdate(payload) {
broadcastPayload = { ...payload };
}
async function getState() {
return { ...currentState };
}
${extractFunction('importSettingsBundle')}
return {
importSettingsBundle,
getPersistedUpdates: () => persistedUpdates,
getStateUpdates: () => stateUpdates,
getBroadcastPayload: () => broadcastPayload,
};
`)();
const result = await api.importSettingsBundle({
schemaVersion: 1,
settings: {
panelMode: 'sub2api',
plusModeEnabled: true,
phoneVerificationEnabled: true,
signupMethod: 'phone',
},
});
assert.deepEqual(api.getPersistedUpdates(), {
panelMode: 'cpa',
plusModeEnabled: false,
phoneVerificationEnabled: false,
signupMethod: 'email',
});
assert.equal(api.getStateUpdates().panelMode, 'cpa');
assert.equal(api.getStateUpdates().plusModeEnabled, false);
assert.equal(api.getStateUpdates().phoneVerificationEnabled, false);
assert.equal(api.getStateUpdates().signupMethod, 'email');
assert.equal(api.getBroadcastPayload().panelMode, 'cpa');
assert.equal(api.getBroadcastPayload().signupMethod, 'email');
assert.equal(result.signupMethod, 'email');
});
@@ -177,6 +177,7 @@ return {
});
assert.deepEqual(api.getCaptured(), [{
activeFlowId: 'openai',
plusModeEnabled: true,
plusPaymentMethod: 'gopay',
signupMethod: 'phone',
+88
View File
@@ -70,3 +70,91 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
assert.equal(capabilityState.panelMode, 'codex2api');
assert.deepEqual(capabilityState.supportedPanelModes, []);
});
test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry({
flowCapabilities: {
openai: api.FLOW_CAPABILITIES.openai,
'site-a': {
...api.DEFAULT_FLOW_CAPABILITIES,
supportsPlatformBinding: ['cpa'],
},
},
});
const plusLockedResult = registry.validateAutoRunStart({
state: {
activeFlowId: 'openai',
panelMode: 'cpa',
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: false,
},
});
assert.equal(plusLockedResult.ok, false);
assert.equal(plusLockedResult.errors[0].code, 'phone_signup_plus_mode_locked');
const unsupportedPanelResult = registry.validateAutoRunStart({
state: {
activeFlowId: 'site-a',
panelMode: 'sub2api',
signupMethod: 'email',
},
});
assert.equal(unsupportedPanelResult.ok, false);
assert.equal(unsupportedPanelResult.errors[0].code, 'panel_mode_unsupported');
});
test('flow capability registry normalizes unsupported mode switches back to the effective capability set', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry({
flowCapabilities: {
openai: api.FLOW_CAPABILITIES.openai,
'site-a': {
...api.DEFAULT_FLOW_CAPABILITIES,
supportsPlatformBinding: ['cpa'],
},
},
});
const validation = registry.validateModeSwitch({
state: {
activeFlowId: 'site-a',
panelMode: 'sub2api',
signupMethod: 'phone',
phoneVerificationEnabled: true,
plusModeEnabled: true,
contributionMode: true,
},
changedKeys: [
'panelMode',
'signupMethod',
'phoneVerificationEnabled',
'plusModeEnabled',
'contributionMode',
],
});
assert.equal(validation.ok, false);
assert.deepEqual(validation.normalizedUpdates, {
panelMode: 'cpa',
signupMethod: 'email',
phoneVerificationEnabled: false,
plusModeEnabled: false,
contributionMode: false,
});
assert.deepEqual(
validation.errors.map((entry) => entry.code),
[
'panel_mode_unsupported',
'plus_mode_unsupported',
'contribution_mode_unsupported',
'phone_verification_unsupported',
'phone_signup_flow_unsupported',
]
);
});
+33
View File
@@ -185,6 +185,15 @@ test('extractVerificationCode returns first six-digit code from multilingual mai
assert.equal(extractVerificationCode('No code here'), null);
});
test('extractVerificationCode supports runtime mail rule patterns', () => {
assert.equal(
extractVerificationCode('Mailbox notice: use pin A-778899 to continue.', {
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
}),
'778899'
);
});
test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
assert.equal(
extractVerificationCodeFromMessage({
@@ -214,6 +223,30 @@ test('extractVerificationCodeFromMessage reads code from the latest message subj
);
});
test('pickVerificationMessageWithTimeFallback supports required keyword hints and runtime code patterns', () => {
const messages = [
{
id: 'mail-1',
subject: 'Security center',
from: { emailAddress: { address: 'alerts@example.com' } },
bodyPreview: 'Use pin A-661122 to continue',
receivedDateTime: '2026-04-14T10:06:00.000Z',
},
];
const result = pickVerificationMessageWithTimeFallback(messages, {
afterTimestamp: 0,
senderFilters: [],
subjectFilters: [],
requiredKeywords: ['security'],
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
excludeCodes: [],
});
assert.equal(result.match?.code, '661122');
assert.equal(result.usedTimeFallback, false);
});
test('getHotmailListToggleLabel reflects expanded state and account count', () => {
assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7');
+31
View File
@@ -54,6 +54,8 @@ function extractFunction(name) {
test('readOpenedMailBody falls back to thread detail pane and extracts verification code', () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
extractFunction('getOpenedMailBodyRoot'),
extractFunction('readOpenedMailBody'),
@@ -83,6 +85,8 @@ return { readOpenedMailBody, extractVerificationCode };
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
const bundle = [
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
@@ -95,6 +99,27 @@ return { extractVerificationCode };
assert.equal(api.extractVerificationCode(bodyText), '982219');
});
test('extractVerificationCode supports runtime mail rule patterns', () => {
const bundle = [
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
const api = new Function(`
${bundle}
return { extractVerificationCode };
`)();
const bodyText = 'Mailbox notice\nUse verification pin A-445566 to continue.';
assert.equal(
api.extractVerificationCode(bodyText, {
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
}),
'445566'
);
});
test('readOpenedMailBody ignores conversation list rows when no detail pane is open', () => {
const bundle = [
extractFunction('normalizeText'),
@@ -199,6 +224,8 @@ test('icloud poll session baseline is reused across calls and enables fallback a
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
@@ -300,6 +327,8 @@ test('icloud step8 polling finds a visible first-row code immediately', async ()
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
@@ -378,6 +407,8 @@ test('icloud step8 visible first-row code still respects excluded codes', async
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
+29
View File
@@ -166,6 +166,8 @@ test('readOpenedMailText prefers opened body content that contains the verificat
extractFunction('collectOpenedMailTextCandidates'),
extractFunction('selectOpenedMailTextCandidate'),
extractFunction('readOpenedMailText'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
@@ -216,6 +218,8 @@ test('openMailAndGetMessageText reads opened body text and returns to inbox', as
extractFunction('readOpenedMailText'),
extractFunction('returnToInbox'),
extractFunction('openMailAndGetMessageText'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
@@ -289,6 +293,8 @@ test('openMailAndGetMessageText ignores stale pre-open text that contains an old
extractFunction('readOpenedMailText'),
extractFunction('returnToInbox'),
extractFunction('openMailAndGetMessageText'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
@@ -359,6 +365,8 @@ return { openMailAndGetMessageText, mailItem };
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
const bundle = [
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
@@ -371,6 +379,27 @@ return { extractVerificationCode };
assert.equal(api.extractVerificationCode(bodyText), '982219');
});
test('extractVerificationCode supports runtime mail rule patterns', () => {
const bundle = [
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
const api = new Function(`
${bundle}
return { extractVerificationCode };
`)();
const bodyText = 'Security Center\nUse verification pin A-778899 to continue.';
assert.equal(
api.extractVerificationCode(bodyText, {
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
}),
'778899'
);
});
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
const bundle = [
extractFunction('normalizeText'),
+60 -2
View File
@@ -319,6 +319,7 @@ return {
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
const bundle = [
extractFunction('extractEmails'),
extractFunction('normalizeTargetEmailHints'),
extractFunction('extractForwardedTargetEmails'),
extractFunction('emailMatchesTarget'),
extractFunction('getTargetEmailMatchState'),
@@ -407,6 +408,34 @@ return {
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-2']);
});
test('getTargetEmailMatchState decodes generic forwarded bounce aliases without OpenAI-specific domains', () => {
const bundle = [
extractFunction('extractEmails'),
extractFunction('normalizeTargetEmailHints'),
extractFunction('extractForwardedTargetEmails'),
extractFunction('emailMatchesTarget'),
extractFunction('getTargetEmailMatchState'),
].join('\n');
const api = new Function(`
${bundle}
return { getTargetEmailMatchState };
`)();
const state = api.getTargetEmailMatchState(
'Return-Path: <bounce+notice-expected.user=example.com@mailer.forwarder.net>',
'expected.user@example.com',
{
targetEmailHints: ['expected.user@example.com', 'expected.user=example.com'],
}
);
assert.deepEqual(state, {
matches: true,
hasExplicitEmail: true,
});
});
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
const bundle = [
extractFunction('normalizeMinuteTimestamp'),
@@ -549,7 +578,9 @@ return {
test('extractVerificationCode strict mode matches the new suspicious log-in mail body', () => {
const bundle = [
extractFunction('extractStrictChatGPTVerificationCode'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractLegacyStrictVerificationCode'),
extractFunction('isLikelyCompactTimeValue'),
extractFunction('isLikelyHeaderTimestampCode'),
extractFunction('findSafeStandaloneSixDigitCode'),
@@ -566,9 +597,36 @@ return { extractVerificationCode };
assert.equal(api.extractVerificationCode(bodyText, false), '982219');
});
test('extractVerificationCode supports runtime mail rule patterns', () => {
const bundle = [
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractLegacyStrictVerificationCode'),
extractFunction('isLikelyCompactTimeValue'),
extractFunction('isLikelyHeaderTimestampCode'),
extractFunction('findSafeStandaloneSixDigitCode'),
extractFunction('extractVerificationCode'),
].join('\n');
const api = new Function(`
${bundle}
return { extractVerificationCode };
`)();
const bodyText = 'System alert\nUse verification pin A-556677 to continue.';
assert.equal(
api.extractVerificationCode(bodyText, {
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
}),
'556677'
);
});
test('extractVerificationCode ignores compact header time before fallback code', () => {
const bundle = [
extractFunction('extractStrictChatGPTVerificationCode'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractLegacyStrictVerificationCode'),
extractFunction('isLikelyCompactTimeValue'),
extractFunction('isLikelyHeaderTimestampCode'),
extractFunction('findSafeStandaloneSixDigitCode'),
+30
View File
@@ -64,6 +64,36 @@ test('extractVerificationCodeFromMessages 支持显式过滤条件并跳过排
});
});
test('extractVerificationCodeFromMessages supports keyword hints and runtime code patterns without OpenAI-specific fallback', () => {
const result = extractVerificationCodeFromMessages([
{
From: { EmailAddress: { Address: 'alerts@example.com' } },
Subject: 'Security center',
BodyPreview: 'Use pin A-551199 to continue',
ReceivedDateTime: '2026-04-14T10:07:00.000Z',
Id: 'mail-1',
},
{
From: { EmailAddress: { Address: 'news@example.com' } },
Subject: 'Newsletter',
BodyPreview: 'pin A-000000',
ReceivedDateTime: '2026-04-14T10:06:00.000Z',
Id: 'mail-2',
},
], {
filterAfterTimestamp: 0,
senderFilters: [],
subjectFilters: [],
requiredKeywords: ['security'],
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
excludeCodes: [],
});
assert.equal(result.code, '551199');
assert.equal(result.messageId, 'mail-1');
assert.equal(result.sender, 'alerts@example.com');
});
test('normalizeMailboxId 将 Junk 归一为微软邮箱夹 ID', () => {
assert.equal(normalizeMailboxId('INBOX'), 'inbox');
assert.equal(normalizeMailboxId('junk'), 'junkemail');
+139
View File
@@ -0,0 +1,139 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/qq-mail.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('qq extractVerificationCode supports runtime mail rule patterns', () => {
const bundle = [
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
].join('\n');
const api = new Function(`
${bundle}
return { extractVerificationCode };
`)();
assert.equal(
api.extractVerificationCode('Mailbox notice: use pin A-441122 to continue.', {
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
}),
'441122'
);
});
test('qq handlePollEmail forwards runtime code patterns to new-mail matching', async () => {
const bundle = [
extractFunction('getCurrentMailIds'),
extractFunction('normalizeRulePatternList'),
extractFunction('extractCodeByRulePatterns'),
extractFunction('extractVerificationCode'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
let currentItems = [];
let refreshCount = 0;
function createMailItem(mailId, sender, subject, digest) {
return {
getAttribute(name) {
if (name === 'data-mailid') return mailId;
return '';
},
querySelector(selector) {
if (selector === '.cmp-account-nick') return { textContent: sender };
if (selector === '.mail-subject') return { textContent: subject };
if (selector === '.mail-digest') return { textContent: digest };
return null;
},
};
}
const document = {
querySelectorAll(selector) {
if (selector === '.mail-list-page-item[data-mailid]') {
return currentItems;
}
return [];
},
};
async function waitForElement() {
return true;
}
async function refreshInbox() {
refreshCount += 1;
if (refreshCount >= 1) {
currentItems = [
createMailItem('mail-1', 'alerts@example.com', 'Security center', 'Use pin A-551188 to continue'),
];
}
}
async function sleep() {}
function log() {}
${bundle}
return { handlePollEmail };
`)();
const result = await api.handlePollEmail(4, {
senderFilters: ['alerts'],
subjectFilters: ['security'],
maxAttempts: 2,
intervalMs: 1,
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
});
assert.equal(result.code, '551188');
});
@@ -196,6 +196,107 @@ test('startAutoRunFromCurrentSettings freezes run count before async settings sy
assert.equal(events[3].message.payload.totalRuns, 20);
});
test('startAutoRunFromCurrentSettings blocks when shared flow capability validation fails', async () => {
const bundle = [
extractFunction('normalizePendingAutoRunStartRunCount'),
extractFunction('registerPendingAutoRunStartRunCount'),
extractFunction('clearPendingAutoRunStartRunCount'),
extractFunction('startAutoRunFromCurrentSettings'),
].join('\n');
const api = new Function(`
const events = [];
const latestState = {
activeFlowId: 'site-a',
panelMode: 'cpa',
signupMethod: 'phone',
contributionMode: false,
phoneVerificationEnabled: true,
};
const inputAutoSkipFailures = { checked: false };
const inputContributionNickname = { value: 'tester' };
const inputContributionQq = { value: '123456' };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const btnAutoRun = { disabled: false, innerHTML: '' };
const inputRunCount = { disabled: false, value: '1' };
const inputPhoneVerificationEnabled = { checked: true };
const inputPlusModeEnabled = { checked: false };
let runCountValue = 1;
let pendingAutoRunStartTotalRuns = 0;
let pendingAutoRunStartExpiresAt = 0;
const chrome = {
runtime: {
async sendMessage(message) {
events.push({ type: 'send', message });
return { ok: true };
},
},
};
const console = {
warn(...args) {
events.push({ type: 'warn', args });
},
};
const window = {
MultiPageFlowCapabilities: {
createFlowCapabilityRegistry() {
return {
validateAutoRunStart() {
return {
ok: false,
errors: [{ message: '当前 flow 不支持手机号注册。' }],
};
},
};
},
},
};
async function sendSidepanelMessage(message) {
return chrome.runtime.sendMessage(message);
}
async function persistCurrentSettingsForAction() {
events.push({ type: 'sync-settings' });
}
function getRunCountValue() { return Math.max(1, Number(runCountValue) || 1); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function shouldOfferAutoModeChoice() { return false; }
async function openAutoStartChoiceDialog() { throw new Error('should not be called'); }
function getFirstUnfinishedStep() { return 1; }
function getRunningSteps() { return []; }
function shouldWarnAutoRunFallbackRisk() { return false; }
function isAutoRunFallbackRiskPromptDismissed() { return false; }
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
function setAutoRunFallbackRiskPromptDismissed() {}
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
return null;
}
async function ensureGpcApiKeyReadyForStart() {
return true;
}
${bundle}
return {
startAutoRunFromCurrentSettings,
getEvents() {
return events;
},
};
`)();
await assert.rejects(
() => api.startAutoRunFromCurrentSettings(),
/当前 flow 不支持手机号注册。/
);
assert.deepEqual(
api.getEvents().map((entry) => entry.type),
['refresh', 'sync-settings']
);
});
test('persistCurrentSettingsForAction forces a silent save even when settings are not marked dirty', async () => {
const bundle = [
extractFunction('waitForSettingsSaveIdle'),
+2 -2
View File
@@ -106,7 +106,7 @@ return {
assert.deepEqual(api.getStepIds(), [7]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email' },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email' },
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
});
@@ -266,7 +266,7 @@ return {
assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
});
});
+6
View File
@@ -16,6 +16,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(Array.isArray(steps), true);
assert.equal(steps.length, 10);
assert.equal(steps.every((step) => step.flowId === 'openai'), true);
assert.deepStrictEqual(
steps.map((step) => step.order),
steps.map((step) => step.order).slice().sort((left, right) => left - right)
@@ -68,6 +69,11 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), '');
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 13);
assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('site-a'), false);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']);
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');