fix: align Plus no-payment flow and step5 completion

This commit is contained in:
QLHazyCoder
2026-05-22 01:35:42 +08:00
parent a7b35ee11a
commit 3f6fc3e1e8
17 changed files with 911 additions and 96 deletions
@@ -111,12 +111,12 @@ function createRouter(overrides = {}) {
deleteIcloudAlias: async () => {},
deleteUsedIcloudAliases: async () => {},
disableUsedLuckmailPurchases: async () => {},
doesNodeUseCompletionSignal: () => false,
doesNodeUseCompletionSignal: overrides.doesNodeUseCompletionSignal || (() => false),
ensureManualInteractionAllowed: async () => ({}),
executeNode: async (nodeId) => {
events.executedSteps.push(getStepForNode(nodeId) || nodeId);
},
executeNodeViaCompletionSignal: async () => {},
executeNodeViaCompletionSignal: overrides.executeNodeViaCompletionSignal || (async () => ({})),
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
@@ -125,6 +125,7 @@ function createRouter(overrides = {}) {
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload);
}),
finalizeStep5Completion: overrides.finalizeStep5Completion,
finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
findHotmailAccount: async () => null,
flushCommand: async () => {},
@@ -752,3 +753,83 @@ test('message router ignores stale step 2 completion while auto-run is already o
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/',
},
]);
});
@@ -110,6 +110,7 @@ async function waitForTabStableComplete() {}
${extractFunction('parseUrlSafely')}
${extractFunction('isSignupEntryHost')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('isStep5CompletionChatgptUrl')}
${extractFunction('getStep5SubmitStateFromContent')}
${extractFunction('recoverStep5SubmitRetryPageOnTab')}
${extractFunction('validateStep5PostCompletion')}
@@ -138,3 +139,65 @@ return {
true
);
});
test('step 5 post-completion validation rejects non-chatgpt success candidates', async () => {
const api = new Function(`
const logs = [];
const chrome = {
tabs: {
async get() {
return { url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' };
},
},
};
async function sendToContentScriptResilient(source, message) {
if (message.type === 'GET_STEP5_SUBMIT_STATE') {
return {
retryPage: false,
retryEnabled: false,
maxCheckAttemptsBlocked: false,
userAlreadyExistsBlocked: false,
successState: 'oauth_consent',
profileVisible: false,
errorText: '',
unknownAuthPage: false,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
throw new Error('unexpected message type: ' + message.type);
}
async function addLog(message, level, meta) {
logs.push({ message, level, meta });
}
async function waitForTabStableComplete() {}
${extractFunction('parseUrlSafely')}
${extractFunction('isSignupEntryHost')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('isStep5CompletionChatgptUrl')}
${extractFunction('getStep5SubmitStateFromContent')}
${extractFunction('recoverStep5SubmitRetryPageOnTab')}
${extractFunction('validateStep5PostCompletion')}
return {
run() {
return validateStep5PostCompletion(99, {});
},
snapshot() {
return { logs };
},
};
`)();
await assert.rejects(
api.run(),
/尚未跳转到 https:\/\/chatgpt\.com/
);
assert.equal(
api.snapshot().logs.some(({ message }) => / chatgpt\.com 的步骤 5 完成候选/.test(message)),
true
);
});
+2
View File
@@ -25,6 +25,8 @@ test('GoPay utils keeps GPC helper payment method distinct', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizePlusPaymentMethod('paypal-hosted'), 'paypal-hosted');
assert.equal(api.normalizePlusPaymentMethod('paypal_direct'), 'paypal-hosted');
assert.equal(api.normalizePlusPaymentMethod('none'), 'none');
assert.equal(api.normalizePlusPaymentMethod('no-payment'), 'none');
assert.equal(api.normalizePlusPaymentMethod('gpc-helper'), 'gpc-helper');
assert.equal(api.normalizePlusPaymentMethod('gopay'), 'gopay');
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
@@ -249,6 +249,66 @@ return {
assert.equal(api.rows.rowPlusHostedCheckoutOauthDelay.style.display, 'none');
});
test('sidepanel Plus UI supports no-payment mode without payment-specific rows', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('shouldPreserveSelectedGpcAutoMode'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'none' };
let currentPlusPaymentMethod = 'none';
let currentPlusAccountAccessStrategy = 'sub2api_codex_session';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'none', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_PAYMENT_METHOD_NONE = 'none';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowHostedCheckoutVerificationUrl = { style: { display: '' } };
const rowHostedCheckoutPhone = { style: { display: '' } };
const rowPlusHostedCheckoutOauthDelay = { style: { display: '' } };
const rowGoPayPhone = { style: { display: '' } };
const rowGpcHelperApi = { style: { display: '' } };
${bundle}
return {
updatePlusModeUI,
plusPaymentMethodCaption,
rows: {
rowPayPalAccount,
rowHostedCheckoutVerificationUrl,
rowHostedCheckoutPhone,
rowPlusHostedCheckoutOauthDelay,
rowGoPayPhone,
rowGpcHelperApi,
},
};
`)();
api.updatePlusModeUI();
assert.match(api.plusPaymentMethodCaption.textContent, /无需配置支付链路/);
Object.values(api.rows).forEach((row) => {
assert.equal(row.style.display, 'none');
});
});
test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
+148 -28
View File
@@ -99,6 +99,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
@@ -110,7 +111,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'platform-verify',
]
);
assert.equal(plusSteps.some((step) => step.key === 'wait-registration-success'), false);
assert.equal(plusSteps[5].title, '等待注册成功');
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true);
assert.equal(plusSteps.find((step) => step.key === 'paypal-approve')?.title, 'PayPal 登录与授权');
assert.equal(plusPhoneSteps[1].title, '注册并输入手机号');
@@ -123,6 +124,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
@@ -150,14 +152,14 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
]
);
assert.equal(goPaySteps.some((step) => step.key === 'paypal-approve'), false);
assert.equal(api.getStepById(8, { plusModeEnabled: true, plusPaymentMethod: 'gopay' }), null);
assert.equal(api.getStepById(9, { plusModeEnabled: true, plusPaymentMethod: 'gopay' })?.key, 'oauth-login');
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), '');
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 14);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 15);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 18);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 15);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 16);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('kiro'), true);
assert.equal(api.hasFlow('site-a'), false);
@@ -208,8 +210,8 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
[],
]
);
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
assert.equal(plusSteps[6].title, '创建 Plus Checkout');
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
assert.deepStrictEqual(
hostedSteps.map((step) => step.key),
@@ -219,6 +221,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'plus-checkout-create',
'paypal-hosted-email',
'paypal-hosted-card',
@@ -236,8 +239,8 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-openai-checkout'), false);
assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-verification'), false);
assert.equal(hostedSteps.find((step) => step.key === 'paypal-hosted-card')?.title, '无卡直绑填写 PayPal 资料');
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 14);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 15);
assert.deepStrictEqual(
goPaySteps.map((step) => step.key),
@@ -247,6 +250,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'plus-checkout-create',
'gopay-subscription-confirm',
'oauth-login',
@@ -256,10 +260,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'platform-verify',
]
);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 14);
assert.equal(goPaySteps[5].title, '打开 GoPay 订阅页');
assert.equal(goPaySteps[6].title, '等待 GoPay 订阅确认');
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 13);
assert.equal(goPaySteps[6].title, '打开 GoPay 订阅页');
assert.equal(goPaySteps[7].title, '等待 GoPay 订阅确认');
assert.deepStrictEqual(
gpcSteps.map((step) => step.key),
@@ -269,6 +273,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'plus-checkout-create',
'plus-checkout-billing',
'oauth-login',
@@ -278,10 +283,120 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'platform-verify',
]
);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 14);
assert.equal(gpcSteps[5].title, '创建 GPC 订单');
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
assert.equal(gpcSteps[6].title, '创建 GPC 订单');
assert.equal(gpcSteps[7].title, '等待 GPC 任务完成');
});
test('Plus no-payment mode removes only payment chain nodes', () => {
const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
const paymentChainKeys = [
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'paypal-hosted-email',
'paypal-hosted-card',
'paypal-hosted-create-account',
'paypal-hosted-review',
'gopay-subscription-confirm',
];
const oauthSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'none' });
const oauthNodes = api.getNodes({ plusModeEnabled: true, plusPaymentMethod: 'none' });
const oauthStepKeys = oauthSteps.map((step) => step.key);
assert.deepStrictEqual(oauthStepKeys, [
'open-chatgpt',
'submit-signup-email',
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'oauth-login',
'fetch-login-code',
'post-login-phone-verification',
'confirm-oauth',
'platform-verify',
]);
paymentChainKeys.forEach((key) => {
assert.equal(oauthStepKeys.includes(key), false, `no-payment OAuth should not keep ${key}`);
assert.equal(oauthNodes.some((node) => node.nodeId === key), false, `no-payment OAuth nodes should not keep ${key}`);
});
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'none' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'none' }), '');
assert.deepStrictEqual(
oauthNodes.find((node) => node.nodeId === 'fill-profile')?.next,
['wait-registration-success']
);
assert.deepStrictEqual(
oauthNodes.find((node) => node.nodeId === 'wait-registration-success')?.next,
['oauth-login']
);
const sub2apiSteps = api.getSteps({
plusModeEnabled: true,
plusPaymentMethod: 'none',
plusAccountAccessStrategy: 'sub2api_codex_session',
});
const sub2apiNodes = api.getNodes({
plusModeEnabled: true,
plusPaymentMethod: 'none',
plusAccountAccessStrategy: 'sub2api_codex_session',
});
assert.deepStrictEqual(sub2apiSteps.map((step) => step.key), [
'open-chatgpt',
'submit-signup-email',
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'sub2api-session-import',
]);
paymentChainKeys.forEach((key) => {
assert.equal(sub2apiSteps.some((step) => step.key === key), false, `no-payment SUB2API should not keep ${key}`);
});
assert.deepStrictEqual(api.getStepIds({
plusModeEnabled: true,
plusPaymentMethod: 'none',
plusAccountAccessStrategy: 'sub2api_codex_session',
}), [1, 2, 3, 4, 5, 6, 7]);
assert.equal(sub2apiNodes.at(-1)?.nodeId, 'sub2api-session-import');
assert.deepStrictEqual(sub2apiNodes.find((node) => node.nodeId === 'fill-profile')?.next, ['wait-registration-success']);
assert.deepStrictEqual(sub2apiNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, ['sub2api-session-import']);
const cpaSteps = api.getSteps({
plusModeEnabled: true,
plusPaymentMethod: 'none',
plusAccountAccessStrategy: 'cpa_codex_session',
});
const cpaNodes = api.getNodes({
plusModeEnabled: true,
plusPaymentMethod: 'none',
plusAccountAccessStrategy: 'cpa_codex_session',
});
assert.deepStrictEqual(cpaSteps.map((step) => step.key), [
'open-chatgpt',
'submit-signup-email',
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'cpa-session-import',
]);
paymentChainKeys.forEach((key) => {
assert.equal(cpaSteps.some((step) => step.key === key), false, `no-payment CPA should not keep ${key}`);
});
assert.deepStrictEqual(api.getStepIds({
plusModeEnabled: true,
plusPaymentMethod: 'none',
plusAccountAccessStrategy: 'cpa_codex_session',
}), [1, 2, 3, 4, 5, 6, 7]);
assert.equal(cpaNodes.at(-1)?.nodeId, 'cpa-session-import');
assert.deepStrictEqual(cpaNodes.find((node) => node.nodeId === 'fill-profile')?.next, ['wait-registration-success']);
assert.deepStrictEqual(cpaNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, ['cpa-session-import']);
});
test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
@@ -304,7 +419,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'plus-checkout-return',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
},
{
label: 'paypal-hosted',
@@ -314,7 +429,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'paypal-hosted-review',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
},
{
label: 'gopay',
@@ -324,7 +439,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'gopay-subscription-confirm',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
},
{
label: 'gpc-helper',
@@ -334,7 +449,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'plus-checkout-billing',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
},
].forEach(({ label, options, previousNodeId, expectedStepIds }) => {
const steps = api.getSteps(options);
@@ -342,6 +457,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
const stepKeys = steps.map((step) => step.key);
const nodeIds = nodes.map((node) => node.nodeId);
const previousNode = nodes.find((node) => node.nodeId === previousNodeId);
const waitNode = nodes.find((node) => node.nodeId === 'wait-registration-success');
const sessionImportNode = nodes.find((node) => node.nodeId === 'sub2api-session-import');
assert.equal(stepKeys.at(-1), 'sub2api-session-import', `${label} should end with session import`);
@@ -352,6 +468,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
});
assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the new tail`);
assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match session import`);
assert.deepStrictEqual(waitNode?.next, ['plus-checkout-create'], `${label} wait node should link to checkout chain`);
assert.deepStrictEqual(previousNode?.next, ['sub2api-session-import'], `${label} previous node should link to session import`);
assert.deepStrictEqual(sessionImportNode?.next, [], `${label} session import should be terminal`);
});
@@ -393,7 +510,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
plusAccountAccessStrategy: 'cpa_codex_session',
},
previousNodeId: 'plus-checkout-return',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
},
{
label: 'paypal-hosted',
@@ -403,7 +520,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
plusAccountAccessStrategy: 'cpa_codex_session',
},
previousNodeId: 'paypal-hosted-review',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
},
{
label: 'gopay',
@@ -413,7 +530,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
plusAccountAccessStrategy: 'cpa_codex_session',
},
previousNodeId: 'gopay-subscription-confirm',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
},
{
label: 'gpc-helper',
@@ -423,7 +540,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
plusAccountAccessStrategy: 'cpa_codex_session',
},
previousNodeId: 'plus-checkout-billing',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
},
].forEach(({ label, options, previousNodeId, expectedStepIds }) => {
const steps = api.getSteps(options);
@@ -431,6 +548,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
const stepKeys = steps.map((step) => step.key);
const nodeIds = nodes.map((node) => node.nodeId);
const previousNode = nodes.find((node) => node.nodeId === previousNodeId);
const waitNode = nodes.find((node) => node.nodeId === 'wait-registration-success');
const sessionImportNode = nodes.find((node) => node.nodeId === 'cpa-session-import');
assert.equal(stepKeys.at(-1), 'cpa-session-import', `${label} should end with CPA session import`);
@@ -441,6 +559,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
});
assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the CPA tail`);
assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match CPA session import`);
assert.deepStrictEqual(waitNode?.next, ['plus-checkout-create'], `${label} wait node should link to checkout chain`);
assert.deepStrictEqual(previousNode?.next, ['cpa-session-import'], `${label} previous node should link to CPA session import`);
assert.deepStrictEqual(sessionImportNode?.next, [], `${label} CPA session import should be terminal`);
});
@@ -460,6 +579,7 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="input-plus-mode-enabled"/);
assert.match(html, /id="select-plus-payment-method"/);
assert.match(html, /<option value="none">无需支付<\/option>/);
assert.match(html, /id="select-paypal-account"/);
assert.match(html, /id="btn-add-paypal-account"/);
assert.match(html, /id="input-gopay-phone"/);
+1
View File
@@ -66,6 +66,7 @@ function getStep5Bundle() {
extractFunction('waitForStep5SubmitButton'),
extractFunction('isStep5SubmitButtonClickable'),
extractFunction('isStep5ProfileStillVisible'),
extractFunction('isStep5CompletionChatgptUrl'),
extractFunction('getStep5PostSubmitSuccessState'),
extractFunction('installStep5NavigationCompletionReporter'),
extractFunction('waitForStep5SubmitOutcome'),
+98
View File
@@ -61,6 +61,7 @@ function getStep5OutcomeBundle() {
extractFunction('waitForStep5SubmitButton'),
extractFunction('isStep5SubmitButtonClickable'),
extractFunction('isStep5ProfileStillVisible'),
extractFunction('isStep5CompletionChatgptUrl'),
extractFunction('getStep5PostSubmitSuccessState'),
extractFunction('installStep5NavigationCompletionReporter'),
extractFunction('waitForStep5SubmitOutcome'),
@@ -1057,6 +1058,7 @@ function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
function isStep5ProfileStillVisible() { return false; }
${extractFunction('isStep5CompletionChatgptUrl')}
${extractFunction('getStep5PostSubmitSuccessState')}
return {
@@ -1068,3 +1070,99 @@ return {
assert.equal(api.run(), null);
});
test('step 5 completion requires https chatgpt.com and rejects auth-success-like pages', () => {
const api = new Function(`
const location = {
href: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
function getStep5AuthRetryPageState() { return null; }
function isStep5ProfileStillVisible() { return false; }
${extractFunction('isStep5CompletionChatgptUrl')}
${extractFunction('getStep5PostSubmitSuccessState')}
return {
run(url) {
location.href = url;
return getStep5PostSubmitSuccessState();
},
isCompletion(url) {
return isStep5CompletionChatgptUrl(url);
},
};
`)();
assert.deepStrictEqual(api.run('https://chatgpt.com/'), {
state: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.deepStrictEqual(api.run('https://www.chatgpt.com/c/abc'), {
state: 'logged_in_home',
url: 'https://www.chatgpt.com/c/abc',
});
assert.equal(api.run('https://auth.openai.com/sign-in-with-chatgpt/codex/consent'), null);
assert.equal(api.run('https://auth.openai.com/add-phone'), null);
assert.equal(api.run('https://chat.openai.com/'), null);
assert.equal(api.run('http://chatgpt.com/'), null);
assert.equal(api.isCompletion('https://chatgpt.com/add-phone'), false);
});
test('step 5 navigation reporter does not complete on beforeunload alone', () => {
const api = new Function(`
const events = [];
const listeners = new Map();
const location = {
href: 'https://auth.openai.com/about-you',
};
const window = {
addEventListener(type, handler) {
listeners.set(type, handler);
},
removeEventListener(type) {
listeners.delete(type);
},
};
function log(message, level = 'info') {
events.push({ type: 'log', message, level });
}
function getStep5SubmitState() {
return {
url: location.href,
retryPage: true,
retryEnabled: true,
successState: '',
profileVisible: true,
unknownAuthPage: false,
maxCheckAttemptsBlocked: false,
userAlreadyExistsBlocked: false,
errorText: '',
};
}
${extractFunction('logStep5SubmitDebug')}
${extractFunction('installStep5NavigationCompletionReporter')}
return {
run() {
let completionCount = 0;
const cleanup = installStep5NavigationCompletionReporter(() => {
completionCount += 1;
events.push({ type: 'complete' });
});
const beforeUnload = listeners.get('beforeunload');
if (beforeUnload) {
beforeUnload({ type: 'beforeunload' });
}
cleanup();
return { completionCount, events };
},
};
`)();
const result = api.run();
assert.equal(result.completionCount, 0);
assert.equal(result.events.some((entry) => entry.type === 'log' && /检测到页面开始导航/.test(entry.message)), true);
});