| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- const test = require('node:test');
- const assert = require('node:assert/strict');
- const fs = require('node:fs');
- const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
- const globalScope = {};
- const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
- test('auto-run controller does not retry add-phone failures even when auto retry is enabled', async () => {
- const events = {
- logs: [],
- broadcasts: [],
- accountRecords: [],
- runCalls: 0,
- };
- let currentState = {
- stepStatuses: {},
- vpsUrl: 'https://example.com/vps',
- vpsPassword: 'secret',
- customPassword: '',
- autoRunSkipFailures: true,
- autoRunFallbackThreadIntervalMinutes: 0,
- autoRunDelayEnabled: false,
- autoRunDelayMinutes: 30,
- autoStepDelaySeconds: null,
- mailProvider: '163',
- emailGenerator: 'duck',
- gmailBaseEmail: '',
- mail2925BaseEmail: '',
- emailPrefix: 'demo',
- inbucketHost: '',
- inbucketMailbox: '',
- cloudflareDomain: '',
- cloudflareDomains: [],
- tabRegistry: {},
- sourceLastUrls: {},
- autoRunRoundSummaries: [],
- };
- const runtime = {
- state: {
- autoRunActive: false,
- autoRunCurrentRun: 0,
- autoRunTotalRuns: 1,
- autoRunAttemptRun: 0,
- autoRunSessionId: 0,
- },
- get() {
- return { ...this.state };
- },
- set(updates = {}) {
- this.state = { ...this.state, ...updates };
- },
- };
- let sessionSeed = 0;
- const controller = api.createAutoRunController({
- addLog: async (message, level = 'info') => {
- events.logs.push({ message, level });
- },
- appendAccountRunRecord: async (status, _state, reason) => {
- events.accountRecords.push({ status, reason });
- return { status, reason };
- },
- AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
- AUTO_RUN_RETRY_DELAY_MS: 3000,
- AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
- AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
- broadcastAutoRunStatus: async (phase, payload = {}) => {
- events.broadcasts.push({ phase, ...payload });
- currentState = {
- ...currentState,
- autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
- autoRunPhase: phase,
- autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
- autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
- autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
- autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
- };
- },
- broadcastStopToContentScripts: async () => {},
- cancelPendingCommands: () => {},
- clearStopRequest: () => {},
- createAutoRunSessionId: () => {
- sessionSeed += 1;
- return sessionSeed;
- },
- getAutoRunStatusPayload: (phase, payload = {}) => ({
- autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
- autoRunPhase: phase,
- autoRunCurrentRun: payload.currentRun ?? 0,
- autoRunTotalRuns: payload.totalRuns ?? 1,
- autoRunAttemptRun: payload.attemptRun ?? 0,
- autoRunSessionId: payload.sessionId ?? 0,
- }),
- getErrorMessage: (error) => error?.message || String(error || ''),
- getFirstUnfinishedStep: () => 1,
- getPendingAutoRunTimerPlan: () => null,
- getRunningSteps: () => [],
- getState: async () => ({
- ...currentState,
- stepStatuses: { ...(currentState.stepStatuses || {}) },
- tabRegistry: { ...(currentState.tabRegistry || {}) },
- sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
- }),
- getStopRequested: () => false,
- hasSavedProgress: () => false,
- isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
- isRestartCurrentAttemptError: () => false,
- isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
- launchAutoRunTimerPlan: async () => false,
- normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
- persistAutoRunTimerPlan: async () => ({}),
- resetState: async () => {
- currentState = {
- ...currentState,
- stepStatuses: {},
- tabRegistry: {},
- sourceLastUrls: {},
- };
- },
- runAutoSequenceFromStep: async () => {
- events.runCalls += 1;
- throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
- },
- runtime,
- setState: async (updates = {}) => {
- currentState = {
- ...currentState,
- ...updates,
- stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
- tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
- sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
- };
- },
- sleepWithStop: async () => {},
- throwIfAutoRunSessionStopped: (sessionId) => {
- if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
- throw new Error('流程已被用户停止。');
- }
- },
- waitForRunningStepsToFinish: async () => currentState,
- chrome: {
- runtime: {
- sendMessage() {
- return Promise.resolve();
- },
- },
- },
- });
- await controller.autoRunLoop(1, {
- autoRunSkipFailures: true,
- mode: 'restart',
- });
- assert.equal(events.runCalls, 1, 'add-phone fatal failure should stop before the next auto attempt starts');
- assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'add-phone fatal failure should not enter retrying phase');
- assert.equal(events.accountRecords.length, 1, 'fatal add-phone should still persist a failed round record');
- assert.equal(events.accountRecords[0].status, 'failed');
- assert.match(events.accountRecords[0].reason, /add-phone/);
- assert.ok(events.logs.some(({ message }) => /add-phone\/手机号页/.test(message)));
- assert.equal(runtime.state.autoRunActive, false);
- assert.equal(runtime.state.autoRunSessionId, 0);
- });
- test('auto-run controller parks 30~60 minutes and continues next round after add-phone when more runs remain', async () => {
- const events = {
- logs: [],
- broadcasts: [],
- accountRecords: [],
- timerPlans: [],
- runCalls: 0,
- };
- let currentState = {
- stepStatuses: {},
- vpsUrl: 'https://example.com/vps',
- vpsPassword: 'secret',
- customPassword: '',
- autoRunSkipFailures: false,
- autoRunFallbackThreadIntervalMinutes: 0,
- autoRunDelayEnabled: false,
- autoRunDelayMinutes: 30,
- autoStepDelaySeconds: null,
- mailProvider: '163',
- emailGenerator: 'duck',
- gmailBaseEmail: '',
- mail2925BaseEmail: '',
- emailPrefix: 'demo',
- inbucketHost: '',
- inbucketMailbox: '',
- cloudflareDomain: '',
- cloudflareDomains: [],
- tabRegistry: {},
- sourceLastUrls: {},
- autoRunRoundSummaries: [],
- };
- const runtime = {
- state: {
- autoRunActive: false,
- autoRunCurrentRun: 0,
- autoRunTotalRuns: 1,
- autoRunAttemptRun: 0,
- autoRunSessionId: 0,
- },
- get() {
- return { ...this.state };
- },
- set(updates = {}) {
- this.state = { ...this.state, ...updates };
- },
- };
- let sessionSeed = 100;
- const broadcastAutoRunStatus = async (phase, payload = {}, extraState = {}) => {
- events.broadcasts.push({ phase, ...payload });
- currentState = {
- ...currentState,
- ...extraState,
- autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
- autoRunPhase: phase,
- autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
- autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
- autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
- autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
- };
- };
- const controller = api.createAutoRunController({
- addLog: async (message, level = 'info') => {
- events.logs.push({ message, level });
- },
- appendAccountRunRecord: async (status, _state, reason) => {
- events.accountRecords.push({ status, reason });
- return { status, reason };
- },
- AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
- AUTO_RUN_RETRY_DELAY_MS: 3000,
- AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
- AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
- broadcastAutoRunStatus,
- broadcastStopToContentScripts: async () => {},
- cancelPendingCommands: () => {},
- chooseAddPhonePauseMinutes: () => 30,
- clearStopRequest: () => {},
- createAutoRunSessionId: () => {
- sessionSeed += 1;
- return sessionSeed;
- },
- getAutoRunStatusPayload: (phase, payload = {}) => ({
- autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
- autoRunPhase: phase,
- autoRunCurrentRun: payload.currentRun ?? 0,
- autoRunTotalRuns: payload.totalRuns ?? 1,
- autoRunAttemptRun: payload.attemptRun ?? 0,
- autoRunSessionId: payload.sessionId ?? 0,
- }),
- getErrorMessage: (error) => error?.message || String(error || ''),
- getFirstUnfinishedStep: () => 1,
- getPendingAutoRunTimerPlan: () => null,
- getRunningSteps: () => [],
- getState: async () => ({
- ...currentState,
- stepStatuses: { ...(currentState.stepStatuses || {}) },
- tabRegistry: { ...(currentState.tabRegistry || {}) },
- sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
- }),
- getStopRequested: () => false,
- hasSavedProgress: () => false,
- isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
- isRestartCurrentAttemptError: () => false,
- isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
- launchAutoRunTimerPlan: async () => false,
- normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
- persistAutoRunTimerPlan: async (plan, extraState = {}) => {
- events.timerPlans.push({ plan, extraState });
- await broadcastAutoRunStatus('waiting_interval', {
- currentRun: plan.currentRun,
- totalRuns: plan.totalRuns,
- attemptRun: plan.attemptRun,
- sessionId: plan.autoRunSessionId,
- }, {
- ...extraState,
- autoRunTimerPlan: plan,
- });
- return plan;
- },
- resetState: async () => {
- currentState = {
- ...currentState,
- stepStatuses: {},
- tabRegistry: {},
- sourceLastUrls: {},
- };
- },
- runAutoSequenceFromStep: async () => {
- events.runCalls += 1;
- throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
- },
- runtime,
- setState: async (updates = {}) => {
- currentState = {
- ...currentState,
- ...updates,
- stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
- tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
- sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
- };
- },
- sleepWithStop: async () => {},
- throwIfAutoRunSessionStopped: (sessionId) => {
- if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
- throw new Error('流程已被用户停止。');
- }
- },
- waitForRunningStepsToFinish: async () => currentState,
- chrome: {
- runtime: {
- sendMessage() {
- return Promise.resolve();
- },
- },
- },
- });
- await controller.autoRunLoop(2, {
- autoRunSkipFailures: false,
- mode: 'restart',
- });
- assert.equal(events.runCalls, 1, 'add-phone should stop current round immediately without retrying the same round');
- assert.equal(events.accountRecords.length, 1, 'failed round should still be recorded');
- assert.equal(events.accountRecords[0].status, 'failed');
- assert.equal(events.timerPlans.length, 1, 'should schedule a delayed continue timer');
- assert.equal(events.timerPlans[0].plan.kind, 'between_rounds');
- assert.equal(events.timerPlans[0].plan.currentRun, 1);
- assert.equal(events.timerPlans[0].plan.totalRuns, 2);
- assert.equal(events.timerPlans[0].plan.countdownTitle, '手机号冷却中');
- assert.match(events.logs.find(({ message }) => /等待 30 分钟后继续下一轮/.test(message))?.message || '', /等待 30 分钟后继续下一轮/);
- assert.equal(runtime.state.autoRunActive, false);
- assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'should not retry same round after add-phone');
- assert.equal(events.broadcasts.some(({ phase }) => phase === 'waiting_interval'), true, 'should enter waiting_interval phase');
- });
|