Files
FlowPilot/tests/auto-run-fresh-attempt-reset.test.js
T
2026-05-04 12:56:35 +08:00

381 lines
12 KiB
JavaScript

const assert = require('assert');
const fs = require('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');
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;
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,
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
signupPhoneNumber: '+6612345',
signupPhoneActivation: { activationId: 'signup-activation', phoneNumber: '+6612345' },
signupPhoneCompletedActivation: { activationId: 'signup-completed', phoneNumber: '+6612345' },
signupPhoneVerificationRequestedAt: 123456,
signupPhoneVerificationPurpose: 'signup',
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: 'demo@gmail.com',
mail2925BaseEmail: 'demo@2925.com',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 1,
maxUses: 3,
},
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,
signupMethod: prev.signupMethod,
resolvedSignupMethod: null,
accountIdentifierType: null,
accountIdentifier: '',
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneCompletedActivation: null,
signupPhoneVerificationRequestedAt: null,
signupPhoneVerificationPurpose: '',
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(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));
}
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();
},
},
};
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',
10: 'completed',
},
tabRegistry: {
'signup-page': { tabId: 88, ready: true },
},
sourceLastUrls: {
'signup-page': 'https://auth.openai.com/authorize',
},
};
}
${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,
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,
getStopRequested: () => stopRequested,
hasSavedProgress,
isRestartCurrentAttemptError,
isStopError,
launchAutoRunTimerPlan,
normalizeAutoRunFallbackThreadIntervalMinutes,
persistAutoRunTimerPlan,
resetState,
runAutoSequenceFromStep,
runtime,
setState,
sleepWithStop,
throwIfAutoRunSessionStopped,
waitForRunningStepsToFinish,
throwIfStopped,
chrome,
});
return {
autoRunLoop: controller.autoRunLoop,
snapshot() {
return {
runCalls,
autoRunActive: runtime.state.autoRunActive,
autoRunCurrentRun: runtime.state.autoRunCurrentRun,
autoRunTotalRuns: runtime.state.autoRunTotalRuns,
autoRunAttemptRun: runtime.state.autoRunAttemptRun,
currentState,
logs,
broadcasts,
};
},
};
`)(autoRunModuleSource);
(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');
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.signupMethod, 'phone', 'signup method setting should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.resolvedSignupMethod, null, 'resolved signup method should be cleared before the next run freezes it again');
assert.strictEqual(snapshot.currentState.accountIdentifierType, null, 'account identifier type should be runtime-only');
assert.strictEqual(snapshot.currentState.accountIdentifier, '', 'account identifier should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneNumber, '', 'signup phone number should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneActivation, null, 'signup phone activation should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneCompletedActivation, null, 'completed signup phone activation should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneVerificationRequestedAt, null, 'signup phone request time should be runtime-only');
assert.strictEqual(snapshot.currentState.signupPhoneVerificationPurpose, '', 'signup phone purpose should be runtime-only');
assert.deepStrictEqual(
snapshot.currentState.reusablePhoneActivation,
{
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 1,
maxUses: 3,
},
'reusable phone activation should survive fresh-attempt reset'
);
console.log('auto-run fresh attempt reset tests passed');
})().catch((error) => {
console.error(error);
process.exit(1);
});