feat: lay groundwork for multi-flow registries
This commit is contained in:
@@ -731,6 +731,9 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
|
||||
'async function getPersistedAliasState() {',
|
||||
' return {};',
|
||||
'}',
|
||||
'function buildStatePatchWithRuntimeState(_currentState, updates) {',
|
||||
' return updates;',
|
||||
'}',
|
||||
'const chrome = {',
|
||||
' storage: {',
|
||||
' session: {',
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports mail rule registry and OpenAI mail rules modules', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/mail-rule-registry\.js/);
|
||||
assert.match(source, /flows\/openai\/mail-rules\.js/);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
|
||||
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
|
||||
const openAiSource = fs.readFileSync('flows/openai/mail-rules.js', 'utf8');
|
||||
const registryApi = new Function('self', `${registrySource}; return self.MultiPageBackgroundMailRuleRegistry;`)({});
|
||||
const openAiApi = new Function('self', `${openAiSource}; return self.MultiPageOpenAiMailRules;`)({});
|
||||
|
||||
const openAiMailRules = openAiApi.createOpenAiMailRules({
|
||||
getHotmailVerificationRequestTimestamp: (step) => (step === 4 ? 123 : 456),
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
});
|
||||
const registry = registryApi.createMailRuleRegistry({
|
||||
defaultFlowId: 'openai',
|
||||
flowBuilders: {
|
||||
openai: openAiMailRules,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
registry.buildVerificationPollPayload(
|
||||
4,
|
||||
{
|
||||
activeFlowId: 'openai',
|
||||
email: 'user@example.com',
|
||||
mailProvider: '2925',
|
||||
mail2925Mode: 'receive',
|
||||
},
|
||||
{ excludeCodes: ['111111'] }
|
||||
),
|
||||
{
|
||||
flowId: 'openai',
|
||||
ruleId: 'openai-signup-code',
|
||||
step: 4,
|
||||
artifactType: 'code',
|
||||
filterAfterTimestamp: 0,
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
||||
targetEmail: 'user@example.com',
|
||||
mail2925MatchTargetEmail: true,
|
||||
maxAttempts: 15,
|
||||
intervalMs: 15000,
|
||||
excludeCodes: ['111111'],
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
registry.buildVerificationPollPayload(8, {
|
||||
activeFlowId: 'openai',
|
||||
email: 'user@example.com',
|
||||
step8VerificationTargetEmail: 'login@example.com',
|
||||
}),
|
||||
{
|
||||
flowId: 'openai',
|
||||
ruleId: 'openai-login-code',
|
||||
step: 8,
|
||||
artifactType: 'code',
|
||||
filterAfterTimestamp: 456,
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||
targetEmail: 'login@example.com',
|
||||
mail2925MatchTargetEmail: false,
|
||||
maxAttempts: 5,
|
||||
intervalMs: 3000,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('mail rule registry rejects unknown active flow ids instead of silently using OpenAI rules', () => {
|
||||
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
|
||||
const registryApi = new Function('self', `${registrySource}; return self.MultiPageBackgroundMailRuleRegistry;`)({});
|
||||
const registry = registryApi.createMailRuleRegistry({
|
||||
defaultFlowId: 'openai',
|
||||
flowBuilders: {},
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => registry.buildVerificationPollPayload(4, {
|
||||
activeFlowId: 'site-a',
|
||||
email: 'user@example.com',
|
||||
}),
|
||||
/未找到 flow=site-a 的邮件轮询规则构造器/
|
||||
);
|
||||
});
|
||||
@@ -162,3 +162,39 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
|
||||
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: false });
|
||||
assert.equal(logs.length, 0);
|
||||
});
|
||||
|
||||
test('SAVE_SETTING re-resolves signup method when panel mode changes', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
let state = {
|
||||
signupMethod: 'phone',
|
||||
phoneVerificationEnabled: true,
|
||||
plusModeEnabled: false,
|
||||
panelMode: 'sub2api',
|
||||
};
|
||||
|
||||
const router = api.createMessageRouter({
|
||||
addLog: async () => {},
|
||||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'panelMode')
|
||||
? { panelMode: input.panelMode }
|
||||
: {},
|
||||
broadcastDataUpdate: () => {},
|
||||
getState: async () => ({ ...state }),
|
||||
resolveSignupMethod: (nextState = {}) => nextState.panelMode === 'cpa' ? 'email' : 'phone',
|
||||
setPersistentSettings: async () => {},
|
||||
setState: async (updates) => {
|
||||
state = { ...state, ...updates };
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
payload: { panelMode: 'cpa' },
|
||||
});
|
||||
|
||||
assert.equal(response.ok, true);
|
||||
assert.equal(state.panelMode, 'cpa');
|
||||
assert.equal(state.signupMethod, 'email');
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ test('navigation utils recognize signup password pages for email and phone signu
|
||||
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/create-account/password'), true);
|
||||
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in/password'), true);
|
||||
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in'), false);
|
||||
assert.equal(utils.isSignupEntryHost('www.chatgpt.com'), true);
|
||||
});
|
||||
|
||||
test('navigation utils treat 126 mail hosts as part of the shared NetEase mail family', () => {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadRuntimeStateApi() {
|
||||
const source = fs.readFileSync('background/runtime-state.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundRuntimeState;`)(globalScope);
|
||||
}
|
||||
|
||||
test('background imports runtime-state module and wires state view helpers', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/runtime-state\.js/);
|
||||
assert.match(source, /createRuntimeStateHelpers/);
|
||||
assert.match(source, /buildStateViewWithRuntimeState/);
|
||||
assert.match(source, /buildStatePatchWithRuntimeState/);
|
||||
assert.match(source, /runtimeState:/);
|
||||
});
|
||||
|
||||
test('runtime-state module exposes a factory', () => {
|
||||
const api = loadRuntimeStateApi();
|
||||
assert.equal(typeof api?.createRuntimeStateHelpers, 'function');
|
||||
});
|
||||
|
||||
test('runtime-state view derives canonical flow metadata from legacy step state', () => {
|
||||
const api = loadRuntimeStateApi();
|
||||
const helpers = api.createRuntimeStateHelpers({
|
||||
DEFAULT_ACTIVE_FLOW_ID: 'openai',
|
||||
defaultStepStatuses: {
|
||||
1: 'pending',
|
||||
2: 'pending',
|
||||
10: 'pending',
|
||||
},
|
||||
getStepDefinitionForState(step) {
|
||||
return {
|
||||
1: { id: 1, key: 'open-chatgpt' },
|
||||
2: { id: 2, key: 'submit-signup-email' },
|
||||
10: { id: 10, key: 'oauth-login' },
|
||||
}[Number(step)] || null;
|
||||
},
|
||||
});
|
||||
|
||||
const view = helpers.buildStateView({
|
||||
currentStep: 2,
|
||||
stepStatuses: {
|
||||
1: 'completed',
|
||||
2: 'running',
|
||||
},
|
||||
oauthUrl: 'https://auth.example.com/start',
|
||||
plusCheckoutTabId: 88,
|
||||
currentPhoneActivation: {
|
||||
activationId: 'active-1',
|
||||
phoneNumber: '+447700900123',
|
||||
},
|
||||
tabRegistry: {
|
||||
'signup-page': { tabId: 12 },
|
||||
},
|
||||
sourceLastUrls: {
|
||||
'signup-page': 'https://auth.example.com/start',
|
||||
},
|
||||
flowStartTime: 12345,
|
||||
});
|
||||
|
||||
assert.equal(view.activeFlowId, 'openai');
|
||||
assert.equal(view.currentNodeId, 'submit-signup-email');
|
||||
assert.deepStrictEqual(view.legacyStepCompat, {
|
||||
currentStep: 2,
|
||||
stepStatuses: {
|
||||
1: 'completed',
|
||||
2: 'running',
|
||||
10: 'pending',
|
||||
},
|
||||
});
|
||||
assert.deepStrictEqual(view.nodeStatuses, {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'running',
|
||||
'oauth-login': 'pending',
|
||||
});
|
||||
assert.equal(view.runtimeState.flowState.openai.auth.oauthUrl, 'https://auth.example.com/start');
|
||||
assert.equal(view.runtimeState.flowState.openai.plus.plusCheckoutTabId, 88);
|
||||
assert.deepStrictEqual(view.runtimeState.flowState.openai.phoneVerification.currentPhoneActivation, {
|
||||
activationId: 'active-1',
|
||||
phoneNumber: '+447700900123',
|
||||
});
|
||||
assert.deepStrictEqual(view.sharedState, {
|
||||
tabRegistry: {
|
||||
'signup-page': { tabId: 12 },
|
||||
},
|
||||
sourceLastUrls: {
|
||||
'signup-page': 'https://auth.example.com/start',
|
||||
},
|
||||
flowStartTime: 12345,
|
||||
});
|
||||
});
|
||||
|
||||
test('runtime-state patch accepts nested flow updates while keeping legacy compatibility fields in sync', () => {
|
||||
const api = loadRuntimeStateApi();
|
||||
const helpers = api.createRuntimeStateHelpers({
|
||||
DEFAULT_ACTIVE_FLOW_ID: 'openai',
|
||||
defaultStepStatuses: {
|
||||
1: 'pending',
|
||||
2: 'pending',
|
||||
10: 'pending',
|
||||
},
|
||||
getStepDefinitionForState(step) {
|
||||
return {
|
||||
1: { id: 1, key: 'open-chatgpt' },
|
||||
2: { id: 2, key: 'submit-signup-email' },
|
||||
10: { id: 10, key: 'oauth-login' },
|
||||
}[Number(step)] || null;
|
||||
},
|
||||
});
|
||||
|
||||
const patch = helpers.buildSessionStatePatch({
|
||||
currentStep: 1,
|
||||
stepStatuses: {
|
||||
1: 'running',
|
||||
2: 'pending',
|
||||
10: 'pending',
|
||||
},
|
||||
oauthUrl: 'https://old.example.com/start',
|
||||
}, {
|
||||
runtimeState: {
|
||||
activeRunId: 'run-001',
|
||||
flowState: {
|
||||
openai: {
|
||||
auth: {
|
||||
oauthUrl: 'https://new.example.com/start',
|
||||
},
|
||||
plus: {
|
||||
plusCheckoutTabId: 99,
|
||||
},
|
||||
},
|
||||
},
|
||||
legacyStepCompat: {
|
||||
currentStep: 10,
|
||||
stepStatuses: {
|
||||
1: 'completed',
|
||||
10: 'running',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(patch.activeFlowId, 'openai');
|
||||
assert.equal(patch.activeRunId, 'run-001');
|
||||
assert.equal(patch.currentNodeId, 'oauth-login');
|
||||
assert.equal(patch.oauthUrl, 'https://new.example.com/start');
|
||||
assert.equal(patch.plusCheckoutTabId, 99);
|
||||
assert.equal(patch.currentStep, 10);
|
||||
assert.deepStrictEqual(patch.stepStatuses, {
|
||||
1: 'completed',
|
||||
2: 'pending',
|
||||
10: 'running',
|
||||
});
|
||||
assert.deepStrictEqual(patch.nodeStatuses, {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'pending',
|
||||
'oauth-login': 'running',
|
||||
});
|
||||
assert.equal(patch.runtimeState.flowState.openai.auth.oauthUrl, 'https://new.example.com/start');
|
||||
assert.equal(patch.runtimeState.flowState.openai.plus.plusCheckoutTabId, 99);
|
||||
});
|
||||
@@ -95,6 +95,51 @@ return {
|
||||
assert.equal(api.logs.some((entry) => /固定为邮箱注册/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
test('signup method resolution respects the shared flow capability registry when available', () => {
|
||||
const api = new Function(`
|
||||
const self = {
|
||||
MultiPageFlowCapabilities: {
|
||||
createFlowCapabilityRegistry() {
|
||||
return {
|
||||
resolveSidepanelCapabilities({ state = {}, signupMethod = 'email' } = {}) {
|
||||
const phoneAllowed = String(state?.activeFlowId || '').trim().toLowerCase() === 'openai';
|
||||
return {
|
||||
canUsePhoneSignup: phoneAllowed,
|
||||
effectiveSignupMethod: signupMethod === 'phone' && phoneAllowed ? 'phone' : 'email',
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
const SIGNUP_METHOD_EMAIL = 'email';
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
${extractFunction('normalizeSignupMethod')}
|
||||
${extractFunction('canUsePhoneSignup')}
|
||||
${extractFunction('resolveSignupMethod')}
|
||||
return {
|
||||
canUsePhoneSignup,
|
||||
resolveSignupMethod,
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.canUsePhoneSignup({
|
||||
activeFlowId: 'site-a',
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
}), false);
|
||||
assert.equal(api.resolveSignupMethod({
|
||||
activeFlowId: 'site-a',
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
}), 'email');
|
||||
assert.equal(api.resolveSignupMethod({
|
||||
activeFlowId: 'openai',
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
}), 'phone');
|
||||
});
|
||||
|
||||
test('background step definitions resolve titles from the frozen signup method', () => {
|
||||
const api = new Function(`
|
||||
const captured = [];
|
||||
|
||||
@@ -16,6 +16,76 @@ test('tab runtime module exposes a factory', () => {
|
||||
assert.equal(typeof api?.createTabRuntime, 'function');
|
||||
});
|
||||
|
||||
test('tab runtime accepts canonical openai-auth readiness for queued signup-page commands', async () => {
|
||||
const runtimeSource = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const registrySource = fs.readFileSync('shared/source-registry.js', 'utf8');
|
||||
const runtimeApi = new Function('self', `${runtimeSource}; return self.MultiPageBackgroundTabRuntime;`)({});
|
||||
const registryApi = new Function('self', `${registrySource}; return self.MultiPageSourceRegistry;`)({});
|
||||
const sourceRegistry = registryApi.createSourceRegistry();
|
||||
|
||||
const sentMessages = [];
|
||||
let currentState = {
|
||||
tabRegistry: {
|
||||
'signup-page': { tabId: 91, ready: true },
|
||||
},
|
||||
sourceLastUrls: {
|
||||
'signup-page': 'https://auth.openai.com/authorize',
|
||||
},
|
||||
};
|
||||
|
||||
const runtime = runtimeApi.createTabRuntime({
|
||||
LOG_PREFIX: '[test]',
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async (tabId) => ({
|
||||
id: tabId,
|
||||
windowId: 1,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
status: 'complete',
|
||||
}),
|
||||
query: async () => [],
|
||||
sendMessage: async (tabId, message) => {
|
||||
if (message.type === 'PING') {
|
||||
return { ok: true, source: 'openai-auth' };
|
||||
}
|
||||
sentMessages.push({ tabId, message });
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
},
|
||||
getSourceLabel: (source) => source || 'unknown',
|
||||
getState: async () => currentState,
|
||||
matchesSourceUrlFamily: (source, candidateUrl, referenceUrl) => (
|
||||
sourceRegistry.matchesSourceUrlFamily(source, candidateUrl, referenceUrl)
|
||||
),
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sourceRegistry,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
assert.equal(await runtime.getTabId('openai-auth'), 91);
|
||||
|
||||
currentState = {
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
};
|
||||
|
||||
const queued = runtime.queueCommand('signup-page', { type: 'STEP2_TEST' }, 1000);
|
||||
runtime.flushCommand('openai-auth', 55);
|
||||
await assert.doesNotReject(queued);
|
||||
assert.deepEqual(sentMessages, [{ tabId: 55, message: { type: 'STEP2_TEST' } }]);
|
||||
|
||||
await runtime.ensureContentScriptReadyOnTab('signup-page', 77, {
|
||||
timeoutMs: 100,
|
||||
});
|
||||
|
||||
assert.deepEqual(currentState.tabRegistry['openai-auth'], { tabId: 77, ready: true, windowId: 1 });
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(currentState.tabRegistry, 'signup-page'), false);
|
||||
});
|
||||
|
||||
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 = {};
|
||||
|
||||
@@ -81,6 +81,38 @@ return { detectScriptSource };
|
||||
);
|
||||
});
|
||||
|
||||
test('detectScriptSource maps OpenAI auth hosts to canonical openai-auth source', () => {
|
||||
const bundle = [extractFunction('detectScriptSource')].join('\n');
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { detectScriptSource };
|
||||
`)();
|
||||
|
||||
assert.equal(
|
||||
api.detectScriptSource({
|
||||
url: 'https://auth.openai.com/create-account',
|
||||
hostname: 'auth.openai.com',
|
||||
}),
|
||||
'openai-auth'
|
||||
);
|
||||
});
|
||||
|
||||
test('detectScriptSource returns unknown-source for unrecognized pages', () => {
|
||||
const bundle = [extractFunction('detectScriptSource')].join('\n');
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { detectScriptSource };
|
||||
`)();
|
||||
|
||||
assert.equal(
|
||||
api.detectScriptSource({
|
||||
url: 'https://example.com/',
|
||||
hostname: 'example.com',
|
||||
}),
|
||||
'unknown-source'
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldReportReadyForFrame suppresses noisy plus checkout child frame ready logs', () => {
|
||||
const bundle = [extractFunction('shouldReportReadyForFrame')].join('\n');
|
||||
const api = new Function(`
|
||||
@@ -91,6 +123,8 @@ return { shouldReportReadyForFrame };
|
||||
assert.equal(api.shouldReportReadyForFrame('plus-checkout', true), false);
|
||||
assert.equal(api.shouldReportReadyForFrame('plus-checkout', false), true);
|
||||
assert.equal(api.shouldReportReadyForFrame('paypal-flow', true), true);
|
||||
assert.equal(api.shouldReportReadyForFrame('unknown-source', false), true);
|
||||
assert.equal(api.shouldReportReadyForFrame('unknown-source', true), false);
|
||||
});
|
||||
|
||||
test('getRuntimeScriptSource follows injected source overrides after utils is already loaded', () => {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('shared/flow-capabilities.js', 'utf8');
|
||||
|
||||
function loadApi() {
|
||||
const scope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageFlowCapabilities;`)(scope);
|
||||
}
|
||||
|
||||
test('flow capability registry keeps OpenAI phone signup available only when runtime locks allow it', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const enabledState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'openai',
|
||||
panelMode: 'cpa',
|
||||
phoneVerificationEnabled: true,
|
||||
plusModeEnabled: false,
|
||||
contributionMode: false,
|
||||
signupMethod: 'phone',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(enabledState.canUsePhoneSignup, true);
|
||||
assert.equal(enabledState.effectiveSignupMethod, 'phone');
|
||||
assert.equal(enabledState.shouldWarnCpaPhoneSignup, true);
|
||||
assert.deepEqual(enabledState.effectiveSignupMethods, ['email', 'phone']);
|
||||
|
||||
const plusLockedState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'openai',
|
||||
panelMode: 'sub2api',
|
||||
phoneVerificationEnabled: true,
|
||||
plusModeEnabled: true,
|
||||
contributionMode: false,
|
||||
signupMethod: 'phone',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(plusLockedState.canUsePhoneSignup, false);
|
||||
assert.equal(plusLockedState.effectiveSignupMethod, 'email');
|
||||
assert.equal(plusLockedState.shouldWarnCpaPhoneSignup, false);
|
||||
assert.deepEqual(plusLockedState.effectiveSignupMethods, ['email']);
|
||||
});
|
||||
|
||||
test('flow capability registry defaults unknown flows to minimal non-phone capabilities', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'codex2api',
|
||||
phoneVerificationEnabled: true,
|
||||
plusModeEnabled: true,
|
||||
contributionMode: true,
|
||||
signupMethod: 'phone',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(capabilityState.activeFlowId, 'site-a');
|
||||
assert.equal(capabilityState.canShowPhoneSettings, false);
|
||||
assert.equal(capabilityState.canShowPlusSettings, false);
|
||||
assert.equal(capabilityState.canShowLuckmail, false);
|
||||
assert.equal(capabilityState.canUsePhoneSignup, false);
|
||||
assert.equal(capabilityState.effectiveSignupMethod, 'email');
|
||||
assert.equal(capabilityState.panelMode, 'codex2api');
|
||||
assert.deepEqual(capabilityState.supportedPanelModes, []);
|
||||
});
|
||||
@@ -294,6 +294,58 @@ return {
|
||||
assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), false);
|
||||
});
|
||||
|
||||
test('sidepanel phone signup gating can follow the shared flow capability registry', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeSignupMethod'),
|
||||
extractFunction('normalizePanelMode'),
|
||||
extractFunction('canSelectPhoneSignupMethod'),
|
||||
extractFunction('shouldWarnCpaPhoneSignup'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const window = {
|
||||
MultiPageFlowCapabilities: {
|
||||
createFlowCapabilityRegistry() {
|
||||
return {
|
||||
resolveSidepanelCapabilities({ state = {}, panelMode = 'cpa', signupMethod = 'email' } = {}) {
|
||||
const phoneAllowed = String(state?.activeFlowId || '').trim().toLowerCase() === 'openai';
|
||||
return {
|
||||
canSelectPhoneSignup: phoneAllowed,
|
||||
shouldWarnCpaPhoneSignup: phoneAllowed && signupMethod === 'phone' && panelMode === 'cpa',
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
let latestState = {
|
||||
activeFlowId: 'site-a',
|
||||
contributionMode: false,
|
||||
panelMode: 'cpa',
|
||||
};
|
||||
const inputPhoneVerificationEnabled = { checked: true };
|
||||
const inputPlusModeEnabled = { checked: false };
|
||||
function getSelectedPanelMode() { return 'cpa'; }
|
||||
function getSelectedSignupMethod() { return 'phone'; }
|
||||
function isCpaPhoneSignupPromptDismissed() { return false; }
|
||||
${bundle}
|
||||
return {
|
||||
canSelectPhoneSignupMethod,
|
||||
shouldWarnCpaPhoneSignup,
|
||||
setFlow(flowId) {
|
||||
latestState.activeFlowId = flowId;
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.canSelectPhoneSignupMethod(), false);
|
||||
assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), false);
|
||||
|
||||
api.setFlow('openai');
|
||||
assert.equal(api.canSelectPhoneSignupMethod(), true);
|
||||
assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), true);
|
||||
});
|
||||
|
||||
test('manual step 3 uses phone identity without requiring registration email', () => {
|
||||
const api = new Function(`
|
||||
let latestState = { signupMethod: 'phone', phoneVerificationEnabled: true, signupPhoneNumber: '+441111111111', accountIdentifierType: 'phone', accountIdentifier: '+441111111111' };
|
||||
|
||||
@@ -124,8 +124,8 @@ test('sidepanel signup method UI syncs shared step definitions with the selected
|
||||
|
||||
test('sidepanel applies restored signup method when rebuilding shared step definitions on load', () => {
|
||||
const source = extractFunction('applySettingsState');
|
||||
assert.match(source, /syncStepDefinitionsForMode\(Boolean\(state\?\.plusModeEnabled\),\s*\{/);
|
||||
assert.match(source, /signupMethod:\s*state\?\.signupMethod/);
|
||||
assert.match(source, /resolveStepDefinitionCapabilityState\(state/);
|
||||
assert.match(source, /signupMethod:\s*stepDefinitionState\.signupMethod/);
|
||||
});
|
||||
|
||||
test('sidepanel Plus UI hides PayPal account selector while GoPay is selected', () => {
|
||||
@@ -165,6 +165,62 @@ return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
|
||||
assert.equal(api.rowPayPalAccount.style.display, '');
|
||||
});
|
||||
|
||||
test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('normalizeGpcAutoModePermissionValue'),
|
||||
extractFunction('getGpcAutoModePermissionFromPayload'),
|
||||
extractFunction('shouldPreserveSelectedGpcAutoMode'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const window = {
|
||||
MultiPageFlowCapabilities: {
|
||||
createFlowCapabilityRegistry() {
|
||||
return {
|
||||
resolveSidepanelCapabilities() {
|
||||
return {
|
||||
canShowPlusSettings: false,
|
||||
runtimeLocks: { plusModeEnabled: false },
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
let latestState = { plusPaymentMethod: 'paypal' };
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const rowPlusMode = { style: { display: '' } };
|
||||
const selectPlusPaymentMethod = { value: 'paypal', style: { display: '' } };
|
||||
const rowPlusPaymentMethod = { style: { display: '' } };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
${bundle}
|
||||
return {
|
||||
rowPlusMode,
|
||||
rowPlusPaymentMethod,
|
||||
rowPayPalAccount,
|
||||
selectPlusPaymentMethod,
|
||||
updatePlusModeUI,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rowPlusMode.style.display, 'none');
|
||||
assert.equal(api.rowPlusPaymentMethod.style.display, 'none');
|
||||
assert.equal(api.rowPayPalAccount.style.display, 'none');
|
||||
assert.equal(api.selectPlusPaymentMethod.style.display, 'none');
|
||||
});
|
||||
|
||||
test('sidepanel step definitions keep GPC helper mode distinct', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeSignupMethod'),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports shared source registry module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /shared\/source-registry\.js/);
|
||||
});
|
||||
|
||||
test('manifest loads shared source registry before content utils in static bundles', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
for (const entry of manifest.content_scripts || []) {
|
||||
const scripts = Array.isArray(entry.js) ? entry.js : [];
|
||||
if (!scripts.includes('content/utils.js')) continue;
|
||||
assert.ok(scripts.includes('shared/source-registry.js'));
|
||||
assert.ok(
|
||||
scripts.indexOf('shared/source-registry.js') < scripts.indexOf('content/utils.js'),
|
||||
'shared/source-registry.js must load before content/utils.js'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('shared source registry exposes canonical source, alias, detection, and ready policies', () => {
|
||||
const source = fs.readFileSync('shared/source-registry.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageSourceRegistry;`)({});
|
||||
const registry = api.createSourceRegistry();
|
||||
|
||||
assert.equal(registry.resolveCanonicalSource('signup-page'), 'openai-auth');
|
||||
assert.deepEqual(registry.getSourceKeys('signup-page'), ['openai-auth', 'signup-page']);
|
||||
assert.equal(registry.getSourceLabel('openai-auth'), '认证页');
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'openai-auth',
|
||||
'https://chatgpt.com/',
|
||||
'https://auth.openai.com/authorize?client_id=test'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://auth.openai.com/create-account',
|
||||
hostname: 'auth.openai.com',
|
||||
}),
|
||||
'openai-auth'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://example.com/',
|
||||
hostname: 'example.com',
|
||||
}),
|
||||
'unknown-source'
|
||||
);
|
||||
assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
|
||||
assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
|
||||
assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundVeri
|
||||
function createVerificationFlowTestHelpers(overrides = {}) {
|
||||
return api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
buildVerificationPollPayload: null,
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
@@ -43,6 +44,42 @@ function createVerificationFlowTestHelpers(overrides = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
test('verification flow prefers injected verification poll payload builder when provided', () => {
|
||||
const helpers = createVerificationFlowTestHelpers({
|
||||
buildVerificationPollPayload: (step, state, overrides = {}) => ({
|
||||
flowId: state.activeFlowId || 'openai',
|
||||
ruleId: 'custom-rule',
|
||||
step,
|
||||
senderFilters: ['custom-sender'],
|
||||
subjectFilters: ['custom-subject'],
|
||||
targetEmail: state.email,
|
||||
maxAttempts: 9,
|
||||
intervalMs: 4321,
|
||||
...overrides,
|
||||
}),
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
helpers.getVerificationPollPayload(4, {
|
||||
activeFlowId: 'openai',
|
||||
email: 'user@example.com',
|
||||
}, {
|
||||
excludeCodes: ['111111'],
|
||||
}),
|
||||
{
|
||||
flowId: 'openai',
|
||||
ruleId: 'custom-rule',
|
||||
step: 4,
|
||||
senderFilters: ['custom-sender'],
|
||||
subjectFilters: ['custom-subject'],
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 9,
|
||||
intervalMs: 4321,
|
||||
excludeCodes: ['111111'],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('verification flow keeps 2925 polling cadence in the default payload', () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
|
||||
Reference in New Issue
Block a user