Merge remote-tracking branch 'origin/dev' into fix/herosms-page-state-deadline

This commit is contained in:
QLHazyCoder
2026-05-11 03:45:40 +08:00
18 changed files with 1202 additions and 50 deletions
@@ -53,10 +53,23 @@ function extractFunction(name) {
}
const bundle = [
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
'const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;',
'const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;',
"const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';",
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isMail2925ThreadTerminatedError'),
extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunStepIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunStepIdleLogWatchdog'),
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
+13
View File
@@ -53,6 +53,10 @@ function extractFunction(name) {
}
const bundle = [
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
'const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;',
'const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;',
"const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';",
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isMail2925ThreadTerminatedError'),
@@ -60,6 +64,15 @@ const bundle = [
extractFunction('getSignupPhonePasswordMismatchRestartPayload'),
extractFunction('restartSignupPhonePasswordMismatchAttemptFromStep'),
extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunStepIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunStepIdleLogWatchdog'),
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
+327 -1
View File
@@ -56,7 +56,16 @@ const bundle = [
extractFunction('isAddPhoneAuthFailure'),
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
extractFunction('isGpcCheckoutRestartRequiredFailure'),
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunStepIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunStepIdleLogWatchdog'),
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
@@ -86,6 +95,10 @@ function createHarness(options = {}) {
stepIds = Object.keys(stepDefinitions).map(Number).sort((a, b) => a - b),
lastStepId = Math.max(...stepIds),
finalOAuthChainStartStep = 7,
idleLogTimeoutMs = 300000,
idleLogCheckIntervalMs = 5000,
hangStep = 0,
hangBudget = 0,
} = options;
return new Function(`
@@ -94,6 +107,10 @@ const LAST_STEP_ID = ${JSON.stringify(lastStepId)};
const FINAL_OAUTH_CHAIN_START_STEP = ${JSON.stringify(finalOAuthChainStartStep)};
const SIGNUP_METHOD_PHONE = 'phone';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = ${JSON.stringify(idleLogTimeoutMs)};
const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = ${JSON.stringify(idleLogCheckIntervalMs)};
const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;
const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
@@ -102,14 +119,17 @@ const chrome = {
};
let remainingFailures = ${JSON.stringify(failureBudget)};
let remainingHangs = ${JSON.stringify(hangBudget)};
const events = {
steps: [],
logs: [],
invalidations: [],
cancellations: [],
stopBroadcasts: 0,
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
events.logs.push({ message, level, timestamp: Date.now() });
}
async function ensureAutoEmailReady() {}
@@ -119,6 +139,7 @@ async function getState() {
return {
stepStatuses: { 3: 'completed' },
mailProvider: '163',
logs: events.logs,
...${JSON.stringify(customState)},
};
}
@@ -140,6 +161,10 @@ function isStepDoneStatus(status) {
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === ${JSON.stringify(hangStep)} && remainingHangs > 0) {
remainingHangs -= 1;
return new Promise(() => {});
}
if (step === ${JSON.stringify(failureStep)} && remainingFailures > 0) {
remainingFailures -= 1;
throw new Error(${JSON.stringify(failureMessage)});
@@ -151,6 +176,12 @@ async function getTabId() {
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
events.invalidations.push({ step, options });
}
function cancelPendingCommands(reason = '') {
events.cancellations.push(reason);
}
async function broadcastStopToContentScripts() {
events.stopBroadcasts += 1;
}
function getLoginAuthStateLabel(state) {
return state || 'unknown';
}
@@ -224,6 +255,62 @@ test('auto-run keeps restarting from step 7 after post-login failures without a
assert.ok(events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run restarts the current step after five minutes without new logs', async () => {
const harness = createHarness({
startStep: 10,
failureStep: 0,
hangStep: 10,
hangBudget: 1,
idleLogTimeoutMs: 20,
idleLogCheckIntervalMs: 5,
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [10, 10]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
assert.equal(events.cancellations.length, 1);
assert.equal(events.stopBroadcasts, 1);
assert.ok(events.logs.some(({ message }) => /5 分钟没有新日志准备重新开始当前步骤/.test(message)));
});
test('auto-run applies the idle-log restart watchdog to early steps too', async () => {
const harness = createHarness({
startStep: 2,
failureStep: 0,
hangStep: 2,
hangBudget: 1,
idleLogTimeoutMs: 20,
idleLogCheckIntervalMs: 5,
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [2, 2, 4, 5, 6, 7, 8, 9, 10]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [1]);
assert.equal(events.cancellations.length, 1);
assert.equal(events.stopBroadcasts, 1);
});
test('auto-run stops current-step idle restarts after the retry cap', async () => {
const harness = createHarness({
startStep: 10,
failureStep: 0,
hangStep: 10,
hangBudget: 4,
idleLogTimeoutMs: 20,
idleLogCheckIntervalMs: 5,
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.match(result.error.message, /AUTO_RUN_STEP_IDLE_RESTART::步骤 10/);
assert.deepStrictEqual(result.events.steps, [10, 10, 10, 10]);
assert.deepStrictEqual(result.events.invalidations.map((entry) => entry.step), [9, 9, 9]);
assert.ok(result.events.logs.some(({ message }) => /已连续 3 次因 5 分钟无新日志而重开/.test(message)));
});
test('auto-run stops restarting once add-phone is detected', async () => {
const harness = createHarness({
failureStep: 7,
@@ -334,6 +421,182 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
});
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 10,
failureStep: 10,
failureBudget: 1,
failureMessage: '步骤 10:判断失败后已重试 2 次,仍未成功。最后原因:点击登录入口后仍未进入手机号/邮箱/密码/验证码页。',
authState: { state: 'entry_page', url: 'https://auth.openai.com/log-in' },
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [10, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
assert.ok(events.logs.some(({ message }) => /回到步骤 10 重新开始授权流程/.test(message)));
assert.ok(!events.logs.some(({ message }) => /停止自动回到步骤 10 重开/.test(message)));
});
test('auto-run restarts Plus/GPC fetch-code and confirm-oauth failures from oauth-login step 10', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
for (const scenario of [
{
failureStep: 11,
failureMessage: '步骤 11:无法获取新的登录验证码。',
expectedSteps: [10, 11, 10, 11, 12, 13],
},
{
failureStep: 12,
failureMessage: '步骤 12:长时间未进入 OAuth 同意页,无法定位“继续”按钮。',
expectedSteps: [10, 11, 12, 10, 11, 12, 13],
},
]) {
const harness = createHarness({
startStep: 10,
failureStep: scenario.failureStep,
failureBudget: 1,
failureMessage: scenario.failureMessage,
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, scenario.expectedSteps);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
assert.ok(events.logs.some(({ message }) => new RegExp(`步骤 ${scenario.failureStep}.*回到步骤 10 重新开始授权流程`).test(message)));
}
});
test('auto-run restarts Plus/GPC transient platform verify exchange failures from confirm-oauth step 12', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 10,
failureStep: 13,
failureBudget: 1,
failureMessage: 'token exchange failed at https://auth.openai.com/oauth/token: token_exchange_user_error: Invalid request. Please try again later.',
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
panelMode: 'sub2api',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [10, 11, 12, 13, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [11]);
assert.ok(events.logs.some(({ message }) => /步骤 13.*回到步骤 12 重新开始授权流程/.test(message)));
});
test('auto-run restarts Plus checkout from step 6 when checkout creation fails', async () => {
const plusPaypalSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
8: { key: 'paypal-approve' },
9: { key: 'plus-checkout-return' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 6,
failureBudget: 1,
failureMessage: '步骤 6:创建 Plus Checkout 失败:checkout request failed',
stepDefinitions: plusPaypalSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [6, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts Plus checkout from step 6 when billing fails for non-free-trial reasons', async () => {
const plusPaypalSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
8: { key: 'paypal-approve' },
9: { key: 'plus-checkout-return' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: '步骤 7:账单地址 iframe 无法注入,请重新创建 checkout。',
stepDefinitions: plusPaypalSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
@@ -399,6 +662,36 @@ 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 () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 6,
failureBudget: 1,
failureMessage: '步骤 6GPC 模式获取 accessToken 失败。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
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 }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
@@ -429,6 +722,39 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
});
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 4,
failureMessage: 'GPC_TASK_ENDED::GPC task status stalled, recreate the task.',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(
events.steps,
[6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 10, 11, 12, 13]
);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5, 5, 5, 5]);
assert.ok(events.logs.some(({ message }) => / 4 /.test(message)));
});
test('auto-run does not restart GPC checkout when Plus account has no free-trial eligibility', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
@@ -126,6 +126,20 @@ test('account run history helper upgrades old records, keeps stopped items and s
assert.equal(genericStoppedRecord.failureLabel, '流程已停止');
assert.equal(genericStoppedRecord.failedStep, null);
const runningRecord = helpers.buildAccountRunHistoryRecord({
email: 'run@b.com',
password: 'z',
autoRunning: true,
autoRunCurrentRun: 1,
autoRunTotalRuns: 2,
autoRunAttemptRun: 1,
}, 'running', '正在运行');
assert.equal(runningRecord.finalStatus, 'running');
assert.equal(runningRecord.failureLabel, '正在运行');
assert.equal(runningRecord.failureDetail, '');
assert.equal(runningRecord.failedStep, null);
assert.equal(runningRecord.source, 'auto');
const normalizedStoppedRecord = helpers.normalizeAccountRunHistoryRecord({
recordId: 'legacy-stop@example.com',
email: 'legacy-stop@example.com',
@@ -471,6 +485,7 @@ test('account run history helper clears persisted records and syncs full snapsho
assert.deepStrictEqual(payload.summary, {
total: 1,
success: 0,
running: 0,
failed: 1,
stopped: 0,
retryTotal: 1,
@@ -486,6 +501,7 @@ test('account run history helper clears persisted records and syncs full snapsho
summary: {
total: 0,
success: 0,
running: 0,
failed: 0,
stopped: 0,
retryTotal: 0,
@@ -586,6 +602,7 @@ test('account run history helper deletes selected records and syncs remaining sn
summary: {
total: 1,
success: 1,
running: 0,
failed: 0,
stopped: 0,
retryTotal: 0,
@@ -1081,6 +1081,75 @@ test('GPC billing fails repeated checkout stage as stale so auto-run can recreat
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:创建订单'), true);
});
test('GPC billing fails unchanged visible created status even when hidden ids change', async () => {
const originalNow = Date.now;
let now = 1710000000000;
let queryCount = 0;
const fetchCalls = [];
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
sleepWithStop: async (ms) => {
events.sleeps.push(ms);
now += ms;
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_created') {
queryCount += 1;
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_created',
phone_mode: 'auto',
status: 'created',
status_text: '已创建',
remote_stage: '',
api_waiting_for: '',
reference_id: `ref_${queryCount}`,
flow_id: `flow_${queryCount}`,
}),
};
}
if (url.endsWith('/api/gp/tasks/task_created/stop')) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_created',
status: 'discarded',
status_text: '已停止',
}),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
Date.now = () => now;
try {
await assert.rejects(
() => executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_created',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
gopayHelperTaskStaleSeconds: 15,
}),
/GPC_TASK_ENDED::GPC 任务状态超过 15 秒无进展(已创建),请重新创建任务。/
);
} finally {
Date.now = originalNow;
}
assert.equal(queryCount > 1, true);
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_created/stop')), true);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:已创建'), true);
});
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
const fetchCalls = [];
let pollCount = 0;
@@ -180,6 +180,112 @@ return { normalizeAccountRunHistoryHelperBaseUrlValue };
);
});
test('account records manager shows current auto-run account as running', () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
const latestState = {
autoRunning: true,
autoRunPhase: 'running',
accountIdentifierType: 'email',
accountIdentifier: 'running@example.com',
email: 'running@example.com',
accountRunHistory: [
{
recordId: 'running@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'running@example.com',
email: 'running@example.com',
password: 'secret',
finalStatus: 'stopped',
finishedAt: '2026-04-17T04:32:00.000Z',
retryCount: 0,
failureLabel: '步骤 2 停止',
},
],
};
const list = createNode();
const stats = createNode();
const meta = createNode();
const pageLabel = createNode();
const manager = api.createAccountRecordsManager({
state: {
getLatestState: () => latestState,
syncLatestState() {},
},
dom: {
accountRecordsList: list,
accountRecordsMeta: meta,
accountRecordsPageLabel: pageLabel,
accountRecordsStats: stats,
},
helpers: {
escapeHtml: (value) => String(value || ''),
},
runtime: {
sendMessage: async () => ({}),
},
});
manager.render();
assert.match(list.innerHTML, /is-running/);
assert.match(list.innerHTML, /正在运行/);
assert.doesNotMatch(list.innerHTML, /步骤 2 停止/);
assert.match(stats.innerHTML, /data-account-record-filter="running"/);
assert.match(stats.innerHTML, /<strong>1<\/strong>运行/);
});
test('account records manager shows full failure detail before short label', () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
const latestState = {
accountRunHistory: [
{
recordId: 'failed-detail@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'failed-detail@example.com',
email: 'failed-detail@example.com',
finalStatus: 'failed',
finishedAt: '2026-04-17T04:32:00.000Z',
retryCount: 0,
failureLabel: '步骤 2 失败',
failureDetail: 'Duck 邮箱自动获取失败:未找到生成新 Duck 地址按钮。',
},
],
};
const list = createNode();
const manager = api.createAccountRecordsManager({
state: {
getLatestState: () => latestState,
syncLatestState() {},
},
dom: {
accountRecordsList: list,
accountRecordsMeta: createNode(),
accountRecordsPageLabel: createNode(),
accountRecordsStats: createNode(),
},
helpers: {
escapeHtml: (value) => String(value || ''),
},
runtime: {
sendMessage: async () => ({}),
},
});
manager.render();
assert.match(list.innerHTML, /Duck 邮箱自动获取失败/);
assert.match(list.innerHTML, /title="failed-detail@example\.com\nDuck 邮箱自动获取失败/);
assert.doesNotMatch(list.innerHTML, /account-record-item-summary">步骤 2 失败/);
});
test('account records manager supports filter chips and partial multi-select delete', async () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
+133
View File
@@ -561,6 +561,139 @@ return {
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
});
test('waitForSignupEntryState treats hidden signup action on collapsed logged-out home as retryable entry', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
let now = 0;
const signupButton = {
textContent: '免费注册',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
};
const document = {
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"]') {
return [signupButton];
}
return [];
},
};
const window = {
innerWidth: 0,
innerHeight: 0,
outerWidth: 159,
outerHeight: 27,
};
const location = {
href: 'https://chatgpt.com/',
};
const Date = {
now() {
return now;
},
};
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
function isVisibleElement(el) {
const rect = el?.getBoundingClientRect?.() || { width: 0, height: 0 };
return rect.width > 0 && rect.height > 0;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getPageTextSnapshot() {
return '跳至内容 ChatGPT 登录 我们先从哪里开始呢?';
}
function getSignupPasswordInput() {
return null;
}
function isSignupPasswordPage() {
return false;
}
function getSignupPasswordSubmitButton() {
return null;
}
function getSignupPasswordDisplayedEmail() {
return '';
}
function throwIfStopped() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
async function humanPause() {}
function simulateClick(target) {
clicks.push(getActionText(target));
}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('findSignupEntryTrigger')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
return {
async run() {
return waitForSignupEntryState({ timeout: 30000, autoOpenEntry: true, step: 2 });
},
getClicks() {
return clicks.slice();
},
getLogs() {
return logs.slice();
},
};
`)();
const snapshot = await api.run();
assert.equal(snapshot.state, 'entry_home');
assert.equal(api.getClicks().length, 6);
assert.equal(api.getLogs().some(({ message }) => message.includes('注册入口仍处于不可见状态')), true);
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
});
test('ensureSignupPhoneEntryReady opens free signup before switching to the phone entry', async () => {
const api = new Function(`
const logs = [];