Merge origin/dev into PR 225 review fix

This commit is contained in:
QLHazyCoder
2026-05-10 06:02:53 +08:00
42 changed files with 3710 additions and 175 deletions
+120 -16
View File
@@ -56,10 +56,24 @@ const bundle = [
extractFunction('isAddPhoneAuthFailure'),
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isGpcCheckoutRestartRequiredFailure'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
const defaultStepDefinitions = {
1: { key: 'open-signup' },
2: { key: 'prepare-email' },
3: { key: 'fill-password' },
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'auth-login' },
8: { key: 'auth-email-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
function createHarness(options = {}) {
const {
startStep = 7,
@@ -68,13 +82,18 @@ function createHarness(options = {}) {
failureMessage = '认证失败: Request failed with status code 502',
authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
customState = {},
stepDefinitions = defaultStepDefinitions,
stepIds = Object.keys(stepDefinitions).map(Number).sort((a, b) => a - b),
lastStepId = Math.max(...stepIds),
finalOAuthChainStartStep = 7,
} = options;
return new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0 };
const LAST_STEP_ID = ${JSON.stringify(lastStepId)};
const FINAL_OAUTH_CHAIN_START_STEP = ${JSON.stringify(finalOAuthChainStartStep)};
const SIGNUP_METHOD_PHONE = 'phone';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
@@ -104,21 +123,10 @@ async function getState() {
};
}
function getStepIdsForState() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return ${JSON.stringify(stepIds)};
}
function getStepDefinitionForState(step) {
const map = {
1: { key: 'open-signup' },
2: { key: 'prepare-email' },
3: { key: 'fill-password' },
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'auth-login' },
8: { key: 'auth-email-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
const map = ${JSON.stringify(stepDefinitions)};
return map[Number(step)] || null;
}
function getStepExecutionKeyForState(step, state = {}) {
@@ -149,6 +157,10 @@ function getLoginAuthStateLabel(state) {
function getErrorMessage(error) {
return error?.message || String(error || '');
}
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === PLUS_PAYMENT_METHOD_GPC_HELPER ? PLUS_PAYMENT_METHOD_GPC_HELPER : normalized;
}
function isPhoneSmsPlatformRateLimitFailure(error) {
const message = getErrorMessage(error);
return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message);
@@ -321,3 +333,95 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
});
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 2,
failureMessage: 'GPC API 请求超时(>30 秒):https://gpc.qlhazycoder.top/api/gp/tasks/task_stalled',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(
events.steps,
[6, 7, 6, 7, 6, 7, 10, 11, 12, 13]
);
assert.deepStrictEqual(
events.invalidations.map((entry) => entry.step),
[5, 5]
);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
});
test('auto-run treats GPC account binding as recoverable step 6 restart', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GOPAY已经绑了订阅,需要手动解绑',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
});
test('auto-run does not restart GPC checkout when Plus account has no free-trial eligibility', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(IDR 299000),当前账号没有免费试用资格,已跳过支付提交。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.deepStrictEqual(result.events.steps, [6, 7]);
assert.equal(result.events.invalidations.length, 0);
});
@@ -55,6 +55,7 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneMode'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeSignupMethod'),
@@ -207,6 +208,12 @@ return {
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiUrl', ''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKey', ' gpc-123 '), 'gpc-123');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'auto'), 'auto');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'builtin'), 'auto');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'unknown'), 'manual');
assert.equal(api.normalizePersistentSettingValue('gopayHelperRemainingUses', '998'), 998);
assert.equal(api.normalizePersistentSettingValue('gopayHelperAutoModeEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKeyStatus', ' active '), 'active');
assert.equal(api.normalizePersistentSettingValue('gopayHelperCountryCode', ' 86 '), '+86');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneNumber', ' +86 138-0013-8000 '), '+8613800138000');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPin', ' 12-34-56 '), '123456');
@@ -198,6 +198,54 @@ test('account run history helper accepts phone-only records without forcing emai
assert.equal(normalized.finalStatus, 'failed');
});
test('account run history does not turn prerequisite guidance into a fake step 2 failure', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
const helpers = api.createAccountRunHistoryHelpers({
chrome: { storage: { local: { get: async () => ({}), set: async () => {} } } },
getState: async () => ({}),
normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(),
});
const genericFailedRecord = helpers.buildAccountRunHistoryRecord({
email: 'late@example.com',
password: 'secret',
autoRunning: true,
autoRunCurrentRun: 1,
autoRunTotalRuns: 3,
autoRunAttemptRun: 2,
}, 'failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(genericFailedRecord.failedStep, null);
assert.equal(genericFailedRecord.failureLabel, '流程失败');
const explicitFailedRecord = helpers.buildAccountRunHistoryRecord({
email: 'late@example.com',
password: 'secret',
autoRunning: true,
autoRunCurrentRun: 1,
autoRunTotalRuns: 3,
autoRunAttemptRun: 2,
}, 'step10_failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(explicitFailedRecord.failedStep, 10);
assert.equal(explicitFailedRecord.failureLabel, '步骤 10 失败');
const migratedOldRecord = helpers.normalizeAccountRunHistoryRecord({
email: 'old@example.com',
password: 'secret',
finalStatus: 'failed',
failureLabel: '步骤 2 失败',
failureDetail: '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。',
failedStep: 2,
});
assert.equal(migratedOldRecord.failedStep, null);
assert.equal(migratedOldRecord.failureLabel, '流程失败');
});
test('account run history merges email and phone identities from the same run', async () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
+28
View File
@@ -15,3 +15,31 @@ test('auto-run controller module exposes a factory', () => {
assert.equal(typeof api?.createAutoRunController, 'function');
});
test('auto-run account record status preserves the real failed step instead of parsing guidance text', () => {
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
const controller = api.createAutoRunController({});
const state = {
currentStep: 11,
stepStatuses: {
2: 'completed',
10: 'completed',
11: 'failed',
},
};
const error = new Error('缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
'step11_failed'
);
error.failedStep = 13;
assert.equal(
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
'step13_failed'
);
});
@@ -23,13 +23,17 @@ function createRouter(overrides = {}) {
securityBlocks: [],
invalidations: [],
executedSteps: [],
accountRecords: [],
};
const router = api.createMessageRouter({
addLog: async (message, level, options = {}) => {
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
appendAccountRunRecord: async () => null,
appendAccountRunRecord: overrides.appendAccountRunRecord || (async (status, state, reason) => {
events.accountRecords.push({ status, state, reason });
return null;
}),
batchUpdateLuckmailPurchases: async () => {},
buildLocalhostCleanupPrefix: () => '',
buildLuckmailSessionSettingsPayload: () => ({}),
@@ -89,7 +93,7 @@ function createRouter(overrides = {}) {
events.invalidations.push({ step, options });
},
isCloudflareSecurityBlockedError: overrides.isCloudflareSecurityBlockedError || ((error) => /^CF_SECURITY_BLOCKED::/.test(typeof error === 'string' ? error : error?.message || '')),
isAutoRunLockedState: () => false,
isAutoRunLockedState: overrides.isAutoRunLockedState || (() => false),
isHotmailProvider: () => false,
isLocalhostOAuthCallbackUrl: () => true,
isLuckmailProvider: () => false,
@@ -146,7 +150,7 @@ function createRouter(overrides = {}) {
verifyHotmailAccount: async () => {},
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
events.balanceRefreshes.push({ state, options });
return { balance: '余额 3' };
return { balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' };
}),
});
@@ -556,9 +560,65 @@ test('message router refreshes GPC balance through explicit sidepanel message',
},
}, {});
assert.deepStrictEqual(response, { ok: true, balance: '余额 3' });
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 ignores stale step 2 errors while auto-run is already on a later step', async () => {
const { router, events } = createRouter({
state: {
autoRunning: true,
autoRunPhase: 'running',
currentStep: 6,
stepStatuses: {
2: 'completed',
6: 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'STEP_ERROR',
step: 2,
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 }) => /忽略过期的步骤 2 失败消息/.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',
currentStep: 6,
stepStatuses: {
2: 'completed',
6: 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 2,
payload: {
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 }) => /忽略过期的步骤 2 完成消息/.test(message)), true);
});
@@ -428,6 +428,85 @@ test('step 2 does not force auth-entry retry on logged-out chatgpt home when con
assert.equal(logs.some((item) => /已登录 ChatGPT 首页/.test(item.message)), false);
});
test('step 2 waits for the existing signup tab to settle before probing the entry state', async () => {
const completedPayloads = [];
const logs = [];
const events = [];
const executor = step2Api.createStep2Executor({
addLog: async (message, level = 'info', meta = {}) => {
logs.push({ message, level, meta });
},
chrome: {
tabs: {
update: async () => {
events.push('tab-update');
},
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
completeStepFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {
events.push('content-ready');
},
ensureSignupAuthEntryPageReady: async () => ({ tabId: 17 }),
ensureSignupEntryPageReady: async () => ({ tabId: 17 }),
ensureSignupPostEmailPageReadyInTab: async () => ({
state: 'password_page',
url: 'https://auth.openai.com/create-account/password',
}),
getTabId: async () => 17,
isTabAlive: async () => true,
resolveSignupEmailForFlow: async () => 'user@example.com',
sendToContentScriptResilient: async (_source, message) => {
events.push(message.type);
if (message.type === 'ENSURE_SIGNUP_ENTRY_READY') {
return { ready: true, state: 'entry_home', url: 'https://chatgpt.com/' };
}
return { submitted: true };
},
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabStableComplete: async (_tabId, options) => {
events.push({ type: 'wait-stable', options });
return { id: 17, url: 'https://chatgpt.com/', status: 'complete' };
},
});
await executor.executeStep2({ email: 'user@example.com' });
assert.deepStrictEqual(events.slice(0, 4), [
'tab-update',
{
type: 'wait-stable',
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 3000,
initialDelayMs: 300,
},
},
'content-ready',
'ENSURE_SIGNUP_ENTRY_READY',
]);
assert.equal(logs.some((item) => /额外稳定 3 /.test(item.message)), true);
assert.equal(logs.some((item) => item.meta.step === 2 && item.meta.stepKey === 'signup-entry'), true);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
nextSignupState: 'password_page',
nextSignupUrl: 'https://auth.openai.com/create-account/password',
skippedPasswordStep: false,
},
},
]);
});
test('signup flow helper recognizes email verification page as post-email landing page', async () => {
let ensureCalls = 0;
let passwordReadyChecks = 0;
@@ -477,6 +556,71 @@ test('signup flow helper recognizes email verification page as post-email landin
assert.equal(passwordReadyChecks, 0);
});
test('signup flow helper waits for the signup entry tab to settle for step 2 before probing the entry page', async () => {
const logs = [];
const events = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
addLog: async (message, level = 'info', meta = {}) => {
logs.push({ message, level, meta });
},
buildGeneratedAliasEmail: () => '',
ensureContentScriptReadyOnTab: async () => {
events.push('content-ready');
},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => false,
reuseOrCreateTab: async () => {
events.push('reuse-or-create');
return 23;
},
sendToContentScriptResilient: async () => {
events.push('probe-entry');
return { ready: true, state: 'entry_home', url: 'https://chatgpt.com/' };
},
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabStableComplete: async (_tabId, options) => {
events.push({ type: 'wait-stable', options });
return { id: 23, url: 'https://chatgpt.com/', status: 'complete' };
},
waitForTabUrlMatch: async () => null,
});
const result = await helpers.ensureSignupEntryPageReady(2);
assert.deepStrictEqual(events, [
'reuse-or-create',
{
type: 'wait-stable',
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 3000,
initialDelayMs: 300,
},
},
'content-ready',
'probe-entry',
]);
assert.equal(logs.some((item) => /额外稳定 3 /.test(item.message)), true);
assert.equal(logs.some((item) => item.meta.step === 2 && item.meta.stepKey === 'signup-entry'), true);
assert.deepStrictEqual(result, {
tabId: 23,
result: {
ready: true,
state: 'entry_home',
url: 'https://chatgpt.com/',
},
});
});
test('signup flow helper accepts phone signup landing on login password page', async () => {
let ensureCalls = 0;
let passwordReadyChecks = 0;
@@ -580,6 +580,62 @@ test('step 7 keeps Plus email login even when phone sms runtime exists', async (
assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
});
test('step 7 keeps phone login after step 8 stores an unbound email for phone signup', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
payloads: [],
};
const phoneSignupState = {
phoneVerificationEnabled: true,
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
email: 'bound.step8@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'bound.step8@example.com',
signupPhoneNumber: '66959916439',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
password: 'secret',
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ ...phoneSignupState }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async (_sourceName, message) => {
events.payloads.push(message.payload);
return {
step6Outcome: 'success',
state: 'phone_verification_page',
loginVerificationRequestedAt: 123456,
};
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7(phoneSignupState);
assert.equal(events.payloads[0].loginIdentifierType, 'phone');
assert.equal(events.payloads[0].phoneNumber, '66959916439');
assert.equal(events.payloads[0].email, '');
assert.equal(events.payloads[0].accountIdentifier, '66959916439');
});
test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
+16 -1
View File
@@ -15,6 +15,10 @@ test('GoPay utils normalize manual OTP input', () => {
assert.equal(api.normalizeGpcOtpChannel('sms'), 'sms');
assert.equal(api.normalizeGpcOtpChannel('wa'), 'whatsapp');
assert.equal(api.normalizeGpcOtpChannel('unknown'), 'whatsapp');
assert.equal(api.normalizeGpcHelperPhoneMode('auto'), 'auto');
assert.equal(api.normalizeGpcHelperPhoneMode('builtin'), 'auto');
assert.equal(api.normalizeGpcHelperPhoneMode('manual'), 'manual');
assert.equal(api.normalizeGpcHelperPhoneMode('unknown'), 'manual');
});
test('GoPay utils keeps GPC helper payment method distinct', () => {
@@ -81,10 +85,21 @@ test('GoPay utils formats balance and maps linked-account errors', () => {
api.formatGpcBalancePayload({
code: 200,
message: 'ok',
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active' },
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active', auto_mode_enabled: false },
}),
'余额 0/3,已用 3,状态 active'
);
assert.equal(
api.formatGpcBalancePayload({
code: 200,
message: 'ok',
data: { remaining_uses: 998, total_uses: 1000, used_uses: 2, status: 'active', auto_mode_enabled: true },
}),
'余额 998/1000,已用 2,状态 active'
);
assert.equal(api.getGpcBalanceRemainingUses({ data: { remaining_uses: 998 } }), 998);
assert.equal(api.isGpcAutoModeEnabled({ data: { auto_mode_enabled: true } }), true);
assert.equal(api.isGpcAutoModeEnabled({ data: { auto_mode_enabled: false } }), false);
assert.deepEqual(
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { task_id: 'task_1' } }),
{ task_id: 'task_1' }
+63
View File
@@ -5714,6 +5714,69 @@ test('phone verification helper stops when add-phone recovery cannot be verified
}
});
test('signup phone verification cancels activation when resend lands on contact-verification HTTP 500 page', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
verificationResendCount: 0,
phoneCodeWaitSeconds: 60,
phoneCodeTimeoutWindows: 2,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
signupPhoneActivation: {
activationId: '920001',
phoneNumber: '66953330001',
provider: 'hero-sms',
countryId: 52,
countryLabel: 'Thailand',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
if (action === 'getStatus') {
return { ok: true, text: async () => 'STATUS_WAIT_CODE' };
}
if (action === 'setStatus') {
return { ok: true, text: async () => `STATUS_UPDATED:${id}` };
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
throw new Error(
'PHONE_RESEND_SERVER_ERROR::This page isn\'t working auth.openai.com is currently unable to handle this request. HTTP ERROR 500'
);
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
() => helpers.completeSignupPhoneVerificationFlow(1, { state: currentState }),
(error) => {
assert.match(error.message, /^PHONE_RESEND_SERVER_ERROR::This page isn't working/);
assert.equal(error.message.includes('PHONE_RESEND_SERVER_ERROR::PHONE_RESEND_SERVER_ERROR::'), false);
return true;
}
);
assert.equal(currentState.signupPhoneActivation, null);
});
test('phone verification helper skips page resend for 5sim timeouts and rotates number directly', async () => {
const requests = [];
const messages = [];
@@ -85,6 +85,7 @@ function createExecutorHarness({
markCurrentRegistrationAccountUsed = async () => {},
probeIpProxyExit = null,
onSetState = null,
sleepWithStop = null,
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
}) {
const api = loadPlusCheckoutBillingModule();
@@ -166,7 +167,7 @@ function createExecutorHarness({
await onSetState(updates, events);
}
},
sleepWithStop: async (ms) => events.sleeps.push(ms),
sleepWithStop: sleepWithStop || (async (ms) => events.sleeps.push(ms)),
waitForTabCompleteUntilStopped: async () => checkoutTab,
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
events.waitedUrls.push({ tabId });
@@ -903,11 +904,183 @@ test('GPC billing polls queue task, submits WhatsApp OTP then PIN, and waits unt
assert.equal(pinCall.options.headers['X-API-Key'], 'gpc_billing_123');
assert.ok(fetchCalls.findIndex((call) => call.url.endsWith('/api/gp/tasks/task_123/pin')) < fetchCalls.length - 1);
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_123' && state.gopayHelperTaskStatus === 'completed'), true);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:等待 WhatsApp OTP'), true);
assert.equal(events.logs.some((entry) => /whatsapp_otp_wait/.test(entry.message)), false);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
assert.ok(events.sleeps.includes(3000));
});
test('GPC billing auto mode only polls until completed without OTP or PIN submission', async () => {
const fetchCalls = [];
let pollCount = 0;
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_auto') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'queued', status_text: '排队中', api_waiting_for: '' }),
};
}
if (pollCount === 2) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'active', status_text: '处理中', remote_stage: 'auto_otp_wait', api_waiting_for: 'auto_otp' }),
};
}
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'completed', status_text: '充值完成', remote_stage: 'completed', api_waiting_for: '' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_auto',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
});
assert.equal(fetchCalls.length, 3);
assert.equal(fetchCalls.some((call) => /\/api\/gp\/tasks\/task_auto\/(otp|pin)$/.test(call.url)), false);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:等待自动 OTP'), true);
assert.equal(events.logs.some((entry) => /auto_otp_wait/.test(entry.message)), false);
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_auto' && state.gopayHelperPhoneMode === 'auto' && state.gopayHelperTaskStatus === 'completed'), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
});
test('GPC billing logs checkout order stage in Chinese', async () => {
let pollCount = 0;
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url) => {
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_stage') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stage',
phone_mode: 'auto',
status: 'active',
status_text: '处理中',
remote_stage: 'checkout_order_start',
api_waiting_for: '',
}),
};
}
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stage',
phone_mode: 'auto',
status: 'completed',
status_text: '充值完成',
remote_stage: 'completed',
api_waiting_for: '',
}),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_stage',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
});
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:创建订单'), true);
assert.equal(events.logs.some((entry) => /checkout_order_start/.test(entry.message)), false);
});
test('GPC billing fails repeated checkout stage as stale so auto-run can recreate task', async () => {
const originalNow = Date.now;
let now = 1710000000000;
const fetchCalls = [];
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
sleepWithStop: async (ms) => {
events.sleeps.push(ms);
now += ms;
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_stale') {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stale',
phone_mode: 'auto',
status: 'active',
status_text: '处理中',
remote_stage: 'checkout_order_start',
api_waiting_for: '',
}),
};
}
if (url.endsWith('/api/gp/tasks/task_stale/stop')) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stale',
status: 'discarded',
status_text: '已停止',
}),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
Date.now = () => now;
try {
await assert.rejects(
() => executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_stale',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
gopayHelperTaskStaleSeconds: 15,
}),
/GPC_TASK_ENDED::GPC 任务状态超过 15 秒无进展(创建订单),请重新创建任务。/
);
} finally {
Date.now = originalNow;
}
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_stale/stop')), true);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:创建订单'), true);
});
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
const fetchCalls = [];
let pollCount = 0;
+224 -27
View File
@@ -5,7 +5,9 @@ const vm = require('node:vm');
const source = fs.readFileSync('background/steps/create-plus-checkout.js', 'utf8');
const plusCheckoutSource = fs.readFileSync('content/plus-checkout.js', 'utf8');
const gopayUtilsSource = fs.readFileSync('gopay-utils.js', 'utf8');
const globalScope = {};
new Function('self', `${gopayUtilsSource};`)(globalScope);
const api = new Function('self', `${source}; return self.MultiPageBackgroundPlusCheckoutCreate;`)(globalScope);
function createCheckoutContentHarness() {
@@ -154,6 +156,37 @@ function createCheckoutContentHarness() {
return { checkoutEvents, send };
}
function createGpcBalanceResponse(overrides = {}) {
return {
code: 200,
message: 'ok',
data: {
api_key: 'gpc_test',
status: 'active',
auto_mode_enabled: false,
total_uses: 1000,
remaining_uses: 998,
used_uses: 2,
...overrides,
},
};
}
function createGpcTaskResponse(overrides = {}) {
return {
code: 200,
message: 'ok',
data: {
task_id: 'task_123',
status: 'active',
status_text: '处理中',
phone_mode: 'manual',
remote_stage: 'checkout_start',
...overrides,
},
};
}
test('Plus checkout create does not wait 20 seconds after opening checkout page', async () => {
const events = [];
const executor = api.createPlusCheckoutCreateExecutor({
@@ -323,7 +356,7 @@ test('Plus checkout content routes same-frame autocomplete query and suggestion
assert.equal(checkoutEvents.some((event) => event.type === 'delay' && event.ms !== 2000), false);
});
test('GPC checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
test('GPC manual checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
const events = [];
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
@@ -344,18 +377,9 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
return {
ok: true,
status: 200,
json: async () => ({
code: 200,
message: 'ok',
data: {
task_id: 'task_123',
status: 'active',
status_text: '处理中',
phone_mode: 'manual',
remote_stage: 'checkout_start',
otp_channel: 'whatsapp',
},
}),
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
: createGpcTaskResponse({ otp_channel: 'whatsapp' }),
};
},
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
@@ -371,6 +395,7 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
await executor.executePlusCheckoutCreate({
email: 'Current.Round+GPC@Example.COM',
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPhoneNumber: '+8613800138000',
gopayPhone: '',
@@ -388,9 +413,11 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
includeSession: true,
includeAccessToken: true,
});
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[0].options.body);
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.deepEqual(helperPayload, {
access_token: 'session-access-token',
phone_mode: 'manual',
@@ -398,7 +425,7 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
phone_number: '13800138000',
otp_channel: 'whatsapp',
});
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_test_123');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'customer_email'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'checkout_ui_mode'), false);
@@ -415,6 +442,165 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
});
test('GPC auto checkout only sends access token and API Key', async () => {
const events = [];
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: true, remaining_uses: 998 })
: createGpcTaskResponse({
task_id: 'task_auto',
status: 'queued',
status_text: '排队中',
phone_mode: 'auto',
api_waiting_for: '',
}),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({}),
setState: async (payload) => events.push({ type: 'set-state', payload }),
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperApiKey: 'gpc_auto_123',
});
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_auto_123');
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.deepEqual(helperPayload, {
access_token: 'state-access-token',
phone_mode: 'auto',
});
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_auto_123');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'country_code'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'phone_number'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'otp_channel'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'pin'), false);
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.gopayHelperTaskId, 'task_auto');
assert.equal(statePayload.gopayHelperPhoneMode, 'auto');
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
});
test('GPC auto checkout blocks API Keys without auto mode permission', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperApiKey: 'gpc_auto_disabled',
}),
/未开通自动模式/
);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
});
test('GPC checkout blocks exhausted API Keys before creating task', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 0 }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_exhausted',
}),
/剩余次数不足/
);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
});
test('GPC checkout forwards selected SMS OTP channel', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
@@ -432,11 +618,9 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
return {
ok: true,
status: 200,
json: async () => ({
code: 200,
message: 'ok',
data: { task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' },
}),
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
: createGpcTaskResponse({ task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' }),
};
},
registerTab: async () => {},
@@ -457,10 +641,13 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
gopayHelperOtpChannel: 'sms',
});
const helperPayload = JSON.parse(fetchCalls[0].options.body);
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.equal(helperPayload.phone_mode, 'manual');
assert.equal(helperPayload.otp_channel, 'sms');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_sms');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
});
@@ -480,6 +667,13 @@ test('GPC checkout surfaces unified queue API errors', async () => {
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.endsWith('/api/gp/balance')) {
return {
ok: true,
status: 200,
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 }),
};
}
return {
ok: false,
status: 400,
@@ -511,9 +705,11 @@ test('GPC checkout surfaces unified queue API errors', async () => {
/创建 GPC 订单失败:access_token 无效/
);
assert.equal(fetchCalls.length, 1);
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[0].options.body), 'card_key'), false);
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_paid_456');
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[1].options.body), 'card_key'), false);
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_paid_456');
});
test('GPC checkout does not fall back to browser GoPay phone fields', async () => {
@@ -542,6 +738,7 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
email: 'helper-phone-test@example.com',
@@ -113,6 +113,9 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
}
async function ensureGpcApiKeyReadyForStart() {
return true;
}
${bundle}
return {
startAutoRunFromCurrentSettings,
+124 -2
View File
@@ -132,6 +132,10 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -141,6 +145,8 @@ let latestState = { plusPaymentMethod: 'gopay' };
let currentPlusPaymentMethod = 'paypal';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gopay', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const rowPayPalAccount = { style: { display: '' } };
${bundle}
return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
@@ -209,21 +215,29 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper' };
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperAutoModeEnabled: true };
let currentPlusPaymentMethod = 'paypal';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
const selectGpcHelperPhoneMode = { value: 'manual' };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
@@ -240,12 +254,13 @@ ${bundle}
return {
updatePlusModeUI,
selectPlusPaymentMethod,
selectGpcHelperPhoneMode,
selectGpcHelperOtpChannel,
inputGpcHelperLocalSmsEnabled,
btnGpcCardKeyPurchase,
rowPayPalAccount,
plusPaymentMethodCaption,
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperPhoneMode, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
};
`)();
@@ -255,6 +270,7 @@ return {
assert.equal(api.btnGpcCardKeyPurchase.style.display, '');
assert.equal(api.rows.rowGpcHelperApi.style.display, '');
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
@@ -272,6 +288,15 @@ return {
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperPhoneMode.value = 'auto';
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /自动/);
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
@@ -279,6 +304,103 @@ return {
assert.equal(api.rowPayPalAccount.style.display, 'none');
});
test('sidepanel hides GPC auto mode selector when API Key has no auto permission', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
let currentPlusPaymentMethod = 'gpc-helper';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
const selectGpcHelperPhoneMode = { value: 'auto' };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
${bundle}
return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin } };
`)();
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, 'none');
assert.equal(api.selectGpcHelperPhoneMode.value, 'manual');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperPin.style.display, '');
assert.match(api.plusPaymentMethodCaption.textContent, /手动/);
});
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: null };
let currentPlusPaymentMethod = 'gpc-helper';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
const selectGpcHelperPhoneMode = { value: 'auto' };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
${bundle}
return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin } };
`)();
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, 'none');
assert.equal(api.rows.rowGpcHelperPin.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /自动/);
});
test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => {
const bundle = [
extractFunction('openPlusManualConfirmationDialog'),
+252
View File
@@ -440,6 +440,127 @@ return {
assert.equal(api.run()?.kind, 'localized-email');
});
test('waitForSignupEntryState retries the signup entry click five times before giving up', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
let now = 0;
const signupButton = {
textContent: '免费注册',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 120, height: 36 };
},
};
const document = {
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"]') {
return [signupButton];
}
return [];
},
};
const location = {
href: 'https://chatgpt.com/',
};
const Date = {
now() {
return now;
},
};
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
function isVisibleElement(el) {
return Boolean(el);
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getSignupPasswordInput() {
return null;
}
function isSignupPasswordPage() {
return false;
}
function getSignupPasswordSubmitButton() {
return null;
}
function getSignupPasswordDisplayedEmail() {
return '';
}
function throwIfStopped() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
async function humanPause() {}
function simulateClick(target) {
clicks.push(getActionText(target));
}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('findSignupEntryTrigger')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
return {
async run() {
return waitForSignupEntryState({ timeout: 30000, autoOpenEntry: true, step: 2 });
},
getClicks() {
return clicks.slice();
},
getLogs() {
return logs.slice();
},
};
`)();
const snapshot = await api.run();
assert.equal(snapshot.state, 'entry_home');
assert.equal(api.getClicks().length, 6);
assert.equal(api.getLogs().some(({ message }) => message.includes('重试 5/5')), true);
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
});
test('ensureSignupPhoneEntryReady opens free signup before switching to the phone entry', async () => {
const api = new Function(`
const logs = [];
@@ -625,6 +746,137 @@ return {
assert.deepEqual(api.getClicks(), ['免费注册', 'Continue with phone number']);
});
test('waitForSignupPhoneEntryState retries the signup entry click five times before giving up', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
let now = 0;
const signupButton = {
textContent: '免费注册',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 120, height: 36 };
},
};
const document = {
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"]') {
return [signupButton];
}
return [];
},
};
const location = {
href: 'https://chatgpt.com/',
};
const Date = {
now() {
return now;
},
};
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
function isVisibleElement(el) {
return Boolean(el);
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getSignupPasswordInput() {
return null;
}
function isSignupPasswordPage() {
return false;
}
function getSignupPasswordSubmitButton() {
return null;
}
function getSignupPasswordDisplayedEmail() {
return '';
}
function throwIfStopped() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
function simulateClick(target) {
clicks.push(getActionText(target));
}
async function humanPause() {}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('findSignupEntryTrigger')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupPhoneEntryState')}
return {
async run() {
return waitForSignupPhoneEntryState({ timeout: 30000, step: 2 });
},
getClicks() {
return clicks.slice();
},
getLogs() {
return logs.slice();
},
};
`)();
const snapshot = await api.run();
assert.equal(snapshot.state, 'entry_home');
assert.equal(api.getClicks().length, 6);
assert.equal(api.getLogs().some(({ message }) => message.includes('重试 5/5')), true);
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
});
test('submitSignupPhoneNumberAndContinue auto-switches signup country before filling the local phone number', async () => {
const api = new Function(`
const logs = [];
+4 -1
View File
@@ -111,7 +111,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
assert.equal(gpcSteps[5].title, '创建 GPC 订单');
assert.equal(gpcSteps[6].title, 'GPC OTP/PIN 验证');
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
});
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
@@ -142,6 +142,9 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
assert.match(html, />转换 API Key</);
assert.match(html, /GPC API Key/);
assert.match(html, /id="input-gpc-helper-card-key"/);
assert.match(html, /GPC 模式/);
assert.match(html, /id="select-gpc-helper-phone-mode"/);
assert.match(html, /<option value="auto">自动模式<\/option>/);
assert.match(html, /id="btn-gpc-helper-balance"/);
assert.match(html, /id="input-gpc-helper-phone"/);
assert.match(html, /id="select-gpc-helper-otp-channel"/);
+53
View File
@@ -341,6 +341,59 @@ return {
assert.equal(Object.prototype.hasOwnProperty.call(result, 'invalidCode'), false);
});
test('resendVerificationCode reports contact-verification HTTP 500 after resend click', async () => {
const api = new Function(`
const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::';
const CONTACT_VERIFICATION_SERVER_ERROR_PATTERN = /this\\s+page\\s+isn[']?t\\s+working|currently\\s+unable\\s+to\\s+handle\\s+this\\s+request|http\\s+error\\s+500|500\\s+internal\\s+server\\s+error/i;
const logs = [];
const location = {
href: 'https://auth.openai.com/email-verification',
pathname: '/email-verification',
};
const document = {
title: 'Verify your email',
body: { textContent: 'Enter the verification code.' },
};
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
function findResendVerificationCodeTrigger() { return { textContent: 'Resend code' }; }
function isActionEnabled() { return true; }
async function humanPause() {}
function simulateClick() {
location.href = 'https://auth.openai.com/contact-verification';
location.pathname = '/contact-verification';
document.title = "This page isn't working";
document.body.textContent = 'auth.openai.com is currently unable to handle this request. HTTP ERROR 500';
}
async function sleep() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function getActionText(element) { return element?.textContent || ''; }
function getPageTextSnapshot() { return document.body.textContent; }
${extractFunction('getContactVerificationServerErrorText')}
${extractFunction('throwIfContactVerificationServerError')}
${extractFunction('resendVerificationCode')}
return {
async run() {
try {
await resendVerificationCode(4, 1000);
return { ok: true };
} catch (error) {
return { ok: false, message: String(error?.message || error), logs };
}
},
};
`)();
const result = await api.run();
assert.equal(result.ok, false);
assert.match(result.message, /^PHONE_RESEND_SERVER_ERROR::This page isn't working/);
assert.equal(result.message.includes('PHONE_RESEND_SERVER_ERROR::PHONE_RESEND_SERVER_ERROR::'), false);
});
test('fillVerificationCode does not short-circuit on mixed email-verification profile page before verification exits', async () => {
const api = new Function(`
const logs = [];
+63 -4
View File
@@ -54,9 +54,21 @@ function extractFunction(name) {
function getStep5Bundle() {
return [
extractFunction('getStep5DirectCompletionPayload'),
extractFunction('isSignupProfilePageUrl'),
extractFunction('isStep5AllConsentText'),
extractFunction('findStep5AllConsentCheckbox'),
extractFunction('isStep5CheckboxChecked'),
extractFunction('getStep5ProfilePathPatterns'),
extractFunction('getStep5AuthRetryPathPatterns'),
extractFunction('isStep5ProfilePageUrl'),
extractFunction('getStep5AuthRetryPageState'),
extractFunction('getStep5SubmitButton'),
extractFunction('waitForStep5SubmitButton'),
extractFunction('isStep5SubmitButtonClickable'),
extractFunction('isStep5ProfileStillVisible'),
extractFunction('getStep5PostSubmitSuccessState'),
extractFunction('installStep5NavigationCompletionReporter'),
extractFunction('waitForStep5SubmitOutcome'),
extractFunction('step5_fillNameBirthday'),
].join('\n');
}
@@ -73,6 +85,9 @@ const completeButton = {
tagName: 'BUTTON',
textContent: '\\u5b8c\\u6210\\u8d26\\u6237\\u521b\\u5efa',
hidden: false,
getAttribute() {
return '';
},
};
const allConsentLabel = {
hidden: false,
@@ -110,6 +125,7 @@ const document = {
return null;
case 'input[name="age"]':
return ageInput;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -136,6 +152,8 @@ function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
async function waitForElement() {
return nameInput;
}
@@ -155,6 +173,14 @@ function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true';
}
function getActionText(el) {
return el.textContent || '';
}
async function setReactAriaBirthdaySelect() {
throw new Error('setReactAriaBirthdaySelect should not run in age-mode test');
}
@@ -168,6 +194,9 @@ function simulateClick(el) {
if (el === allConsentLabel || el === allConsentCheckbox) {
allConsentCheckbox.checked = true;
}
if (el === completeButton) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete(step, payload) {
@@ -178,6 +207,17 @@ function normalizeInlineText(text) {
return String(text || '').replace(/\\s+/g, ' ').trim();
}
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
@@ -205,9 +245,11 @@ return {
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
ageMode: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.equal(snapshot.nameValue, 'Mia Harris');
assert.equal(snapshot.ageValue, '19');
@@ -220,9 +262,11 @@ return {
{
step: 5,
payload: {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
ageMode: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
},
},
]);
@@ -291,6 +335,7 @@ const document = {
return null;
case 'input[name="age"]':
return ageInput;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -311,6 +356,7 @@ const document = {
const location = { href: 'https://auth.openai.com/u/signup/profile' };
function log() {}
function throwIfStopped() {}
async function waitForElement() { return nameInput; }
async function humanPause() {}
async function sleep() {}
@@ -321,9 +367,22 @@ async function setReactAriaBirthdaySelect() { throw new Error('setReactAriaBirth
async function waitForElementByText() { throw new Error('waitForElementByText should not run in this test'); }
function simulateClick(el) {
operationEvents.push(\`simulate-click:\${activeOperationLabel || 'outside'}:\${el.textContent || el.tagName || 'element'}\`);
if (el === completeButton) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete() {}
function normalizeInlineText(text) { return String(text || '').replace(/\\s+/g, ' ').trim(); }
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
+272 -10
View File
@@ -51,8 +51,26 @@ function extractFunction(name) {
return source.slice(start, end);
}
function getStep5OutcomeBundle() {
return [
extractFunction('getStep5ProfilePathPatterns'),
extractFunction('getStep5AuthRetryPathPatterns'),
extractFunction('isStep5ProfilePageUrl'),
extractFunction('getStep5AuthRetryPageState'),
extractFunction('getStep5SubmitButton'),
extractFunction('waitForStep5SubmitButton'),
extractFunction('isStep5SubmitButtonClickable'),
extractFunction('isStep5ProfileStillVisible'),
extractFunction('getStep5PostSubmitSuccessState'),
extractFunction('installStep5NavigationCompletionReporter'),
extractFunction('waitForStep5SubmitOutcome'),
].join('\n');
}
function getStep5Bundle() {
return [
extractFunction('isSignupProfilePageUrl'),
getStep5OutcomeBundle(),
extractFunction('getStep5DirectCompletionPayload'),
extractFunction('isStep5AllConsentText'),
extractFunction('findStep5AllConsentCheckbox'),
@@ -61,11 +79,11 @@ function getStep5Bundle() {
].join('\n');
}
test('step 5 clicks submit and completes immediately on birthday page', async () => {
test('step 5 clicks submit and completes after confirmed birthday page result', async () => {
const step5Source = extractFunction('step5_fillNameBirthday');
assert.ok(
!step5Source.includes('waitForStep5SubmitOutcome('),
'Step 5 提交后不应再等待页面结果'
step5Source.includes('waitForStep5SubmitOutcome('),
'Step 5 提交后等待页面结果,避免资料提交假完成'
);
const api = new Function(`
@@ -84,6 +102,7 @@ const completeButton = {
tagName: 'BUTTON',
textContent: '完成帐户创建',
hidden: false,
getAttribute() { return ''; },
};
const birthdaySelects = {
@@ -102,6 +121,7 @@ const document = {
return null;
case 'input[name="birthday"]':
return hiddenBirthday;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -112,6 +132,12 @@ const document = {
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') {
return [];
}
if (selector === 'input[type="checkbox"]') {
return [];
}
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') {
return [completeButton];
}
return [];
},
execCommand() {},
@@ -130,6 +156,8 @@ function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
async function waitForElement() {
return nameInput;
}
@@ -149,6 +177,14 @@ function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true';
}
function getActionText(el) {
return el.textContent || '';
}
async function setReactAriaBirthdaySelect(select, value) {
selectedBirthday[select.label] = String(value).padStart(select.label === '年' ? 4 : 2, '0');
if (selectedBirthday['年'] && selectedBirthday['月'] && selectedBirthday['天']) {
@@ -162,6 +198,9 @@ async function waitForElementByText() {
function simulateClick(el) {
clicks.push(el.textContent || el.tagName || 'element');
if (el === completeButton) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete(step, payload) {
@@ -172,6 +211,17 @@ function reportComplete(step, payload) {
return String(text || '').replace(/\\s+/g, ' ').trim();
}
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
@@ -202,17 +252,21 @@ return {
assert.deepStrictEqual(
result,
{
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
},
'生日模式点击提交后应直接返回完成载荷'
'生日模式点击提交后应等待并确认页面结果'
);
assert.deepStrictEqual(snapshot.completions, [
{
step: 5,
payload: {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
},
},
]);
@@ -220,8 +274,8 @@ return {
assert.equal(snapshot.nameValue, 'Test User');
assert.equal(snapshot.birthdayValue, '2003-06-19');
assert.ok(
snapshot.logs.some(({ message }) => /不再等待页面结果/.test(message)),
'日志应明确说明 Step 5 已直接完成'
snapshot.logs.some(({ message }) => /资料提交结果已确认/.test(message)),
'日志应明确说明 Step 5 已完成提交后检测'
);
});
@@ -237,6 +291,7 @@ const completeButton = {
tagName: 'BUTTON',
textContent: '完成帐户创建',
hidden: false,
getAttribute() { return ''; },
};
const window = {
@@ -261,6 +316,7 @@ const document = {
return null;
case 'input[name="age"]':
return ageInput;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -271,6 +327,12 @@ const document = {
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') {
return [];
}
if (selector === 'input[type="checkbox"]') {
return [];
}
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') {
return [completeButton];
}
return [];
},
execCommand() {},
@@ -284,6 +346,8 @@ function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
async function waitForElement() {
return nameInput;
}
@@ -304,6 +368,14 @@ function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true';
}
function getActionText(el) {
return el.textContent || '';
}
async function setReactAriaBirthdaySelect() {}
async function waitForElementByText() {
@@ -312,6 +384,7 @@ async function waitForElementByText() {
function simulateClick() {
profileEvents.push('click:complete');
location.href = 'https://chatgpt.com/';
}
function reportComplete(step, payload) {
@@ -322,6 +395,17 @@ function normalizeInlineText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
@@ -613,3 +697,181 @@ return {
'delay:fill-birthday-day:2000',
]);
});
test('step 5 retries submit while profile page remains visible', async () => {
const api = new Function(`
const realDateNow = Date.now;
let now = 0;
const logs = [];
const completions = [];
const clicks = [];
const nameInput = { value: '', hidden: false };
const ageInput = { value: '', hidden: false };
const completeButton = {
tagName: 'BUTTON',
textContent: '完成帐户创建',
hidden: false,
getAttribute() { return ''; },
};
const location = {
href: 'https://auth.openai.com/u/signup/profile',
};
const document = {
querySelector(selector) {
switch (selector) {
case '[role="spinbutton"][data-type="year"]':
case '[role="spinbutton"][data-type="month"]':
case '[role="spinbutton"][data-type="day"]':
case 'input[name="birthday"]':
return null;
case 'input[name="age"]':
return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href) ? ageInput : null;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
return null;
}
},
querySelectorAll(selector) {
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') return [];
if (selector === 'input[type="checkbox"]') return [];
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [completeButton];
return [];
},
execCommand() {},
};
Date.now = () => now;
function log(message, level = 'info') { logs.push({ message, level }); }
function throwIfStopped() {}
async function waitForElement() { return nameInput; }
async function humanPause() {}
async function sleep(ms = 0) { now += ms || 250; }
function fillInput(input, value) { input.value = value; }
function findBirthdayReactAriaSelect() { return null; }
function isVisibleElement(el) { return Boolean(el) && !el.hidden; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true'; }
function getActionText(el) { return el.textContent || ''; }
async function setReactAriaBirthdaySelect() { throw new Error('setReactAriaBirthdaySelect should not run'); }
async function waitForElementByText() { return completeButton; }
function simulateClick(el) {
clicks.push(el.textContent || el.tagName || 'element');
if (el === completeButton && clicks.filter((text) => text === '完成帐户创建').length >= 3) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete(step, payload) { completions.push({ step, payload }); }
function normalizeInlineText(text) { return String(text || '').replace(/\\s+/g, ' ').trim(); }
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
run() {
return step5_fillNameBirthday({
firstName: 'Mia',
lastName: 'Harris',
age: 19,
});
},
snapshot() {
return { logs, completions, clicks, nameValue: nameInput.value, ageValue: ageInput.value };
},
restore() {
Date.now = realDateNow;
},
};
`)();
let result;
try {
result = await api.run();
} finally {
api.restore();
}
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
profileSubmitted: true,
postSubmitChecked: true,
ageMode: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.equal(snapshot.nameValue, 'Mia Harris');
assert.equal(snapshot.ageValue, '19');
assert.equal(snapshot.clicks.filter((text) => text === '完成帐户创建').length, 3);
assert.equal(snapshot.logs.some(({ message }) => /仍停留在资料页正在重新点击/.test(message)), true);
});
test('step 5 recovers auth retry page after profile submit', async () => {
const api = new Function(`
let retryVisible = true;
let recoverCalls = 0;
const location = { href: 'https://auth.openai.com/create-account/profile' };
const document = {
querySelector() { return null; },
querySelectorAll() { return []; },
};
function throwIfStopped() {}
function log() {}
async function sleep() {}
async function humanPause() {}
function simulateClick() {}
function isVisibleElement() { return true; }
function isActionEnabled() { return true; }
function getActionText(el) { return el?.textContent || ''; }
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() {
if (!retryVisible) return null;
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
maxCheckAttemptsBlocked: false,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
retryVisible = false;
location.href = 'https://chatgpt.com/';
}
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${extractFunction('isSignupProfilePageUrl')}
${getStep5OutcomeBundle()}
return {
run() {
return waitForStep5SubmitOutcome({ timeoutMs: 1000 });
},
snapshot() {
return { recoverCalls };
},
};
`)();
const result = await api.run();
assert.deepStrictEqual(result, {
state: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.equal(api.snapshot().recoverCalls, 1);
});
+172
View File
@@ -182,3 +182,175 @@ return {
},
});
});
test('step9 defers callback timeout while auth security verification is still redirecting', async () => {
const api = new Function('step9ModuleSource', `
const self = {};
let webNavListener = null;
let webNavCommittedListener = null;
let step8TabUpdatedListener = null;
let cleanupCalls = 0;
let deferCalls = 0;
let completePayload = null;
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
const chrome = {
webNavigation: {
onBeforeNavigate: {
addListener(listener) {
webNavListener = listener;
setTimeout(() => {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 25);
},
removeListener() {},
},
onCommitted: {
addListener(listener) {
webNavCommittedListener = listener;
},
removeListener() {},
},
},
tabs: {
onUpdated: {
addListener(listener) {
step8TabUpdatedListener = listener;
},
removeListener() {},
},
async update() {},
},
};
async function addLog() {}
function cleanupStep8NavigationListeners() {
cleanupCalls += 1;
webNavListener = null;
webNavCommittedListener = null;
step8TabUpdatedListener = null;
}
function throwIfStep8SettledOrStopped(resolved) {
if (resolved) throw new Error('already resolved');
}
async function getTabId() { return 123; }
async function isTabAlive() { return true; }
async function ensureStep8SignupPageReady() {}
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
if (options.actionLabel === 'OAuth localhost 回调') {
return 1;
}
return defaultTimeoutMs;
}
async function shouldDeferStep9CallbackTimeout(details = {}) {
deferCalls += 1;
return Number(details.tabId) === 123;
}
async function waitForStep8Ready() {
return {
consentReady: true,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function triggerStep8ContentStrategy() {}
async function waitForStep8ClickEffect() {
return {
progressed: true,
reason: 'url_changed',
url: 'https://auth.openai.com/api/oauth/oauth2/auth?client_id=app_test',
};
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
return Number(details?.tabId) === Number(signupTabId) ? details.url : '';
}
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'URL 已变化'; }
async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; }
async function clickWithDebugger() {}
async function reloadStep8ConsentPage() {}
async function reuseOrCreateTab() { return 123; }
async function sleepWithStop() {}
function setWebNavListener(listener) { webNavListener = listener; }
function getWebNavListener() { return webNavListener; }
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject() {}
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
const STEP8_READY_WAIT_TIMEOUT_MS = 5;
const STEP8_MAX_ROUNDS = 1;
const STEP8_STRATEGIES = [
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
];
${step9ModuleSource}
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
shouldDeferStep9CallbackTimeout,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
STEP8_READY_WAIT_TIMEOUT_MS,
STEP8_STRATEGIES,
throwIfStep8SettledOrStopped,
triggerStep8ContentStrategy,
waitForStep8ClickEffect,
waitForStep8Ready,
});
return {
executeStep9: executor.executeStep9,
snapshot() {
return {
cleanupCalls,
deferCalls,
completePayload,
};
},
};
`)(step9ModuleSource);
await api.executeStep9({
oauthUrl: 'https://auth.openai.com/original-oauth',
visibleStep: 12,
});
const snapshot = api.snapshot();
assert.equal(snapshot.deferCalls >= 1, true);
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.deepEqual(snapshot.completePayload, {
step: 12,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
});
});
+151 -1
View File
@@ -547,7 +547,12 @@ test('verification flow keeps step 8 successful when code submit transport fails
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
throw new Error('message channel is closed before a response was received');
}
return {};
},
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'FILL_CODE') {
throw new Error('message channel is closed before a response was received');
@@ -1381,6 +1386,151 @@ test('verification flow uses resilient signup-page transport when submitting ver
assert.ok(resilientCalls[0].options.timeoutMs >= 30000);
});
test('verification flow does not replay step 8 code submit after transient auth-page transport failure', async () => {
const directMessages = [];
const resilientMessages = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isRetryableContentScriptTransportError: (error) => /did not respond/i.test(String(error?.message || error || '')),
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
directMessages.push(message.type);
if (message.type === 'FILL_CODE') {
throw new Error('步骤 8:页面通信异常 did not respond in 30s');
}
return {};
},
sendToContentScriptResilient: async (_source, message) => {
resilientMessages.push(message.type);
if (message.type === 'GET_LOGIN_AUTH_STATE') {
return {
state: 'verification_page',
verificationErrorText: '代码不正确',
url: 'https://auth.openai.com/email-verification',
};
}
throw new Error('FILL_CODE should not be replayed through resilient transport');
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(8, '510725', { completionStep: 11 });
assert.deepStrictEqual(result, {
invalidCode: true,
errorText: '代码不正确',
url: 'https://auth.openai.com/email-verification',
});
assert.deepStrictEqual(directMessages, ['FILL_CODE']);
assert.deepStrictEqual(resilientMessages, ['GET_LOGIN_AUTH_STATE']);
});
test('verification flow requests a new code immediately after Cloudflare Temp Email code is rejected', async () => {
const events = [];
const pollPayloads = [];
const completed = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (_step, payload) => {
completed.push(payload);
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
pollPayloads.push(payload);
const code = pollPayloads.length === 1 ? '111111' : '222222';
events.push(['poll', code]);
return { code, emailTimestamp: pollPayloads.length };
},
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
events.push(['resend', message.step]);
return {};
}
if (message.type === 'FILL_CODE') {
events.push(['submit', message.payload.code]);
return message.payload.code === '111111'
? { invalidCode: true, errorText: '代码不正确' }
: { success: true };
}
return {};
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
loginVerificationRequestedAt: Date.now(),
verificationResendCount: 1,
},
{ provider: 'cloudflare-temp-email', label: 'Cloudflare Temp Email' },
{
completionStep: 11,
lastResendAt: Date.now(),
resendIntervalMs: 25000,
}
);
assert.deepStrictEqual(events, [
['poll', '111111'],
['submit', '111111'],
['resend', 8],
['poll', '222222'],
['submit', '222222'],
]);
assert.deepStrictEqual(pollPayloads[1].excludeCodes, ['111111']);
assert.equal(completed[0].code, '222222');
});
test('verification flow forwards optional signup profile payload when submitting signup verification code', async () => {
const resilientCalls = [];