986 lines
35 KiB
JavaScript
986 lines
35 KiB
JavaScript
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
|
||
test('background imports message router module', () => {
|
||
const source = fs.readFileSync('background.js', 'utf8');
|
||
assert.match(source, /background\/message-router\.js/);
|
||
});
|
||
|
||
test('background defaults enable free phone reuse switches', () => {
|
||
const source = fs.readFileSync('background.js', 'utf8');
|
||
const defaultsStart = source.indexOf('const PERSISTED_SETTING_DEFAULTS = {');
|
||
const defaultsEnd = source.indexOf('const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);');
|
||
const defaultsBlock = source.slice(defaultsStart, defaultsEnd);
|
||
|
||
assert.match(defaultsBlock, /freePhoneReuseEnabled:\s*true/);
|
||
assert.match(defaultsBlock, /freePhoneReuseAutoEnabled:\s*true/);
|
||
assert.match(defaultsBlock, /phoneSmsReuseEnabled:\s*DEFAULT_HERO_SMS_REUSE_ENABLED/);
|
||
});
|
||
|
||
test('background free reusable phone setter does not depend on module-scoped phone flow constants', () => {
|
||
const source = fs.readFileSync('background.js', 'utf8');
|
||
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
|
||
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
|
||
const setterBlock = source.slice(setterStart, setterEnd);
|
||
|
||
assert.ok(setterStart >= 0, 'expected setFreeReusablePhoneActivation to exist');
|
||
assert.doesNotMatch(setterBlock, /DEFAULT_PHONE_NUMBER_MAX_USES/);
|
||
assert.match(setterBlock, /maxUses:\s*Math\.max\(1,\s*Math\.floor\(Number\(record\.maxUses\)\s*\|\|\s*3\)\)/);
|
||
});
|
||
|
||
test('background free reusable phone setter can recover local HeroSMS activation id by phone number', () => {
|
||
const source = fs.readFileSync('background.js', 'utf8');
|
||
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
|
||
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
|
||
const setterBlock = source.slice(setterStart, setterEnd);
|
||
|
||
assert.match(source, /function findLocalHeroSmsActivationForPhone\(/);
|
||
assert.match(source, /state\.currentPhoneActivation/);
|
||
assert.match(source, /state\.reusablePhoneActivation/);
|
||
assert.match(source, /state\.signupPhoneActivation/);
|
||
assert.match(source, /state\.signupPhoneCompletedActivation/);
|
||
assert.match(source, /state\.phonePreferredActivation/);
|
||
assert.match(source, /state\.phoneReusableActivationPool/);
|
||
assert.match(setterBlock, /findLocalHeroSmsActivationForPhone\(state,\s*phoneNumber\)/);
|
||
assert.match(setterBlock, /activationId = String\(\s*record\.activationId[\s\S]*localActivation\?\.activationId/);
|
||
assert.match(setterBlock, /manualOnly:\s*!activationId/);
|
||
});
|
||
|
||
test('background blocks free reusable phone mutations while phone signup owns the identity', () => {
|
||
const source = fs.readFileSync('background.js', 'utf8');
|
||
const clearStart = source.indexOf('async function clearFreeReusablePhoneActivation');
|
||
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
|
||
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
|
||
const clearBlock = source.slice(clearStart, setterStart);
|
||
const setterBlock = source.slice(setterStart, setterEnd);
|
||
|
||
assert.match(source, /function isPhoneSignupIdentityStateForReuse\(/);
|
||
assert.match(source, /function hasSignupPhoneActivationState\(/);
|
||
assert.match(clearBlock, /isPhoneSignupIdentityStateForReuse\(state\)/);
|
||
assert.match(setterBlock, /isPhoneSignupIdentityStateForReuse\(state\)/);
|
||
});
|
||
|
||
test('background HeroSMS phone prefix inference covers built-in major countries', () => {
|
||
const source = fs.readFileSync('background.js', 'utf8');
|
||
const supportedStart = source.indexOf('const HERO_SMS_SUPPORTED_COUNTRY_IDS = [');
|
||
const prefixStart = source.indexOf('const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([');
|
||
const prefixEnd = source.indexOf(']);', prefixStart);
|
||
const supportedBlock = source.slice(supportedStart, source.indexOf('];', supportedStart));
|
||
const prefixBlock = source.slice(prefixStart, prefixEnd);
|
||
|
||
assert.match(supportedBlock, /\[6,\s*52,\s*187,\s*16,\s*151,\s*43,\s*73,\s*10/);
|
||
[
|
||
['84', 10, 'Vietnam'],
|
||
['66', 52, 'Thailand'],
|
||
['62', 6, 'Indonesia'],
|
||
['44', 16, 'United Kingdom'],
|
||
['81', 151, 'Japan'],
|
||
['49', 43, 'Germany'],
|
||
['33', 73, 'France'],
|
||
['1', 187, 'USA'],
|
||
].forEach(([prefix, id, label]) => {
|
||
assert.match(prefixBlock, new RegExp(`prefix:\\s*'${prefix}'[\\s\\S]*id:\\s*${id}[\\s\\S]*label:\\s*'${label}'`));
|
||
});
|
||
});
|
||
|
||
test('message router module exposes a factory', () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = {};
|
||
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
|
||
assert.equal(typeof api?.createMessageRouter, 'function');
|
||
});
|
||
|
||
test('SAVE_SETTING broadcasts free phone reuse setting updates for realtime sidepanel sync', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const broadcasts = [];
|
||
let state = {
|
||
freePhoneReuseEnabled: false,
|
||
freePhoneReuseAutoEnabled: false,
|
||
plusModeEnabled: false,
|
||
plusPaymentMethod: 'paypal',
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => {
|
||
const updates = {};
|
||
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseEnabled')) {
|
||
updates.freePhoneReuseEnabled = Boolean(input.freePhoneReuseEnabled);
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseAutoEnabled')) {
|
||
updates.freePhoneReuseAutoEnabled = Boolean(input.freePhoneReuseAutoEnabled);
|
||
}
|
||
return updates;
|
||
},
|
||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||
getState: async () => ({ ...state }),
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: {
|
||
freePhoneReuseEnabled: true,
|
||
freePhoneReuseAutoEnabled: true,
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.freePhoneReuseEnabled, true);
|
||
assert.equal(state.freePhoneReuseAutoEnabled, true);
|
||
assert.ok(
|
||
broadcasts.some((payload) => (
|
||
payload.freePhoneReuseEnabled === true
|
||
&& payload.freePhoneReuseAutoEnabled === true
|
||
)),
|
||
'expected SAVE_SETTING to broadcast free reuse switch updates'
|
||
);
|
||
});
|
||
|
||
test('SAVE_SETTING preserves phone reuse preferences while phone signup is selected', 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 = {
|
||
signupMethod: 'phone',
|
||
phoneVerificationEnabled: true,
|
||
plusModeEnabled: false,
|
||
phoneSmsReuseEnabled: true,
|
||
heroSmsReuseEnabled: true,
|
||
freePhoneReuseEnabled: true,
|
||
freePhoneReuseAutoEnabled: true,
|
||
phonePreferredActivation: {
|
||
provider: 'hero-sms',
|
||
activationId: 'stored',
|
||
phoneNumber: '66950001111',
|
||
},
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => ({
|
||
signupMethod: String(input.signupMethod || state.signupMethod),
|
||
phoneSmsReuseEnabled: Boolean(input.phoneSmsReuseEnabled),
|
||
heroSmsReuseEnabled: Boolean(input.heroSmsReuseEnabled),
|
||
freePhoneReuseEnabled: Boolean(input.freePhoneReuseEnabled),
|
||
freePhoneReuseAutoEnabled: Boolean(input.freePhoneReuseAutoEnabled),
|
||
phonePreferredActivation: input.phonePreferredActivation ?? null,
|
||
}),
|
||
broadcastDataUpdate: () => {},
|
||
getState: async () => ({ ...state }),
|
||
setPersistentSettings: async (updates) => {
|
||
persistedPayloads.push({ ...updates });
|
||
return { ...updates };
|
||
},
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: {
|
||
signupMethod: 'phone',
|
||
phoneSmsReuseEnabled: false,
|
||
heroSmsReuseEnabled: false,
|
||
freePhoneReuseEnabled: false,
|
||
freePhoneReuseAutoEnabled: false,
|
||
phonePreferredActivation: null,
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.phoneSmsReuseEnabled, true);
|
||
assert.equal(state.heroSmsReuseEnabled, true);
|
||
assert.equal(state.freePhoneReuseEnabled, true);
|
||
assert.equal(state.freePhoneReuseAutoEnabled, true);
|
||
assert.deepStrictEqual(state.phonePreferredActivation, {
|
||
provider: 'hero-sms',
|
||
activationId: 'stored',
|
||
phoneNumber: '66950001111',
|
||
});
|
||
assert.equal(persistedPayloads[0].phoneSmsReuseEnabled, true);
|
||
assert.equal(persistedPayloads[0].freePhoneReuseEnabled, true);
|
||
});
|
||
|
||
test('SAVE_SETTING allows phone reuse preferences after switching back to email signup', 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,
|
||
phoneSmsReuseEnabled: true,
|
||
heroSmsReuseEnabled: true,
|
||
freePhoneReuseEnabled: true,
|
||
freePhoneReuseAutoEnabled: true,
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => ({
|
||
signupMethod: String(input.signupMethod || state.signupMethod),
|
||
phoneSmsReuseEnabled: Boolean(input.phoneSmsReuseEnabled),
|
||
heroSmsReuseEnabled: Boolean(input.heroSmsReuseEnabled),
|
||
freePhoneReuseEnabled: Boolean(input.freePhoneReuseEnabled),
|
||
freePhoneReuseAutoEnabled: Boolean(input.freePhoneReuseAutoEnabled),
|
||
}),
|
||
broadcastDataUpdate: () => {},
|
||
getState: async () => ({ ...state }),
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: {
|
||
signupMethod: 'email',
|
||
phoneSmsReuseEnabled: false,
|
||
heroSmsReuseEnabled: false,
|
||
freePhoneReuseEnabled: false,
|
||
freePhoneReuseAutoEnabled: false,
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.signupMethod, 'email');
|
||
assert.equal(state.phoneSmsReuseEnabled, false);
|
||
assert.equal(state.heroSmsReuseEnabled, false);
|
||
assert.equal(state.freePhoneReuseEnabled, false);
|
||
assert.equal(state.freePhoneReuseAutoEnabled, false);
|
||
});
|
||
|
||
test('SAVE_SETTING broadcasts operation delay setting without background success log', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const broadcasts = [];
|
||
const logs = [];
|
||
let state = { operationDelayEnabled: true, plusModeEnabled: false, plusPaymentMethod: 'paypal' };
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async (message, level = 'info') => logs.push({ message, level }),
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'operationDelayEnabled')
|
||
? { operationDelayEnabled: true }
|
||
: {},
|
||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||
getState: async () => ({ ...state }),
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => { state = { ...state, ...updates }; },
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
source: 'sidepanel',
|
||
payload: { operationDelayEnabled: false },
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.operationDelayEnabled, true);
|
||
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: true });
|
||
assert.equal(logs.length, 0);
|
||
});
|
||
|
||
test('SAVE_SETTING rebuilds Plus node statuses when the account access strategy changes', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const broadcasts = [];
|
||
let state = {
|
||
plusModeEnabled: true,
|
||
plusPaymentMethod: 'paypal',
|
||
plusAccountAccessStrategy: 'oauth',
|
||
oauthUrl: 'https://oauth.example/current',
|
||
localhostUrl: 'http://localhost:38080/callback',
|
||
oauthFlowDeadlineAt: Date.now() + 60000,
|
||
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
|
||
cpaOAuthState: 'cpa-state',
|
||
cpaManagementOrigin: 'https://cpa.example.com',
|
||
sub2apiSessionId: 'sub-session',
|
||
sub2apiOAuthState: 'sub-oauth-state',
|
||
sub2apiGroupId: 'group-id',
|
||
sub2apiGroupIds: ['group-id'],
|
||
sub2apiDraftName: 'draft-name',
|
||
sub2apiProxyId: 'proxy-id',
|
||
codex2apiSessionId: 'codex-session',
|
||
codex2apiOAuthState: 'codex-oauth-state',
|
||
plusManualConfirmationPending: true,
|
||
plusManualConfirmationRequestId: 'gopay-req',
|
||
plusManualConfirmationStep: 9,
|
||
plusManualConfirmationMethod: 'gopay',
|
||
plusManualConfirmationTitle: 'GoPay 订阅确认',
|
||
plusManualConfirmationMessage: '完成后继续 OAuth 登录。',
|
||
currentNodeId: 'confirm-oauth',
|
||
nodeStatuses: {
|
||
'open-chatgpt': 'completed',
|
||
'plus-checkout-create': 'completed',
|
||
'plus-checkout-billing': 'completed',
|
||
'paypal-approve': 'completed',
|
||
'plus-checkout-return': 'completed',
|
||
'oauth-login': 'completed',
|
||
'fetch-login-code': 'completed',
|
||
'post-login-phone-verification': 'completed',
|
||
'confirm-oauth': 'running',
|
||
'platform-verify': 'pending',
|
||
},
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'plusAccountAccessStrategy')
|
||
? { plusAccountAccessStrategy: input.plusAccountAccessStrategy }
|
||
: {},
|
||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||
getNodeIdsForState: (nextState = {}) => (
|
||
String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
|
||
? [
|
||
'open-chatgpt',
|
||
'plus-checkout-create',
|
||
'plus-checkout-billing',
|
||
'paypal-approve',
|
||
'plus-checkout-return',
|
||
'sub2api-session-import',
|
||
]
|
||
: [
|
||
'open-chatgpt',
|
||
'plus-checkout-create',
|
||
'plus-checkout-billing',
|
||
'paypal-approve',
|
||
'plus-checkout-return',
|
||
'oauth-login',
|
||
'fetch-login-code',
|
||
'post-login-phone-verification',
|
||
'confirm-oauth',
|
||
'platform-verify',
|
||
]
|
||
),
|
||
getState: async () => ({ ...state }),
|
||
getStepIdsForState: () => [],
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: {
|
||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||
assert.equal(state.currentNodeId, '');
|
||
assert.equal(state.oauthUrl, null);
|
||
assert.equal(state.localhostUrl, null);
|
||
assert.equal(state.oauthFlowDeadlineAt, null);
|
||
assert.equal(state.oauthFlowDeadlineSourceUrl, null);
|
||
assert.equal(state.cpaOAuthState, null);
|
||
assert.equal(state.cpaManagementOrigin, null);
|
||
assert.equal(state.sub2apiSessionId, null);
|
||
assert.equal(state.sub2apiOAuthState, null);
|
||
assert.equal(state.sub2apiGroupId, null);
|
||
assert.deepStrictEqual(state.sub2apiGroupIds, []);
|
||
assert.equal(state.sub2apiDraftName, null);
|
||
assert.equal(state.sub2apiProxyId, null);
|
||
assert.equal(state.codex2apiSessionId, null);
|
||
assert.equal(state.codex2apiOAuthState, null);
|
||
assert.equal(state.plusManualConfirmationPending, false);
|
||
assert.equal(state.plusManualConfirmationRequestId, '');
|
||
assert.equal(state.plusManualConfirmationStep, 0);
|
||
assert.equal(state.plusManualConfirmationMethod, '');
|
||
assert.equal(state.plusManualConfirmationTitle, '');
|
||
assert.equal(state.plusManualConfirmationMessage, '');
|
||
assert.deepStrictEqual(state.nodeStatuses, {
|
||
'open-chatgpt': 'pending',
|
||
'plus-checkout-create': 'pending',
|
||
'plus-checkout-billing': 'pending',
|
||
'paypal-approve': 'pending',
|
||
'plus-checkout-return': 'pending',
|
||
'sub2api-session-import': 'pending',
|
||
});
|
||
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'oauth-login'), false);
|
||
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'platform-verify'), false);
|
||
assert.deepStrictEqual(broadcasts.at(-1), {
|
||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||
oauthUrl: null,
|
||
localhostUrl: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
cpaOAuthState: null,
|
||
cpaManagementOrigin: null,
|
||
sub2apiSessionId: null,
|
||
sub2apiOAuthState: null,
|
||
sub2apiGroupId: null,
|
||
sub2apiGroupIds: [],
|
||
sub2apiDraftName: null,
|
||
sub2apiProxyId: null,
|
||
codex2apiSessionId: null,
|
||
codex2apiOAuthState: null,
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
nodeStatuses: {
|
||
'open-chatgpt': 'pending',
|
||
'plus-checkout-create': 'pending',
|
||
'plus-checkout-billing': 'pending',
|
||
'paypal-approve': 'pending',
|
||
'plus-checkout-return': 'pending',
|
||
'sub2api-session-import': 'pending',
|
||
},
|
||
currentNodeId: '',
|
||
});
|
||
});
|
||
|
||
test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effective strategy back to OAuth', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const broadcasts = [];
|
||
let state = {
|
||
targetId: 'sub2api',
|
||
plusModeEnabled: true,
|
||
plusPaymentMethod: 'paypal',
|
||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||
oauthUrl: 'https://oauth.example/current',
|
||
localhostUrl: 'http://localhost:38080/callback',
|
||
sub2apiSessionId: 'sub-session',
|
||
plusManualConfirmationPending: true,
|
||
plusManualConfirmationRequestId: 'gopay-req',
|
||
plusManualConfirmationStep: 9,
|
||
plusManualConfirmationMethod: 'gopay',
|
||
plusManualConfirmationTitle: 'GoPay 订阅确认',
|
||
plusManualConfirmationMessage: '完成后继续导入当前 ChatGPT 会话到 SUB2API。',
|
||
currentNodeId: 'sub2api-session-import',
|
||
nodeStatuses: {
|
||
'open-chatgpt': 'completed',
|
||
'plus-checkout-create': 'completed',
|
||
'plus-checkout-billing': 'completed',
|
||
'paypal-approve': 'completed',
|
||
'plus-checkout-return': 'completed',
|
||
'sub2api-session-import': 'running',
|
||
},
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'targetId')
|
||
? { targetId: input.targetId }
|
||
: {},
|
||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||
getNodeIdsForState: (nextState = {}) => (
|
||
String(nextState.targetId || '').trim() === 'sub2api'
|
||
&& String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
|
||
? [
|
||
'open-chatgpt',
|
||
'plus-checkout-create',
|
||
'plus-checkout-billing',
|
||
'paypal-approve',
|
||
'plus-checkout-return',
|
||
'sub2api-session-import',
|
||
]
|
||
: [
|
||
'open-chatgpt',
|
||
'plus-checkout-create',
|
||
'plus-checkout-billing',
|
||
'paypal-approve',
|
||
'plus-checkout-return',
|
||
'oauth-login',
|
||
'fetch-login-code',
|
||
'post-login-phone-verification',
|
||
'confirm-oauth',
|
||
'platform-verify',
|
||
]
|
||
),
|
||
getState: async () => ({ ...state }),
|
||
getStepIdsForState: () => [],
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: {
|
||
targetId: 'cpa',
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.targetId, 'cpa');
|
||
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||
assert.equal(state.currentNodeId, '');
|
||
assert.equal(state.oauthUrl, null);
|
||
assert.equal(state.localhostUrl, null);
|
||
assert.equal(state.sub2apiSessionId, null);
|
||
assert.equal(state.plusManualConfirmationPending, false);
|
||
assert.equal(state.plusManualConfirmationMessage, '');
|
||
assert.deepStrictEqual(state.nodeStatuses, {
|
||
'open-chatgpt': 'pending',
|
||
'plus-checkout-create': 'pending',
|
||
'plus-checkout-billing': 'pending',
|
||
'paypal-approve': 'pending',
|
||
'plus-checkout-return': 'pending',
|
||
'oauth-login': 'pending',
|
||
'fetch-login-code': 'pending',
|
||
'post-login-phone-verification': 'pending',
|
||
'confirm-oauth': 'pending',
|
||
'platform-verify': 'pending',
|
||
});
|
||
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'sub2api-session-import'), false);
|
||
assert.deepStrictEqual(broadcasts.at(-1), {
|
||
targetId: 'cpa',
|
||
signupMethod: 'email',
|
||
oauthUrl: null,
|
||
localhostUrl: null,
|
||
oauthFlowDeadlineAt: null,
|
||
oauthFlowDeadlineSourceUrl: null,
|
||
cpaOAuthState: null,
|
||
cpaManagementOrigin: null,
|
||
sub2apiSessionId: null,
|
||
sub2apiOAuthState: null,
|
||
sub2apiGroupId: null,
|
||
sub2apiGroupIds: [],
|
||
sub2apiDraftName: null,
|
||
sub2apiProxyId: null,
|
||
codex2apiSessionId: null,
|
||
codex2apiOAuthState: null,
|
||
plusManualConfirmationPending: false,
|
||
plusManualConfirmationRequestId: '',
|
||
plusManualConfirmationStep: 0,
|
||
plusManualConfirmationMethod: '',
|
||
plusManualConfirmationTitle: '',
|
||
plusManualConfirmationMessage: '',
|
||
nodeStatuses: {
|
||
'open-chatgpt': 'pending',
|
||
'plus-checkout-create': 'pending',
|
||
'plus-checkout-billing': 'pending',
|
||
'paypal-approve': 'pending',
|
||
'plus-checkout-return': 'pending',
|
||
'oauth-login': 'pending',
|
||
'fetch-login-code': 'pending',
|
||
'post-login-phone-verification': 'pending',
|
||
'confirm-oauth': 'pending',
|
||
'platform-verify': 'pending',
|
||
},
|
||
currentNodeId: '',
|
||
});
|
||
});
|
||
|
||
test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const broadcasts = [];
|
||
let state = { activeFlowId: 'openai', flowId: 'openai', targetId: 'cpa', plusModeEnabled: false, plusPaymentMethod: 'paypal' };
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'activeFlowId')
|
||
? { activeFlowId: input.activeFlowId }
|
||
: {},
|
||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||
getState: async () => ({ ...state }),
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => { state = { ...state, ...updates }; },
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
source: 'sidepanel',
|
||
payload: { activeFlowId: 'kiro' },
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.activeFlowId, 'kiro');
|
||
assert.equal(state.flowId, 'kiro');
|
||
assert.deepStrictEqual(broadcasts.at(-1), {
|
||
activeFlowId: 'kiro',
|
||
flowId: 'kiro',
|
||
signupMethod: 'email',
|
||
});
|
||
});
|
||
|
||
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const canonicalSettingsState = {
|
||
schemaVersion: 4,
|
||
activeFlowId: 'kiro',
|
||
services: {
|
||
account: { customPassword: '' },
|
||
email: { provider: 'duck' },
|
||
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
|
||
},
|
||
flows: {
|
||
openai: {
|
||
integrationTargetId: 'cpa',
|
||
integrationTargets: {
|
||
cpa: { vpsUrl: '', vpsPassword: '', localCpaStep9Mode: 'submit' },
|
||
sub2api: {
|
||
sub2apiUrl: '',
|
||
sub2apiEmail: '',
|
||
sub2apiPassword: '',
|
||
sub2apiGroupName: 'codex',
|
||
sub2apiGroupNames: ['codex', 'openai-plus'],
|
||
sub2apiAccountPriority: 1,
|
||
sub2apiDefaultProxyName: '',
|
||
},
|
||
codex2api: { codex2apiUrl: '', codex2apiAdminKey: '' },
|
||
},
|
||
signup: {
|
||
signupMethod: 'email',
|
||
phoneVerificationEnabled: false,
|
||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||
},
|
||
plus: {
|
||
plusModeEnabled: false,
|
||
plusPaymentMethod: 'paypal',
|
||
},
|
||
autoRun: {
|
||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
|
||
},
|
||
},
|
||
kiro: {
|
||
targetId: 'kiro-rs',
|
||
targets: {
|
||
'kiro-rs': {
|
||
baseUrl: 'https://kiro.example.com/admin',
|
||
apiKey: 'live-key',
|
||
},
|
||
},
|
||
autoRun: {
|
||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 9 },
|
||
},
|
||
},
|
||
},
|
||
};
|
||
let state = {
|
||
activeFlowId: 'kiro',
|
||
flowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||
kiroRsKey: '',
|
||
settingsSchemaVersion: 4,
|
||
settingsState: {
|
||
...canonicalSettingsState,
|
||
flows: {
|
||
...canonicalSettingsState.flows,
|
||
kiro: {
|
||
...canonicalSettingsState.flows.kiro,
|
||
targets: {
|
||
'kiro-rs': {
|
||
baseUrl: 'https://kiro.example.com/admin',
|
||
apiKey: '',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
plusModeEnabled: false,
|
||
plusPaymentMethod: 'paypal',
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => ({
|
||
activeFlowId: String(input.activeFlowId || 'kiro'),
|
||
kiroRsKey: String(input.kiroRsKey || ''),
|
||
}),
|
||
broadcastDataUpdate: () => {},
|
||
getState: async () => ({ ...state }),
|
||
setPersistentSettings: async () => ({
|
||
activeFlowId: 'kiro',
|
||
flowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
kiroRsUrl: 'https://kiro.example.com/admin',
|
||
kiroRsKey: 'live-key',
|
||
settingsSchemaVersion: 4,
|
||
settingsState: canonicalSettingsState,
|
||
}),
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: {
|
||
activeFlowId: 'kiro',
|
||
kiroRsKey: 'live-key',
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.kiroRsKey, 'live-key');
|
||
assert.equal(state.settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'live-key');
|
||
});
|
||
|
||
test('CHECK_KIRO_RS_CONNECTION prefers current sidepanel payload over stale saved kiro.rs config', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const calls = [];
|
||
const router = api.createMessageRouter({
|
||
getState: async () => ({
|
||
activeFlowId: 'kiro',
|
||
flowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
kiroRsUrl: 'https://old.example.com/admin',
|
||
kiroRsKey: 'old-key',
|
||
settingsState: {
|
||
flows: {
|
||
kiro: {
|
||
targetId: 'kiro-rs',
|
||
targets: {
|
||
'kiro-rs': {
|
||
baseUrl: 'https://old.example.com/admin',
|
||
apiKey: 'old-key',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}),
|
||
testKiroRsConnection: async (baseUrl, apiKey) => {
|
||
calls.push({ baseUrl, apiKey });
|
||
return {
|
||
ok: false,
|
||
status: 401,
|
||
message: 'kiro.rs API Key 被拒绝(HTTP 401:Invalid or missing admin API key)',
|
||
};
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'CHECK_KIRO_RS_CONNECTION',
|
||
payload: {
|
||
activeFlowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
baseUrl: ' https://new.example.com/admin/ ',
|
||
apiKey: ' new-key ',
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, false);
|
||
assert.equal(response.status, 401);
|
||
assert.equal(response.message, 'kiro.rs API Key 被拒绝(HTTP 401:Invalid or missing admin API key)');
|
||
assert.deepStrictEqual(calls, [
|
||
{
|
||
baseUrl: 'https://new.example.com/admin/',
|
||
apiKey: ' new-key ',
|
||
},
|
||
]);
|
||
});
|
||
|
||
test('AUTO_RUN applies current flow selection from payload before starting loop', async () => {
|
||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||
const globalScope = { console };
|
||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||
const calls = [];
|
||
const validations = [];
|
||
let state = {
|
||
activeFlowId: 'openai',
|
||
flowId: 'openai',
|
||
targetId: 'cpa',
|
||
plusModeEnabled: false,
|
||
plusPaymentMethod: 'paypal',
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
clearStopRequest: () => {},
|
||
getPendingAutoRunTimerPlan: () => null,
|
||
getState: async () => ({ ...state }),
|
||
normalizeRunCount: (value) => Number(value) || 1,
|
||
setState: async (updates) => {
|
||
calls.push({ type: 'setState', updates: { ...updates } });
|
||
state = { ...state, ...updates };
|
||
},
|
||
startAutoRunLoop: (totalRuns, options) => {
|
||
calls.push({ type: 'startAutoRunLoop', totalRuns, options });
|
||
},
|
||
validateAutoRunStart: (validationState, options = {}) => {
|
||
validations.push({
|
||
activeFlowId: validationState?.activeFlowId,
|
||
flowId: validationState?.flowId,
|
||
targetId: validationState?.targetId,
|
||
optionActiveFlowId: options?.activeFlowId,
|
||
});
|
||
return { ok: true, errors: [] };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'AUTO_RUN',
|
||
payload: {
|
||
totalRuns: 1,
|
||
activeFlowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
},
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.activeFlowId, 'kiro');
|
||
assert.equal(state.flowId, 'kiro');
|
||
assert.equal(state.targetId, 'kiro-rs');
|
||
assert.deepStrictEqual(calls, [
|
||
{
|
||
type: 'setState',
|
||
updates: {
|
||
activeFlowId: 'kiro',
|
||
flowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
},
|
||
},
|
||
{
|
||
type: 'setState',
|
||
updates: {
|
||
autoRunSkipFailures: false,
|
||
},
|
||
},
|
||
{
|
||
type: 'startAutoRunLoop',
|
||
totalRuns: 1,
|
||
options: {
|
||
autoRunSkipFailures: false,
|
||
mode: 'restart',
|
||
},
|
||
},
|
||
]);
|
||
assert.deepStrictEqual(validations, [
|
||
{
|
||
activeFlowId: 'kiro',
|
||
flowId: 'kiro',
|
||
targetId: 'kiro-rs',
|
||
optionActiveFlowId: 'kiro',
|
||
},
|
||
]);
|
||
});
|
||
|
||
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,
|
||
targetId: 'sub2api',
|
||
};
|
||
|
||
const router = api.createMessageRouter({
|
||
addLog: async () => {},
|
||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'targetId')
|
||
? { targetId: input.targetId }
|
||
: {},
|
||
broadcastDataUpdate: () => {},
|
||
getState: async () => ({ ...state }),
|
||
resolveSignupMethod: (nextState = {}) => nextState.targetId === 'cpa' ? 'email' : 'phone',
|
||
setPersistentSettings: async (updates) => ({ ...updates }),
|
||
setState: async (updates) => {
|
||
state = { ...state, ...updates };
|
||
},
|
||
});
|
||
|
||
const response = await router.handleMessage({
|
||
type: 'SAVE_SETTING',
|
||
payload: { targetId: 'cpa' },
|
||
});
|
||
|
||
assert.equal(response.ok, true);
|
||
assert.equal(state.targetId, '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,
|
||
targetId: '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 });
|
||
return { ...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');
|
||
});
|