Files
FlowPilot/tests/background-step-execution-range.test.js
T
2026-05-17 18:18:09 +08:00

165 lines
4.8 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
const NODE_IDS = [
'open-chatgpt',
'submit-signup-email',
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'oauth-login',
'fetch-login-code',
'confirm-oauth',
'platform-verify',
];
const NODE_STEPS = Object.fromEntries(NODE_IDS.map((nodeId, index) => [nodeId, index + 1]));
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);
}
function createApi() {
const bundle = [
'isPlainObjectValue',
'normalizeStepExecutionRangeFlowId',
'hasStepExecutionRangeShape',
'normalizePositiveStepNumber',
'normalizeStepExecutionRangeEntry',
'normalizeStepExecutionRangeByFlow',
'getStepExecutionRangeForState',
'isStepAllowedByExecutionRangeForState',
'isNodeExecutionAllowedForState',
'getExecutionAllowedNodeIdsForState',
'assertNodeExecutionAllowedForState',
'isStepDoneStatus',
'normalizeStatusMapForNodes',
'getFirstUnfinishedNodeId',
'hasSavedNodeProgress',
].map(extractFunction).join('\n');
return new Function(`
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const DEFAULT_STATE = { nodeStatuses: ${JSON.stringify(Object.fromEntries(NODE_IDS.map((nodeId) => [nodeId, 'pending'])))} };
function getNodeIdsForState() {
return ${JSON.stringify(NODE_IDS)};
}
function getStepIdByNodeIdForState(nodeId) {
return ${JSON.stringify(NODE_STEPS)}[String(nodeId || '').trim()] || 0;
}
function getNodeIdByStepForState(step) {
return ${JSON.stringify(Object.fromEntries(Object.entries(NODE_STEPS).map(([nodeId, step]) => [step, nodeId])))}[Number(step)] || '';
}
${bundle}
return {
normalizeStepExecutionRangeByFlow,
getStepExecutionRangeForState,
isNodeExecutionAllowedForState,
getExecutionAllowedNodeIdsForState,
assertNodeExecutionAllowedForState,
getFirstUnfinishedNodeId,
hasSavedNodeProgress,
};
`)();
}
test('step execution range normalizes codex flow config to the active OpenAI flow', () => {
const api = createApi();
assert.deepStrictEqual(
api.normalizeStepExecutionRangeByFlow({
codex: { enabled: true, fromStep: 6, toStep: 3 },
}),
{
openai: { enabled: true, fromStep: 3, toStep: 6 },
}
);
});
test('step execution range disables nodes outside the configured bounds', () => {
const api = createApi();
const state = {
activeFlowId: 'openai',
stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 3, toStep: 6 },
},
nodeStatuses: Object.fromEntries(NODE_IDS.map((nodeId) => [nodeId, 'pending'])),
};
assert.equal(api.isNodeExecutionAllowedForState('open-chatgpt', state), false);
assert.equal(api.isNodeExecutionAllowedForState('fill-password', state), true);
assert.deepStrictEqual(api.getExecutionAllowedNodeIdsForState(state), [
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
]);
assert.equal(api.getFirstUnfinishedNodeId(state.nodeStatuses, state), 'fill-password');
assert.throws(
() => api.assertNodeExecutionAllowedForState('open-chatgpt', state, '手动执行节点'),
/执行范围禁用/
);
});
test('step execution range ignores progress outside the allowed range', () => {
const api = createApi();
const state = {
activeFlowId: 'openai',
stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 3, toStep: 6 },
},
nodeStatuses: {
'open-chatgpt': 'completed',
'submit-signup-email': 'completed',
'fill-password': 'pending',
},
};
assert.equal(api.hasSavedNodeProgress(state.nodeStatuses, state), false);
state.nodeStatuses['fill-password'] = 'completed';
assert.equal(api.hasSavedNodeProgress(state.nodeStatuses, state), true);
assert.equal(api.getFirstUnfinishedNodeId(state.nodeStatuses, state), 'fetch-signup-code');
});