diff --git a/background.js b/background.js index c43111c..58ecaea 100644 --- a/background.js +++ b/background.js @@ -3511,8 +3511,10 @@ async function autoRunLoop(totalRuns, options = {}) { inbucketMailbox: prevState.inbucketMailbox, cloudflareDomain: prevState.cloudflareDomain, cloudflareDomains: prevState.cloudflareDomains, + // Fresh attempts must drop stale tab/url runtime state from the prior run. + tabRegistry: {}, + sourceLastUrls: {}, ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }), - ...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}), }; await resetState(); await setState(keepSettings); diff --git a/tests/auto-run-fresh-attempt-reset.test.js b/tests/auto-run-fresh-attempt-reset.test.js new file mode 100644 index 0000000..f7078af --- /dev/null +++ b/tests/auto-run-fresh-attempt-reset.test.js @@ -0,0 +1,257 @@ +const assert = require('assert'); +const fs = require('fs'); + +const source = fs.readFileSync('background.js', 'utf8'); + +function extractFunction(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 bundle = [ + extractFunction('clearStopRequest'), + extractFunction('throwIfStopped'), + extractFunction('isStopError'), + extractFunction('isStepDoneStatus'), + extractFunction('isRestartCurrentAttemptError'), + extractFunction('getFirstUnfinishedStep'), + extractFunction('hasSavedProgress'), + extractFunction('getRunningSteps'), + extractFunction('getAutoRunStatusPayload'), + extractFunction('autoRunLoop'), +].join('\n'); + +const api = new Function(` +const STOP_ERROR_MESSAGE = 'Flow stopped.'; +const DEFAULT_STATE = { + stepStatuses: { + 1: 'pending', + 2: 'pending', + 3: 'pending', + 4: 'pending', + 5: 'pending', + 6: 'pending', + 7: 'pending', + 8: 'pending', + 9: 'pending', + }, +}; + +let stopRequested = false; +let autoRunActive = false; +let autoRunCurrentRun = 0; +let autoRunTotalRuns = 1; +let autoRunAttemptRun = 0; +let runCalls = 0; + +const logs = []; +const broadcasts = []; +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: '163', + emailGenerator: 'duck', + emailPrefix: 'demo', + 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, + emailPrefix: prev.emailPrefix, + inbucketHost: prev.inbucketHost, + inbucketMailbox: prev.inbucketMailbox, + cloudflareDomain: prev.cloudflareDomain, + cloudflareDomains: [...(prev.cloudflareDomains || [])], + tabRegistry: { ...(prev.tabRegistry || {}) }, + sourceLastUrls: { ...(prev.sourceLastUrls || {}) }, + }; +} + +async function addLog(message, level = 'info') { + logs.push({ message, level }); +} + +async function broadcastAutoRunStatus(phase, payload = {}) { + broadcasts.push({ 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)); +} + +const chrome = { + runtime: { + sendMessage() { + return Promise.resolve(); + }, + }, +}; + +async function runAutoSequenceFromStep() { + runCalls += 1; + const state = await getState(); + + if ( + runCalls === 2 + && (Object.keys(state.tabRegistry || {}).length || Object.keys(state.sourceLastUrls || {}).length) + ) { + throw new Error('fresh auto-run attempt reused stale runtime tab context'); + } + + currentState = { + ...currentState, + stepStatuses: { + 1: 'completed', + 2: 'completed', + 3: 'completed', + 4: 'completed', + 5: 'completed', + 6: 'completed', + 7: 'completed', + 8: 'completed', + 9: 'completed', + }, + tabRegistry: { + 'signup-page': { tabId: 88, ready: true }, + }, + sourceLastUrls: { + 'signup-page': 'https://auth.openai.com/authorize', + }, + }; +} + +${bundle} + +return { + autoRunLoop, + snapshot() { + return { + runCalls, + autoRunActive, + autoRunCurrentRun, + autoRunTotalRuns, + autoRunAttemptRun, + currentState, + logs, + broadcasts, + }; + }, +}; +`)(); + +(async () => { + await api.autoRunLoop(2, { autoRunSkipFailures: false, mode: 'restart' }); + + const snapshot = api.snapshot(); + assert.strictEqual(snapshot.runCalls, 2, 'auto-run should enter the second fresh attempt'); + assert.strictEqual(snapshot.currentState.autoRunPhase, 'complete', 'both runs should complete after reset'); + assert.strictEqual(snapshot.currentState.autoRunCurrentRun, 2, 'final run index should be recorded'); + assert.strictEqual(snapshot.autoRunActive, false, 'auto-run should exit active state after completion'); + + console.log('auto-run fresh attempt reset tests passed'); +})().catch((error) => { + console.error(error); + process.exit(1); +});