feat: 添加停止记录推断逻辑,支持归一化步骤状态并更新相关测试

This commit is contained in:
QLHazyCoder
2026-04-19 16:54:15 +08:00
parent 2fe04f6f22
commit 17e1c672f1
8 changed files with 379 additions and 19 deletions
@@ -100,18 +100,42 @@ test('account run history helper upgrades old records, keeps stopped items and s
assert.equal(fetchCalled, false);
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), false);
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
const stoppedRecord = helpers.buildAccountRunHistoryRecord({ email: 'a@b.com', password: 'x' }, 'stopped', 'stop');
const stoppedRecord = helpers.buildAccountRunHistoryRecord(
{ email: 'a@b.com', password: 'x' },
'step7_stopped',
'步骤 7 已被用户停止'
);
assert.equal(stoppedRecord.recordId, 'a@b.com');
assert.equal(stoppedRecord.email, 'a@b.com');
assert.equal(stoppedRecord.password, 'x');
assert.equal(stoppedRecord.finalStatus, 'stopped');
assert.equal(stoppedRecord.retryCount, 0);
assert.equal(stoppedRecord.failureLabel, '流程已停止');
assert.equal(stoppedRecord.failureDetail, 'stop');
assert.equal(stoppedRecord.failedStep, null);
assert.equal(stoppedRecord.failureLabel, '步骤 7 停止');
assert.equal(stoppedRecord.failureDetail, '步骤 7 已被用户停止');
assert.equal(stoppedRecord.failedStep, 7);
assert.equal(stoppedRecord.source, 'manual');
assert.equal(stoppedRecord.autoRunContext, null);
assert.ok(stoppedRecord.finishedAt);
const genericStoppedRecord = helpers.buildAccountRunHistoryRecord({ email: 'stop@b.com', password: 'y' }, 'stopped', 'stop');
assert.equal(genericStoppedRecord.failureLabel, '流程已停止');
assert.equal(genericStoppedRecord.failedStep, null);
const normalizedStoppedRecord = helpers.normalizeAccountRunHistoryRecord({
recordId: 'legacy-stop@example.com',
email: 'legacy-stop@example.com',
password: 'secret',
finalStatus: 'stopped',
finishedAt: '2026-04-17T00:12:00.000Z',
retryCount: 0,
failureLabel: '流程已停止',
failureDetail: '步骤 7 已被用户停止。',
failedStep: 7,
source: 'manual',
autoRunContext: null,
});
assert.equal(normalizedStoppedRecord.failureLabel, '步骤 7 停止');
assert.equal(normalizedStoppedRecord.failedStep, 7);
});
test('account run history helper clears persisted records and syncs full snapshot payload to local helper', async () => {
+237
View File
@@ -0,0 +1,237 @@
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 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);
});
@@ -207,7 +207,7 @@ test('account records manager supports filter chips and partial multi-select del
finalStatus: 'stopped',
finishedAt: '2026-04-17T04:28:00.000Z',
retryCount: 1,
failureLabel: '流程已停止',
failureLabel: '步骤 7 停止',
},
],
};
@@ -303,6 +303,7 @@ test('account records manager supports filter chips and partial multi-select del
assert.doesNotMatch(list.innerHTML, /success@example\.com/);
assert.match(list.innerHTML, /failed@example\.com/);
assert.match(list.innerHTML, /stopped@example\.com/);
assert.match(list.innerHTML, /步骤 7 停止/);
btnToggleAccountRecordsSelection.listeners.click();
+6
View File
@@ -58,6 +58,8 @@ const helperBundle = [
extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
extractFunction(helperSource, 'rejectPendingStep8'),
extractFunction(helperSource, 'throwIfStep8SettledOrStopped'),
extractFunction(helperSource, 'getRunningSteps'),
extractFunction(helperSource, 'inferStoppedRecordStep'),
extractFunction(helperSource, 'requestStop'),
].join('\n');
@@ -75,6 +77,9 @@ let autoRunTotalRuns = 3;
let autoRunAttemptRun = 4;
let autoRunSessionId = 99;
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 STEP8_CLICK_RETRY_DELAY_MS = 500;
const STEP8_MAX_ROUNDS = 5;
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
@@ -137,6 +142,7 @@ async function addLog() {}
async function broadcastStopToContentScripts() {}
async function markRunningStepsStopped() {}
async function broadcastAutoRunStatus() {}
async function appendAndBroadcastAccountRunRecord() {}
async function getState() {
return { autoRunning: false };
}