auto-run-add-phone-stop.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
  5. const globalScope = {};
  6. const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
  7. test('auto-run controller does not retry add-phone failures even when auto retry is enabled', async () => {
  8. const events = {
  9. logs: [],
  10. broadcasts: [],
  11. accountRecords: [],
  12. cleanupCalls: [],
  13. runCalls: 0,
  14. };
  15. let currentState = {
  16. stepStatuses: {},
  17. vpsUrl: 'https://example.com/vps',
  18. vpsPassword: 'secret',
  19. customPassword: '',
  20. autoRunSkipFailures: true,
  21. autoRunFallbackThreadIntervalMinutes: 0,
  22. autoRunDelayEnabled: false,
  23. autoRunDelayMinutes: 30,
  24. autoStepDelaySeconds: null,
  25. mailProvider: '163',
  26. emailGenerator: 'duck',
  27. gmailBaseEmail: '',
  28. mail2925BaseEmail: '',
  29. emailPrefix: 'demo',
  30. inbucketHost: '',
  31. inbucketMailbox: '',
  32. cloudflareDomain: '',
  33. cloudflareDomains: [],
  34. tabRegistry: {},
  35. sourceLastUrls: {},
  36. autoRunRoundSummaries: [],
  37. };
  38. const runtime = {
  39. state: {
  40. autoRunActive: false,
  41. autoRunCurrentRun: 0,
  42. autoRunTotalRuns: 1,
  43. autoRunAttemptRun: 0,
  44. autoRunSessionId: 0,
  45. },
  46. get() {
  47. return { ...this.state };
  48. },
  49. set(updates = {}) {
  50. this.state = { ...this.state, ...updates };
  51. },
  52. };
  53. let sessionSeed = 0;
  54. const controller = api.createAutoRunController({
  55. addLog: async (message, level = 'info') => {
  56. events.logs.push({ message, level });
  57. },
  58. appendAccountRunRecord: async (status, _state, reason) => {
  59. events.accountRecords.push({ status, reason });
  60. return { status, reason };
  61. },
  62. AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
  63. AUTO_RUN_RETRY_DELAY_MS: 3000,
  64. AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
  65. AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
  66. broadcastAutoRunStatus: async (phase, payload = {}) => {
  67. events.broadcasts.push({ phase, ...payload });
  68. currentState = {
  69. ...currentState,
  70. autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
  71. autoRunPhase: phase,
  72. autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
  73. autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
  74. autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
  75. autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
  76. };
  77. },
  78. broadcastStopToContentScripts: async () => {},
  79. cancelPendingCommands: () => {},
  80. cleanupAfterAddPhone: async (payload = {}) => {
  81. events.cleanupCalls.push(payload);
  82. },
  83. clearStopRequest: () => {},
  84. createAutoRunSessionId: () => {
  85. sessionSeed += 1;
  86. return sessionSeed;
  87. },
  88. getAutoRunStatusPayload: (phase, payload = {}) => ({
  89. autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
  90. autoRunPhase: phase,
  91. autoRunCurrentRun: payload.currentRun ?? 0,
  92. autoRunTotalRuns: payload.totalRuns ?? 1,
  93. autoRunAttemptRun: payload.attemptRun ?? 0,
  94. autoRunSessionId: payload.sessionId ?? 0,
  95. }),
  96. getErrorMessage: (error) => error?.message || String(error || ''),
  97. getFirstUnfinishedStep: () => 1,
  98. getPendingAutoRunTimerPlan: () => null,
  99. getRunningSteps: () => [],
  100. getState: async () => ({
  101. ...currentState,
  102. stepStatuses: { ...(currentState.stepStatuses || {}) },
  103. tabRegistry: { ...(currentState.tabRegistry || {}) },
  104. sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
  105. }),
  106. getStopRequested: () => false,
  107. hasSavedProgress: () => false,
  108. isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
  109. isRestartCurrentAttemptError: () => false,
  110. isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
  111. launchAutoRunTimerPlan: async () => false,
  112. normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
  113. persistAutoRunTimerPlan: async () => ({}),
  114. resetState: async () => {
  115. currentState = {
  116. ...currentState,
  117. stepStatuses: {},
  118. tabRegistry: {},
  119. sourceLastUrls: {},
  120. };
  121. },
  122. runAutoSequenceFromStep: async () => {
  123. events.runCalls += 1;
  124. throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
  125. },
  126. runtime,
  127. setState: async (updates = {}) => {
  128. currentState = {
  129. ...currentState,
  130. ...updates,
  131. stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
  132. tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
  133. sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
  134. };
  135. },
  136. sleepWithStop: async () => {},
  137. throwIfAutoRunSessionStopped: (sessionId) => {
  138. if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
  139. throw new Error('流程已被用户停止。');
  140. }
  141. },
  142. waitForRunningStepsToFinish: async () => currentState,
  143. chrome: {
  144. runtime: {
  145. sendMessage() {
  146. return Promise.resolve();
  147. },
  148. },
  149. },
  150. });
  151. await controller.autoRunLoop(1, {
  152. autoRunSkipFailures: true,
  153. mode: 'restart',
  154. });
  155. assert.equal(events.runCalls, 1, 'add-phone fatal failure should stop before the next auto attempt starts');
  156. assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'add-phone fatal failure should not enter retrying phase');
  157. assert.equal(events.accountRecords.length, 1, 'fatal add-phone should still persist a failed round record');
  158. assert.equal(events.accountRecords[0].status, 'failed');
  159. assert.match(events.accountRecords[0].reason, /add-phone/);
  160. assert.equal(events.cleanupCalls.length, 1, 'add-phone failure should trigger cleanup before stopping');
  161. assert.equal(events.cleanupCalls[0].currentRun, 1);
  162. assert.ok(events.logs.some(({ message }) => /add-phone\/手机号页/.test(message)));
  163. assert.equal(runtime.state.autoRunActive, false);
  164. assert.equal(runtime.state.autoRunSessionId, 0);
  165. });
  166. test('auto-run controller parks 30~60 minutes and continues next round after add-phone when more runs remain', async () => {
  167. const events = {
  168. logs: [],
  169. broadcasts: [],
  170. accountRecords: [],
  171. cleanupCalls: [],
  172. timerPlans: [],
  173. runCalls: 0,
  174. };
  175. let currentState = {
  176. stepStatuses: {},
  177. vpsUrl: 'https://example.com/vps',
  178. vpsPassword: 'secret',
  179. customPassword: '',
  180. autoRunSkipFailures: false,
  181. autoRunFallbackThreadIntervalMinutes: 0,
  182. autoRunDelayEnabled: false,
  183. autoRunDelayMinutes: 30,
  184. autoStepDelaySeconds: null,
  185. mailProvider: '163',
  186. emailGenerator: 'duck',
  187. gmailBaseEmail: '',
  188. mail2925BaseEmail: '',
  189. emailPrefix: 'demo',
  190. inbucketHost: '',
  191. inbucketMailbox: '',
  192. cloudflareDomain: '',
  193. cloudflareDomains: [],
  194. tabRegistry: {},
  195. sourceLastUrls: {},
  196. autoRunRoundSummaries: [],
  197. };
  198. const runtime = {
  199. state: {
  200. autoRunActive: false,
  201. autoRunCurrentRun: 0,
  202. autoRunTotalRuns: 1,
  203. autoRunAttemptRun: 0,
  204. autoRunSessionId: 0,
  205. },
  206. get() {
  207. return { ...this.state };
  208. },
  209. set(updates = {}) {
  210. this.state = { ...this.state, ...updates };
  211. },
  212. };
  213. let sessionSeed = 100;
  214. const broadcastAutoRunStatus = async (phase, payload = {}, extraState = {}) => {
  215. events.broadcasts.push({ phase, ...payload });
  216. currentState = {
  217. ...currentState,
  218. ...extraState,
  219. autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
  220. autoRunPhase: phase,
  221. autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
  222. autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
  223. autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
  224. autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
  225. };
  226. };
  227. const controller = api.createAutoRunController({
  228. addLog: async (message, level = 'info') => {
  229. events.logs.push({ message, level });
  230. },
  231. appendAccountRunRecord: async (status, _state, reason) => {
  232. events.accountRecords.push({ status, reason });
  233. return { status, reason };
  234. },
  235. AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
  236. AUTO_RUN_RETRY_DELAY_MS: 3000,
  237. AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
  238. AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
  239. broadcastAutoRunStatus,
  240. broadcastStopToContentScripts: async () => {},
  241. cancelPendingCommands: () => {},
  242. cleanupAfterAddPhone: async (payload = {}) => {
  243. events.cleanupCalls.push(payload);
  244. },
  245. chooseAddPhonePauseMinutes: () => 30,
  246. clearStopRequest: () => {},
  247. createAutoRunSessionId: () => {
  248. sessionSeed += 1;
  249. return sessionSeed;
  250. },
  251. getAutoRunStatusPayload: (phase, payload = {}) => ({
  252. autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
  253. autoRunPhase: phase,
  254. autoRunCurrentRun: payload.currentRun ?? 0,
  255. autoRunTotalRuns: payload.totalRuns ?? 1,
  256. autoRunAttemptRun: payload.attemptRun ?? 0,
  257. autoRunSessionId: payload.sessionId ?? 0,
  258. }),
  259. getErrorMessage: (error) => error?.message || String(error || ''),
  260. getFirstUnfinishedStep: () => 1,
  261. getPendingAutoRunTimerPlan: () => null,
  262. getRunningSteps: () => [],
  263. getState: async () => ({
  264. ...currentState,
  265. stepStatuses: { ...(currentState.stepStatuses || {}) },
  266. tabRegistry: { ...(currentState.tabRegistry || {}) },
  267. sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
  268. }),
  269. getStopRequested: () => false,
  270. hasSavedProgress: () => false,
  271. isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
  272. isRestartCurrentAttemptError: () => false,
  273. isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
  274. launchAutoRunTimerPlan: async () => false,
  275. normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
  276. persistAutoRunTimerPlan: async (plan, extraState = {}) => {
  277. events.timerPlans.push({ plan, extraState });
  278. await broadcastAutoRunStatus('waiting_interval', {
  279. currentRun: plan.currentRun,
  280. totalRuns: plan.totalRuns,
  281. attemptRun: plan.attemptRun,
  282. sessionId: plan.autoRunSessionId,
  283. }, {
  284. ...extraState,
  285. autoRunTimerPlan: plan,
  286. });
  287. return plan;
  288. },
  289. resetState: async () => {
  290. currentState = {
  291. ...currentState,
  292. stepStatuses: {},
  293. tabRegistry: {},
  294. sourceLastUrls: {},
  295. };
  296. },
  297. runAutoSequenceFromStep: async () => {
  298. events.runCalls += 1;
  299. throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone');
  300. },
  301. runtime,
  302. setState: async (updates = {}) => {
  303. currentState = {
  304. ...currentState,
  305. ...updates,
  306. stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
  307. tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
  308. sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
  309. };
  310. },
  311. sleepWithStop: async () => {},
  312. throwIfAutoRunSessionStopped: (sessionId) => {
  313. if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
  314. throw new Error('流程已被用户停止。');
  315. }
  316. },
  317. waitForRunningStepsToFinish: async () => currentState,
  318. chrome: {
  319. runtime: {
  320. sendMessage() {
  321. return Promise.resolve();
  322. },
  323. },
  324. },
  325. });
  326. await controller.autoRunLoop(2, {
  327. autoRunSkipFailures: false,
  328. mode: 'restart',
  329. });
  330. assert.equal(events.runCalls, 1, 'add-phone should stop current round immediately without retrying the same round');
  331. assert.equal(events.accountRecords.length, 1, 'failed round should still be recorded');
  332. assert.equal(events.accountRecords[0].status, 'failed');
  333. assert.equal(events.cleanupCalls.length, 1, 'add-phone cooldown should trigger immediate cleanup');
  334. assert.equal(events.cleanupCalls[0].currentRun, 1);
  335. assert.equal(events.timerPlans.length, 1, 'should schedule a delayed continue timer');
  336. assert.equal(events.timerPlans[0].plan.kind, 'between_rounds');
  337. assert.equal(events.timerPlans[0].plan.currentRun, 1);
  338. assert.equal(events.timerPlans[0].plan.totalRuns, 2);
  339. assert.equal(events.timerPlans[0].plan.countdownTitle, '手机号冷却中');
  340. assert.match(events.logs.find(({ message }) => /等待 30 分钟后继续下一轮/.test(message))?.message || '', /等待 30 分钟后继续下一轮/);
  341. assert.equal(runtime.state.autoRunActive, false);
  342. assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'should not retry same round after add-phone');
  343. assert.equal(events.broadcasts.some(({ phase }) => phase === 'waiting_interval'), true, 'should enter waiting_interval phase');
  344. });