chore: merge latest dev into pr 208 fixes

This commit is contained in:
QLHazyCoder
2026-05-07 02:26:08 +08:00
28 changed files with 2713 additions and 44 deletions
@@ -2,6 +2,47 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function extractFunctionBody(source, name) {
const start = source.indexOf(`function ${name}(`);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
for (let i = braceStart; i < source.length; i += 1) {
const ch = source[i];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(braceStart + 1, i);
}
}
}
throw new Error(`unterminated function ${name}`);
}
test('icloud login helper distinguishes auth-required errors from transient context errors', () => {
const source = fs.readFileSync('background.js', 'utf8');
@@ -107,3 +148,20 @@ test('icloud login helper distinguishes auth-required errors from transient cont
'icloud auto-fetch should fallback to reusable aliases when create-new fails due transient session/context issues'
);
});
test('icloud login helper does not redeclare safeActionLabel in transient branch', () => {
const source = fs.readFileSync('background.js', 'utf8');
const body = extractFunctionBody(source, 'withIcloudLoginHelp');
const declarations = body.match(/\bconst\s+safeActionLabel\b/g) || [];
assert.equal(
declarations.length,
1,
'withIcloudLoginHelp should declare safeActionLabel once to avoid temporal-dead-zone crashes'
);
assert.match(
body,
/const transientError = new Error\(`iCloud\$\{safeActionLabel\}受网络\/上下文波动影响,请稍后重试。`\);/,
'transient context errors should use the already-initialized safeActionLabel'
);
});
@@ -7,6 +7,27 @@ test('background imports message router module', () => {
assert.match(source, /background\/message-router\.js/);
});
test('background defaults enable free phone reuse switches', () => {
const source = fs.readFileSync('background.js', 'utf8');
const defaultsStart = source.indexOf('const PERSISTED_SETTING_DEFAULTS = {');
const defaultsEnd = source.indexOf('const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);');
const defaultsBlock = source.slice(defaultsStart, defaultsEnd);
assert.match(defaultsBlock, /freePhoneReuseEnabled:\s*true/);
assert.match(defaultsBlock, /freePhoneReuseAutoEnabled:\s*true/);
});
test('background free reusable phone setter does not depend on module-scoped phone flow constants', () => {
const source = fs.readFileSync('background.js', 'utf8');
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
const setterBlock = source.slice(setterStart, setterEnd);
assert.ok(setterStart >= 0, 'expected setFreeReusablePhoneActivation to exist');
assert.doesNotMatch(setterBlock, /DEFAULT_PHONE_NUMBER_MAX_USES/);
assert.match(setterBlock, /maxUses:\s*Math\.max\(1,\s*Math\.floor\(Number\(record\.maxUses\)\s*\|\|\s*3\)\)/);
});
test('message router module exposes a factory', () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
@@ -15,3 +36,56 @@ test('message router module exposes a factory', () => {
assert.equal(typeof api?.createMessageRouter, 'function');
});
test('SAVE_SETTING broadcasts free phone reuse setting updates for realtime sidepanel sync', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
const broadcasts = [];
let state = {
freePhoneReuseEnabled: false,
freePhoneReuseAutoEnabled: false,
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
};
const router = api.createMessageRouter({
addLog: async () => {},
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: (input = {}) => {
const updates = {};
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseEnabled')) {
updates.freePhoneReuseEnabled = Boolean(input.freePhoneReuseEnabled);
}
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseAutoEnabled')) {
updates.freePhoneReuseAutoEnabled = Boolean(input.freePhoneReuseAutoEnabled);
}
return updates;
},
broadcastDataUpdate: (payload) => broadcasts.push(payload),
getState: async () => ({ ...state }),
setPersistentSettings: async () => {},
setState: async (updates) => {
state = { ...state, ...updates };
},
});
const response = await router.handleMessage({
type: 'SAVE_SETTING',
payload: {
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
},
});
assert.equal(response.ok, true);
assert.equal(state.freePhoneReuseEnabled, true);
assert.equal(state.freePhoneReuseAutoEnabled, true);
assert.ok(
broadcasts.some((payload) => (
payload.freePhoneReuseEnabled === true
&& payload.freePhoneReuseAutoEnabled === true
)),
'expected SAVE_SETTING to broadcast free reuse switch updates'
);
});
@@ -116,3 +116,49 @@ test('panel bridge can request cpa oauth url via management api', async () => {
globalThis.fetch = originalFetch;
}
});
test('panel bridge forwards SUB2API account priority when requesting oauth url', async () => {
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
const sentMessages = [];
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
const bridge = api.createPanelBridge({
addLog: async () => {},
chrome: {
tabs: {
create: async () => ({ id: 72 }),
},
},
closeConflictingTabsForSource: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'sub2api',
normalizeCodex2ApiUrl: (value) => value,
normalizeSub2ApiUrl: (value) => value,
rememberSourceLastUrl: async () => {},
sendToContentScript: async (sourceName, message, options) => {
sentMessages.push({ sourceName, message, options });
return {
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
sub2apiSessionId: 'session-123',
sub2apiOAuthState: 'oauth-state',
};
},
sendToContentScriptResilient: async () => ({}),
waitForTabUrlFamily: async () => ({ id: 72 }),
DEFAULT_SUB2API_GROUP_NAME: 'codex',
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
});
await bridge.requestOAuthUrlFromPanel({
panelMode: 'sub2api',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiAccountPriority: 3,
}, { logLabel: '步骤 7' });
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 3);
});
@@ -272,3 +272,34 @@ test('platform verify module sends Plus visible step 13 to SUB2API panel', async
assert.equal(sentMessages[0].message.step, 13);
});
test('platform verify module forwards SUB2API account priority to panel', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const sentMessages = [];
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps } = createDeps({
getPanelMode: () => 'sub2api',
getTabId: async () => 91,
isTabAlive: async () => true,
sendToContentScript: async (sourceName, message, options) => {
sentMessages.push({ sourceName, message, options });
return { ok: true };
},
});
const executor = api.createStep10Executor(deps);
await executor.executeStep10({
panelMode: 'sub2api',
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
sub2apiAccountPriority: 2,
});
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 2);
});
+149
View File
@@ -294,3 +294,152 @@ return {
assert.equal(result.code, '556677');
});
test('icloud step8 polling finds a visible first-row code immediately', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
const currentThreadData = [
{
signature: 'visible-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 576773',
preview: '输入此临时验证码以继续:576773',
timestamp: '21:05',
ariaLabel: 'visible-code',
},
{
signature: 'older-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 697852',
preview: '输入此临时验证码以继续:697852',
timestamp: '21:04',
ariaLabel: 'older-code',
},
];
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return { handlePollEmail };
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['openai', 'noreply', 'chatgpt'],
subjectFilters: ['code', '验证码', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: [],
sessionKey: '8:visible-first',
});
assert.equal(result.code, '576773');
});
test('icloud step8 visible first-row code still respects excluded codes', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
const currentThreadData = [
{
signature: 'visible-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 576773',
preview: '输入此临时验证码以继续:576773',
timestamp: '21:05',
ariaLabel: 'visible-code',
},
];
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return { handlePollEmail };
`)();
await assert.rejects(
() => api.handlePollEmail(8, {
senderFilters: ['openai', 'noreply', 'chatgpt'],
subjectFilters: ['code', '验证码', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: ['576773'],
sessionKey: '8:excluded-visible-first',
}),
/仍未在 iCloud 邮箱中找到新的匹配邮件/
);
});
+49
View File
@@ -448,3 +448,52 @@ test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of en
global.window = originalWindow;
}
});
test('phone auth exposes resend page error checks for banned numbers', () => {
const originalLocation = global.location;
const originalDocument = global.document;
try {
global.location = {
href: 'https://auth.openai.com/phone-verification',
pathname: '/phone-verification',
};
global.document = {
querySelector() {
return {
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
};
},
};
const helpers = api.createPhoneAuthHelpers({
fillInput: () => {},
getActionText: () => '',
getPageTextSnapshot: () => 'We cannot send a text message to this phone number. Please try another.',
getVerificationErrorText: () => '',
humanPause: async () => {},
isActionEnabled: () => true,
isAddPhonePageReady: () => false,
isConsentReady: () => false,
isPhoneVerificationPageReady: () => true,
isVisibleElement: () => true,
simulateClick: () => {},
sleep: async () => {},
throwIfStopped: () => {},
waitForElement: async () => null,
});
const result = helpers.checkPhoneResendError();
assert.equal(result.hasError, true);
assert.equal(result.reason, 'resend_phone_banned');
assert.equal(result.prefix, 'PHONE_RESEND_BANNED_NUMBER::');
assert.match(result.message, /cannot send/i);
} finally {
global.location = originalLocation;
global.document = originalDocument;
}
});
+819
View File
@@ -2794,6 +2794,28 @@ test('phone verification helper immediately replaces number when page says the p
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
phoneReusableActivationPool: [
{
activationId: 'pool-used-match',
phoneNumber: '+66 95 000 0011',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
},
],
freeReusablePhoneActivation: {
activationId: 'free-used-match',
phoneNumber: '+66 95 000 0011',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const numbers = [
@@ -2903,6 +2925,11 @@ test('phone verification helper immediately replaces number when page says the p
'SUBMIT_PHONE_NUMBER',
'SUBMIT_PHONE_VERIFICATION_CODE',
]);
assert.equal(currentState.freeReusablePhoneActivation, null);
assert.deepStrictEqual(
currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 0011'),
[]
);
});
test('phone verification helper treats phone_max_usage_exceeded as used-number and rotates immediately', async () => {
@@ -2913,6 +2940,28 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
phoneReusableActivationPool: [
{
activationId: 'pool-max-match',
phoneNumber: '+66 95 000 1011',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
},
],
freeReusablePhoneActivation: {
activationId: 'free-max-match',
phoneNumber: '+66 95 000 1011',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const numbers = [
@@ -3022,6 +3071,11 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
'SUBMIT_PHONE_NUMBER',
'SUBMIT_PHONE_VERIFICATION_CODE',
]);
assert.equal(currentState.freeReusablePhoneActivation, null);
assert.deepStrictEqual(
currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 1011'),
[]
);
});
test('phone verification helper rotates number when submitPhoneVerificationCode throws phone_max_usage_exceeded', async () => {
@@ -3418,6 +3472,771 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
});
test('phone verification helper gives automatic free reuse priority over paid reuse and new acquisition', async () => {
const requests = [];
let statusPollCount = 0;
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: true,
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
reusablePhoneActivation: {
activationId: 'paid-reuse',
phoneNumber: '66950003333',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
},
freeReusablePhoneActivation: {
activationId: 'free-priority',
phoneNumber: '66950004444',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: 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 === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
throw new Error(`${action} should not be called before automatic free reuse`);
}
if (action === 'setStatus' && id === 'free-priority') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'free-priority') {
statusPollCount += 1;
return { ok: true, text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_CODE' : 'STATUS_OK:112233') };
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.deepStrictEqual(result, {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.deepStrictEqual(
requests.map((requestUrl) => requestUrl.searchParams.get('action')),
['setStatus', 'getStatus', 'getStatus']
);
assert.deepStrictEqual(
requests
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
.map((requestUrl) => requestUrl.searchParams.get('status')),
['3']
);
assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 1);
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'free-priority');
assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse');
});
test('phone verification helper accepts HeroSMS WAIT_RETRY as free-reuse ready without using its suffix as code', async () => {
const requests = [];
const events = [];
const submittedCodes = [];
let statusPollCount = 0;
let currentState = {
heroSmsApiKey: 'demo-key',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
activationId: 'retry-ready-free',
phoneNumber: '6283184934060',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 6,
countryLabel: 'Indonesia',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
const status = parsedUrl.searchParams.get('status');
events.push(action === 'setStatus' ? `${action}:${status}` : action);
if (action === 'setStatus' && id === 'retry-ready-free' && status === '3') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'retry-ready-free') {
statusPollCount += 1;
return {
ok: true,
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RETRY:100001' : 'STATUS_OK:654321'),
};
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
events.push(message.type);
if (message.type === 'SUBMIT_PHONE_NUMBER') {
assert.equal(message.payload.phoneNumber, '6283184934060');
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
submittedCodes.push(message.payload.code);
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {
events.push('sleep');
},
throwIfStopped: () => {},
});
const result = await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.deepStrictEqual(result, {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.deepStrictEqual(submittedCodes, ['654321']);
assert.equal(submittedCodes.includes('100001'), false);
assert.deepStrictEqual(
events.slice(0, 4),
['setStatus:3', 'sleep', 'getStatus', 'SUBMIT_PHONE_NUMBER']
);
assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 1);
assert.equal(
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
false
);
});
test('phone verification helper stops failed automatic free reuse without buying a new number', async () => {
const requests = [];
const messages = [];
const stops = [];
let currentState = {
heroSmsApiKey: 'demo-key',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
activationId: 'cancelled-free',
phoneNumber: '66950007777',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: 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 === 'setStatus' && id === 'cancelled-free') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'cancelled-free') {
return { ok: true, text: async () => 'STATUS_CANCEL' };
}
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
throw new Error(`${action} should not be called after automatic free reuse preparation fails`);
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
requestStop: async (payload) => {
stops.push(payload);
},
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
throw new Error(`Auth page should not be touched after failed free reuse: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
}),
/PHONE_AUTO_FREE_REUSE_PREPARE::/
);
assert.deepStrictEqual(messages, []);
assert.equal(stops.length, 1);
assert.match(stops[0].logMessage, /不购买新 HeroSMS 号码/);
assert.equal(currentState.freeReusablePhoneActivation, null);
assert.equal(
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
false
);
});
test('phone verification helper stops never-ready automatic free reuse without buying or reactivating', async () => {
const requests = [];
const messages = [];
const stops = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: true,
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
activationId: 'never-ready-free',
phoneNumber: '66950008888',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: 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 === 'setStatus' && id === 'never-ready-free') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'never-ready-free') {
return { ok: true, text: async () => 'STATUS_OK:445566' };
}
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
throw new Error(`${action} should not be called after automatic free reuse never becomes ready`);
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
requestStop: async (payload) => {
stops.push(payload);
},
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
throw new Error(`Auth page should not be touched when automatic free reuse is never ready: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
}),
/PHONE_AUTO_FREE_REUSE_PREPARE::/
);
assert.deepStrictEqual(messages, []);
assert.equal(stops.length, 1);
assert.match(stops[0].logMessage, /不购买新 HeroSMS 号码/);
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'never-ready-free');
assert.equal(
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
false
);
assert.equal(requests.every((requestUrl) => requestUrl.searchParams.get('id') === 'never-ready-free'), true);
});
test('phone verification helper accepts HeroSMS WAIT_RESEND as free-reuse ready before submit', async () => {
const requests = [];
const messages = [];
let statusPollCount = 0;
let currentState = {
heroSmsApiKey: 'demo-key',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
activationId: 'not-waiting-free',
phoneNumber: '66950005555',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: 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 === 'setStatus' && id === 'not-waiting-free') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'not-waiting-free') {
statusPollCount += 1;
return {
ok: true,
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RESEND' : 'STATUS_OK:778899'),
};
}
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
throw new Error(`${action} should not be called for accepted saved free reuse waiting state`);
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.deepStrictEqual(result, {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.deepStrictEqual(
messages.map((message) => message.type),
['SUBMIT_PHONE_NUMBER', 'SUBMIT_PHONE_VERIFICATION_CODE']
);
assert.deepStrictEqual(
requests
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
.map((requestUrl) => requestUrl.searchParams.get('status')),
['3']
);
});
test('phone verification helper accepts HeroSMS WAIT_RESEND suffix as free-reuse ready before submit', async () => {
const requests = [];
const messages = [];
let statusPollCount = 0;
let currentState = {
heroSmsApiKey: 'demo-key',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
activationId: 'resend-info-free',
phoneNumber: '66950005656',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: 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 === 'setStatus' && id === 'resend-info-free') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'resend-info-free') {
statusPollCount += 1;
return {
ok: true,
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RESEND:100001' : 'STATUS_OK:887766'),
};
}
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
throw new Error(`${action} should not be called for accepted saved free reuse resend state`);
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
if (message.type === 'SUBMIT_PHONE_NUMBER') {
assert.equal(message.payload.phoneNumber, '66950005656');
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
assert.equal(message.payload.code, '887766');
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.deepStrictEqual(result, {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.deepStrictEqual(
messages.map((message) => message.type),
['SUBMIT_PHONE_NUMBER', 'SUBMIT_PHONE_VERIFICATION_CODE']
);
assert.deepStrictEqual(
requests
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
.map((requestUrl) => requestUrl.searchParams.get('status')),
['3']
);
});
test('phone verification helper retires automatic free reuse record at max uses', async () => {
const requests = [];
let statusPollCount = 0;
let currentState = {
heroSmsApiKey: 'demo-key',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
activationId: 'free-max',
phoneNumber: '66950009999',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 2,
maxUses: 3,
source: 'free-manual-reuse',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: 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 === 'setStatus' && id === 'free-max') {
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
}
if (action === 'getStatus' && id === 'free-max') {
statusPollCount += 1;
return { ok: true, text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_CODE' : 'STATUS_OK:778899') };
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.equal(currentState.freeReusablePhoneActivation, null);
assert.deepStrictEqual(
requests.map((requestUrl) => requestUrl.searchParams.get('action')),
['setStatus', 'getStatus', 'getStatus']
);
assert.deepStrictEqual(
requests
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
.map((requestUrl) => requestUrl.searchParams.get('status')),
['3']
);
});
test('phone verification helper preserves coded free-reuse activation when code submission errors', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
freePhoneReuseEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: null,
reusablePhoneActivation: null,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
const status = parsedUrl.searchParams.get('status');
if (action === 'getPrices') {
return { ok: true, text: async () => buildHeroSmsPricesPayload() };
}
if (action === 'getNumber') {
return { ok: true, text: async () => 'ACCESS_NUMBER:new-free-save:66950006666' };
}
if (action === 'getStatus' && id === 'new-free-save') {
return { ok: true, text: async () => 'STATUS_OK:445566' };
}
if (action === 'setStatus' && id === 'new-free-save' && status === '8') {
throw new Error('free-reuse activation should not be cancelled after SMS code was received');
}
if (action === 'setStatus' && id === 'new-free-save' && status === '6') {
throw new Error('free-reuse activation should not be completed after SMS code was received');
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
throw new Error('simulated auth-page failure after code submit');
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
}),
/simulated auth-page failure/
);
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'new-free-save');
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '66950006666');
assert.equal(
requests.some((requestUrl) => (
requestUrl.searchParams.get('action') === 'setStatus'
&& requestUrl.searchParams.get('id') === 'new-free-save'
&& ['6', '8'].includes(requestUrl.searchParams.get('status'))
)),
false
);
assert.deepStrictEqual(
requests
.filter((requestUrl) => (
requestUrl.searchParams.get('action') === 'setStatus'
&& requestUrl.searchParams.get('id') === 'new-free-save'
))
.map((requestUrl) => requestUrl.searchParams.get('status')),
[]
);
});
test('phone verification helper preserves newly saved free-reuse activation after OAuth success without source marker', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
freePhoneReuseEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: null,
reusablePhoneActivation: null,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
const status = parsedUrl.searchParams.get('status');
if (action === 'getPrices') {
return { ok: true, text: async () => buildHeroSmsPricesPayload() };
}
if (action === 'getNumber') {
return { ok: true, text: async () => 'ACCESS_NUMBER:new-free-success:66950007777' };
}
if (action === 'getStatus' && id === 'new-free-success') {
return { ok: true, text: async () => 'STATUS_OK:778899' };
}
if (action === 'setStatus' && id === 'new-free-success' && ['6', '8'].includes(status)) {
throw new Error(`free-reuse activation should not receive terminal setStatus(${status}) after OAuth success`);
}
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
});
assert.deepStrictEqual(result, {
success: true,
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'new-free-success');
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '66950007777');
assert.equal(currentState.freeReusablePhoneActivation.source, 'free-manual-reuse');
assert.equal(
requests.some((requestUrl) => (
requestUrl.searchParams.get('action') === 'setStatus'
&& requestUrl.searchParams.get('id') === 'new-free-success'
&& ['6', '8'].includes(requestUrl.searchParams.get('status'))
)),
false
);
});
test('phone verification helper replaces number immediately when resend is throttled and does not spam resend clicks', async () => {
const requests = [];
const messages = [];
@@ -932,6 +932,65 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
assert.equal(events.completed[0].step, 7);
});
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
const fetchCalls = [];
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.startsWith('http://127.0.0.1:18767/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }),
};
}
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_wa', challenge_id: 'challenge_wa' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_wa',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_wa',
gopayHelperOtpChannel: 'whatsapp',
gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperPhoneNumber: '+8613800138000',
});
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true);
const helperUrl = new URL(fetchCalls[0].url);
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_wa');
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_wa',
otp: '765432',
card_key: 'card_wa',
});
assert.equal(events.completed[0].step, 7);
});
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
const fetchCalls = [];
let currentState = {
+2
View File
@@ -110,6 +110,7 @@ const inputAutoSkipFailures = { disabled: false };
const autoContinueBar = { style: { display: '' } };
function setSettingsCardLocked() {}
function setFreePhoneReuseControlsLocked() {}
function getAutoRunLabel() { return ''; }
function getLockedRunCountFromEmailPool() { return lockedRunCount; }
function isCustomMailProvider() { return false; }
@@ -198,6 +199,7 @@ const inputAutoSkipFailures = { disabled: false };
const autoContinueBar = { style: { display: '' } };
function setSettingsCardLocked() {}
function setFreePhoneReuseControlsLocked() {}
function getAutoRunLabel() { return ''; }
function getLockedRunCountFromEmailPool() { return 0; }
function isCustomMailProvider() { return false; }
@@ -108,6 +108,18 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="row-hero-sms-current-code"/);
assert.match(html, /id="row-hero-sms-preferred-activation"/);
assert.match(html, /id="select-hero-sms-preferred-activation"/);
assert.match(html, /id="row-free-phone-reuse-enabled"/);
assert.match(html, /id="input-free-phone-reuse-enabled"/);
assert.match(html, /id="row-free-phone-reuse-auto-enabled"/);
assert.match(html, /id="input-free-phone-reuse-auto-enabled"/);
assert.match(html, /id="row-free-reusable-phone"/);
assert.match(html, /id="display-free-reusable-phone"/);
assert.match(html, /id="display-free-reusable-phone-country"/);
assert.match(html, /id="input-free-reusable-phone"/);
assert.match(html, /id="btn-save-free-reusable-phone"/);
assert.match(html, /id="btn-clear-free-reusable-phone"/);
assert.match(html, /白嫖复用/);
assert.match(html, /自动白嫖复用/);
assert.match(html, /id="row-phone-replacement-limit"/);
assert.match(html, /id="row-phone-verification-resend-count"/);
assert.match(html, /id="row-phone-code-wait-seconds"/);
@@ -134,6 +146,49 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
test('sidepanel source wires free reusable phone save and clear actions to runtime messages', () => {
assert.match(sidepanelSource, /const inputFreePhoneReuseEnabled = document\.getElementById\('input-free-phone-reuse-enabled'\);/);
assert.match(sidepanelSource, /const inputFreePhoneReuseAutoEnabled = document\.getElementById\('input-free-phone-reuse-auto-enabled'\);/);
assert.match(sidepanelSource, /const displayFreeReusablePhone = document\.getElementById\('display-free-reusable-phone'\);/);
assert.match(sidepanelSource, /const inputFreeReusablePhone = document\.getElementById\('input-free-reusable-phone'\);/);
assert.match(sidepanelSource, /const btnSaveFreeReusablePhone = document\.getElementById\('btn-save-free-reusable-phone'\);/);
assert.match(sidepanelSource, /const btnClearFreeReusablePhone = document\.getElementById\('btn-clear-free-reusable-phone'\);/);
assert.match(sidepanelSource, /type:\s*'SET_FREE_REUSABLE_PHONE'/);
assert.match(sidepanelSource, /payload:\s*\{\s*phoneNumber\s*\}/s);
assert.match(sidepanelSource, /type:\s*'CLEAR_FREE_REUSABLE_PHONE'/);
});
test('sidepanel keeps free reuse switches realtime and locks them during auto run', () => {
assert.match(
sidepanelSource,
/message\.payload\.freePhoneReuseEnabled !== undefined[\s\S]*updatePhoneVerificationSettingsUI\(\);/
);
assert.match(
sidepanelSource,
/message\.payload\.freePhoneReuseAutoEnabled !== undefined[\s\S]*updatePhoneVerificationSettingsUI\(\);/
);
assert.match(sidepanelSource, /setFreePhoneReuseControlsLocked\(settingsCardLocked\);/);
assert.match(
sidepanelSource,
/inputFreePhoneReuseEnabled\.disabled = locked;[\s\S]*inputFreePhoneReuseAutoEnabled\.disabled = locked/
);
});
test('sidepanel free reusable phone paths avoid stale identifiers and empty-save errors', () => {
assert.doesNotMatch(
sidepanelSource,
/applyHeroSmsFallbackSelection\(\s*\[\.\.\.nextPrimaryCountries,\s*\.\.\.nextFallback\]/
);
assert.match(
sidepanelSource,
/applyHeroSmsFallbackSelection\(\s*\[\s*nextPrimary,\s*\.\.\.nextFallback\]/
);
assert.match(
sidepanelSource,
/if \(!phoneNumber\) \{[\s\S]*请先填写白嫖复用手机号[\s\S]*return;[\s\S]*chrome\.runtime\.sendMessage\(\{\s*type:\s*'SET_FREE_REUSABLE_PHONE'/
);
});
test('sidepanel source wires runtime signup phone field to background sync messages', () => {
assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/);
assert.match(sidepanelSource, /function shouldExecuteStep3WithSignupPhoneIdentity\(state = latestState\)/);
@@ -494,6 +549,9 @@ function updateSignupMethodUI() {
function syncSignupPhoneInputFromState() {
rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none';
}
function setFreePhoneReuseControlsLocked() {}
function isAutoRunLockedPhase() { return false; }
function isAutoRunScheduledPhase() { return false; }
${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -686,6 +744,8 @@ const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputFreePhoneReuseEnabled = { checked: true };
const inputFreePhoneReuseAutoEnabled = { checked: true };
const selectPhoneSmsProvider = { value: 'hero-sms' };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
@@ -832,6 +892,8 @@ return { collectSettingsPayload };
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
assert.equal(payload.nexSmsServiceCode, 'ot');
assert.equal(payload.heroSmsReuseEnabled, true);
assert.equal(payload.freePhoneReuseEnabled, true);
assert.equal(payload.freePhoneReuseAutoEnabled, true);
assert.equal(payload.heroSmsAcquirePriority, 'price');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
+6 -10
View File
@@ -257,24 +257,20 @@ return {
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
api.inputGpcHelperLocalSmsEnabled.checked = true;
api.updatePlusModeUI();
assert.equal(api.selectGpcHelperOtpChannel.value, 'whatsapp');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperOtpChannel.value = 'whatsapp';
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, false);
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, true);
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
@@ -0,0 +1,52 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
test('sidepanel exposes SUB2API account priority below group setting', () => {
assert.match(html, /id="row-sub2api-account-priority"/);
assert.match(html, /id="input-sub2api-account-priority"/);
assert.match(html, /<span class="data-label">优先级<\/span>/);
const inputTag = html.match(/<input[^>]*id="input-sub2api-account-priority"[^>]*>/)?.[0] || '';
assert.match(inputTag, /type="number"/);
assert.match(inputTag, /min="1"/);
assert.match(inputTag, /step="1"/);
assert.ok(
html.indexOf('id="row-sub2api-account-priority"') > html.indexOf('id="row-sub2api-group"'),
'priority row should be placed below the SUB2API group row'
);
assert.ok(
html.indexOf('id="row-sub2api-account-priority"') < html.indexOf('id="row-sub2api-default-proxy"'),
'priority row should remain above the SUB2API default proxy row'
);
});
test('sidepanel persists and locks SUB2API account priority setting', () => {
assert.match(
source,
/const rowSub2ApiAccountPriority = document\.getElementById\('row-sub2api-account-priority'\);/
);
assert.match(
source,
/const inputSub2ApiAccountPriority = document\.getElementById\('input-sub2api-account-priority'\);/
);
assert.match(source, /function normalizeSub2ApiAccountPriorityValue\(/);
assert.match(source, /const sub2apiAccountPriorityNormalizer = typeof normalizeSub2ApiAccountPriorityValue === 'function'/);
assert.match(source, /sub2apiAccountPriority: sub2apiAccountPriorityNormalizer\(/);
assert.match(
source,
/inputSub2ApiAccountPriority\.value = String\(normalizeSub2ApiAccountPriorityValue\(state\?\.sub2apiAccountPriority\)\);/
);
assert.match(source, /rowSub2ApiAccountPriority\.style\.display = useSub2Api \? '' : 'none';/);
assert.match(source, /inputSub2ApiAccountPriority\.disabled = locked;/);
assert.match(
source,
/inputSub2ApiAccountPriority\.addEventListener\('input', \(\) => \{[\s\S]*scheduleSettingsAutoSave\(\);[\s\S]*\}\);/
);
assert.match(
source,
/inputSub2ApiAccountPriority\.addEventListener\('blur', \(\) => \{[\s\S]*saveSettings\(\{ silent: true \}\)/
);
});
+46
View File
@@ -277,3 +277,49 @@ test('SUB2API step 10 omits proxy_id when no proxy is configured', async () => {
assert.equal(Object.hasOwn(exchangeCall.body, 'proxy_id'), false);
assert.equal(Object.hasOwn(createCall.body, 'proxy_id'), false);
});
test('SUB2API step 10 creates account with configured account priority', async () => {
const fetchCalls = [];
const context = createSub2ApiPanelContext(fetchCalls);
await vm.runInContext(`
step9_submitOpenAiCallback({
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
sub2apiGroupId: 5,
sub2apiAccountPriority: 3
})
`, context);
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
assert.equal(createCall.body.priority, 3);
});
test('SUB2API account priority must be an integer greater than or equal to 1', async () => {
const fetchCalls = [];
const context = createSub2ApiPanelContext(fetchCalls);
await assert.rejects(
() => vm.runInContext(`
step9_submitOpenAiCallback({
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
sub2apiGroupId: 5,
sub2apiAccountPriority: 0
})
`, context),
/SUB2API 账号优先级必须是大于等于 1 的整数/
);
assert.equal(fetchCalls.some((call) => call.path === '/api/v1/admin/accounts'), false);
});