Refactor GPC plus flow to page recharge

This commit is contained in:
QLHazyCoder
2026-05-29 10:09:26 +08:00
parent 9ef9cb4c46
commit 56f52cb858
40 changed files with 1278 additions and 5203 deletions
+7 -7
View File
@@ -441,7 +441,7 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller treats ended GPC task as round-fatal and skips same-round retries', async () => {
test('auto-run controller treats ended GPC page flow as round-fatal and skips same-round retries', async () => {
const events = {
logs: [],
broadcasts: [],
@@ -528,7 +528,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => String(error?.message || error || '').replace(/^GPC_TASK_ENDED::/i, ''),
getErrorMessage: (error) => String(error?.message || error || '').replace(/^GPC_PAGE_FLOW_ENDED::/i, ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
@@ -541,7 +541,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: (error) => /GPC_TASK_ENDED::/i.test(error?.message || String(error || '')),
isGpcPageFlowEndedFailure: (error) => /GPC_PAGE_FLOW_ENDED::/i.test(error?.message || String(error || '')),
isRestartCurrentAttemptError: () => false,
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
@@ -558,7 +558,7 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls === 1) {
throw new Error('GPC_TASK_ENDED::等待 OTP 超过 60 秒,任务已超时');
throw new Error('GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。');
}
},
runtime,
@@ -592,12 +592,12 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
mode: 'restart',
});
assert.equal(events.runCalls, 2, 'ended GPC task should fail current round and continue next round');
assert.equal(events.runCalls, 2, 'ended GPC page flow should fail current round and continue next round');
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false);
assert.equal(events.accountRecords.length, 1);
assert.equal(events.accountRecords[0].status, 'failed');
assert.match(events.accountRecords[0].reason, /等待 OTP/);
assert.ok(events.logs.some(({ message }) => /GPC 任务.*继续下一轮|继续下一轮/.test(message)));
assert.match(events.accountRecords[0].reason, /GPC 页面等待超时/);
assert.ok(events.logs.some(({ message }) => /GPC 页面流程.*继续下一轮|继续下一轮/.test(message)));
});
test('auto-run controller keeps same-round retrying for step9 local replacement exhaustion errors', async () => {
+3 -3
View File
@@ -152,7 +152,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
getStopRequested: () => false,
hasSavedNodeProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: () => false,
isGpcPageFlowEndedFailure: () => false,
isPhoneSmsPlatformRateLimitFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
isRestartCurrentAttemptError: () => false,
@@ -355,7 +355,7 @@ test('auto-run controller clears stale kiro completed statuses before a fresh re
getStopRequested: () => false,
hasSavedNodeProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: () => false,
isGpcPageFlowEndedFailure: () => false,
isKiroProxyFailure: () => false,
isPhoneSmsPlatformRateLimitFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
@@ -536,7 +536,7 @@ test('auto-run controller stops immediately on kiro proxy failures even when ski
getStopRequested: () => false,
hasSavedNodeProgress: () => false,
isAddPhoneAuthFailure: () => false,
isGpcTaskEndedFailure: () => false,
isGpcPageFlowEndedFailure: () => false,
isKiroProxyFailure: (error) => /Kiro 注册页出现 AWS 请求异常|Kiro 注册页返回 403|切换代理|更换代理/i.test(error?.message || String(error || '')),
isPhoneSmsPlatformRateLimitFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
+13 -13
View File
@@ -752,7 +752,7 @@ test('auto-run restarts Plus checkout from step 6 when billing fails for non-fre
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
test('auto-run restarts GPC checkout from step 6 when step 7 page flow stalls', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
@@ -765,7 +765,7 @@ test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls
startStep: 6,
failureStep: 7,
failureBudget: 2,
failureMessage: 'GPC API 请求超时(>30 秒):https://gpc.qlhazycoder.top/api/gp/tasks/task_stalled',
failureMessage: 'GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -785,7 +785,7 @@ test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls
events.invalidations.map((entry) => entry.step),
[5, 5]
);
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run treats GPC account binding as recoverable step 6 restart', async () => {
@@ -801,7 +801,7 @@ test('auto-run treats GPC account binding as recoverable step 6 restart', async
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GOPAY已经绑了订阅,需要手动解绑',
failureMessage: 'GPC_PAGE_FLOW_ENDED::GOPAY已经绑了订阅,需要手动解绑',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -817,7 +817,7 @@ test('auto-run treats GPC account binding as recoverable step 6 restart', async
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
});
test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read', async () => {
test('auto-run restarts GPC checkout from step 6 when ChatGPT session cannot be read', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
@@ -830,7 +830,7 @@ test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read
startStep: 6,
failureStep: 6,
failureBudget: 1,
failureMessage: '步骤 6GPC 模式获取 accessToken 失败。',
failureMessage: '步骤 6GPC 模式获取 ChatGPT session 失败。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -844,10 +844,10 @@ test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read
assert.deepStrictEqual(events.steps, [6, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
test('auto-run restarts GPC checkout from step 6 when page returns without completion', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
@@ -860,7 +860,7 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GPC 任务状态超过 60 秒无进展(已创建),请重新创建任务。',
failureMessage: 'GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面已尝试启动 10 次仍未显示订阅完成。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -874,7 +874,7 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
@@ -890,7 +890,7 @@ test('auto-run keeps rebuilding GPC checkout beyond three failures', async () =>
startStep: 6,
failureStep: 7,
failureBudget: 4,
failureMessage: 'GPC_TASK_ENDED::GPC task status stalled, recreate the task.',
failureMessage: 'GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -947,7 +947,7 @@ test('auto-run does not restart GPC checkout when account already has a ChatGPT
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::该账号已经开通过ChatGPT订阅套餐,不能重复订阅。(checkout_order',
failureMessage: 'GPC_PAGE_FLOW_ENDED::该账号已经开通过ChatGPT订阅套餐,不能重复订阅。(checkout_order',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
@@ -962,7 +962,7 @@ test('auto-run does not restart GPC checkout when account already has a ChatGPT
assert.ok(result?.error);
assert.deepStrictEqual(result.events.steps, [6, 7]);
assert.equal(result.events.invalidations.length, 0);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
assert.ok(!result.events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新准备 GPC 页面/.test(message)));
});
test('auto-run does not reroute SUB2API session import failures into OAuth restarts', async () => {
@@ -58,7 +58,6 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneMode'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeSignupMethod'),
@@ -186,22 +185,23 @@ const self = {
normalizeGoPayPin(value) {
return String(value || '').trim().replace(/[^\\d]/g, '');
},
normalizeGpcHelperBaseUrl(value) {
return String(value || '')
normalizeGpcBaseUrl(value) {
return String(value || 'https://gpc.qlhazycoder.top')
.trim()
.replace(/\\/+$/g, '')
.replace(/\\/api\\/checkout\\/start$/i, '')
.replace(/\\/api\\/gopay\\/(?:otp|pin)$/i, '')
.replace(/\\/api\\/gp\\/tasks(?:\\/[^/?#]+)?(?:\\/(?:otp|pin|stop))?(?:\\?.*)?$/i, '')
.replace(/\\/api\\/gp\\/balance(?:\\?.*)?$/i, '')
.replace(/\\/api\\/web\\/card\\/balance(?:\\?.*)?$/i, '')
.replace(/\\/api\\/card\\/balance(?:\\?.*)?$/i, '')
.replace(/\\/api\\/card\\/redeem-api-key(?:\\?.*)?$/i, '');
|| 'https://gpc.qlhazycoder.top';
},
normalizeGpcCardKey(value) {
return String(value || '').trim().toUpperCase();
},
},
};
const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top',
gpcBaseUrl: 'https://gpc.qlhazycoder.top',
mailProvider: '163',
heroSmsMinPrice: '',
fiveSimMinPrice: '',
@@ -249,37 +249,19 @@ return {
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'cpa_codex_session'), 'cpa_codex_session');
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'unknown'), 'oauth');
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
api.normalizePersistentSettingValue('gpcBaseUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
'https://gpc.qlhazycoder.top'
);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/gp/tasks/task_1/pin '),
api.normalizePersistentSettingValue('gpcBaseUrl', ' https://gpc.qlhazycoder.top/api/web/card/balance?card_key=old '),
'https://gpc.qlhazycoder.top'
);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/gp/balance '),
'https://gpc.qlhazycoder.top'
);
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');
assert.equal(api.normalizePersistentSettingValue('gopayHelperOtpChannel', 'SMS'), 'sms');
assert.equal(api.normalizePersistentSettingValue('gopayHelperOtpChannel', 'unknown'), 'whatsapp');
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsHelperEnabled', 1), true);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperLocalSmsHelperUrl', 'http://127.0.0.1:18767/otp?x=1'),
'http://127.0.0.1:18767'
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsTimeoutSeconds', '999'), 300);
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsPollIntervalSeconds', '0'), 1);
assert.equal(api.normalizePersistentSettingValue('gpcBaseUrl', ''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizePersistentSettingValue('gpcCardKey', ' gpc-6c9f1a32-45734795-914e6f00 '), 'GPC-6C9F1A32-45734795-914E6F00');
assert.equal(api.normalizePersistentSettingValue('gpcRemainingUses', '998'), 998);
assert.equal(api.normalizePersistentSettingValue('gpcCardStatus', ' active '), 'active');
assert.equal(api.normalizePersistentSettingValue('gpcPageStatus', ' running '), 'running');
assert.equal(api.normalizePersistentSettingValue('gpcPageStatusText', ' 页面启动 '), '页面启动');
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
@@ -48,8 +48,8 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
assert.equal(loggingStatus.getLoginAuthStateLabel('choose_account_page'), 'OpenAI choose account page');
assert.equal(
loggingStatus.getErrorMessage(new Error('GPC_TASK_ENDED::GPC OTP 超时,请重新创建任务')),
'GPC OTP 超时,请重新创建任务'
loggingStatus.getErrorMessage(new Error('GPC_PAGE_FLOW_ENDED::步骤 7GPC 页面等待超时,未检测到订阅完成。')),
'步骤 7:GPC 页面等待超时,未检测到订阅完成。'
);
assert.equal(
loggingStatus.isKiroProxyFailure('Kiro 注册页出现 AWS 请求异常,通常是当前代理 IP 或出口区域异常,请先切换代理后再重试。'),
@@ -233,7 +233,7 @@ function createRouter(overrides = {}) {
verifyHotmailAccount: async () => {},
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
events.balanceRefreshes.push({ state, options });
return { balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' };
return { balance: '余额 3', remainingUses: 3, cardStatus: 'active' };
}),
});
@@ -868,39 +868,11 @@ test('message router delegates Kiro manual step 4 without OpenAI auth-tab prereq
assert.deepStrictEqual(events.executedSteps, [4]);
});
test('message router resolves GPC OTP manual confirmation without completing step early', async () => {
const state = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
};
const { router, events } = createRouter({ state });
const response = await router.handleMessage({
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
source: 'sidepanel',
payload: {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: ' 12-34 56 ',
},
}, {});
assert.deepStrictEqual(response, { ok: true });
assert.equal(events.notifyCompletions.length, 0);
assert.equal(events.stepStatuses.length, 0);
assert.equal(events.stateUpdates[0].gopayHelperResolvedOtp, '123456');
assert.equal(events.stateUpdates[0].plusManualConfirmationPending, false);
assert.deepStrictEqual(events.broadcasts[0], events.stateUpdates[0]);
});
test('message router refreshes GPC balance through explicit sidepanel message', async () => {
const state = {
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'http://localhost:18473/',
gopayHelperApiKey: 'state_api_key',
gpcBaseUrl: 'http://localhost:18473/',
gpcCardKey: 'GPC-11111111-22222222-33333333',
};
const { router, events } = createRouter({ state });
@@ -908,15 +880,15 @@ test('message router refreshes GPC balance through explicit sidepanel message',
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperApiKey: 'payload_api_key',
gpcCardKey: 'GPC-6C9F1A32-45734795-914E6F00',
reason: 'manual',
},
}, {});
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' });
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, cardStatus: '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.equal(events.balanceRefreshes[0].state.gpcBaseUrl, 'http://localhost:18473/');
assert.equal(events.balanceRefreshes[0].state.gpcCardKey, 'GPC-6C9F1A32-45734795-914E6F00');
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
});
@@ -410,7 +410,7 @@ return {
cloudflareTempEmailBaseUrl: 'https://mail.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudMailToken: 'cloud-token',
gopayHelperApiKey: 'gpc-key',
gpcCardKey: 'GPC-6C9F1A32-45734795-914E6F00',
heroSmsApiKey: 'hero-key',
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
@@ -443,7 +443,7 @@ return {
assert.equal(api.getPayloadInput().cloudflareTempEmailBaseUrl, 'https://mail.example.com');
assert.equal(api.getPayloadInput().cloudflareTempEmailAdminAuth, 'admin-secret');
assert.equal(api.getPayloadInput().cloudMailToken, 'cloud-token');
assert.equal(api.getPayloadInput().gopayHelperApiKey, 'gpc-key');
assert.equal(api.getPayloadInput().gpcCardKey, 'GPC-6C9F1A32-45734795-914E6F00');
assert.equal(api.getPayloadInput().heroSmsApiKey, 'hero-key');
assert.equal(api.getPayloadInput().phoneSmsProvider, 'madao');
assert.equal(api.getPayloadInput().madaoBaseUrl, 'http://127.0.0.1:7822');
+18 -51
View File
@@ -12,16 +12,9 @@ test('GoPay utils normalize manual OTP input', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizeGoPayOtp(' 12-34 56 '), '123456');
assert.equal(api.normalizeGoPayOtp('abc'), '');
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', () => {
test('GoPay utils keeps GPC payment method distinct', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizePlusPaymentMethod('paypal-hosted'), 'paypal-hosted');
assert.equal(api.normalizePlusPaymentMethod('paypal_direct'), 'paypal-hosted');
@@ -32,51 +25,26 @@ test('GoPay utils keeps GPC helper payment method distinct', () => {
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
});
test('GoPay utils builds GPC queue task and balance URLs from helper endpoints', () => {
test('GoPay utils builds GPC card balance URLs from portal endpoints', () => {
const api = loadGoPayUtils();
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcHelperBaseUrl('https://example.com/api/gp/tasks'), 'https://gpc.qlhazycoder.top');
assert.equal(api.DEFAULT_GPC_BASE_URL, 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcBaseUrl(''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizeGpcBaseUrl('https://example.com/api/web/card/balance'), 'https://gpc.qlhazycoder.top');
assert.equal(
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
api.buildGpcApiUrl('', '/api/checkout/start'),
'https://gpc.qlhazycoder.top/api/checkout/start'
);
assert.equal(
api.buildGpcApiKeyBalanceUrl('http://localhost:18473/'),
'http://localhost:18473/api/gp/balance'
api.buildGpcCardBalanceUrl('https://gpc.qlhazycoder.top/api/web/card/balance'),
'https://gpc.qlhazycoder.top/api/web/card/balance'
);
assert.equal(
api.buildGpcCardBalanceUrl('https://gpc.qlhazycoder.top/api/gp/balance'),
'https://gpc.qlhazycoder.top/api/gp/balance'
);
assert.deepEqual(
api.buildGpcApiKeyHeaders(' gpc-123 ', { Accept: 'application/json' }),
{ Accept: 'application/json', 'X-API-Key': 'gpc-123' }
);
assert.equal(
api.buildGpcTaskCreateUrl('https://gpc.qlhazycoder.top/api/checkout/start'),
'https://gpc.qlhazycoder.top/api/gp/tasks'
);
assert.equal(
api.buildGpcTaskQueryUrl('https://gpc.qlhazycoder.top/api/gp/tasks/task_old?card_key=old', 'task/1'),
'https://gpc.qlhazycoder.top/api/gp/tasks/task%2F1'
);
assert.equal(
api.buildGpcTaskActionUrl('https://gpc.qlhazycoder.top/api/gp/tasks/task_old/stop', 'task_1', 'pin'),
'https://gpc.qlhazycoder.top/api/gp/tasks/task_1/pin'
);
});
test('GoPay utils builds GPC queue OTP/PIN payloads without card_key', () => {
const api = loadGoPayUtils();
assert.deepEqual(
api.buildGpcTaskOtpPayload({ otp: ' 12-34 56 ', card_key: ' card_1 ', reference_id: 'ref_1' }),
{ otp: '123456' }
);
assert.deepEqual(
api.buildGpcTaskPinPayload({ pin: '65-43-21', cardKey: 'card_1', challengeId: 'challenge_1' }),
{ pin: '654321' }
api.buildGpcCardBalanceUrl('https://gpc.qlhazycoder.top/api/web/card/balance?card_key=old', ' gpc-6c9f1a32-45734795-914e6f00 '),
'https://gpc.qlhazycoder.top/api/web/card/balance?card_key=GPC-6C9F1A32-45734795-914E6F00'
);
assert.equal(api.normalizeGpcCardKey(' gpc-6c9f1a32-45734795-914e6f00 '), 'GPC-6C9F1A32-45734795-914E6F00');
assert.equal(api.isGpcCardKeyFormat('GPC-6C9F1A32-45734795-914E6F00'), true);
assert.equal(api.isGpcCardKeyFormat('card-key-1'), false);
});
test('GoPay utils formats balance and maps linked-account errors', () => {
@@ -102,15 +70,14 @@ test('GoPay utils formats balance and maps linked-account errors', () => {
'余额 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.equal(api.getGpcCardStatus({ data: { status: 'active' } }), 'active');
assert.deepEqual(
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { task_id: 'task_1' } }),
{ task_id: 'task_1' }
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { remaining_uses: 1 } }),
{ remaining_uses: 1 }
);
assert.equal(
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['body', 'otp'], msg: 'Field required' }] }, 422),
'body.otp: Field required'
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['query', 'card_key'], msg: 'Field required' }] }, 422),
'query.card_key: Field required'
);
assert.equal(
api.extractGpcResponseErrorDetail({
-201
View File
@@ -1,201 +0,0 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const scriptPath = path.join(process.cwd(), 'scripts/gpc_sms_helper_macos.py');
function runPython(args, options = {}) {
for (const command of ['python3', 'python']) {
const result = spawnSync(command, args, {
encoding: 'utf8',
...options,
env: {
...process.env,
PYTHONIOENCODING: 'utf-8',
...(options.env || {}),
},
});
if (result.error && result.error.code === 'ENOENT') {
continue;
}
if (process.platform === 'win32' && result.status === 9009) {
continue;
}
return result;
}
return null;
}
test('GPC SMS helper shows macOS and iPhone forwarding guidance on non-macOS', () => {
const help = runPython([scriptPath, '--help']);
if (!help) {
return;
}
assert.equal(help.status, 0);
if (process.platform === 'darwin') {
return;
}
const run = runPython([scriptPath, '--db', '/tmp/nonexistent-gpc-chat.db'], {
timeout: 3000,
});
assert.ok(run);
assert.notEqual(run.status, 0);
assert.match(run.stderr, /仅支持 macOS/);
assert.match(run.stderr, /iPhone 短信已转发|短信转发/);
});
test('GPC SMS helper filters cached OTP records by order timestamp', () => {
const code = `
import importlib.util
import json
script_path = ${JSON.stringify(scriptPath)}
spec = importlib.util.spec_from_file_location("gpc_sms_helper_macos", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
old = {"otp": "111111", "code": "111111", "message_id": "old", "received_at": "2026-05-05T00:00:00+00:00"}
fresh = {"otp": "222222", "code": "222222", "message_id": "fresh", "received_at": "2026-05-05T00:00:10+00:00"}
state = {"last_otp": fresh, "otps": [fresh, old]}
payload = {
"without_filter": module.select_otp_record(state, 0)["otp"],
"fresh_only": module.select_otp_record(state, module.parse_timestamp_ms("2026-05-05T00:00:05+00:00"))["otp"],
"none_after_fresh": module.select_otp_record(state, module.parse_timestamp_ms("2026-05-05T00:00:11+00:00")) is None,
}
print(json.dumps(payload))
`;
const run = runPython(['-c', code], {
timeout: 3000,
env: { GPC_SMS_HELPER_ALLOW_NON_MAC: '1' },
});
if (!run) {
return;
}
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout.trim()), {
without_filter: '222222',
fresh_only: '222222',
none_after_fresh: true,
});
});
test('GPC SMS helper selects and consumes cached OTP records by phone', () => {
const code = `
import importlib.util
import json
script_path = ${JSON.stringify(scriptPath)}
spec = importlib.util.spec_from_file_location("gpc_sms_helper_macos", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
record_a = {
"otp": "111111",
"code": "111111",
"message_id": "a",
"rowid": 1,
"phone_e164": "+8615808505050",
"account_phone": "+8615808505050",
"received_at": "2026-05-05T00:00:00+00:00",
}
record_b = {
"otp": "222222",
"code": "222222",
"message_id": "b",
"rowid": 2,
"phone_e164": "+8618984829950",
"account_phone": "+8618984829950",
"received_at": "2026-05-05T00:00:10+00:00",
}
module.STATE.update({"last_otp": record_b, "otps": [record_b, record_a]})
selected_a = module.select_otp_record(module.get_state(), phone="+8615808505050")
module.consume_otp_record(phone="+8615808505050", record=selected_a)
state_after = module.get_state()
payload = {
"selected_a": selected_a["otp"],
"selected_b_after": module.select_otp_record(state_after, phone="+8618984829950")["otp"],
"selected_a_after": module.select_otp_record(state_after, phone="+8615808505050") is None,
"global_after": module.select_otp_record(state_after)["otp"],
}
print(json.dumps(payload))
`;
const run = runPython(['-c', code], {
timeout: 3000,
env: { GPC_SMS_HELPER_ALLOW_NON_MAC: '1' },
});
if (!run) {
return;
}
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout.trim()), {
selected_a: '111111',
selected_b_after: '222222',
selected_a_after: true,
global_after: '222222',
});
});
test('GPC SMS helper consume keeps newer same-phone OTP when consuming an older record', () => {
const code = `
import importlib.util
import json
script_path = ${JSON.stringify(scriptPath)}
spec = importlib.util.spec_from_file_location("gpc_sms_helper_macos", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
record_old = {
"otp": "111111",
"code": "111111",
"message_id": "old",
"rowid": 1,
"phone_e164": "+8613800138000",
"account_phone": "+8613800138000",
"received_at": "2026-05-05T00:00:00+00:00",
}
record_new = {
"otp": "222222",
"code": "222222",
"message_id": "new",
"rowid": 2,
"phone_e164": "+8613800138000",
"account_phone": "+8613800138000",
"received_at": "2026-05-05T00:00:10+00:00",
}
record_other = {
"otp": "333333",
"code": "333333",
"message_id": "other",
"rowid": 3,
"phone_e164": "+8618984829950",
"account_phone": "+8618984829950",
"received_at": "2026-05-05T00:00:20+00:00",
}
module.STATE.update({"last_otp": record_new, "otps": [record_new, record_old, record_other]})
module.consume_otp_record(phone="+8613800138000", record=record_old)
state_after = module.get_state()
payload = {
"same_phone_after": module.select_otp_record(state_after, phone="+8613800138000")["otp"],
"other_phone_after": module.select_otp_record(state_after, phone="+8618984829950")["otp"],
"records_after": [item["message_id"] for item in state_after.get("otps", [])],
"global_after": module.select_otp_record(state_after)["otp"],
}
print(json.dumps(payload))
`;
const run = runPython(['-c', code], {
timeout: 3000,
env: { GPC_SMS_HELPER_ALLOW_NON_MAC: '1' },
});
if (!run) {
return;
}
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout.trim()), {
same_phone_after: '222222',
other_phone_after: '333333',
records_after: ['new', 'other'],
global_after: '222222',
});
});
@@ -60,8 +60,6 @@ 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 = 'oauth';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
${extractFunction('normalizePlusAccountAccessStrategy')}
${extractFunction('getRequestedPlusAccountAccessStrategy')}
${extractFunction('updatePlusModeUI')}
@@ -72,15 +70,6 @@ function normalizePlusPaymentMethod(value = '') {
function getSelectedPlusPaymentMethod() {
return normalizePlusPaymentMethod(selectPlusPaymentMethod.value || latestState?.plusPaymentMethod || currentPlusPaymentMethod || 'paypal');
}
function normalizeGpcHelperPhoneModeValue(value = '') {
return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
}
function normalizeGpcOtpChannelValue(value = '') {
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
}
function isGpcAutoModePermissionDenied() {
return false;
}
function getSelectedPanelMode() {
return latestState?.targetId || 'cpa';
}
File diff suppressed because it is too large Load Diff
+151 -464
View File
@@ -156,37 +156,6 @@ 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({
@@ -878,435 +847,199 @@ 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 manual checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
function createFakeGpcPrepareDom() {
const dispatches = [];
function createElement({ tagName = 'DIV', text = '', placeholder = '', className = '', value = '', disabled = false } = {}) {
return {
tagName,
textContent: text,
innerText: text,
placeholder,
className,
value,
disabled,
clicked: false,
style: { display: 'block', visibility: 'visible', opacity: '1' },
name: '',
id: '',
getAttribute(name) {
if (name === 'class') return this.className;
if (name === 'aria-disabled') return this.disabled ? 'true' : '';
if (name === 'placeholder') return this.placeholder;
return '';
},
focus() {},
blur() {},
scrollIntoView() {},
click() {
this.clicked = true;
},
dispatchEvent(event) {
dispatches.push({ element: this, type: event?.type || '' });
return true;
},
getBoundingClientRect() {
return { width: 120, height: 32 };
},
};
}
const freeButton = createElement({ tagName: 'BUTTON', text: '免费充值' });
const cardModeButton = createElement({ tagName: 'BUTTON', text: '卡密充值 使用付费卡密扣次充值' });
const startButton = createElement({ tagName: 'BUTTON', text: '开始 Plus 充值' });
const cardInputs = [
createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }),
createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }),
createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }),
];
const sessionTextarea = createElement({
tagName: 'TEXTAREA',
placeholder: '可直接粘贴完整 session JSON,会自动提取 accessToken',
className: 'design-session-input',
});
return {
cardModeButton,
cardInputs,
dispatches,
sessionTextarea,
document: {
querySelectorAll(selector) {
const text = String(selector || '');
if (text.includes('button') || text.includes('[role="button"]') || text.includes('.design-mode-card')) {
return [freeButton, cardModeButton, startButton];
}
if (text.includes('input.card-key-seg') || text.includes('placeholder*="XXXXXXXX"') || text.includes('maxlength="8"')) {
return cardInputs;
}
if (text === 'textarea') {
return [sessionTextarea];
}
if (text === 'input') {
return cardInputs;
}
return [];
},
querySelector(selector) {
return String(selector || '').includes('textarea') ? sessionTextarea : null;
},
body: { innerText: 'SYSTEM 页面已就绪' },
documentElement: { innerText: 'SYSTEM 页面已就绪' },
},
};
}
test('GPC checkout prepare opens page, fills segmented card key, and writes full ChatGPT session JSON', async () => {
const events = [];
const fetchCalls = [];
const dom = createFakeGpcPrepareDom();
const session = {
user: { email: 'current@example.com' },
accessToken: 'session-access-token',
expires: '2026-06-01T00:00:00.000Z',
};
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
chrome: {
tabs: {
create: async (payload) => {
events.push({ type: 'tab-create', payload });
return { id: 77 };
return { id: payload.url === 'https://gpc.qlhazycoder.top/' ? 77 : 88, url: payload.url };
},
get: async () => null,
query: async () => [],
remove: async (tabId) => events.push({ type: 'tab-remove', tabId }),
update: async (tabId, payload) => events.push({ type: 'tab-update', tabId, payload }),
},
scripting: {
executeScript: async (details) => {
events.push({ type: 'execute-script', target: details.target, args: details.args });
const previous = {
document: global.document,
window: global.window,
location: global.location,
Event: global.Event,
};
global.document = dom.document;
global.window = { getComputedStyle: (element) => element?.style || {} };
global.location = { href: 'https://gpc.qlhazycoder.top/' };
global.Event = class TestEvent { constructor(type) { this.type = type; } };
try {
return [{ result: details.func(...(details.args || [])) }];
} finally {
global.document = previous.document;
global.window = previous.window;
global.location = previous.location;
global.Event = previous.Event;
}
},
},
},
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
: createGpcTaskResponse({ otp_channel: 'whatsapp' }),
};
createAutomationTab: async (payload) => {
events.push({ type: 'automation-tab-create', payload });
return { id: payload.url === 'https://gpc.qlhazycoder.top/' ? 77 : 88, url: payload.url };
},
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
queryTabsInAutomationWindow: async () => [],
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
sendTabMessageUntilStopped: async (tabId, source, message) => {
events.push({ type: 'tab-message', tabId, source, message });
return { accessToken: 'session-access-token' };
return { session, accessToken: session.accessToken };
},
setState: async (payload) => events.push({ type: 'set-state', payload }),
sleepWithStop: async (ms) => events.push({ type: 'sleep', ms }),
waitForTabCompleteUntilStopped: async () => events.push({ type: 'tab-complete' }),
waitForTabCompleteUntilStopped: async (tabId) => events.push({ type: 'tab-complete', tabId }),
});
await executor.executePlusCheckoutCreate({
email: 'Current.Round+GPC@Example.COM',
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPhoneNumber: '+8613800138000',
gopayPhone: '',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_test_123',
gpcCardKey: 'AAAA1111BBBB2222CCCC3333',
});
const readyIndex = events.findIndex((event) => event.type === 'ready');
const messageIndex = events.findIndex((event) => event.type === 'tab-message');
assert.ok(readyIndex >= 0);
assert.ok(messageIndex > readyIndex);
assert.equal(events[messageIndex].message.type, 'PLUS_CHECKOUT_GET_STATE');
assert.deepEqual(events[messageIndex].message.payload, {
assert.equal(dom.cardModeButton.clicked, true);
assert.deepEqual(dom.cardInputs.map((input) => input.value), ['AAAA1111', 'BBBB2222', 'CCCC3333']);
assert.equal(dom.sessionTextarea.value, JSON.stringify(session));
const sessionMessage = events.find((event) => event.type === 'tab-message');
assert.deepEqual(sessionMessage.message.payload, {
includeSession: true,
includeAccessToken: true,
});
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',
country_code: '86',
phone_number: '13800138000',
otp_channel: 'whatsapp',
});
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);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'gopay_link'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'plan_name'), false);
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.plusCheckoutSource, 'gpc-helper');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperTaskId, 'task_123');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperTaskStatus, 'active');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperStatusText, '处理中');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperRemoteStage, 'checkout_start');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, '');
assert.ok(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperOrderCreatedAt > 0);
const executeEvent = events.find((event) => event.type === 'execute-script');
assert.equal(executeEvent.target.tabId, 77);
assert.equal(executeEvent.args[1], JSON.stringify(session));
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.plusCheckoutTabId, 77);
assert.equal(statePayload.plusCheckoutSource, 'gpc-helper');
assert.equal(statePayload.gpcPageStatus, 'prepared');
assert.equal(statePayload.gpcPageStatusText, '页面已准备');
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
});
test('GPC auto checkout only sends access token and API Key', async () => {
test('GPC checkout prepare rejects missing card key before opening pages', 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 () => {},
},
},
completeNodeFromBackground: 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(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
});
test('GPC auto checkout keeps running when balance payload omits auto mode permission', async () => {
const events = [];
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');
events.push({ type: 'tab-create' });
throw new Error('should not open tab without card key');
},
remove: async () => {},
},
},
completeNodeFromBackground: 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: undefined, remaining_uses: 998 })
: createGpcTaskResponse({
task_id: 'task_auto_unknown_permission',
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[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.equal(helperPayload.phone_mode, 'auto');
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.gopayHelperTaskId, 'task_auto_unknown_permission');
assert.equal(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
});
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');
scripting: {
executeScript: async () => {
throw new Error('should not inject without card key');
},
remove: async () => {},
},
},
completeNodeFromBackground: async () => {},
createAutomationTab: async () => {
events.push({ type: 'automation-tab-create' });
throw new Error('should not open tab without card key');
},
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 () => {},
},
},
completeNodeFromBackground: 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({
addLog: async () => {},
chrome: {
tabs: {
create: async () => ({ id: 88 }),
remove: async () => {},
},
},
completeNodeFromBackground: async () => {},
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: false, remaining_uses: 998 })
: createGpcTaskResponse({ task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({ accessToken: 'session-access-token' }),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await executor.executePlusCheckoutCreate({
email: 'sms@example.com',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_sms',
gopayHelperOtpChannel: 'sms',
});
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[1].options.headers['X-API-Key'], 'gpc_sms');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
});
test('GPC checkout surfaces unified queue API errors', 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 () => {},
},
},
completeNodeFromBackground: 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,
json: async () => ({
code: 400,
message: 'invalid_param',
data: { detail: 'access_token 无效' },
}),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
email: 'paid@example.com',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_paid_456',
}),
/创建 GPC 订单失败:access_token 无效/
);
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 () => {
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 () => {},
},
},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without helper phone');
},
queryTabsInAutomationWindow: async () => [],
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
@@ -1317,55 +1050,9 @@ 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',
gopayPhone: '+8613800138000',
gopayCountryCode: '+86',
gopayPin: '123456',
gopayHelperPhoneNumber: '',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_phone_test',
gpcCardKey: '',
}),
/缺少手机号/
);
});
test('GPC checkout rejects missing API Key before calling helper API', async () => {
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 () => {},
},
},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without API Key');
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
email: 'missing-card@example.com',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: '',
}),
/缺少 API Key/
/缺少卡密/
);
assert.equal(events.length, 0);
});
@@ -134,7 +134,7 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
}
async function ensureGpcApiKeyReadyForStart() {
async function ensureGpcCardKeyReadyForStart() {
return true;
}
${bundle}
@@ -323,7 +323,7 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
return null;
}
async function ensureGpcApiKeyReadyForStart() {
async function ensureGpcCardKeyReadyForStart() {
return true;
}
${bundle}
@@ -204,6 +204,7 @@ const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
const inputVpsUrl = { value: 'https://panel.example.com' };
const inputVpsPassword = { value: 'panel-secret' };
const inputSub2ApiUrl = { value: 'https://sub.example.com' };
+1
View File
@@ -95,6 +95,7 @@ const selectCfDomain = { value: '' };
const selectTempEmailDomain = { value: '' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
@@ -168,6 +168,7 @@ const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
@@ -1245,6 +1245,7 @@ function getCloudflareTempEmailDomainsFromState() { return { domains: [], active
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function normalizeGpcCardKeyInput(value = '') { return String(value || '').trim().toUpperCase(); }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() { return { gmailBaseEmail: '', mail2925BaseEmail: '', emailPrefix: '' }; }
+36 -485
View File
@@ -151,14 +151,6 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
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');
@@ -168,8 +160,6 @@ let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
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 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';
@@ -195,14 +185,6 @@ test('sidepanel Plus UI separates PayPal account mode from PayPal no-card bindin
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');
@@ -212,8 +194,6 @@ let currentPlusPaymentMethod = 'paypal-hosted';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'paypal-hosted', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
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';
@@ -256,14 +236,6 @@ test('sidepanel Plus UI supports no-payment mode without payment-specific rows',
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');
@@ -273,8 +245,6 @@ 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';
@@ -286,7 +256,7 @@ const rowHostedCheckoutVerificationUrl = { style: { display: '' } };
const rowHostedCheckoutPhone = { style: { display: '' } };
const rowPlusHostedCheckoutOauthDelay = { style: { display: '' } };
const rowGoPayPhone = { style: { display: '' } };
const rowGpcHelperApi = { style: { display: '' } };
const rowGpcCardKey = { style: { display: '' } };
${bundle}
return {
updatePlusModeUI,
@@ -297,7 +267,7 @@ return {
rowHostedCheckoutPhone,
rowPlusHostedCheckoutOauthDelay,
rowGoPayPhone,
rowGpcHelperApi,
rowGpcCardKey,
},
};
`)();
@@ -316,14 +286,6 @@ test('sidepanel Plus UI can hide Plus controls when the shared flow capability r
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');
@@ -349,8 +311,6 @@ const rowPlusMode = { style: { display: '' } };
const selectPlusPaymentMethod = { value: 'paypal', style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: '' } };
const rowPayPalAccount = { style: { display: '' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
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';
@@ -373,7 +333,7 @@ return {
assert.equal(api.selectPlusPaymentMethod.style.display, 'none');
});
test('sidepanel step definitions keep GPC helper mode distinct', () => {
test('sidepanel step definitions keep GPC payment mode distinct', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
@@ -436,25 +396,15 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
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: 'gpc-helper', gopayHelperAutoModeEnabled: true };
let latestState = { plusPaymentMethod: 'gpc-helper' };
let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
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 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';
@@ -463,18 +413,7 @@ 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' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
const rowGpcCardKey = { style: { display: 'none' } };
const rowGoPayCountryCode = { style: { display: 'none' } };
const rowGoPayPhone = { style: { display: 'none' } };
const rowGoPayOtp = { style: { display: 'none' } };
@@ -483,13 +422,10 @@ ${bundle}
return {
updatePlusModeUI,
selectPlusPaymentMethod,
selectGpcHelperPhoneMode,
selectGpcHelperOtpChannel,
inputGpcHelperLocalSmsEnabled,
btnGpcCardKeyPurchase,
rowPayPalAccount,
plusPaymentMethodCaption,
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperPhoneMode, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
rows: { rowGpcCardKey },
};
`)();
@@ -497,374 +433,61 @@ return {
assert.equal(api.rowPayPalAccount.style.display, 'none');
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, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
api.inputGpcHelperLocalSmsEnabled.checked = true;
api.updatePlusModeUI();
assert.equal(api.selectGpcHelperOtpChannel.value, 'whatsapp');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, true);
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, /自动/);
assert.equal(api.rows.rowGpcCardKey.style.display, '');
assert.equal(api.plusPaymentMethodCaption.textContent, 'GPC 网页充值链路');
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
assert.equal(api.rows.rowGpcHelperApi.style.display, 'none');
assert.equal(api.rows.rowGpcCardKey.style.display, 'none');
assert.equal(api.rowPayPalAccount.style.display, 'none');
});
test('sidepanel keeps selected GPC auto mode when API Key has no auto permission', () => {
test('sidepanel start check only requires a GPC card key', async () => {
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'),
extractFunction('normalizeGpcCardKeyInput'),
extractFunction('isGpcCardKeyInputFormat'),
extractFunction('setGpcCardKeyStatus'),
extractFunction('ensureGpcCardKeyReadyForStart'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
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 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 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, '');
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 keeps selected GPC auto mode when persisted permission survives stop refresh', () => {
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: 'gpc-helper',
gopayHelperPhoneMode: 'auto',
gopayHelperAutoModeEnabled: true,
gopayHelperBalancePayload: { auto_mode_enabled: true },
};
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
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 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 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}
function syncLatestState(nextState) { latestState = { ...latestState, ...nextState }; }
return {
updatePlusModeUI,
selectGpcHelperPhoneMode,
getSelectedPhoneMode() { return selectGpcHelperPhoneMode.value; },
getPayloadPhoneMode() {
return (() => {
return normalizeGpcHelperPhoneModeValue(selectGpcHelperPhoneMode.value);
})();
},
applyDataUpdated(payload) {
syncLatestState(payload);
if (payload.gopayHelperPhoneMode !== undefined) {
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(payload.gopayHelperPhoneMode);
}
updatePlusModeUI();
},
rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin },
};
`)();
api.updatePlusModeUI();
assert.equal(api.getSelectedPhoneMode(), 'auto');
assert.equal(api.getPayloadPhoneMode(), 'auto');
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
api.applyDataUpdated({
autoRunning: false,
autoRunPhase: 'stopped',
gopayHelperAutoModeEnabled: false,
});
assert.equal(api.getSelectedPhoneMode(), 'auto');
assert.equal(api.getPayloadPhoneMode(), 'auto');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
});
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
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: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: null };
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
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 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 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 start check keeps GPC auto mode when balance payload omits permission field', async () => {
const bundle = [
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcRemainingUsesValue'),
extractFunction('ensureGpcApiKeyReadyForStart'),
].join('\n');
const api = new Function(`
let latestState = { gopayHelperPhoneMode: 'auto' };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const selectGpcHelperPhoneMode = { value: 'auto' };
let latestState = { gpcCardKey: '' };
const inputGpcCardKey = { value: '' };
const displayGpcCardKeyStatus = { textContent: '', dataset: {} };
const dialogs = [];
let saveCalls = 0;
let updateCalls = 0;
const toasts = [];
const window = { GoPayUtils: null };
${bundle}
function isGpcHelperCheckoutSelected() { return true; }
function getSelectedGpcHelperPhoneMode() { return selectGpcHelperPhoneMode.value; }
async function refreshGpcBalanceForStart() {
return {
gopayHelperRemainingUses: 998,
gopayHelperApiKeyStatus: 'active',
gopayHelperAutoModeEnabled: false,
gopayHelperBalancePayload: {
status: 'active',
remaining_uses: 998,
},
};
}
async function showGpcStartBlockedDialog(message) {
dialogs.push(message);
}
function syncLatestState(nextState) {
latestState = { ...latestState, ...nextState };
function showToast(message, type, duration) {
toasts.push({ message, type, duration });
}
function updatePlusModeUI() {
updateCalls += 1;
}
async function saveSettings() {
saveCalls += 1;
}
function showToast() {}
return {
ensureGpcApiKeyReadyForStart,
selectGpcHelperPhoneMode,
ensureGpcCardKeyReadyForStart,
inputGpcCardKey,
displayGpcCardKeyStatus,
setLatestState(nextState) { latestState = { ...latestState, ...nextState }; },
getDialogs: () => dialogs.slice(),
getSaveCalls: () => saveCalls,
getUpdateCalls: () => updateCalls,
getPersistedPhoneMode: () => latestState.gopayHelperPhoneMode,
getToasts: () => toasts.slice(),
};
`)();
const allowed = await api.ensureGpcApiKeyReadyForStart();
assert.equal(await api.ensureGpcCardKeyReadyForStart(), false);
assert.deepEqual(api.getDialogs(), ['请先填写 GPC 卡密。']);
assert.equal(allowed, true);
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
assert.equal(api.getPersistedPhoneMode(), 'auto');
assert.equal(api.getSaveCalls(), 0);
assert.equal(api.getUpdateCalls(), 0);
assert.deepEqual(api.getDialogs(), []);
});
api.inputGpcCardKey.value = ' card-key-1 ';
assert.equal(await api.ensureGpcCardKeyReadyForStart(), false);
assert.match(api.getDialogs()[1], /GPC/);
assert.equal(api.displayGpcCardKeyStatus.dataset.tone, 'error');
test('sidepanel start check blocks unsupported GPC auto mode without rewriting selection', async () => {
const bundle = [
extractFunction('normalizeGpcAutoModePermissionValue'),
extractFunction('getGpcAutoModePermissionFromPayload'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcRemainingUsesValue'),
extractFunction('ensureGpcApiKeyReadyForStart'),
].join('\n');
const api = new Function(`
let latestState = { gopayHelperPhoneMode: 'auto' };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const selectGpcHelperPhoneMode = { value: 'auto' };
const dialogs = [];
let saveCalls = 0;
let updateCalls = 0;
${bundle}
function isGpcHelperCheckoutSelected() { return true; }
function getSelectedGpcHelperPhoneMode() { return selectGpcHelperPhoneMode.value; }
async function refreshGpcBalanceForStart() {
return {
gopayHelperRemainingUses: 998,
gopayHelperApiKeyStatus: 'active',
gopayHelperAutoModeEnabled: false,
gopayHelperBalancePayload: {
status: 'active',
remaining_uses: 998,
auto_mode_enabled: false,
},
};
}
async function showGpcStartBlockedDialog(message) {
dialogs.push(message);
}
function syncLatestState(nextState) {
latestState = { ...latestState, ...nextState };
}
function updatePlusModeUI() {
updateCalls += 1;
}
async function saveSettings() {
saveCalls += 1;
}
function showToast() {}
return {
ensureGpcApiKeyReadyForStart,
selectGpcHelperPhoneMode,
getDialogs: () => dialogs.slice(),
getSaveCalls: () => saveCalls,
getUpdateCalls: () => updateCalls,
getPersistedPhoneMode: () => latestState.gopayHelperPhoneMode,
};
`)();
const allowed = await api.ensureGpcApiKeyReadyForStart();
assert.equal(allowed, false);
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
assert.equal(api.getPersistedPhoneMode(), 'auto');
assert.equal(api.getSaveCalls(), 0);
assert.equal(api.getUpdateCalls(), 0);
assert.equal(api.getDialogs().length, 1);
api.inputGpcCardKey.value = ' gpc-6c9f1a32-45734795-914e6f00 ';
assert.equal(await api.ensureGpcCardKeyReadyForStart({ notify: true }), true);
assert.equal(api.inputGpcCardKey.value, 'GPC-6C9F1A32-45734795-914E6F00');
assert.deepEqual(api.getToasts(), [{ message: 'GPC 卡密已填写。', type: 'success', duration: 1800 }]);
});
test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => {
@@ -928,75 +551,3 @@ return { events, syncPlusManualConfirmationDialog };
assert.match(api.events[2].message, /GoPay/);
assert.equal(api.events[2].tone, 'info');
});
test('sidepanel resolves pending GPC OTP with typed code', async () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('normalizePlusStrategyTargetId'),
extractFunction('getPlusAccountAccessStrategyContinuationLabel'),
extractFunction('resolvePlusManualContinuationActionLabelFromState'),
extractLastFunction('openPlusManualConfirmationDialog'),
extractLastFunction('syncPlusManualConfirmationDialog'),
].join('\n');
const api = new Function(`
const events = [];
let latestState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
plusManualConfirmationTitle: 'GPC OTP 验证',
plusManualConfirmationMessage: '',
};
let activePlusManualConfirmationRequestId = '';
let plusManualConfirmationDialogInFlight = false;
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = 'email';
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 = 'oauth';
const sharedFormDialog = {
async open(options) {
events.push({ type: 'form', options });
return { otp: ' 12-34 56 ' };
},
};
function openActionModal(options) {
events.push({ type: 'modal', options });
return Promise.resolve('confirm');
}
function showToast(message, tone) {
events.push({ type: 'toast', message, tone });
}
const chrome = {
runtime: {
async sendMessage(message) {
events.push({ type: 'send', message });
latestState = { ...latestState, plusManualConfirmationPending: false };
return { ok: true };
},
},
};
${bundle}
return { events, syncPlusManualConfirmationDialog };
`)();
await api.syncPlusManualConfirmationDialog();
assert.equal(api.events[0].type, 'form');
assert.equal(api.events[0].options.message, '请在WhatsApp里面获取验证码(耐心等待三十秒左右)');
assert.equal(api.events[0].options.confirmLabel, '提交 OTP');
const sendEvent = api.events.find((event) => event.type === 'send');
assert.deepEqual(sendEvent.message.payload, {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: '123456',
});
assert.equal(api.events.some((event) => event.type === 'modal'), false);
});
+20 -17
View File
@@ -3,6 +3,16 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const { readStepDefinitionsBundle } = require('./helpers/script-bundles.js');
function stripHtmlComments(html) {
return String(html || '').replace(/<!--[\s\S]*?-->/g, '');
}
function getSelectMarkup(html, selectId) {
const match = String(html || '').match(new RegExp(`<select\\b[^>]*id="${selectId}"[^>]*>[\\s\\S]*?<\\/select>`));
assert.ok(match, `Expected select #${selectId} to exist`);
return match[0];
}
test('step definitions module exposes ordered normal and Plus step metadata', () => {
const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
@@ -319,8 +329,8 @@ 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, 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 任务完成');
assert.equal(gpcSteps[6].title, '打开 GPC 页面并准备');
assert.equal(gpcSteps[7].title, '启动并等待 GPC 完成');
});
test('Plus no-payment mode removes only payment chain nodes', () => {
@@ -639,8 +649,12 @@ test('sidepanel html loads shared step definitions before sidepanel bootstrap',
test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const visibleHtml = stripHtmlComments(html);
const plusPaymentSelect = getSelectMarkup(visibleHtml, 'select-plus-payment-method');
assert.match(html, /id="input-plus-mode-enabled"/);
assert.match(html, /id="select-plus-payment-method"/);
assert.match(plusPaymentSelect, /<option value="gpc-helper">GPC<\/option>/);
assert.doesNotMatch(plusPaymentSelect, /<option value="gopay">GoPay<\/option>/);
assert.match(html, /<option value="none">无需支付<\/option>/);
assert.match(html, /id="select-paypal-account"/);
assert.match(html, /id="btn-add-paypal-account"/);
@@ -649,21 +663,10 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
assert.match(html, /id="input-gopay-pin"/);
assert.match(html, /<option value="gpc-helper">GPC<\/option>/);
assert.match(html, /id="btn-gpc-card-key-purchase"/);
assert.match(html, /id="btn-gpc-card-key-query"/);
assert.match(html, />购买卡密</);
assert.match(html, /GPC API/);
assert.match(html, /id="input-gpc-helper-api"/);
assert.match(html, /id="btn-gpc-helper-convert-api-key"/);
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"/);
assert.match(html, /id="input-gpc-helper-local-sms-enabled"/);
assert.match(html, /id="input-gpc-helper-local-sms-url"/);
assert.match(html, /id="input-gpc-helper-pin"/);
assert.match(html, />查询</);
assert.match(html, /GPC 卡密/);
assert.match(html, /id="input-gpc-card-key"/);
assert.match(html, /id="shared-form-modal"/);
});