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'); });