|
|
@@ -0,0 +1,229 @@
|
|
|
+const test = require('node:test');
|
|
|
+const assert = require('node:assert/strict');
|
|
|
+const fs = require('node:fs');
|
|
|
+
|
|
|
+const helperSource = fs.readFileSync('background.js', 'utf8');
|
|
|
+const autoRunModuleSource = fs.readFileSync('background/auto-run-controller.js', 'utf8');
|
|
|
+
|
|
|
+function extractFunction(source, name) {
|
|
|
+ const markers = [`async function ${name}(`, `function ${name}(`];
|
|
|
+ const start = markers.map((marker) => source.indexOf(marker)).find((index) => index >= 0);
|
|
|
+ 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;
|
|
|
+ let end = braceStart;
|
|
|
+ for (; end < source.length; end += 1) {
|
|
|
+ const ch = source[end];
|
|
|
+ if (ch === '{') depth += 1;
|
|
|
+ if (ch === '}') {
|
|
|
+ depth -= 1;
|
|
|
+ if (depth === 0) {
|
|
|
+ end += 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return source.slice(start, end);
|
|
|
+}
|
|
|
+
|
|
|
+const helperBundle = [
|
|
|
+ extractFunction(helperSource, 'clearStopRequest'),
|
|
|
+ extractFunction(helperSource, 'normalizeAutoRunSessionId'),
|
|
|
+ extractFunction(helperSource, 'throwIfStopped'),
|
|
|
+ extractFunction(helperSource, 'isStopError'),
|
|
|
+ extractFunction(helperSource, 'isStepDoneStatus'),
|
|
|
+ extractFunction(helperSource, 'isRestartCurrentAttemptError'),
|
|
|
+ extractFunction(helperSource, 'getFirstUnfinishedStep'),
|
|
|
+ extractFunction(helperSource, 'hasSavedProgress'),
|
|
|
+ extractFunction(helperSource, 'getRunningSteps'),
|
|
|
+ extractFunction(helperSource, 'getAutoRunStatusPayload'),
|
|
|
+].join('\n');
|
|
|
+
|
|
|
+test('auto-run fresh attempt preserves A4Sky mailbox tab context', async () => {
|
|
|
+ const api = new Function('autoRunModuleSource', `
|
|
|
+const self = {};
|
|
|
+const STOP_ERROR_MESSAGE = 'Flow stopped.';
|
|
|
+const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
|
|
+const AUTO_RUN_RETRY_DELAY_MS = 3000;
|
|
|
+const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
|
|
+const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
|
|
|
+const STEP_IDS = [1,2,3,4,5,6,7,8,9,10];
|
|
|
+const DEFAULT_STATE = {
|
|
|
+ stepStatuses: { 1:'pending',2:'pending',3:'pending',4:'pending',5:'pending',6:'pending',7:'pending',8:'pending',9:'pending',10:'pending' },
|
|
|
+};
|
|
|
+let stopRequested = false;
|
|
|
+let runCalls = 0;
|
|
|
+let autoRunSessionId = 0;
|
|
|
+let autoRunSessionSeed = 1000;
|
|
|
+let currentState = {
|
|
|
+ ...DEFAULT_STATE,
|
|
|
+ stepStatuses: { ...DEFAULT_STATE.stepStatuses },
|
|
|
+ vpsUrl: 'https://example.com/vps',
|
|
|
+ vpsPassword: 'secret',
|
|
|
+ customPassword: '',
|
|
|
+ autoRunSkipFailures: false,
|
|
|
+ autoRunFallbackThreadIntervalMinutes: 0,
|
|
|
+ autoRunDelayEnabled: false,
|
|
|
+ autoRunDelayMinutes: 30,
|
|
|
+ autoStepDelaySeconds: null,
|
|
|
+ mailProvider: 'a4sky',
|
|
|
+ emailGenerator: 'duck',
|
|
|
+ gmailBaseEmail: '',
|
|
|
+ mail2925BaseEmail: '',
|
|
|
+ emailPrefix: '',
|
|
|
+ inbucketHost: '',
|
|
|
+ inbucketMailbox: '',
|
|
|
+ cloudflareDomain: '',
|
|
|
+ cloudflareDomains: [],
|
|
|
+ tabRegistry: {},
|
|
|
+ sourceLastUrls: {},
|
|
|
+};
|
|
|
+async function getState() {
|
|
|
+ return {
|
|
|
+ ...currentState,
|
|
|
+ stepStatuses: { ...(currentState.stepStatuses || {}) },
|
|
|
+ tabRegistry: { ...(currentState.tabRegistry || {}) },
|
|
|
+ sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
|
|
+ };
|
|
|
+}
|
|
|
+async function setState(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,
|
|
|
+ };
|
|
|
+}
|
|
|
+async function resetState() {
|
|
|
+ const prev = await getState();
|
|
|
+ currentState = {
|
|
|
+ ...DEFAULT_STATE,
|
|
|
+ stepStatuses: { ...DEFAULT_STATE.stepStatuses },
|
|
|
+ vpsUrl: prev.vpsUrl,
|
|
|
+ vpsPassword: prev.vpsPassword,
|
|
|
+ customPassword: prev.customPassword,
|
|
|
+ autoRunSkipFailures: prev.autoRunSkipFailures,
|
|
|
+ autoRunFallbackThreadIntervalMinutes: prev.autoRunFallbackThreadIntervalMinutes,
|
|
|
+ autoRunDelayEnabled: prev.autoRunDelayEnabled,
|
|
|
+ autoRunDelayMinutes: prev.autoRunDelayMinutes,
|
|
|
+ autoStepDelaySeconds: prev.autoStepDelaySeconds,
|
|
|
+ mailProvider: prev.mailProvider,
|
|
|
+ emailGenerator: prev.emailGenerator,
|
|
|
+ gmailBaseEmail: prev.gmailBaseEmail,
|
|
|
+ mail2925BaseEmail: prev.mail2925BaseEmail,
|
|
|
+ emailPrefix: prev.emailPrefix,
|
|
|
+ inbucketHost: prev.inbucketHost,
|
|
|
+ inbucketMailbox: prev.inbucketMailbox,
|
|
|
+ cloudflareDomain: prev.cloudflareDomain,
|
|
|
+ cloudflareDomains: [...(prev.cloudflareDomains || [])],
|
|
|
+ tabRegistry: { ...(prev.tabRegistry || {}) },
|
|
|
+ sourceLastUrls: { ...(prev.sourceLastUrls || {}) },
|
|
|
+ };
|
|
|
+}
|
|
|
+async function addLog() {}
|
|
|
+async function broadcastAutoRunStatus(phase, payload = {}) {
|
|
|
+ await setState({ ...getAutoRunStatusPayload(phase, payload) });
|
|
|
+}
|
|
|
+async function sleepWithStop() {}
|
|
|
+async function waitForRunningStepsToFinish() { return getState(); }
|
|
|
+async function broadcastStopToContentScripts() {}
|
|
|
+function cancelPendingCommands() {}
|
|
|
+function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Math.max(0, Math.floor(Number(value) || 0)); }
|
|
|
+async function persistAutoRunTimerPlan() {}
|
|
|
+async function launchAutoRunTimerPlan() { return false; }
|
|
|
+function getPendingAutoRunTimerPlan() { return null; }
|
|
|
+function getErrorMessage(error) { return error?.message || String(error || ''); }
|
|
|
+function createAutoRunSessionId() { autoRunSessionSeed += 1; autoRunSessionId = autoRunSessionSeed; return autoRunSessionId; }
|
|
|
+function throwIfAutoRunSessionStopped(sessionId) { if (sessionId && sessionId !== autoRunSessionId) throw new Error(STOP_ERROR_MESSAGE); throwIfStopped(); }
|
|
|
+const chrome = { runtime: { sendMessage() { return Promise.resolve(); } } };
|
|
|
+function getStopRequested() { return false; }
|
|
|
+async function runAutoSequenceFromStep() {
|
|
|
+ runCalls += 1;
|
|
|
+ const state = await getState();
|
|
|
+ if (runCalls === 2) {
|
|
|
+ if (state.tabRegistry['mail-phplife']?.tabId !== 77) {
|
|
|
+ throw new Error('fresh auto-run attempt did not preserve mail-phplife tab id');
|
|
|
+ }
|
|
|
+ if (state.sourceLastUrls['mail-phplife'] !== 'https://mail.phplife.net/?_task=mail&_mbox=INBOX') {
|
|
|
+ throw new Error('fresh auto-run attempt did not preserve mail-phplife sourceLastUrl');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ currentState = {
|
|
|
+ ...currentState,
|
|
|
+ stepStatuses: { 1:'completed',2:'completed',3:'completed',4:'completed',5:'completed',6:'completed',7:'completed',8:'completed',9:'completed',10:'completed' },
|
|
|
+ tabRegistry: { 'mail-phplife': { tabId: 77, ready: true } },
|
|
|
+ sourceLastUrls: { 'mail-phplife': 'https://mail.phplife.net/?_task=mail&_mbox=INBOX' },
|
|
|
+ };
|
|
|
+}
|
|
|
+${helperBundle}
|
|
|
+${autoRunModuleSource}
|
|
|
+const runtime = {
|
|
|
+ state: { autoRunActive:false, autoRunCurrentRun:0, autoRunTotalRuns:1, autoRunAttemptRun:0, autoRunSessionId:0 },
|
|
|
+ get() { return { ...this.state }; },
|
|
|
+ set(updates) { this.state = { ...this.state, ...updates }; },
|
|
|
+};
|
|
|
+const controller = self.MultiPageBackgroundAutoRunController.createAutoRunController({
|
|
|
+ addLog,
|
|
|
+ appendAccountRunRecord: async () => null,
|
|
|
+ AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
|
|
+ AUTO_RUN_RETRY_DELAY_MS,
|
|
|
+ AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
|
|
+ AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
|
|
|
+ broadcastAutoRunStatus,
|
|
|
+ broadcastStopToContentScripts,
|
|
|
+ cancelPendingCommands,
|
|
|
+ clearStopRequest,
|
|
|
+ createAutoRunSessionId,
|
|
|
+ getAutoRunStatusPayload,
|
|
|
+ getErrorMessage,
|
|
|
+ getFirstUnfinishedStep,
|
|
|
+ getPendingAutoRunTimerPlan,
|
|
|
+ getRunningSteps,
|
|
|
+ getState,
|
|
|
+ hasSavedProgress,
|
|
|
+ isAddPhoneAuthFailure: () => false,
|
|
|
+ isRestartCurrentAttemptError,
|
|
|
+ isStopError,
|
|
|
+ getStopRequested,
|
|
|
+ launchAutoRunTimerPlan,
|
|
|
+ normalizeAutoRunFallbackThreadIntervalMinutes,
|
|
|
+ persistAutoRunTimerPlan,
|
|
|
+ resetState,
|
|
|
+ runAutoSequenceFromStep,
|
|
|
+ runtime,
|
|
|
+ setState,
|
|
|
+ sleepWithStop,
|
|
|
+ throwIfAutoRunSessionStopped,
|
|
|
+ waitForRunningStepsToFinish,
|
|
|
+ chrome,
|
|
|
+});
|
|
|
+return {
|
|
|
+ autoRunLoop: controller.autoRunLoop,
|
|
|
+ snapshot() { return { currentState, runCalls }; },
|
|
|
+};
|
|
|
+ `)(autoRunModuleSource);
|
|
|
+
|
|
|
+ await api.autoRunLoop(2, { autoRunSkipFailures: false, mode: 'restart' });
|
|
|
+ const snapshot = api.snapshot();
|
|
|
+ assert.equal(snapshot.runCalls, 2);
|
|
|
+ assert.equal(snapshot.currentState.tabRegistry['mail-phplife']?.tabId, 77);
|
|
|
+ assert.equal(snapshot.currentState.sourceLastUrls['mail-phplife'], 'https://mail.phplife.net/?_task=mail&_mbox=INBOX');
|
|
|
+});
|