import GuJumpgate snapshot from GitHub

This commit is contained in:
2026-05-20 20:33:51 +08:00
commit f8903dcd14
335 changed files with 179790 additions and 0 deletions
@@ -0,0 +1,865 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
function createRouter(overrides = {}) {
const events = {
logs: [],
stepStatuses: [],
nodeStatuses: [],
stateUpdates: [],
broadcasts: [],
balanceRefreshes: [],
emailStates: [],
persistedRegistrationEmails: [],
signupPhoneStates: [],
signupPhoneSilentStates: [],
finalizePayloads: [],
phoneFinalizations: [],
notifyCompletions: [],
notifyErrors: [],
securityBlocks: [],
invalidations: [],
executedSteps: [],
postRegistrationCheckoutCalls: [],
accountRecords: [],
};
const nodeByStep = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
11: 'fetch-login-code',
12: 'confirm-oauth',
13: 'platform-verify',
};
const normalStepByNode = {
'open-chatgpt': 1,
'submit-signup-email': 2,
'fill-password': 3,
'fetch-signup-code': 4,
'fill-profile': 5,
'wait-registration-success': 6,
'plus-checkout-billing': 7,
'paypal-approve': 8,
'oauth-login': 9,
'fetch-login-code': 10,
'confirm-oauth': 11,
'platform-verify': 12,
};
const plusStepByNode = {
'open-chatgpt': 1,
'submit-signup-email': 2,
'fill-password': 3,
'fetch-signup-code': 4,
'fill-profile': 5,
'plus-checkout-create': 6,
'plus-checkout-billing': 7,
'paypal-approve': 8,
'plus-checkout-return': 9,
'oauth-login': 10,
'fetch-login-code': 11,
'confirm-oauth': 12,
'platform-verify': 13,
};
const getStepForNode = (nodeId) => {
const state = normalizeState(overrides.state || {});
return (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0;
};
const normalizeState = (state = {}) => {
const next = { ...(state || {}) };
if (!next.nodeStatuses && next.stepStatuses) {
next.nodeStatuses = Object.fromEntries(
Object.entries(next.stepStatuses)
.map(([step, status]) => [nodeByStep[Number(step)], status])
.filter(([nodeId]) => Boolean(nodeId))
);
}
return next;
};
const router = api.createMessageRouter({
addLog: async (message, level, options = {}) => {
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey, nodeId: options.nodeId });
},
appendAccountRunRecord: overrides.appendAccountRunRecord || (async (status, state, reason) => {
events.accountRecords.push({ status, state, reason });
return null;
}),
batchUpdateLuckmailPurchases: async () => {},
buildLocalhostCleanupPrefix: () => '',
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: () => ({}),
broadcastDataUpdate: (updates) => {
events.broadcasts.push(updates);
},
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {},
clearLuckmailRuntimeState: async () => {},
clearStopRequest: () => {},
closeLocalhostCallbackTabs: async () => {},
closeTabsByUrlPrefix: async () => {},
completeNodeFromBackground: async (nodeId, payload) => {
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload, via: 'completeNodeFromBackground' });
},
deleteHotmailAccount: async () => {},
deleteHotmailAccounts: async () => {},
deleteIcloudAlias: async () => {},
deleteUsedIcloudAliases: async () => {},
disableUsedLuckmailPurchases: async () => {},
doesNodeUseCompletionSignal: () => false,
ensureManualInteractionAllowed: async () => ({}),
executeNode: async (nodeId) => {
events.executedSteps.push(getStepForNode(nodeId) || nodeId);
},
executeNodeViaCompletionSignal: async () => {},
executePostRegistrationCheckoutBilling: overrides.executePostRegistrationCheckoutBilling || (async (state) => {
events.postRegistrationCheckoutCalls.push(state);
}),
exportSettingsBundle: async () => ({}),
fetchHostedCheckoutVerificationCodeManually: overrides.fetchHostedCheckoutVerificationCodeManually || (async () => ({
code: '654321',
verificationUrl: 'https://mail.test.com/api/text-relay/example',
})),
fetchGeneratedEmail: async () => '',
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
events.phoneFinalizations.push(state);
}),
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload);
}),
finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
findHotmailAccount: async () => null,
flushCommand: async () => {},
getCurrentLuckmailPurchase: () => null,
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
getState: async () => normalizeState(overrides.state || { nodeStatuses: { 'fill-password': 'pending' } }),
getNodeIdsForState: (state = {}) => (state.plusModeEnabled
? ['open-chatgpt', 'submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'plus-checkout-create']
: ['open-chatgpt', 'submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success', 'plus-checkout-create', 'plus-checkout-billing', 'paypal-approve', 'plus-checkout-return', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify']),
getStepIdByNodeIdForState: (nodeId, state = {}) => (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0,
getStepDefinitionForState: overrides.getStepDefinitionForState,
getStepIdsForState: overrides.getStepIdsForState,
getLastStepIdForState: overrides.getLastStepIdForState,
getTabId: overrides.getTabId || (async () => null),
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
handleCloudflareSecurityBlocked: overrides.handleCloudflareSecurityBlocked || (async (error) => {
const message = typeof error === 'string' ? error : error?.message || '';
events.securityBlocks.push(message);
return message.replace(/^CF_SECURITY_BLOCKED::/, '') || message;
}),
importSettingsBundle: async () => {},
invalidateDownstreamAfterStepRestart: async (step, options) => {
events.invalidations.push({ step, options });
},
isCloudflareSecurityBlockedError: overrides.isCloudflareSecurityBlockedError || ((error) => /^CF_SECURITY_BLOCKED::/.test(typeof error === 'string' ? error : error?.message || '')),
isAutoRunLockedState: overrides.isAutoRunLockedState || (() => false),
isHotmailProvider: overrides.isHotmailProvider || (() => false),
isLocalhostOAuthCallbackUrl: () => true,
isLuckmailProvider: () => false,
isStopError: () => false,
isTabAlive: overrides.isTabAlive || (async () => false),
launchAutoRunTimerPlan: async () => {},
listIcloudAliases: async () => [],
listLuckmailPurchasesForManagement: async () => [],
markCurrentRegistrationAccountUsed: overrides.markCurrentRegistrationAccountUsed,
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyNodeComplete: (nodeId, payload) => {
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload });
},
notifyNodeError: (nodeId, error) => {
events.notifyErrors.push({ step: getStepForNode(nodeId), nodeId, error });
},
patchHotmailAccount: async () => {},
registerTab: async () => {},
requestStop: async () => {},
resetState: async () => {},
resumeAutoRun: async () => {},
scheduleAutoRun: async () => {},
selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {},
setEmailState: async (email) => {
events.emailStates.push(email);
},
setEmailStateSilently: async () => {},
persistRegistrationEmailState: async (state, email, options) => {
events.persistedRegistrationEmails.push({ state, email, options });
},
setSignupPhoneState: async (phoneNumber) => {
events.signupPhoneStates.push(phoneNumber);
},
setSignupPhoneStateSilently: async (phoneNumber) => {
events.signupPhoneSilentStates.push(phoneNumber);
},
setIcloudAliasPreservedState: async () => {},
setIcloudAliasUsedState: async () => {},
setLuckmailPurchaseDisabledState: async () => {},
setLuckmailPurchasePreservedState: async () => {},
setLuckmailPurchaseUsedState: async () => {},
setPersistentSettings: async () => {},
setState: async (updates) => {
events.stateUpdates.push(updates);
},
setNodeStatus: async (nodeId, status) => {
events.nodeStatuses.push({ nodeId, status });
const step = getStepForNode(nodeId);
events.stepStatuses.push({ step, status });
},
skipAutoRunCountdown: async () => false,
skipNode: async () => {},
startAutoRunLoop: async () => {},
syncHotmailAccounts: async () => {},
testHotmailAccountMailAccess: async () => {},
upsertHotmailAccount: async () => {},
verifyHotmailAccount: async () => {},
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
events.balanceRefreshes.push({ state, options });
return { balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' };
}),
});
return { router, events };
}
test('message router skips step 3 when step 2 lands on verification page', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 3: 'pending' } },
});
await router.handleStepData(2, {
email: 'user@example.com',
skippedPasswordStep: true,
});
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'skipped' }]);
assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入验证码页,已自动跳过步骤 3。');
});
test('message router syncs signup phone runtime state from step 2 payload immediately', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 3: 'pending' } },
});
await router.handleStepData(2, {
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
signupPhoneNumber: '66959916439',
});
assert.deepStrictEqual(events.emailStates, []);
assert.deepStrictEqual(events.signupPhoneSilentStates, ['66959916439']);
assert.deepStrictEqual(events.signupPhoneStates, []);
});
test('message router clears stale signup phone runtime when step 2 resolves email identity', async () => {
const { router, events } = createRouter({
state: {
stepStatuses: { 3: 'pending' },
accountIdentifierType: 'phone',
accountIdentifier: '+66959916439',
signupPhoneNumber: '+66959916439',
signupPhoneActivation: { activationId: 'old', phoneNumber: '+66959916439' },
},
});
await router.handleStepData(2, {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
});
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.signupPhoneSilentStates, []);
assert.deepStrictEqual(events.stateUpdates, []);
});
test('message router preserves phone signup identity when step payload only reports registration email', async () => {
const { router, events } = createRouter({
state: {
stepStatuses: { 3: 'pending' },
accountIdentifierType: 'phone',
accountIdentifier: '+66959916439',
signupPhoneNumber: '+66959916439',
signupPhoneActivation: { activationId: 'active', phoneNumber: '+66959916439' },
},
});
await router.handleStepData(3, {
email: 'bound@example.com',
signupVerificationRequestedAt: 123456,
});
assert.deepStrictEqual(events.emailStates, []);
assert.equal(events.persistedRegistrationEmails.length, 1);
assert.equal(events.persistedRegistrationEmails[0].email, 'bound@example.com');
assert.deepStrictEqual(events.persistedRegistrationEmails[0].options, {
source: 'step_identity',
preserveAccountIdentity: true,
});
assert.equal(events.persistedRegistrationEmails[0].state.signupPhoneNumber, '+66959916439');
assert.equal(events.persistedRegistrationEmails[0].state.accountIdentifierType, 'phone');
assert.equal(events.signupPhoneSilentStates.length, 0);
assert.ok(!events.stateUpdates.some((updates) => updates.signupPhoneNumber === ''));
assert.ok(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456));
});
test('message router persists phone signup identity from step 7 completion payload', async () => {
const completedActivation = {
activationId: 'signup-done',
phoneNumber: '+5511917097811',
};
const { router, events } = createRouter({
state: { stepStatuses: { 7: 'completed', 8: 'pending' } },
getStepDefinitionForState: (step) => (
step === 7
? { key: 'oauth-login' }
: (step === 8 ? { key: 'fetch-login-code' } : null)
),
});
await router.handleStepData(7, {
loginVerificationRequestedAt: 123456,
accountIdentifierType: 'phone',
accountIdentifier: '+5511917097811',
signupPhoneNumber: '+5511917097811',
signupPhoneActivation: null,
signupPhoneCompletedActivation: completedActivation,
});
assert.deepStrictEqual(events.signupPhoneSilentStates, ['+5511917097811']);
assert.ok(events.stateUpdates.some((updates) => updates.signupPhoneActivation === null));
assert.ok(events.stateUpdates.some((updates) => updates.signupPhoneCompletedActivation === completedActivation));
assert.ok(events.stateUpdates.some((updates) => updates.loginVerificationRequestedAt === 123456));
assert.deepStrictEqual(events.emailStates, []);
});
test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 3: 'completed' } },
});
await router.handleStepData(2, {
skippedPasswordStep: true,
});
assert.deepStrictEqual(events.stepStatuses, []);
});
test('message router skips steps 3/4/5 when step 2 detects already logged-in session', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 3: 'pending', 4: 'completed', 5: 'pending' } },
});
await router.handleStepData(2, {
email: 'user@example.com',
skipRegistrationFlow: true,
skippedPasswordStep: true,
});
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.stepStatuses, [
{ step: 3, status: 'skipped' },
{ step: 5, status: 'skipped' },
]);
assert.equal(events.logs[0]?.message, '步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。');
});
test('message router skips step 5 when step 4 reports already logged-in transition', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 5: 'pending' } },
});
await router.handleStepData(4, {
emailTimestamp: 123,
skipProfileStep: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 5, status: 'skipped' }]);
assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。');
});
test('message router skips login-code step when oauth login lands on consent page', async () => {
const stepKeys = {
9: 'oauth-login',
10: 'fetch-login-code',
11: 'confirm-oauth',
};
const { router, events } = createRouter({
state: { stepStatuses: { 9: 'completed', 10: 'pending', 11: 'pending' } },
getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }),
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
});
await router.handleStepData(9, {
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 10, status: 'skipped' }]);
assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 10/.test(message)), true);
});
test('message router skips Plus login-code step when oauth login lands on consent page', async () => {
const stepKeys = {
10: 'oauth-login',
11: 'fetch-login-code',
12: 'confirm-oauth',
13: 'platform-verify',
};
const { router, events } = createRouter({
state: { plusModeEnabled: true, stepStatuses: { 10: 'completed', 11: 'pending', 12: 'pending', 13: 'pending' } },
getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }),
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
});
await router.handleStepData(10, {
skipLoginVerificationStep: true,
directOAuthConsentPage: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 11, status: 'skipped' }]);
assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 11/.test(message)), true);
});
test('message router finalizes step 3 before marking it completed', async () => {
const { router, events } = createRouter();
const response = await router.handleMessage({
type: 'NODE_COMPLETE',
nodeId: 'fill-password',
source: 'signup-page',
payload: {
nodeId: 'fill-password',
email: 'user@example.com',
signupVerificationRequestedAt: 123,
},
}, {});
assert.deepStrictEqual(events.finalizePayloads, [
{
nodeId: 'fill-password',
email: 'user@example.com',
signupVerificationRequestedAt: 123,
step: 3,
},
]);
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'completed' }]);
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.notifyCompletions, [
{
step: 3,
nodeId: 'fill-password',
payload: {
nodeId: 'fill-password',
email: 'user@example.com',
signupVerificationRequestedAt: 123,
step: 3,
},
},
]);
assert.deepStrictEqual(response, { ok: true });
});
test('message router saves runtime signup phone from sidepanel message', async () => {
const { router, events } = createRouter();
const response = await router.handleMessage({
type: 'SAVE_SIGNUP_PHONE',
source: 'sidepanel',
payload: {
phoneNumber: '66959916439',
},
}, {});
assert.deepStrictEqual(events.signupPhoneStates, ['66959916439']);
assert.deepStrictEqual(events.signupPhoneSilentStates, []);
assert.deepStrictEqual(response, { ok: true, phoneNumber: '66959916439' });
});
test('message router finalizes pending phone activation on platform verify success', async () => {
const state = {
stepStatuses: { 10: 'pending' },
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
};
const { router, events } = createRouter({
state,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
});
await router.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
});
assert.equal(events.phoneFinalizations.length, 1);
assert.deepStrictEqual(events.phoneFinalizations[0].pendingPhoneActivationConfirmation, state.pendingPhoneActivationConfirmation);
});
test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
const state = {
stepStatuses: { 10: 'pending' },
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
};
const { router, events } = createRouter({
state,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
finalizeIcloudAliasAfterSuccessfulFlow: async () => {
throw new Error('icloud finalize failed');
},
});
await assert.rejects(
() => router.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
}),
/icloud finalize failed/
);
assert.deepStrictEqual(events.phoneFinalizations, []);
});
test('message router marks step 3 failed when post-submit finalize fails', async () => {
const { router, events } = createRouter({
finalizeStep3Completion: async () => {
throw new Error('步骤 3 提交后仍停留在密码页。');
},
});
const response = await router.handleMessage({
type: 'NODE_COMPLETE',
nodeId: 'fill-password',
source: 'signup-page',
payload: {
nodeId: 'fill-password',
email: 'user@example.com',
},
}, {});
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'failed' }]);
assert.deepStrictEqual(events.notifyErrors, [
{
step: 3,
nodeId: 'fill-password',
error: '步骤 3 提交后仍停留在密码页。',
},
]);
assert.equal(events.logs.some(({ message, nodeId }) => /失败步骤 3 提交后仍停留在密码页/.test(message) && nodeId === 'fill-password'), true);
assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' });
});
test('message router does not duplicate step 3 mismatch failure log after finalize already failed', async () => {
const mismatchError = 'SIGNUP_PHONE_PASSWORD_MISMATCH::步骤 3:检测到注册手机号或密码不正确,需要重新开始当前轮。页面提示:Incorrect phone number or password';
const state = {
stepStatuses: {
3: 'failed',
},
};
const { router, events } = createRouter({
state,
});
const response = await router.handleMessage({
type: 'NODE_ERROR',
nodeId: 'fill-password',
source: 'signup-page',
payload: { nodeId: 'fill-password' },
error: mismatchError,
}, {});
assert.deepStrictEqual(events.stepStatuses, []);
assert.equal(events.logs.some(({ message, step }) => /失败SIGNUP_PHONE_PASSWORD_MISMATCH::/.test(message) && step === 3), false);
assert.deepStrictEqual(events.notifyErrors, [
{
step: 3,
nodeId: 'fill-password',
error: mismatchError,
},
]);
assert.deepStrictEqual(response, { ok: true });
});
test('message router stops the flow and surfaces cloudflare security block errors', async () => {
const { router, events } = createRouter();
const response = await router.handleMessage({
type: 'NODE_ERROR',
nodeId: 'oauth-login',
source: 'signup-page',
payload: { nodeId: 'oauth-login' },
error: 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统',
}, {});
assert.deepStrictEqual(events.securityBlocks, ['CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统']);
assert.deepStrictEqual(events.notifyErrors, [
{
step: 9,
nodeId: 'oauth-login',
error: '流程已被用户停止。',
},
]);
assert.deepStrictEqual(response, {
ok: true,
error: '您已触发Cloudflare 安全防护系统',
});
});
test('message router blocks manual step 4 execution when signup page tab is missing', async () => {
const { router, events } = createRouter({
getTabId: async () => null,
isTabAlive: async () => false,
});
await assert.rejects(
() => router.handleMessage({
type: 'EXECUTE_NODE',
source: 'sidepanel',
nodeId: 'fetch-signup-code',
payload: { nodeId: 'fetch-signup-code' },
}, {}),
/手动执行步骤 4 前,请先执行步骤 1 或步骤 2/
);
assert.deepStrictEqual(events.invalidations, []);
assert.deepStrictEqual(events.executedSteps, []);
});
test('message router stops after manual PayPal checkout create when hosted checkout is the final default step', async () => {
const { router, events } = createRouter({
state: {
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
},
});
const response = await router.handleMessage({
type: 'EXECUTE_NODE',
source: 'sidepanel',
nodeId: 'plus-checkout-create',
payload: { nodeId: 'plus-checkout-create' },
}, {});
assert.deepStrictEqual(response, { ok: true });
assert.deepStrictEqual(events.executedSteps, [6]);
assert.equal(events.logs.some(({ message }) => /步骤 6 已完成正在继续执行下一节点/.test(message)), false);
});
test('message router marks the registration account used when PayPal flow ends at plus checkout create', async () => {
const markCalls = [];
const { router } = createRouter({
state: {
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
email: 'checkout@example.com',
},
getStepDefinitionForState: (step) => ({ id: step, key: step === 6 ? 'plus-checkout-create' : '' }),
markCurrentRegistrationAccountUsed: async (state, options) => {
markCalls.push({ state, options });
return { updated: true };
},
});
await router.handleStepData(6, {
nodeId: 'plus-checkout-create',
});
assert.equal(markCalls.length, 1);
assert.equal(markCalls[0].state.email, 'checkout@example.com');
assert.equal(markCalls[0].options.logPrefix, '流程完成');
});
test('message router marks the current hotmail pool account used right after fill-profile completes', async () => {
const markCalls = [];
const { router } = createRouter({
state: {
mailProvider: 'hotmail-api',
currentHotmailAccountId: 'hotmail-1',
hotmailAccounts: [
{ id: 'hotmail-1', email: 'demo@hotmail.com', used: false },
],
email: 'demo@hotmail.com',
},
isHotmailProvider: () => true,
markCurrentRegistrationAccountUsed: async (state, options) => {
markCalls.push({ state, options });
return { updated: true };
},
});
await router.handleStepData(5, {
nodeId: 'fill-profile',
});
assert.equal(markCalls.length, 1);
assert.equal(markCalls[0].state.currentHotmailAccountId, 'hotmail-1');
assert.equal(markCalls[0].options.logPrefix, '步骤 5 完成');
});
test('message router resolves GPC OTP manual confirmation without completing step early', async () => {
const state = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
};
const { router, events } = createRouter({ state });
const response = await router.handleMessage({
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
source: 'sidepanel',
payload: {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: ' 12-34 56 ',
},
}, {});
assert.deepStrictEqual(response, { ok: true });
assert.equal(events.notifyCompletions.length, 0);
assert.equal(events.stepStatuses.length, 0);
assert.equal(events.stateUpdates[0].gopayHelperResolvedOtp, '123456');
assert.equal(events.stateUpdates[0].plusManualConfirmationPending, false);
assert.deepStrictEqual(events.broadcasts[0], events.stateUpdates[0]);
});
test('message router refreshes GPC balance through explicit sidepanel message', async () => {
const state = {
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'http://localhost:18473/',
gopayHelperApiKey: 'state_api_key',
};
const { router, events } = createRouter({ state });
const response = await router.handleMessage({
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperApiKey: 'payload_api_key',
reason: 'manual',
},
}, {});
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' });
assert.equal(events.balanceRefreshes.length, 1);
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiUrl, 'http://localhost:18473/');
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiKey, 'payload_api_key');
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
});
test('message router fetches hosted checkout verification code through explicit sidepanel message', async () => {
const { router } = createRouter({
fetchHostedCheckoutVerificationCodeManually: async (payload = {}) => ({
code: '112233',
verificationUrl: payload.verificationUrl || 'https://mail.test.com/api/text-relay/manual',
}),
});
const response = await router.handleMessage({
type: 'FETCH_HOSTED_CHECKOUT_VERIFICATION_CODE',
source: 'sidepanel',
payload: {
verificationUrl: 'https://mail.test.com/api/text-relay/manual?t=123',
},
}, {});
assert.deepStrictEqual(response, {
ok: true,
code: '112233',
verificationUrl: 'https://mail.test.com/api/text-relay/manual?t=123',
});
});
test('message router ignores stale step 2 errors while auto-run is already on a later step', async () => {
const { router, events } = createRouter({
state: {
autoRunning: true,
autoRunPhase: 'running',
currentNodeId: 'wait-registration-success',
nodeStatuses: {
'submit-signup-email': 'completed',
'wait-registration-success': 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'NODE_ERROR',
nodeId: 'submit-signup-email',
payload: { nodeId: 'submit-signup-email' },
error: '步骤 2:旧页面异步失败,不应覆盖当前第 6 步记录。',
}, {});
assert.deepStrictEqual(response, { ok: true, ignored: true });
assert.deepStrictEqual(events.stepStatuses, []);
assert.deepStrictEqual(events.notifyErrors, []);
assert.deepStrictEqual(events.accountRecords, []);
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 失败消息/.test(message)), true);
});
test('message router ignores stale step 2 completion while auto-run is already on a later step', async () => {
const { router, events } = createRouter({
state: {
autoRunning: true,
autoRunPhase: 'running',
currentNodeId: 'wait-registration-success',
nodeStatuses: {
'submit-signup-email': 'completed',
'wait-registration-success': 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'NODE_COMPLETE',
nodeId: 'submit-signup-email',
payload: {
nodeId: 'submit-signup-email',
email: 'late@example.com',
},
}, {});
assert.deepStrictEqual(response, { ok: true, ignored: true });
assert.deepStrictEqual(events.stepStatuses, []);
assert.deepStrictEqual(events.notifyCompletions, []);
assert.deepStrictEqual(events.emailStates, []);
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 完成消息/.test(message)), true);
});