auto-run-a4sky-mail-tab-reuse.test.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const helperSource = fs.readFileSync('background.js', 'utf8');
  5. const autoRunModuleSource = fs.readFileSync('background/auto-run-controller.js', 'utf8');
  6. function extractFunction(source, name) {
  7. const markers = [`async function ${name}(`, `function ${name}(`];
  8. const start = markers.map((marker) => source.indexOf(marker)).find((index) => index >= 0);
  9. if (start < 0) throw new Error(`missing function ${name}`);
  10. let parenDepth = 0;
  11. let signatureEnded = false;
  12. let braceStart = -1;
  13. for (let i = start; i < source.length; i += 1) {
  14. const ch = source[i];
  15. if (ch === '(') parenDepth += 1;
  16. else if (ch === ')') {
  17. parenDepth -= 1;
  18. if (parenDepth === 0) signatureEnded = true;
  19. } else if (ch === '{' && signatureEnded) {
  20. braceStart = i;
  21. break;
  22. }
  23. }
  24. if (braceStart < 0) throw new Error(`missing body for function ${name}`);
  25. let depth = 0;
  26. let end = braceStart;
  27. for (; end < source.length; end += 1) {
  28. const ch = source[end];
  29. if (ch === '{') depth += 1;
  30. if (ch === '}') {
  31. depth -= 1;
  32. if (depth === 0) {
  33. end += 1;
  34. break;
  35. }
  36. }
  37. }
  38. return source.slice(start, end);
  39. }
  40. const helperBundle = [
  41. extractFunction(helperSource, 'clearStopRequest'),
  42. extractFunction(helperSource, 'normalizeAutoRunSessionId'),
  43. extractFunction(helperSource, 'throwIfStopped'),
  44. extractFunction(helperSource, 'isStopError'),
  45. extractFunction(helperSource, 'isStepDoneStatus'),
  46. extractFunction(helperSource, 'isRestartCurrentAttemptError'),
  47. extractFunction(helperSource, 'getFirstUnfinishedStep'),
  48. extractFunction(helperSource, 'hasSavedProgress'),
  49. extractFunction(helperSource, 'getRunningSteps'),
  50. extractFunction(helperSource, 'getAutoRunStatusPayload'),
  51. ].join('\n');
  52. test('auto-run fresh attempt preserves A4Sky mailbox tab context', async () => {
  53. const api = new Function('autoRunModuleSource', `
  54. const self = {};
  55. const STOP_ERROR_MESSAGE = 'Flow stopped.';
  56. const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
  57. const AUTO_RUN_RETRY_DELAY_MS = 3000;
  58. const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
  59. const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
  60. const STEP_IDS = [1,2,3,4,5,6,7,8,9,10];
  61. const DEFAULT_STATE = {
  62. stepStatuses: { 1:'pending',2:'pending',3:'pending',4:'pending',5:'pending',6:'pending',7:'pending',8:'pending',9:'pending',10:'pending' },
  63. };
  64. let stopRequested = false;
  65. let runCalls = 0;
  66. let autoRunSessionId = 0;
  67. let autoRunSessionSeed = 1000;
  68. let currentState = {
  69. ...DEFAULT_STATE,
  70. stepStatuses: { ...DEFAULT_STATE.stepStatuses },
  71. vpsUrl: 'https://example.com/vps',
  72. vpsPassword: 'secret',
  73. customPassword: '',
  74. autoRunSkipFailures: false,
  75. autoRunFallbackThreadIntervalMinutes: 0,
  76. autoRunDelayEnabled: false,
  77. autoRunDelayMinutes: 30,
  78. autoStepDelaySeconds: null,
  79. mailProvider: 'a4sky',
  80. emailGenerator: 'duck',
  81. gmailBaseEmail: '',
  82. mail2925BaseEmail: '',
  83. emailPrefix: '',
  84. inbucketHost: '',
  85. inbucketMailbox: '',
  86. cloudflareDomain: '',
  87. cloudflareDomains: [],
  88. tabRegistry: {},
  89. sourceLastUrls: {},
  90. };
  91. async function getState() {
  92. return {
  93. ...currentState,
  94. stepStatuses: { ...(currentState.stepStatuses || {}) },
  95. tabRegistry: { ...(currentState.tabRegistry || {}) },
  96. sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
  97. };
  98. }
  99. async function setState(updates) {
  100. currentState = {
  101. ...currentState,
  102. ...updates,
  103. stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
  104. tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
  105. sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
  106. };
  107. }
  108. async function resetState() {
  109. const prev = await getState();
  110. currentState = {
  111. ...DEFAULT_STATE,
  112. stepStatuses: { ...DEFAULT_STATE.stepStatuses },
  113. vpsUrl: prev.vpsUrl,
  114. vpsPassword: prev.vpsPassword,
  115. customPassword: prev.customPassword,
  116. autoRunSkipFailures: prev.autoRunSkipFailures,
  117. autoRunFallbackThreadIntervalMinutes: prev.autoRunFallbackThreadIntervalMinutes,
  118. autoRunDelayEnabled: prev.autoRunDelayEnabled,
  119. autoRunDelayMinutes: prev.autoRunDelayMinutes,
  120. autoStepDelaySeconds: prev.autoStepDelaySeconds,
  121. mailProvider: prev.mailProvider,
  122. emailGenerator: prev.emailGenerator,
  123. gmailBaseEmail: prev.gmailBaseEmail,
  124. mail2925BaseEmail: prev.mail2925BaseEmail,
  125. emailPrefix: prev.emailPrefix,
  126. inbucketHost: prev.inbucketHost,
  127. inbucketMailbox: prev.inbucketMailbox,
  128. cloudflareDomain: prev.cloudflareDomain,
  129. cloudflareDomains: [...(prev.cloudflareDomains || [])],
  130. tabRegistry: { ...(prev.tabRegistry || {}) },
  131. sourceLastUrls: { ...(prev.sourceLastUrls || {}) },
  132. };
  133. }
  134. async function addLog() {}
  135. async function broadcastAutoRunStatus(phase, payload = {}) {
  136. await setState({ ...getAutoRunStatusPayload(phase, payload) });
  137. }
  138. async function sleepWithStop() {}
  139. async function waitForRunningStepsToFinish() { return getState(); }
  140. async function broadcastStopToContentScripts() {}
  141. function cancelPendingCommands() {}
  142. function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Math.max(0, Math.floor(Number(value) || 0)); }
  143. async function persistAutoRunTimerPlan() {}
  144. async function launchAutoRunTimerPlan() { return false; }
  145. function getPendingAutoRunTimerPlan() { return null; }
  146. function getErrorMessage(error) { return error?.message || String(error || ''); }
  147. function createAutoRunSessionId() { autoRunSessionSeed += 1; autoRunSessionId = autoRunSessionSeed; return autoRunSessionId; }
  148. function throwIfAutoRunSessionStopped(sessionId) { if (sessionId && sessionId !== autoRunSessionId) throw new Error(STOP_ERROR_MESSAGE); throwIfStopped(); }
  149. const chrome = { runtime: { sendMessage() { return Promise.resolve(); } } };
  150. function getStopRequested() { return false; }
  151. async function runAutoSequenceFromStep() {
  152. runCalls += 1;
  153. const state = await getState();
  154. if (runCalls === 2) {
  155. if (state.tabRegistry['mail-phplife']?.tabId !== 77) {
  156. throw new Error('fresh auto-run attempt did not preserve mail-phplife tab id');
  157. }
  158. if (state.sourceLastUrls['mail-phplife'] !== 'https://mail.phplife.net/?_task=mail&_mbox=INBOX') {
  159. throw new Error('fresh auto-run attempt did not preserve mail-phplife sourceLastUrl');
  160. }
  161. }
  162. currentState = {
  163. ...currentState,
  164. stepStatuses: { 1:'completed',2:'completed',3:'completed',4:'completed',5:'completed',6:'completed',7:'completed',8:'completed',9:'completed',10:'completed' },
  165. tabRegistry: { 'mail-phplife': { tabId: 77, ready: true } },
  166. sourceLastUrls: { 'mail-phplife': 'https://mail.phplife.net/?_task=mail&_mbox=INBOX' },
  167. };
  168. }
  169. ${helperBundle}
  170. ${autoRunModuleSource}
  171. const runtime = {
  172. state: { autoRunActive:false, autoRunCurrentRun:0, autoRunTotalRuns:1, autoRunAttemptRun:0, autoRunSessionId:0 },
  173. get() { return { ...this.state }; },
  174. set(updates) { this.state = { ...this.state, ...updates }; },
  175. };
  176. const controller = self.MultiPageBackgroundAutoRunController.createAutoRunController({
  177. addLog,
  178. appendAccountRunRecord: async () => null,
  179. AUTO_RUN_MAX_RETRIES_PER_ROUND,
  180. AUTO_RUN_RETRY_DELAY_MS,
  181. AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
  182. AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
  183. broadcastAutoRunStatus,
  184. broadcastStopToContentScripts,
  185. cancelPendingCommands,
  186. clearStopRequest,
  187. createAutoRunSessionId,
  188. getAutoRunStatusPayload,
  189. getErrorMessage,
  190. getFirstUnfinishedStep,
  191. getPendingAutoRunTimerPlan,
  192. getRunningSteps,
  193. getState,
  194. hasSavedProgress,
  195. isAddPhoneAuthFailure: () => false,
  196. isRestartCurrentAttemptError,
  197. isStopError,
  198. getStopRequested,
  199. launchAutoRunTimerPlan,
  200. normalizeAutoRunFallbackThreadIntervalMinutes,
  201. persistAutoRunTimerPlan,
  202. resetState,
  203. runAutoSequenceFromStep,
  204. runtime,
  205. setState,
  206. sleepWithStop,
  207. throwIfAutoRunSessionStopped,
  208. waitForRunningStepsToFinish,
  209. chrome,
  210. });
  211. return {
  212. autoRunLoop: controller.autoRunLoop,
  213. snapshot() { return { currentState, runCalls }; },
  214. };
  215. `)(autoRunModuleSource);
  216. await api.autoRunLoop(2, { autoRunSkipFailures: false, mode: 'restart' });
  217. const snapshot = api.snapshot();
  218. assert.equal(snapshot.runCalls, 2);
  219. assert.equal(snapshot.currentState.tabRegistry['mail-phplife']?.tabId, 77);
  220. assert.equal(snapshot.currentState.sourceLastUrls['mail-phplife'], 'https://mail.phplife.net/?_task=mail&_mbox=INBOX');
  221. });