diff --git a/background.js b/background.js index 2ef4de9..e1f1324 100644 --- a/background.js +++ b/background.js @@ -5600,6 +5600,19 @@ function notifyStepError(step, error) { if (waiter) waiter.reject(new Error(error)); } +async function runCompletedStepSideEffects(step, payload, completionState, lastStepId) { + await handleStepData(step, payload); + if (step === lastStepId) { + await appendAndBroadcastAccountRunRecord('success', completionState); + } +} + +async function reportCompletedStepSideEffectError(step, error) { + const message = getErrorMessage(error); + console.warn(LOG_PREFIX, `[completeStepFromBackground] step ${step} post-completion side effect failed:`, error); + await addLog(`步骤 ${step} 已完成,但完成后的收尾处理失败:${message}`, 'warn'); +} + async function completeStepFromBackground(step, payload = {}) { if (stopRequested) { await setStepStatus(step, 'stopped'); @@ -5615,10 +5628,15 @@ async function completeStepFromBackground(step, payload = {}) { const completionState = step === lastStepId ? latestState : null; await setStepStatus(step, 'completed'); await addLog(`步骤 ${step} 已完成`, 'ok'); - await handleStepData(step, payload); + if (step === lastStepId) { - await appendAndBroadcastAccountRunRecord('success', completionState); + notifyStepComplete(step, payload); + void runCompletedStepSideEffects(step, payload, completionState, lastStepId) + .catch((error) => reportCompletedStepSideEffectError(step, error)); + return; } + + await runCompletedStepSideEffects(step, payload, completionState, lastStepId); notifyStepComplete(step, payload); } diff --git a/tests/background-step-completion.test.js b/tests/background-step-completion.test.js new file mode 100644 index 0000000..3efe6bf --- /dev/null +++ b/tests/background-step-completion.test.js @@ -0,0 +1,127 @@ +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; + } + } + 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); +} + +function createApi(events, lastStepId = 10) { + return new Function('events', 'lastStepId', ` +let stopRequested = false; +const LOG_PREFIX = '[test]'; +const STOP_ERROR_MESSAGE = '流程已被用户停止。'; +const LAST_STEP_ID = 10; +function getErrorMessage(error) { + return error?.message || String(error || ''); +} +async function getState() { + events.push({ type: 'getState' }); + return { stepStatuses: {}, contributionMode: true }; +} +function getLastStepIdForState() { + return lastStepId; +} +async function setStepStatus(step, status) { + events.push({ type: 'status', step, status }); +} +async function addLog(message, level) { + events.push({ type: 'log', message, level }); +} +async function appendManualAccountRunRecordIfNeeded() { + events.push({ type: 'manual-record' }); +} +function notifyStepError(step, error) { + events.push({ type: 'error', step, error }); +} +function notifyStepComplete(step, payload) { + events.push({ type: 'notify', step, payload }); +} +async function handleStepData(step, payload) { + events.push({ type: 'handle-start', step, payload }); + await new Promise((resolve) => setTimeout(resolve, 25)); + events.push({ type: 'handle-done', step }); +} +async function appendAndBroadcastAccountRunRecord(status, state) { + events.push({ type: 'record', status, state }); +} +${extractFunction('runCompletedStepSideEffects')} +${extractFunction('reportCompletedStepSideEffectError')} +${extractFunction('completeStepFromBackground')} +return { completeStepFromBackground }; +`)(events, lastStepId); +} + +test('completeStepFromBackground releases final step before slow post-completion side effects', async () => { + const events = []; + const api = createApi(events, 10); + + await api.completeStepFromBackground(10, { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' }); + + const types = events.map((event) => event.type); + assert.equal(types.indexOf('notify') < types.indexOf('handle-start'), true); + assert.equal(types.includes('handle-done'), false); + assert.equal(types.includes('record'), false); + + await new Promise((resolve) => setTimeout(resolve, 40)); + + const settledTypes = events.map((event) => event.type); + assert.equal(settledTypes.includes('handle-done'), true); + assert.equal(settledTypes.includes('record'), true); +}); + +test('completeStepFromBackground keeps non-final step data handling before completion signal', async () => { + const events = []; + const api = createApi(events, 10); + + await api.completeStepFromBackground(9, { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' }); + + const types = events.map((event) => event.type); + assert.equal(types.indexOf('handle-done') < types.indexOf('notify'), true); + assert.equal(types.includes('record'), false); +});