const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node: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; } } 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); } test('generic stopped record resolves to next unfinished step during execution gap', async () => { const bundle = [ extractFunction('getRunningSteps'), extractFunction('inferStoppedRecordStep'), extractFunction('resolveAccountRunRecordStatusForStop'), extractFunction('extractStoppedStepFromRecordStatus'), extractFunction('resolveAccountRunRecordReasonForStop'), extractFunction('appendAndBroadcastAccountRunRecord'), ].join('\n'); const api = new Function(` const STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const DEFAULT_STATE = { stepStatuses: Object.fromEntries(STEP_IDS.map((step) => [step, 'pending'])), }; let captured = null; const accountRunHistoryHelpers = { appendAccountRunRecord: async (status, state, reason) => { captured = { status, state, reason }; return { status, state, reason }; }, }; async function broadcastAccountRunHistoryUpdate() {} async function getState() { return {}; } ${bundle} return { inferStoppedRecordStep, resolveAccountRunRecordStatusForStop, resolveAccountRunRecordReasonForStop, appendAndBroadcastAccountRunRecord, getCaptured() { return captured; }, }; `)(); const state = { email: 'user@example.com', password: 'secret', stepStatuses: { 1: 'completed', 2: 'completed', 3: 'completed', 4: 'completed', 5: 'completed', 6: 'completed', 7: 'pending', 8: 'pending', 9: 'pending', 10: 'pending', }, }; assert.equal(api.inferStoppedRecordStep(state), 7); assert.equal(api.resolveAccountRunRecordStatusForStop('stopped', state), 'step7_stopped'); assert.equal(api.resolveAccountRunRecordReasonForStop('step7_stopped', '流程已被用户停止。'), '步骤 7 已被用户停止。'); assert.equal( api.resolveAccountRunRecordReasonForStop('step2_stopped', '步骤 2 已使用邮箱,流程尚未完成。'), '步骤 2 已停止:邮箱已设置,流程尚未完成。' ); await api.appendAndBroadcastAccountRunRecord('stopped', state, '流程已被用户停止。'); assert.deepStrictEqual(api.getCaptured(), { status: 'step7_stopped', state, reason: '步骤 7 已被用户停止。', }); }); test('requestStop appends a stopped record for the next unfinished step when no step is running', async () => { const bundle = [ extractFunction('normalizeAutoRunSessionId'), extractFunction('clearCurrentAutoRunSessionId'), extractFunction('cleanupStep8NavigationListeners'), extractFunction('rejectPendingStep8'), extractFunction('getRunningSteps'), extractFunction('inferStoppedRecordStep'), extractFunction('requestStop'), ].join('\n'); const api = new Function(` let stopRequested = false; let autoRunActive = false; let autoRunCurrentRun = 0; let autoRunTotalRuns = 1; let autoRunAttemptRun = 0; let autoRunSessionId = 99; let webNavListener = null; let webNavCommittedListener = null; let step8TabUpdatedListener = null; let step8PendingReject = null; let resumeWaiter = null; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start'; const DEFAULT_STATE = { stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])), }; const stepWaiters = new Map(); const appended = []; const logs = []; const chrome = { webNavigation: { onBeforeNavigate: { removeListener() {} }, onCommitted: { removeListener() {} }, }, tabs: { onUpdated: { removeListener() {} }, }, }; function cancelPendingCommands() {} function abortActiveIcloudRequests() {} function getPendingAutoRunTimerPlan() { return null; } async function cancelScheduledAutoRun() {} async function clearAutoRunTimerAlarm() {} function clearStopRequest() { stopRequested = false; } async function addLog(message, level) { logs.push({ message, level }); } async function broadcastStopToContentScripts() {} async function markRunningStepsStopped() {} async function broadcastAutoRunStatus() {} async function appendAndBroadcastAccountRunRecord(status, state, reason) { appended.push({ status, state, reason }); return { status, state, reason }; } async function getState() { return { email: 'user@example.com', password: 'secret', stepStatuses: { 1: 'completed', 2: 'completed', 3: 'completed', 4: 'completed', 5: 'completed', 6: 'completed', 7: 'pending', 8: 'pending', 9: 'pending', 10: 'pending', }, }; } ${bundle} return { requestStop, snapshot() { return { appended, logs, stopRequested, autoRunSessionId }; }, }; `)(); await api.requestStop(); const state = api.snapshot(); assert.deepStrictEqual(state.appended, [{ status: 'stopped', state: { email: 'user@example.com', password: 'secret', stepStatuses: { 1: 'completed', 2: 'completed', 3: 'completed', 4: 'completed', 5: 'completed', 6: 'completed', 7: 'pending', 8: 'pending', 9: 'pending', 10: 'pending', }, }, reason: '流程已被用户停止。', }]); assert.equal(state.autoRunSessionId, 0); assert.equal(state.stopRequested, true); });