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: [], accountRecords: [], }; const nodeByStep = overrides.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 stepByNode = overrides.stepByNode || null; const normalStepByNode = { 'open-chatgpt': 1, 'submit-signup-email': 2, 'fill-password': 3, 'fetch-signup-code': 4, 'fill-profile': 5, 'wait-registration-success': 6, 'oauth-login': 7, 'fetch-login-code': 8, 'confirm-oauth': 9, 'platform-verify': 10, }; const plusStepByNode = { 'open-chatgpt': 1, 'submit-signup-email': 2, 'fill-password': 3, 'fetch-signup-code': 4, 'fill-profile': 5, 'oauth-login': 10, 'fetch-login-code': 11, 'confirm-oauth': 12, 'platform-verify': 13, }; const getStepForNode = (nodeId) => { const state = normalizeState(overrides.state || {}); if (typeof overrides.getStepIdByNodeIdForState === 'function') { return overrides.getStepIdByNodeIdForState(nodeId, state) || 0; } if (stepByNode && Object.prototype.hasOwnProperty.call(stepByNode, nodeId)) { return stepByNode[nodeId] || 0; } 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; }; let currentState = normalizeState(overrides.state || { nodeStatuses: { 'fill-password': 'pending' } }); 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); }, 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: overrides.doesNodeUseCompletionSignal || (() => false), ensureManualInteractionAllowed: async () => ({}), executeNode: async (nodeId) => { events.executedSteps.push(getStepForNode(nodeId) || nodeId); }, executeNodeViaCompletionSignal: overrides.executeNodeViaCompletionSignal || (async () => ({})), exportSettingsBundle: async () => ({}), fetchGeneratedEmail: async () => '', finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => { events.phoneFinalizations.push(state); }), finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => { events.finalizePayloads.push(payload); }), finalizeStep5Completion: overrides.finalizeStep5Completion, finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}), findHotmailAccount: async () => null, flushCommand: async () => {}, getCurrentLuckmailPurchase: () => null, getPendingAutoRunTimerPlan: () => null, getSourceLabel: () => '', getState: async () => normalizeState(currentState), getNodeIdsForState: overrides.getNodeIdsForState || (() => ['open-chatgpt', 'submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify']), getStepIdByNodeIdForState: overrides.getStepIdByNodeIdForState || ((nodeId, state = {}) => (stepByNode && Object.prototype.hasOwnProperty.call(stepByNode, nodeId)) ? stepByNode[nodeId] || 0 : (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0), getStepDefinitionForState: overrides.getStepDefinitionForState, getStepIdsForState: overrides.getStepIdsForState || (() => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 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: () => false, isLocalhostOAuthCallbackUrl: () => true, isLuckmailProvider: () => false, isStopError: () => false, isTabAlive: overrides.isTabAlive || (async () => false), launchAutoRunTimerPlan: async () => {}, listIcloudAliases: async () => [], listLuckmailPurchasesForManagement: async () => [], normalizeHotmailAccounts: (items) => items, normalizeRunCount: (value) => value, 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 () => {}, 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); currentState = normalizeState({ ...currentState, ...updates, nodeStatuses: updates.nodeStatuses ? { ...updates.nodeStatuses } : currentState.nodeStatuses, stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses, }); }, setNodeStatus: async (nodeId, status) => { events.nodeStatuses.push({ nodeId, status }); const step = getStepForNode(nodeId); events.stepStatuses.push({ step, status }); currentState = normalizeState({ ...currentState, nodeStatuses: { ...(currentState.nodeStatuses || {}), [nodeId]: 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, cardStatus: 'active' }; }), }); return { router, events, getState: () => normalizeState(currentState) }; } 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 = { 7: 'oauth-login', 8: 'fetch-login-code', 9: 'confirm-oauth', }; const { router, events } = createRouter({ state: { stepStatuses: { 7: 'completed', 8: 'pending', 9: 'pending' } }, getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }), getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], }); await router.handleStepData(7, { skipLoginVerificationStep: true, directOAuthConsentPage: true, }); assert.deepStrictEqual(events.stepStatuses, [{ step: 8, status: 'skipped' }]); assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 8/.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: 'openai-auth', 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: 'openai-auth', 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 skips signup tail after finalize confirms phone login password page advanced', async () => { const finalizeResult = { ready: true, state: 'phone_verification_page' }; const { router, events } = createRouter({ state: { signupMethod: 'phone', accountIdentifierType: 'phone', accountIdentifier: '+66959916439', signupPhoneNumber: '+66959916439', nodeStatuses: { 'fill-password': 'running', 'fetch-signup-code': 'pending', 'fill-profile': 'pending', 'wait-registration-success': 'pending', }, }, finalizeStep3Completion: async (payload) => { events.finalizePayloads.push(payload); return finalizeResult; }, }); const response = await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'fill-password', source: 'openai-auth', payload: { nodeId: 'fill-password', accountIdentifierType: 'phone', accountIdentifier: '+66959916439', signupPhoneNumber: '+66959916439', signupVerificationRequestedAt: 123456, deferredSubmit: true, passwordPageUrl: 'https://auth.openai.com/log-in/password', passwordPagePath: '/log-in/password', passwordPageMode: 'login', }, }, {}); assert.deepStrictEqual(response, { ok: true }); assert.deepStrictEqual(events.finalizePayloads.map((payload) => payload.passwordPageMode), ['login']); assert.deepStrictEqual(events.stepStatuses, [ { step: 3, status: 'completed' }, { step: 4, status: 'skipped' }, { step: 5, status: 'skipped' }, { step: 6, status: 'skipped' }, ]); assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), false); assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), true); assert.equal(events.logs.some(({ message }) => /手机号密码提交后已确认账号进入登录后续状态/.test(message)), true); assert.equal(events.notifyCompletions[0].payload.passwordLoginFlow, true); assert.equal(events.notifyCompletions[0].payload.skipRegistrationFlow, true); assert.equal(events.notifyCompletions[0].payload.signupVerificationRequestedAt, null); assert.equal(events.notifyCompletions[0].payload.state, 'phone_verification_page'); }); test('message router keeps signup tail when phone password submit used create-account page', async () => { const finalizeResult = { ready: true, state: 'verification' }; const { router, events } = createRouter({ state: { signupMethod: 'phone', accountIdentifierType: 'phone', accountIdentifier: '+66959916439', signupPhoneNumber: '+66959916439', nodeStatuses: { 'fill-password': 'running', 'fetch-signup-code': 'pending', 'fill-profile': 'pending', 'wait-registration-success': 'pending', }, }, finalizeStep3Completion: async (payload) => { events.finalizePayloads.push(payload); return finalizeResult; }, }); const response = await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'fill-password', source: 'openai-auth', payload: { nodeId: 'fill-password', accountIdentifierType: 'phone', accountIdentifier: '+66959916439', signupPhoneNumber: '+66959916439', signupVerificationRequestedAt: 123456, deferredSubmit: true, passwordPageUrl: 'https://auth.openai.com/create-account/password', passwordPagePath: '/create-account/password', passwordPageMode: 'signup', }, }, {}); assert.deepStrictEqual(response, { ok: true }); assert.deepStrictEqual(events.finalizePayloads.map((payload) => payload.passwordPageMode), ['signup']); assert.deepStrictEqual(events.stepStatuses, [ { step: 3, status: 'completed' }, ]); assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456), true); assert.equal(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === null), false); assert.equal(events.notifyCompletions[0].payload.state, 'verification'); assert.equal(events.notifyCompletions[0].payload.skipRegistrationFlow, undefined); }); test('message router does not skip signup tail when login password finalize fails', async () => { const { router, events } = createRouter({ state: { signupMethod: 'phone', accountIdentifierType: 'phone', accountIdentifier: '+66959916439', signupPhoneNumber: '+66959916439', nodeStatuses: { 'fill-password': 'running', 'fetch-signup-code': 'pending', 'fill-profile': 'pending', 'wait-registration-success': 'pending', }, }, finalizeStep3Completion: async (payload) => { events.finalizePayloads.push(payload); throw new Error('步骤 3 收尾仍停留在登录密码页。'); }, }); const response = await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'fill-password', source: 'openai-auth', payload: { nodeId: 'fill-password', accountIdentifierType: 'phone', accountIdentifier: '+66959916439', signupPhoneNumber: '+66959916439', signupVerificationRequestedAt: null, deferredSubmit: true, passwordPageUrl: 'https://auth.openai.com/log-in/password', passwordPagePath: '/log-in/password', passwordPageMode: 'login', passwordLoginFlow: true, }, }, {}); assert.deepStrictEqual(response, { ok: true, error: '步骤 3 收尾仍停留在登录密码页。' }); assert.deepStrictEqual(events.finalizePayloads.map((payload) => payload.passwordPageMode), ['login']); assert.deepStrictEqual(events.stepStatuses, [ { step: 3, status: 'failed' }, ]); assert.equal(events.stepStatuses.some(({ step, status }) => step === 4 && status === 'skipped'), false); assert.equal(events.stepStatuses.some(({ step, status }) => step === 5 && status === 'skipped'), false); assert.equal(events.stepStatuses.some(({ step, status }) => step === 6 && status === 'skipped'), false); assert.equal(events.notifyCompletions.length, 0); }); 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: 'openai-auth', 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: 'openai-auth', payload: { nodeId: 'oauth-login' }, error: 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统', }, {}); assert.deepStrictEqual(events.securityBlocks, ['CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统']); assert.deepStrictEqual(events.notifyErrors, [ { step: 7, nodeId: 'oauth-login', error: '流程已被用户停止。', }, ]); assert.deepStrictEqual(response, { ok: true, error: '您已触发Cloudflare 安全防护系统', }); }); test('message router delegates OpenAI manual step 4 to the OpenAI node executor', async () => { const { router, events } = createRouter({ getTabId: async () => null, isTabAlive: async () => false, }); await router.handleMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', nodeId: 'fetch-signup-code', payload: { nodeId: 'fetch-signup-code' }, }, {}); assert.deepStrictEqual(events.invalidations, [ { step: 4, options: { logLabel: '节点 fetch-signup-code 重新执行' } }, ]); assert.deepStrictEqual(events.executedSteps, [4]); }); test('message router delegates Kiro manual step 4 without OpenAI auth-tab prerequisites', async () => { const kiroNodeByStep = { 1: 'kiro-open-register-page', 2: 'kiro-submit-email', 3: 'kiro-submit-name', 4: 'kiro-submit-verification-code', 5: 'kiro-submit-password', 6: 'kiro-complete-register-consent', 7: 'kiro-start-desktop-authorize', 8: 'kiro-complete-desktop-authorize', 9: 'kiro-upload-credential', }; const kiroStepByNode = Object.fromEntries( Object.entries(kiroNodeByStep).map(([step, nodeId]) => [nodeId, Number(step)]) ); const { router, events } = createRouter({ state: { activeFlowId: 'kiro', flowId: 'kiro', nodeStatuses: { 'kiro-open-register-page': 'completed', 'kiro-submit-email': 'completed', 'kiro-submit-name': 'completed', 'kiro-submit-verification-code': 'failed', }, }, nodeByStep: kiroNodeByStep, stepByNode: kiroStepByNode, getNodeIdsForState: () => Object.values(kiroNodeByStep), getTabId: async (sourceId) => { assert.notEqual(sourceId, 'openai-auth'); return null; }, isTabAlive: async (sourceId) => { assert.notEqual(sourceId, 'openai-auth'); return false; }, }); await router.handleMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', nodeId: 'kiro-submit-verification-code', payload: { nodeId: 'kiro-submit-verification-code' }, }, {}); assert.deepStrictEqual(events.invalidations, [ { step: 4, options: { logLabel: '节点 kiro-submit-verification-code 重新执行' } }, ]); assert.deepStrictEqual(events.executedSteps, [4]); }); test('message router refreshes GPC balance through explicit sidepanel message', async () => { const state = { plusPaymentMethod: 'gpc-helper', gpcBaseUrl: 'http://localhost:18473/', gpcCardKey: 'GPC-11111111-22222222-33333333', }; const { router, events } = createRouter({ state }); const response = await router.handleMessage({ type: 'REFRESH_GPC_CARD_BALANCE', source: 'sidepanel', payload: { gpcCardKey: 'GPC-6C9F1A32-45734795-914E6F00', reason: 'manual', }, }, {}); assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, cardStatus: 'active' }); assert.equal(events.balanceRefreshes.length, 1); assert.equal(events.balanceRefreshes[0].state.gpcBaseUrl, 'http://localhost:18473/'); assert.equal(events.balanceRefreshes[0].state.gpcCardKey, 'GPC-6C9F1A32-45734795-914E6F00'); assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' }); }); 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); }); test('message router defers fill-profile completion status until background validation', async () => { const { router, events } = createRouter({ state: { currentNodeId: 'fill-profile', nodeStatuses: { 'fill-profile': 'running', 'wait-registration-success': 'pending', }, }, }); const response = await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'fill-profile', payload: { nodeId: 'fill-profile', outcome: 'navigation_started', url: 'https://auth.openai.com/about-you', navigationStarted: true, }, }, {}); assert.deepStrictEqual(response, { ok: true }); assert.deepStrictEqual(events.stepStatuses, []); assert.deepStrictEqual(events.nodeStatuses, []); assert.deepStrictEqual(events.notifyCompletions, [ { step: 5, nodeId: 'fill-profile', payload: { nodeId: 'fill-profile', outcome: 'navigation_started', url: 'https://auth.openai.com/about-you', navigationStarted: true, step: 5, }, }, ]); assert.equal(events.logs.some(({ message }) => /等待后台最终复核后再标记完成/.test(message)), true); }); test('message router finalizes manual fill-profile execution through background validation', async () => { const finalizePayloads = []; const { router } = createRouter({ state: { currentNodeId: 'fill-profile', nodeStatuses: { 'fill-profile': 'pending', }, }, executeNodeViaCompletionSignal: async (nodeId) => ({ nodeId, outcome: 'logged_in_home', url: 'https://chatgpt.com/', }), doesNodeUseCompletionSignal: (nodeId) => nodeId === 'fill-profile', finalizeStep5Completion: async (payload) => { finalizePayloads.push(payload); }, }); const response = await router.handleMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', nodeId: 'fill-profile', payload: { nodeId: 'fill-profile', }, }, {}); assert.deepStrictEqual(response, { ok: true }); assert.deepStrictEqual(finalizePayloads, [ { nodeId: 'fill-profile', outcome: 'logged_in_home', url: 'https://chatgpt.com/', }, ]); });