Refactor workflow auto-run to node graph

This commit is contained in:
QLHazyCoder
2026-05-15 17:41:35 +08:00
parent f6f804f1a2
commit 81fc40706a
76 changed files with 4028 additions and 1453 deletions
+113
View File
@@ -5,6 +5,119 @@ const fs = require('node:fs');
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
const rawCreateAutoRunController = api.createAutoRunController.bind(api);
const TEST_STEP_NODE_IDS = Object.freeze({
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
});
const TEST_NODE_STEP_IDS = Object.fromEntries(
Object.entries(TEST_STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)])
);
function getTestNodeIdByStep(step) {
return TEST_STEP_NODE_IDS[Number(step)] || '';
}
function getTestStepIdByNodeId(nodeId) {
return TEST_NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getTestNodeIdByStep(step);
if (nodeId) {
nodeStatuses[nodeId] = status;
}
}
return nodeStatuses;
}
function normalizeAutoRunControllerTestDeps(deps = {}) {
const normalized = { ...deps };
if (typeof normalized.getState === 'function') {
const getState = normalized.getState;
normalized.getState = async () => {
const state = await getState();
return {
...state,
nodeStatuses: {
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
...(state?.nodeStatuses || {}),
},
};
};
}
if (
typeof normalized.runAutoSequenceFromNode !== 'function'
&& typeof normalized.runAutoSequenceFromStep === 'function'
) {
const runAutoSequenceFromStep = normalized.runAutoSequenceFromStep;
normalized.runAutoSequenceFromNode = async (nodeId, context = {}) => (
runAutoSequenceFromStep(getTestStepIdByNodeId(nodeId) || 1, context)
);
}
if (
typeof normalized.getFirstUnfinishedNodeId !== 'function'
&& typeof normalized.getFirstUnfinishedStep === 'function'
) {
const getFirstUnfinishedStep = normalized.getFirstUnfinishedStep;
normalized.getFirstUnfinishedNodeId = (statuses = {}, state = {}) => (
getTestNodeIdByStep(getFirstUnfinishedStep(statuses, state))
);
}
if (
typeof normalized.getRunningNodeIds !== 'function'
&& typeof normalized.getRunningSteps === 'function'
) {
const getRunningSteps = normalized.getRunningSteps;
normalized.getRunningNodeIds = (statuses = {}, state = {}) => (
getRunningSteps(statuses, state)
.map(getTestNodeIdByStep)
.filter(Boolean)
);
}
if (
typeof normalized.hasSavedNodeProgress !== 'function'
&& typeof normalized.hasSavedProgress === 'function'
) {
normalized.hasSavedNodeProgress = normalized.hasSavedProgress;
}
if (
typeof normalized.waitForRunningNodesToFinish !== 'function'
&& typeof normalized.waitForRunningStepsToFinish === 'function'
) {
normalized.waitForRunningNodesToFinish = normalized.waitForRunningStepsToFinish;
}
delete normalized.runAutoSequenceFromStep;
delete normalized.getFirstUnfinishedStep;
delete normalized.getRunningSteps;
delete normalized.hasSavedProgress;
delete normalized.waitForRunningStepsToFinish;
return normalized;
}
api.createAutoRunController = (deps = {}) => (
rawCreateAutoRunController(normalizeAutoRunControllerTestDeps(deps))
);
test('auto-run controller skips add-phone failures to the next round instead of stopping when auto retry is enabled', async () => {
const events = {
+73 -5
View File
@@ -72,6 +72,33 @@ 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 STEP_NODE_IDS = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
};
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
function getNodeIdByStepForState(step) {
return STEP_NODE_IDS[Number(step)] || '';
}
function getStepIdByNodeIdForState(nodeId) {
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getNodeIdByStepForState(step);
if (nodeId) nodeStatuses[nodeId] = status;
}
return nodeStatuses;
}
const DEFAULT_STATE = {
stepStatuses: {
1: 'pending',
@@ -86,6 +113,7 @@ const DEFAULT_STATE = {
10: 'pending',
},
};
DEFAULT_STATE.nodeStatuses = projectStepStatusesToNodeStatuses(DEFAULT_STATE.stepStatuses);
let stopRequested = false;
let runCalls = 0;
@@ -137,6 +165,10 @@ async function getState() {
return {
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
nodeStatuses: {
...projectStepStatusesToNodeStatuses(currentState.stepStatuses || {}),
...(currentState.nodeStatuses || {}),
},
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
};
@@ -149,6 +181,9 @@ async function setState(updates) {
stepStatuses: updates.stepStatuses
? { ...updates.stepStatuses }
: currentState.stepStatuses,
nodeStatuses: updates.nodeStatuses
? { ...updates.nodeStatuses }
: currentState.nodeStatuses,
tabRegistry: updates.tabRegistry
? { ...updates.tabRegistry }
: currentState.tabRegistry,
@@ -163,6 +198,7 @@ async function resetState() {
currentState = {
...DEFAULT_STATE,
stepStatuses: { ...DEFAULT_STATE.stepStatuses },
nodeStatuses: { ...DEFAULT_STATE.nodeStatuses },
vpsUrl: prev.vpsUrl,
vpsPassword: prev.vpsPassword,
customPassword: prev.customPassword,
@@ -262,6 +298,18 @@ async function runAutoSequenceFromStep() {
9: 'completed',
10: 'completed',
},
nodeStatuses: projectStepStatusesToNodeStatuses({
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 },
},
@@ -271,6 +319,26 @@ async function runAutoSequenceFromStep() {
};
}
async function runAutoSequenceFromNode(nodeId, context = {}) {
return runAutoSequenceFromStep(getStepIdByNodeIdForState(nodeId) || 1, context);
}
function getFirstUnfinishedNodeId() {
return 'open-chatgpt';
}
function getRunningNodeIds() {
return [];
}
function hasSavedNodeProgress() {
return false;
}
async function waitForRunningNodesToFinish() {
return getState();
}
${helperBundle}
${autoRunModuleSource}
@@ -303,24 +371,24 @@ const controller = self.MultiPageBackgroundAutoRunController.createAutoRunContro
createAutoRunSessionId,
getAutoRunStatusPayload,
getErrorMessage,
getFirstUnfinishedStep,
getFirstUnfinishedNodeId,
getPendingAutoRunTimerPlan,
getRunningSteps,
getRunningNodeIds,
getState,
getStopRequested: () => stopRequested,
hasSavedProgress,
hasSavedNodeProgress,
isRestartCurrentAttemptError,
isStopError,
launchAutoRunTimerPlan,
normalizeAutoRunFallbackThreadIntervalMinutes,
persistAutoRunTimerPlan,
resetState,
runAutoSequenceFromStep,
runAutoSequenceFromNode,
runtime,
setState,
sleepWithStop,
throwIfAutoRunSessionStopped,
waitForRunningStepsToFinish,
waitForRunningNodesToFinish,
throwIfStopped,
chrome,
});
+84
View File
@@ -5,6 +5,90 @@ const fs = require('node:fs');
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
const rawCreateAutoRunController = api.createAutoRunController.bind(api);
const TEST_STEP_NODE_IDS = Object.freeze({
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
});
const TEST_NODE_STEP_IDS = Object.fromEntries(
Object.entries(TEST_STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)])
);
function getTestNodeIdByStep(step) {
return TEST_STEP_NODE_IDS[Number(step)] || '';
}
function getTestStepIdByNodeId(nodeId) {
return TEST_NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getTestNodeIdByStep(step);
if (nodeId) {
nodeStatuses[nodeId] = status;
}
}
return nodeStatuses;
}
api.createAutoRunController = (deps = {}) => {
const normalized = { ...deps };
if (typeof normalized.getState === 'function') {
const getState = normalized.getState;
normalized.getState = async () => {
const state = await getState();
return {
...state,
nodeStatuses: {
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
...(state?.nodeStatuses || {}),
},
};
};
}
if (typeof normalized.runAutoSequenceFromNode !== 'function' && typeof normalized.runAutoSequenceFromStep === 'function') {
const runAutoSequenceFromStep = normalized.runAutoSequenceFromStep;
normalized.runAutoSequenceFromNode = (nodeId, context = {}) => (
runAutoSequenceFromStep(getTestStepIdByNodeId(nodeId) || 1, context)
);
}
if (typeof normalized.getFirstUnfinishedNodeId !== 'function' && typeof normalized.getFirstUnfinishedStep === 'function') {
const getFirstUnfinishedStep = normalized.getFirstUnfinishedStep;
normalized.getFirstUnfinishedNodeId = (statuses = {}, state = {}) => (
getTestNodeIdByStep(getFirstUnfinishedStep(statuses, state))
);
}
if (typeof normalized.getRunningNodeIds !== 'function' && typeof normalized.getRunningSteps === 'function') {
const getRunningSteps = normalized.getRunningSteps;
normalized.getRunningNodeIds = (statuses = {}, state = {}) => (
getRunningSteps(statuses, state).map(getTestNodeIdByStep).filter(Boolean)
);
}
if (typeof normalized.hasSavedNodeProgress !== 'function' && typeof normalized.hasSavedProgress === 'function') {
normalized.hasSavedNodeProgress = normalized.hasSavedProgress;
}
if (typeof normalized.waitForRunningNodesToFinish !== 'function' && typeof normalized.waitForRunningStepsToFinish === 'function') {
normalized.waitForRunningNodesToFinish = normalized.waitForRunningStepsToFinish;
}
delete normalized.runAutoSequenceFromStep;
delete normalized.getFirstUnfinishedStep;
delete normalized.getRunningSteps;
delete normalized.hasSavedProgress;
delete normalized.waitForRunningStepsToFinish;
return rawCreateAutoRunController(normalized);
};
test('auto-run controller verifies hotmail mailbox before each fresh attempt starts', async () => {
const events = {
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -52,6 +52,97 @@ function extractFunction(name) {
return source.slice(start, end);
}
const NODE_COMPAT_HELPERS = `
const STEP_NODE_IDS = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
};
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
function getNodeIdByStepForState(step) {
return STEP_NODE_IDS[Number(step)] || '';
}
function getStepIdByNodeIdForState(nodeId) {
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function getNodeIdsForState() {
return Object.keys(STEP_NODE_IDS)
.map(Number)
.sort((left, right) => left - right)
.map((step) => STEP_NODE_IDS[step])
.filter(Boolean);
}
function getNodeDefinitionForState(nodeId) {
const normalizedNodeId = String(nodeId || '').trim();
return normalizedNodeId ? { nodeId: normalizedNodeId, executeKey: normalizedNodeId } : null;
}
function getNodeTitleForState(nodeId) {
return String(nodeId || '').trim();
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getNodeIdByStepForState(step);
if (nodeId) nodeStatuses[nodeId] = status;
}
return nodeStatuses;
}
function projectNodeStatusesToStepStatuses(nodeStatuses = {}) {
const stepStatuses = {};
for (const [nodeId, status] of Object.entries(nodeStatuses || {})) {
const step = getStepIdByNodeIdForState(nodeId);
if (step) stepStatuses[step] = status;
}
return stepStatuses;
}
const rawGetStateForNodeCompat = getState;
getState = async function getStateWithNodeStatuses() {
const state = await rawGetStateForNodeCompat();
return {
...state,
nodeStatuses: {
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
...(state?.nodeStatuses || {}),
},
};
};
const rawSetStateForNodeCompat = setState;
setState = async function setStateWithNodeStatuses(updates = {}) {
const stepStatusUpdates = updates?.nodeStatuses
? projectNodeStatusesToStepStatuses(updates.nodeStatuses)
: {};
return rawSetStateForNodeCompat({
...updates,
...(Object.keys(stepStatusUpdates).length ? {
stepStatuses: {
...stepStatusUpdates,
...(updates.stepStatuses || {}),
},
} : {}),
});
};
async function executeNodeAndWait(nodeId, delayAfter) {
const directStep = Number(nodeId);
if (Number.isInteger(directStep) && directStep > 0) {
return executeStepAndWait(directStep, delayAfter);
}
return executeStepAndWait(getStepIdByNodeIdForState(nodeId), delayAfter);
}
function getAutoRunNodeDelayMs() {
return 0;
}
async function runAutoSequenceFromStep(step, context = {}) {
return runAutoSequenceFromNode(getNodeIdByStepForState(step), context);
}
`;
const bundle = [
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
'const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;',
@@ -65,13 +156,16 @@ const bundle = [
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunStepIdleRestartError'),
extractFunction('buildAutoRunNodeIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunStepIdleLogWatchdog'),
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('startAutoRunNodeIdleLogWatchdog'),
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
NODE_COMPAT_HELPERS,
extractFunction('getAutoRunWorkflowNodeIds'),
extractFunction('runAutoSequenceFromNode'),
extractFunction('runAutoSequenceFromNodeGraph'),
].join('\n');
test('auto-run stops step4 restart path when mail2925 ends the current thread', async () => {
+113 -19
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -52,6 +52,97 @@ function extractFunction(name) {
return source.slice(start, end);
}
const NODE_COMPAT_HELPERS = `
const STEP_NODE_IDS = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
};
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
function getNodeIdByStepForState(step) {
return STEP_NODE_IDS[Number(step)] || '';
}
function getStepIdByNodeIdForState(nodeId) {
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function getNodeIdsForState() {
return Object.keys(STEP_NODE_IDS)
.map(Number)
.sort((left, right) => left - right)
.map((step) => STEP_NODE_IDS[step])
.filter(Boolean);
}
function getNodeDefinitionForState(nodeId) {
const normalizedNodeId = String(nodeId || '').trim();
return normalizedNodeId ? { nodeId: normalizedNodeId, executeKey: normalizedNodeId } : null;
}
function getNodeTitleForState(nodeId) {
return String(nodeId || '').trim();
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getNodeIdByStepForState(step);
if (nodeId) nodeStatuses[nodeId] = status;
}
return nodeStatuses;
}
function projectNodeStatusesToStepStatuses(nodeStatuses = {}) {
const stepStatuses = {};
for (const [nodeId, status] of Object.entries(nodeStatuses || {})) {
const step = getStepIdByNodeIdForState(nodeId);
if (step) stepStatuses[step] = status;
}
return stepStatuses;
}
const rawGetStateForNodeCompat = getState;
getState = async function getStateWithNodeStatuses() {
const state = await rawGetStateForNodeCompat();
return {
...state,
nodeStatuses: {
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
...(state?.nodeStatuses || {}),
},
};
};
const rawSetStateForNodeCompat = setState;
setState = async function setStateWithNodeStatuses(updates = {}) {
const stepStatusUpdates = updates?.nodeStatuses
? projectNodeStatusesToStepStatuses(updates.nodeStatuses)
: {};
return rawSetStateForNodeCompat({
...updates,
...(Object.keys(stepStatusUpdates).length ? {
stepStatuses: {
...stepStatusUpdates,
...(updates.stepStatuses || {}),
},
} : {}),
});
};
async function executeNodeAndWait(nodeId, delayAfter) {
const directStep = Number(nodeId);
if (Number.isInteger(directStep) && directStep > 0) {
return executeStepAndWait(directStep, delayAfter);
}
return executeStepAndWait(getStepIdByNodeIdForState(nodeId), delayAfter);
}
function getAutoRunNodeDelayMs() {
return 0;
}
async function runAutoSequenceFromStep(step, context = {}) {
return runAutoSequenceFromNode(getNodeIdByStepForState(step), context);
}
`;
const bundle = [
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
'const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;',
@@ -62,19 +153,22 @@ const bundle = [
extractFunction('isMail2925ThreadTerminatedError'),
extractFunction('isSignupPhonePasswordMismatchFailure'),
extractFunction('getSignupPhonePasswordMismatchRestartPayload'),
extractFunction('restartSignupPhonePasswordMismatchAttemptFromStep'),
extractFunction('restartSignupPhonePasswordMismatchAttemptFromNode'),
extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunStepIdleRestartError'),
extractFunction('buildAutoRunNodeIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunStepIdleLogWatchdog'),
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('startAutoRunNodeIdleLogWatchdog'),
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
NODE_COMPAT_HELPERS,
extractFunction('getAutoRunWorkflowNodeIds'),
extractFunction('runAutoSequenceFromNode'),
extractFunction('runAutoSequenceFromNodeGraph'),
].join('\n');
test('auto-run restarts from step 1 with the same email after step 4 failure', async () => {
@@ -217,7 +311,7 @@ return {
{
step: 1,
options: {
logLabel: '步骤 4 报错后准备回到步骤 1 沿用当前邮箱重试(第 1 次重开)',
logLabel: '节点 fetch-signup-code 报错后准备回到 open-chatgpt 沿用当前邮箱重试(第 1 次重开)',
},
},
]);
@@ -225,7 +319,7 @@ return {
assert.deepStrictEqual(events.steps, [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(currentState.email, 'keep@example.com');
assert.equal(currentState.password, 'Secret123!');
assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到节点 open-chatgpt 重新开始/.test(message)), true);
});
test('auto-run does not restart step 4 current attempt when user_already_exists is detected', async () => {
@@ -348,7 +442,7 @@ return {
assert.match(result.error, /SIGNUP_USER_ALREADY_EXISTS::/);
assert.deepStrictEqual(result.events.invalidations, []);
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), false);
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到节点 open-chatgpt 重新开始/.test(message)), false);
});
test('auto-run skips steps 4/5 when step 2 has already marked registration chain as skipped', async () => {
@@ -470,8 +564,8 @@ return {
const { events } = await api.run();
assert.deepStrictEqual(events.steps, [1, 2, 6, 7, 8, 9, 10]);
assert.equal(events.logs.some(({ message }) => /步骤 4 当前状态为 skipped/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /步骤 5 当前状态为 skipped/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /节点 fetch-signup-code 当前状态为 skipped/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /节点 fill-profile 当前状态为 skipped/.test(message)), true);
});
test('auto-run clears fetched signup phone state before restarting when step 4 detects phone/password mismatch', async () => {
@@ -622,7 +716,7 @@ return {
{
step: 1,
options: {
logLabel: '步骤 4 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
logLabel: '节点 fetch-signup-code 检测到手机号/密码不匹配后准备回到 open-chatgpt 重新获取手机号重试(第 1 次重开)',
},
},
]);
@@ -632,7 +726,7 @@ return {
assert.equal(currentState.accountIdentifierType, null);
assert.equal(currentState.accountIdentifier, '');
assert.equal(currentState.password, 'Secret123!');
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到步骤 1 重新开始/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到节点 open-chatgpt 重新开始/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /已清空本轮注册手机号与接码订单/.test(message)), true);
});
@@ -786,7 +880,7 @@ return {
{
step: 1,
options: {
logLabel: '步骤 4 检测到当前注册手机号无法接收短信后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
logLabel: '节点 fetch-signup-code 检测到当前注册手机号无法接收短信后准备回到 open-chatgpt 重新获取手机号重试(第 1 次重开)',
},
},
]);
@@ -796,7 +890,7 @@ return {
assert.equal(currentState.accountIdentifierType, null);
assert.equal(currentState.accountIdentifier, '');
assert.equal(currentState.password, 'Secret123!');
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到步骤 1 重新开始/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到节点 open-chatgpt 重新开始/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /已清空本轮注册手机号与接码订单/.test(message)), true);
});
@@ -943,7 +1037,7 @@ return {
{
step: 1,
options: {
logLabel: '步骤 3 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
logLabel: '节点 fill-password 检测到手机号/密码不匹配后准备回到 open-chatgpt 重新获取手机号重试(第 1 次重开)',
},
},
]);
@@ -953,6 +1047,6 @@ return {
assert.equal(currentState.accountIdentifierType, null);
assert.equal(currentState.accountIdentifier, '');
assert.equal(currentState.password, 'Secret123!');
assert.equal(events.logs.some(({ message }) => /步骤 3检测到手机号\/密码不匹配/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /步骤 3已清空本轮注册手机号与接码订单/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /节点 fill-password检测到手机号\/密码不匹配/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /节点 fill-password已清空本轮注册手机号与接码订单/.test(message)), true);
});
+118 -22
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -52,6 +52,99 @@ function extractFunction(name) {
return source.slice(start, end);
}
const NODE_COMPAT_HELPERS = `
const FALLBACK_STEP_NODE_IDS = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'plus-checkout-create',
7: 'plus-checkout-billing',
8: 'paypal-approve',
9: 'plus-checkout-return',
10: 'oauth-login',
11: 'fetch-login-code',
12: 'confirm-oauth',
13: 'platform-verify',
};
function getNodeIdByStepForState(step, state = {}) {
if (typeof getStepDefinitionForState === 'function') {
const key = String(getStepDefinitionForState(step, state)?.key || '').trim();
if (key) return key;
}
return FALLBACK_STEP_NODE_IDS[Number(step)] || '';
}
function getStepIdByNodeIdForState(nodeId, state = {}) {
const normalizedNodeId = String(nodeId || '').trim();
const ids = typeof getStepIdsForState === 'function'
? getStepIdsForState(state)
: Object.keys(FALLBACK_STEP_NODE_IDS).map(Number);
for (const step of ids) {
if (getNodeIdByStepForState(step, state) === normalizedNodeId) {
return Number(step);
}
}
for (const [step, fallbackNodeId] of Object.entries(FALLBACK_STEP_NODE_IDS)) {
if (fallbackNodeId === normalizedNodeId) {
return Number(step);
}
}
return null;
}
function getNodeIdsForState(state = {}) {
const ids = typeof getStepIdsForState === 'function'
? getStepIdsForState(state)
: Object.keys(FALLBACK_STEP_NODE_IDS).map(Number);
return ids
.map((step) => getNodeIdByStepForState(step, state))
.filter(Boolean);
}
function getNodeDefinitionForState(nodeId, state = {}) {
const normalizedNodeId = String(nodeId || '').trim();
const step = getStepIdByNodeIdForState(normalizedNodeId, state);
const executeKey = typeof getStepExecutionKeyForState === 'function'
? getStepExecutionKeyForState(step, state)
: normalizedNodeId;
return normalizedNodeId ? { nodeId: normalizedNodeId, legacyStepId: step, executeKey } : null;
}
function getNodeTitleForState(nodeId) {
return String(nodeId || '').trim();
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}, state = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getNodeIdByStepForState(step, state);
if (nodeId) nodeStatuses[nodeId] = status;
}
return nodeStatuses;
}
const rawGetStateForNodeCompat = getState;
getState = async function getStateWithNodeStatuses() {
const state = await rawGetStateForNodeCompat();
return {
...state,
nodeStatuses: {
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}, state),
...(state?.nodeStatuses || {}),
},
};
};
async function executeNodeAndWait(nodeId, delayAfter) {
const directStep = Number(nodeId);
if (Number.isInteger(directStep) && directStep > 0) {
return executeStepAndWait(directStep, delayAfter);
}
return executeStepAndWait(getStepIdByNodeIdForState(nodeId, await getState()), delayAfter);
}
function getAutoRunNodeDelayMs() {
return 0;
}
async function runAutoSequenceFromStep(step, context = {}) {
return runAutoSequenceFromNode(getNodeIdByStepForState(step, await getState()), context);
}
`;
const bundle = [
extractFunction('isAddPhoneAuthFailure'),
extractFunction('isAddPhoneAuthUrl'),
@@ -61,13 +154,16 @@ const bundle = [
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunStepIdleRestartError'),
extractFunction('buildAutoRunNodeIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunStepIdleLogWatchdog'),
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('startAutoRunNodeIdleLogWatchdog'),
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
NODE_COMPAT_HELPERS,
extractFunction('getAutoRunWorkflowNodeIds'),
extractFunction('runAutoSequenceFromNode'),
extractFunction('runAutoSequenceFromNodeGraph'),
].join('\n');
const defaultStepDefinitions = {
@@ -311,7 +407,7 @@ test('auto-run keeps restarting from step 7 after post-login failures without a
7, 8, 9, 10,
]
);
assert.ok(events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 auth-login 重新开始授权流程/.test(message)));
});
test('auto-run restarts the current step after five minutes without new logs', async () => {
@@ -330,7 +426,7 @@ test('auto-run restarts the current step after five minutes without new logs', a
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
assert.equal(events.cancellations.length, 1);
assert.equal(events.stopBroadcasts, 1);
assert.ok(events.logs.some(({ message }) => /5 分钟没有新日志准备重新开始当前步骤/.test(message)));
assert.ok(events.logs.some(({ message }) => /5 分钟没有新日志准备重新开始当前节点/.test(message)));
});
test('auto-run applies the idle-log restart watchdog to early steps too', async () => {
@@ -364,7 +460,7 @@ test('auto-run stops current-step idle restarts after the retry cap', async () =
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.match(result.error.message, /AUTO_RUN_STEP_IDLE_RESTART::步骤 10/);
assert.match(result.error.message, /AUTO_RUN_STEP_IDLE_RESTART::节点 platform-verify/);
assert.deepStrictEqual(result.events.steps, [10, 10, 10, 10]);
assert.deepStrictEqual(result.events.invalidations.map((entry) => entry.step), [9, 9, 9]);
assert.ok(result.events.logs.some(({ message }) => /已连续 3 次因 5 分钟无新日志而重开/.test(message)));
@@ -474,10 +570,10 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
assert.deepStrictEqual(events.invalidations[0], {
step: 8,
options: {
logLabel: '步骤 10 报错后准备回到步骤 9 重试(第 1 次重开)',
logLabel: '节点 platform-verify 报错后准备回到 confirm-oauth 重试(第 1 次重开)',
},
});
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message)));
});
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
@@ -508,9 +604,9 @@ test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from s
const events = await harness.run();
assert.deepStrictEqual(events.steps, [10, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
assert.ok(events.logs.some(({ message }) => /回到步骤 10 重新开始授权流程/.test(message)));
assert.ok(!events.logs.some(({ message }) => /停止自动回到步骤 10 重开/.test(message)));
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [7]);
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
assert.ok(!events.logs.some(({ message }) => /停止自动回到节点 oauth-login 重开/.test(message)));
});
test('auto-run restarts Plus/GPC fetch-code and confirm-oauth failures from oauth-login step 10', async () => {
@@ -554,8 +650,8 @@ test('auto-run restarts Plus/GPC fetch-code and confirm-oauth failures from oaut
const events = await harness.run();
assert.deepStrictEqual(events.steps, scenario.expectedSteps);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
assert.ok(events.logs.some(({ message }) => new RegExp(`步骤 ${scenario.failureStep}.*回到步骤 10 重新开始授权流程`).test(message)));
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [7]);
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
}
});
@@ -589,7 +685,7 @@ test('auto-run restarts Plus/GPC transient platform verify exchange failures fro
assert.deepStrictEqual(events.steps, [10, 11, 12, 13, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [11]);
assert.ok(events.logs.some(({ message }) => /步骤 13.*回到步骤 12 重新开始授权流程/.test(message)));
assert.ok(events.logs.some(({ message }) => /节点 platform-verify.*回到节点 confirm-oauth 重新开始授权流程/.test(message)));
});
test('auto-run restarts Plus checkout from step 6 when checkout creation fails', async () => {
@@ -621,7 +717,7 @@ test('auto-run restarts Plus checkout from step 6 when checkout creation fails',
assert.deepStrictEqual(events.steps, [6, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts Plus checkout from step 6 when billing fails for non-free-trial reasons', async () => {
@@ -653,7 +749,7 @@ test('auto-run restarts Plus checkout from step 6 when billing fails for non-fre
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 8, 9, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
@@ -689,7 +785,7 @@ test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls
events.invalidations.map((entry) => entry.step),
[5, 5]
);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
});
test('auto-run treats GPC account binding as recoverable step 6 restart', async () => {
@@ -748,7 +844,7 @@ test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read
assert.deepStrictEqual(events.steps, [6, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
@@ -778,7 +874,7 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
});
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
+4
View File
@@ -47,9 +47,13 @@ const bundle = [
'const PERSISTED_SETTING_DEFAULTS = { autoStepDelaySeconds: null };',
"const AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY = new Map([['plus-checkout-create', 20000]]);",
'function getStepDefinitionForState(step, state = {}) { return state.definitions?.[step] || null; }',
'function getNodeIdByStepForState(step, state = {}) { return String(getStepDefinitionForState(step, state)?.key || step || "").trim(); }',
'function getNodeDefinitionForState(nodeId, state = {}) { return Object.values(state.definitions || {}).find((definition) => String(definition?.key || "").trim() === String(nodeId || "").trim()) || { executeKey: String(nodeId || "").trim() }; }',
extractFunction('normalizeAutoStepDelaySeconds'),
extractFunction('resolveLegacyAutoStepDelaySeconds'),
extractFunction('getStepExecutionKeyForState'),
extractFunction('getNodeExecutionKeyForState'),
extractFunction('getAutoRunPreExecutionDelayMsForNode'),
extractFunction('getAutoRunPreExecutionDelayMs'),
].join('\n');
@@ -46,7 +46,13 @@ test('account run history helper upgrades old records, keeps stopped items and s
},
},
getErrorMessage: (error) => error?.message || String(error || ''),
getNodeTitleForState: (nodeId) => ({
'fetch-login-code': '获取登录验证码',
'oauth-login': '刷新 OAuth 并登录',
})[nodeId] || nodeId,
getState: async () => ({
flowId: 'openai',
runId: 'run-2',
email: ' latest@example.com ',
password: ' secret ',
autoRunning: true,
@@ -61,6 +67,8 @@ test('account run history helper upgrades old records, keeps stopped items and s
const record = helpers.buildAccountRunHistoryRecord(
{
flowId: 'openai',
runId: 'run-2',
email: ' latest@example.com ',
password: ' secret ',
autoRunning: true,
@@ -68,11 +76,13 @@ test('account run history helper upgrades old records, keeps stopped items and s
autoRunTotalRuns: 10,
autoRunAttemptRun: 3,
},
'step8_failed',
'node:fetch-login-code:failed',
'步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'
);
assert.deepStrictEqual(record, {
recordId: 'latest@example.com',
flowId: 'openai',
runId: 'run-2',
accountIdentifierType: 'email',
accountIdentifier: 'latest@example.com',
email: 'latest@example.com',
@@ -83,7 +93,8 @@ test('account run history helper upgrades old records, keeps stopped items and s
retryCount: 2,
failureLabel: '出现手机号验证',
failureDetail: '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。',
failedStep: 8,
failedNodeId: 'fetch-login-code',
failedStep: null,
source: 'auto',
autoRunContext: {
currentRun: 2,
@@ -94,9 +105,12 @@ test('account run history helper upgrades old records, keeps stopped items and s
contributionMode: false,
});
const appended = await helpers.appendAccountRunRecord('step8_failed', null, '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
const appended = await helpers.appendAccountRunRecord('node:fetch-login-code:failed', null, '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
assert.equal(appended.email, 'latest@example.com');
assert.equal(appended.finalStatus, 'failed');
assert.equal(appended.flowId, 'openai');
assert.equal(appended.runId, 'run-2');
assert.equal(appended.failedNodeId, 'fetch-login-code');
assert.equal(appended.failureLabel, '出现手机号验证');
assert.equal(storedHistory.length, 3, '旧的 stopped 记录应在新结构中保留');
assert.equal(storedHistory.some((item) => item.email === 'stop@example.com' && item.finalStatus === 'stopped'), true);
@@ -106,18 +120,21 @@ test('account run history helper upgrades old records, keeps stopped items and s
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
const stoppedRecord = helpers.buildAccountRunHistoryRecord(
{ email: 'a@b.com', password: 'x' },
'step7_stopped',
'步骤 7 已被用户停止'
{ flowId: 'openai', runId: 'manual-run', email: 'a@b.com', password: 'x' },
'node:oauth-login:stopped',
'节点 oauth-login 已被用户停止'
);
assert.equal(stoppedRecord.recordId, 'a@b.com');
assert.equal(stoppedRecord.flowId, 'openai');
assert.equal(stoppedRecord.runId, 'manual-run');
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, '步骤 7 停止');
assert.equal(stoppedRecord.failureDetail, '步骤 7 已被用户停止');
assert.equal(stoppedRecord.failedStep, 7);
assert.equal(stoppedRecord.failureLabel, '节点 刷新 OAuth 并登录 停止');
assert.equal(stoppedRecord.failureDetail, '节点 oauth-login 已被用户停止');
assert.equal(stoppedRecord.failedNodeId, 'oauth-login');
assert.equal(stoppedRecord.failedStep, null);
assert.equal(stoppedRecord.source, 'manual');
assert.equal(stoppedRecord.autoRunContext, null);
assert.ok(stoppedRecord.finishedAt);
@@ -149,11 +166,13 @@ test('account run history helper upgrades old records, keeps stopped items and s
retryCount: 0,
failureLabel: '流程已停止',
failureDetail: '步骤 7 已被用户停止。',
failedNodeId: 'oauth-login',
failedStep: 7,
source: 'manual',
autoRunContext: null,
});
assert.equal(normalizedStoppedRecord.failureLabel, '步骤 7 停止');
assert.equal(normalizedStoppedRecord.failureLabel, '节点 刷新 OAuth 并登录 停止');
assert.equal(normalizedStoppedRecord.failedNodeId, 'oauth-login');
assert.equal(normalizedStoppedRecord.failedStep, 7);
});
@@ -177,6 +196,8 @@ test('account run history helper accepts phone-only records without forcing emai
assert.deepStrictEqual(record, {
recordId: 'phone:+6612345',
flowId: '',
runId: '',
accountIdentifierType: 'phone',
accountIdentifier: '+6612345',
email: '',
@@ -187,6 +208,7 @@ test('account run history helper accepts phone-only records without forcing emai
retryCount: 0,
failureLabel: '流程完成',
failureDetail: '',
failedNodeId: '',
failedStep: null,
source: 'manual',
autoRunContext: null,
@@ -242,10 +264,11 @@ test('account run history does not turn prerequisite guidance into a fake step 2
autoRunCurrentRun: 1,
autoRunTotalRuns: 3,
autoRunAttemptRun: 2,
}, 'step10_failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
}, 'node:platform-verify:failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(explicitFailedRecord.failedStep, 10);
assert.equal(explicitFailedRecord.failureLabel, '步骤 10 失败');
assert.equal(explicitFailedRecord.failedNodeId, 'platform-verify');
assert.equal(explicitFailedRecord.failedStep, null);
assert.equal(explicitFailedRecord.failureLabel, '节点 platform-verify 失败');
const migratedOldRecord = helpers.normalizeAccountRunHistoryRecord({
email: 'old@example.com',
@@ -314,10 +337,12 @@ test('account run history merges email and phone identities from the same run',
activationId: 'a1',
phoneNumber: '+44 7799 342687',
},
}, 'step9_failed', '步骤 9:手机号验证失败。');
}, 'node:confirm-oauth:failed', '步骤 9:手机号验证失败。');
assert.equal(failedRecord.accountIdentifierType, 'email');
assert.equal(failedRecord.accountIdentifier, 'tmp@example.com');
assert.equal(failedRecord.phoneNumber, '+44 7799 342687');
assert.equal(failedRecord.failedNodeId, 'confirm-oauth');
assert.equal(failedRecord.failedStep, null);
const successRecord = await helpers.appendAccountRunRecord('success', {
accountIdentifierType: 'email',
+84 -16
View File
@@ -48,6 +48,71 @@ function extractFunction(name) {
return source.slice(start, end);
}
const NODE_EXECUTE_COMPAT_HELPERS = `
const AUTH_CHAIN_NODE_IDS = new Set(['oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify']);
const STEP_NODE_IDS = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
};
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
function isAuthChainNode(nodeId) {
return AUTH_CHAIN_NODE_IDS.has(String(nodeId || '').trim());
}
function getNodeIdByStepForState(step) {
const definition = typeof getStepDefinitionForState === 'function' ? getStepDefinitionForState(step) : null;
const definitionKey = String(definition?.key || '').trim();
return definitionKey && definitionKey !== 'test-step'
? definitionKey
: String(STEP_NODE_IDS[Number(step)] || '').trim();
}
function getStepIdByNodeIdForState(nodeId) {
const normalizedNodeId = String(nodeId || '').trim();
return NODE_STEP_IDS[normalizedNodeId] || null;
}
function getNodeDefinitionForState(nodeId, state = {}) {
const step = getStepIdByNodeIdForState(nodeId, state);
const stepDefinition = typeof getStepDefinitionForState === 'function'
? getStepDefinitionForState(step, state)
: null;
return stepDefinition
? { nodeId: String(nodeId || '').trim(), legacyStepId: step, executeKey: String(stepDefinition.key || nodeId || '').trim() }
: null;
}
async function setNodeStatus(nodeId, status) {
return setStepStatus(getStepIdByNodeIdForState(nodeId), status);
}
function doesNodeUseCompletionSignal(nodeId, state = {}) {
return typeof doesStepUseCompletionSignal === 'function'
? doesStepUseCompletionSignal(getStepIdByNodeIdForState(nodeId), state)
: false;
}
const rawGetStepRegistryForState = getStepRegistryForState;
getStepRegistryForState = function getNodeAdaptedStepRegistryForState(state = {}) {
const registry = rawGetStepRegistryForState(state);
if (!registry || typeof registry !== 'object' || typeof registry.getNodeDefinition === 'function') {
return registry;
}
return {
...registry,
getNodeDefinition(nodeId) {
return getNodeDefinitionForState(nodeId, state);
},
async executeNode(nodeId, payload = {}) {
const step = getStepIdByNodeIdForState(nodeId);
return registry.executeStep(step, payload);
},
};
};
`;
test('throwIfStopped rethrows an explicit stop error even when stopRequested has been cleared', () => {
const api = new Function(`
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
@@ -67,7 +132,7 @@ return {
);
});
test('executeStep reuses the active top-level auth chain instead of starting a duplicate step', async () => {
test('executeNode reuses the active top-level auth chain instead of starting a duplicate node', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
@@ -137,14 +202,15 @@ function getStepDefinitionForState(step) {
return { id: step, key: 'test-step' };
}
${NODE_EXECUTE_COMPAT_HELPERS}
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('executeStep')}
${extractFunction('acquireTopLevelAuthChainExecutionForNode')}
${extractFunction('executeNode')}
return {
executeStep,
executeNode,
releaseStep8() {
if (releaseStep8) {
releaseStep8();
@@ -156,9 +222,9 @@ return {
};
`)();
const firstRun = api.executeStep(8);
const firstRun = api.executeNode('fetch-login-code');
await new Promise((resolve) => setImmediate(resolve));
const duplicateRun = api.executeStep(7);
const duplicateRun = api.executeNode('oauth-login');
await new Promise((resolve) => setImmediate(resolve));
api.releaseStep8();
@@ -173,7 +239,7 @@ return {
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
test('executeStep stops flow when browser-switch-required error is raised', async () => {
test('executeNode stops flow when browser-switch-required error is raised', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
@@ -235,17 +301,18 @@ function getStepDefinitionForState(step) {
return { id: step, key: 'test-step' };
}
${NODE_EXECUTE_COMPAT_HELPERS}
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('acquireTopLevelAuthChainExecutionForNode')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
${extractFunction('executeNode')}
return {
executeStep,
executeNode,
snapshot() {
return events;
},
@@ -253,7 +320,7 @@ return {
`)();
await assert.rejects(
() => api.executeStep(10),
() => api.executeNode('platform-verify'),
/流程已被用户停止。/
);
@@ -393,7 +460,7 @@ return {
assert.match(events.logs[0].message, /授权后链总超时已关闭/);
});
test('executeStep retries fetch-network errors for step 4 with cooldown and bounded attempts', async () => {
test('executeNode retries fetch-network errors for fetch-signup-code with cooldown and bounded attempts', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
@@ -456,27 +523,28 @@ function getStepDefinitionForState(step) {
return { id: step, key: 'fetch-signup-code' };
}
${NODE_EXECUTE_COMPAT_HELPERS}
${extractFunction('isStopError')}
${extractFunction('isRetryableContentScriptTransportError')}
${extractFunction('isStepFetchNetworkRetryableError')}
${extractFunction('getStepFetchNetworkRetryPolicy')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('acquireTopLevelAuthChainExecutionForNode')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
${extractFunction('executeNode')}
return {
executeStep,
executeNode,
snapshot() {
return events;
},
};
`)();
await api.executeStep(4);
await api.executeNode('fetch-signup-code');
const events = api.snapshot();
assert.deepStrictEqual(events.registryCalls, [4, 4, 4]);
+9 -9
View File
@@ -16,30 +16,30 @@ test('auto-run controller module exposes a factory', () => {
assert.equal(typeof api?.createAutoRunController, 'function');
});
test('auto-run account record status preserves the real failed step instead of parsing guidance text', () => {
test('auto-run account record status preserves the real failed node instead of parsing guidance text', () => {
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
const controller = api.createAutoRunController({});
const state = {
currentStep: 11,
stepStatuses: {
2: 'completed',
10: 'completed',
11: 'failed',
currentNodeId: 'fetch-login-code',
nodeStatuses: {
'submit-signup-email': 'completed',
'oauth-login': 'completed',
'fetch-login-code': 'failed',
},
};
const error = new Error('缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
'step11_failed'
'node:fetch-login-code:failed'
);
error.failedStep = 13;
error.failedNodeId = 'platform-verify';
assert.equal(
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
'step13_failed'
'node:platform-verify:failed'
);
});
@@ -40,6 +40,7 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
{
flowId: 'openai',
ruleId: 'openai-signup-code',
nodeId: 'fetch-signup-code',
step: 4,
artifactType: 'code',
codePatterns: [
@@ -78,6 +79,7 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
{
flowId: 'openai',
ruleId: 'openai-login-code',
nodeId: 'fetch-login-code',
step: 8,
artifactType: 'code',
codePatterns: [
@@ -105,6 +107,14 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
intervalMs: 3000,
}
);
assert.equal(
registry.buildVerificationPollPayloadForNode('fetch-signup-code', {
activeFlowId: 'openai',
email: 'node@example.com',
}).nodeId,
'fetch-signup-code'
);
});
test('mail rule registry rejects unknown active flow ids instead of silently using OpenAI rules', () => {
@@ -30,10 +30,10 @@ test('message router appends success record on Plus final step instead of hard-c
deleteIcloudAlias: async () => {},
deleteUsedIcloudAliases: async () => {},
disableUsedLuckmailPurchases: async () => {},
doesStepUseCompletionSignal: () => false,
doesNodeUseCompletionSignal: () => false,
ensureManualInteractionAllowed: async () => ({}),
executeStep: async () => {},
executeStepViaCompletionSignal: async () => {},
executeNode: async () => {},
executeNodeViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
finalizeStep3Completion: async () => {},
@@ -43,7 +43,9 @@ test('message router appends success record on Plus final step instead of hard-c
getCurrentLuckmailPurchase: () => null,
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
getState: async () => ({ plusModeEnabled: true, stepStatuses: { 13: 'pending' } }),
getState: async () => ({ plusModeEnabled: true, nodeStatuses: { 'platform-verify': 'pending' } }),
getNodeIdsForState: () => ['open-chatgpt', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
getStepIdByNodeIdForState: (nodeId) => ({ 'oauth-login': 10, 'fetch-login-code': 11, 'confirm-oauth': 12, 'platform-verify': 13 }[nodeId] || 0),
getLastStepIdForState: () => 13,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
@@ -66,8 +68,8 @@ test('message router appends success record on Plus final step instead of hard-c
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyStepComplete: () => {},
notifyStepError: () => {},
notifyNodeComplete: () => {},
notifyNodeError: () => {},
patchHotmailAccount: async () => {},
patchMail2925Account: async () => {},
registerTab: async () => {},
@@ -88,9 +90,9 @@ test('message router appends success record on Plus final step instead of hard-c
setLuckmailPurchaseUsedState: async () => {},
setPersistentSettings: async () => {},
setState: async () => {},
setStepStatus: async () => {},
setNodeStatus: async () => {},
skipAutoRunCountdown: async () => false,
skipStep: async () => {},
skipNode: async () => {},
startAutoRunLoop: async () => {},
syncHotmailAccounts: async () => {},
testHotmailAccountMailAccess: async () => {},
@@ -98,7 +100,7 @@ test('message router appends success record on Plus final step instead of hard-c
verifyHotmailAccount: async () => {},
});
await router.handleMessage({ type: 'STEP_COMPLETE', step: 13, payload: {} }, {});
await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'platform-verify', payload: { nodeId: 'platform-verify' } }, {});
assert.equal(appendCalls.length, 1);
assert.equal(appendCalls[0][0], 'success');
@@ -10,6 +10,7 @@ function createRouter(overrides = {}) {
const events = {
logs: [],
stepStatuses: [],
nodeStatuses: [],
stateUpdates: [],
broadcasts: [],
balanceRefreshes: [],
@@ -26,10 +27,63 @@ function createRouter(overrides = {}) {
executedSteps: [],
accountRecords: [],
};
const nodeByStep = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
11: 'fetch-login-code',
12: 'confirm-oauth',
13: 'platform-verify',
};
const normalStepByNode = {
'open-chatgpt': 1,
'submit-signup-email': 2,
'fill-password': 3,
'fetch-signup-code': 4,
'fill-profile': 5,
'wait-registration-success': 6,
'oauth-login': 7,
'fetch-login-code': 8,
'confirm-oauth': 9,
'platform-verify': 10,
};
const plusStepByNode = {
'open-chatgpt': 1,
'submit-signup-email': 2,
'fill-password': 3,
'fetch-signup-code': 4,
'fill-profile': 5,
'oauth-login': 10,
'fetch-login-code': 11,
'confirm-oauth': 12,
'platform-verify': 13,
};
const getStepForNode = (nodeId) => {
const state = normalizeState(overrides.state || {});
return (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0;
};
const normalizeState = (state = {}) => {
const next = { ...(state || {}) };
if (!next.nodeStatuses && next.stepStatuses) {
next.nodeStatuses = Object.fromEntries(
Object.entries(next.stepStatuses)
.map(([step, status]) => [nodeByStep[Number(step)], status])
.filter(([nodeId]) => Boolean(nodeId))
);
}
return next;
};
const router = api.createMessageRouter({
addLog: async (message, level, options = {}) => {
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey });
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey, nodeId: options.nodeId });
},
appendAccountRunRecord: overrides.appendAccountRunRecord || (async (status, state, reason) => {
events.accountRecords.push({ status, state, reason });
@@ -49,20 +103,20 @@ function createRouter(overrides = {}) {
clearStopRequest: () => {},
closeLocalhostCallbackTabs: async () => {},
closeTabsByUrlPrefix: async () => {},
completeStepFromBackground: async (step, payload) => {
events.notifyCompletions.push({ step, payload, via: 'completeStepFromBackground' });
completeNodeFromBackground: async (nodeId, payload) => {
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload, via: 'completeNodeFromBackground' });
},
deleteHotmailAccount: async () => {},
deleteHotmailAccounts: async () => {},
deleteIcloudAlias: async () => {},
deleteUsedIcloudAliases: async () => {},
disableUsedLuckmailPurchases: async () => {},
doesStepUseCompletionSignal: () => false,
doesNodeUseCompletionSignal: () => false,
ensureManualInteractionAllowed: async () => ({}),
executeStep: async (step) => {
events.executedSteps.push(step);
executeNode: async (nodeId) => {
events.executedSteps.push(getStepForNode(nodeId) || nodeId);
},
executeStepViaCompletionSignal: async () => {},
executeNodeViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
@@ -77,7 +131,9 @@ function createRouter(overrides = {}) {
getCurrentLuckmailPurchase: () => null,
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
getState: async () => normalizeState(overrides.state || { nodeStatuses: { 'fill-password': 'pending' } }),
getNodeIdsForState: () => ['open-chatgpt', 'submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
getStepIdByNodeIdForState: (nodeId, state = {}) => (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0,
getStepDefinitionForState: overrides.getStepDefinitionForState,
getStepIdsForState: overrides.getStepIdsForState,
getLastStepIdForState: overrides.getLastStepIdForState,
@@ -106,11 +162,11 @@ function createRouter(overrides = {}) {
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyStepComplete: (step, payload) => {
events.notifyCompletions.push({ step, payload });
notifyNodeComplete: (nodeId, payload) => {
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload });
},
notifyStepError: (step, error) => {
events.notifyErrors.push({ step, error });
notifyNodeError: (nodeId, error) => {
events.notifyErrors.push({ step: getStepForNode(nodeId), nodeId, error });
},
patchHotmailAccount: async () => {},
registerTab: async () => {},
@@ -142,11 +198,13 @@ function createRouter(overrides = {}) {
setState: async (updates) => {
events.stateUpdates.push(updates);
},
setStepStatus: async (step, status) => {
setNodeStatus: async (nodeId, status) => {
events.nodeStatuses.push({ nodeId, status });
const step = getStepForNode(nodeId);
events.stepStatuses.push({ step, status });
},
skipAutoRunCountdown: async () => false,
skipStep: async () => {},
skipNode: async () => {},
startAutoRunLoop: async () => {},
syncHotmailAccounts: async () => {},
testHotmailAccountMailAccess: async () => {},
@@ -366,10 +424,11 @@ test('message router finalizes step 3 before marking it completed', async () =>
const { router, events } = createRouter();
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 3,
type: 'NODE_COMPLETE',
nodeId: 'fill-password',
source: 'signup-page',
payload: {
nodeId: 'fill-password',
email: 'user@example.com',
signupVerificationRequestedAt: 123,
},
@@ -377,8 +436,10 @@ test('message router finalizes step 3 before marking it completed', async () =>
assert.deepStrictEqual(events.finalizePayloads, [
{
nodeId: 'fill-password',
email: 'user@example.com',
signupVerificationRequestedAt: 123,
step: 3,
},
]);
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'completed' }]);
@@ -386,9 +447,12 @@ test('message router finalizes step 3 before marking it completed', async () =>
assert.deepStrictEqual(events.notifyCompletions, [
{
step: 3,
nodeId: 'fill-password',
payload: {
nodeId: 'fill-password',
email: 'user@example.com',
signupVerificationRequestedAt: 123,
step: 3,
},
},
]);
@@ -436,7 +500,8 @@ test('message router finalizes pending phone activation on platform verify succe
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
});
assert.deepStrictEqual(events.phoneFinalizations, [state]);
assert.equal(events.phoneFinalizations.length, 1);
assert.deepStrictEqual(events.phoneFinalizations[0].pendingPhoneActivationConfirmation, state.pendingPhoneActivationConfirmation);
});
test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
@@ -481,10 +546,11 @@ test('message router marks step 3 failed when post-submit finalize fails', async
});
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 3,
type: 'NODE_COMPLETE',
nodeId: 'fill-password',
source: 'signup-page',
payload: {
nodeId: 'fill-password',
email: 'user@example.com',
},
}, {});
@@ -493,10 +559,11 @@ test('message router marks step 3 failed when post-submit finalize fails', async
assert.deepStrictEqual(events.notifyErrors, [
{
step: 3,
nodeId: 'fill-password',
error: '步骤 3 提交后仍停留在密码页。',
},
]);
assert.equal(events.logs.some(({ message, step }) => /失败步骤 3 提交后仍停留在密码页/.test(message) && step === 3), true);
assert.equal(events.logs.some(({ message, nodeId }) => /失败步骤 3 提交后仍停留在密码页/.test(message) && nodeId === 'fill-password'), true);
assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' });
});
@@ -512,10 +579,10 @@ test('message router does not duplicate step 3 mismatch failure log after finali
});
const response = await router.handleMessage({
type: 'STEP_ERROR',
step: 3,
type: 'NODE_ERROR',
nodeId: 'fill-password',
source: 'signup-page',
payload: {},
payload: { nodeId: 'fill-password' },
error: mismatchError,
}, {});
@@ -524,6 +591,7 @@ test('message router does not duplicate step 3 mismatch failure log after finali
assert.deepStrictEqual(events.notifyErrors, [
{
step: 3,
nodeId: 'fill-password',
error: mismatchError,
},
]);
@@ -534,10 +602,10 @@ test('message router stops the flow and surfaces cloudflare security block error
const { router, events } = createRouter();
const response = await router.handleMessage({
type: 'STEP_ERROR',
step: 7,
type: 'NODE_ERROR',
nodeId: 'oauth-login',
source: 'signup-page',
payload: {},
payload: { nodeId: 'oauth-login' },
error: 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统',
}, {});
@@ -545,6 +613,7 @@ test('message router stops the flow and surfaces cloudflare security block error
assert.deepStrictEqual(events.notifyErrors, [
{
step: 7,
nodeId: 'oauth-login',
error: '流程已被用户停止。',
},
]);
@@ -562,9 +631,10 @@ test('message router blocks manual step 4 execution when signup page tab is miss
await assert.rejects(
() => router.handleMessage({
type: 'EXECUTE_STEP',
type: 'EXECUTE_NODE',
source: 'sidepanel',
payload: { step: 4 },
nodeId: 'fetch-signup-code',
payload: { nodeId: 'fetch-signup-code' },
}, {}),
/手动执行步骤 4 前,请先执行步骤 1 或步骤 2/
);
@@ -630,18 +700,19 @@ test('message router ignores stale step 2 errors while auto-run is already on a
state: {
autoRunning: true,
autoRunPhase: 'running',
currentStep: 6,
stepStatuses: {
2: 'completed',
6: 'running',
currentNodeId: 'wait-registration-success',
nodeStatuses: {
'submit-signup-email': 'completed',
'wait-registration-success': 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'STEP_ERROR',
step: 2,
type: 'NODE_ERROR',
nodeId: 'submit-signup-email',
payload: { nodeId: 'submit-signup-email' },
error: '步骤 2:旧页面异步失败,不应覆盖当前第 6 步记录。',
}, {});
@@ -649,7 +720,7 @@ test('message router ignores stale step 2 errors while auto-run is already on a
assert.deepStrictEqual(events.stepStatuses, []);
assert.deepStrictEqual(events.notifyErrors, []);
assert.deepStrictEqual(events.accountRecords, []);
assert.equal(events.logs.some(({ message }) => /忽略过期的步骤 2 失败消息/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 失败消息/.test(message)), true);
});
test('message router ignores stale step 2 completion while auto-run is already on a later step', async () => {
@@ -657,19 +728,20 @@ test('message router ignores stale step 2 completion while auto-run is already o
state: {
autoRunning: true,
autoRunPhase: 'running',
currentStep: 6,
stepStatuses: {
2: 'completed',
6: 'running',
currentNodeId: 'wait-registration-success',
nodeStatuses: {
'submit-signup-email': 'completed',
'wait-registration-success': 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 2,
type: 'NODE_COMPLETE',
nodeId: 'submit-signup-email',
payload: {
nodeId: 'submit-signup-email',
email: 'late@example.com',
},
}, {});
@@ -678,5 +750,5 @@ test('message router ignores stale step 2 completion while auto-run is already o
assert.deepStrictEqual(events.stepStatuses, []);
assert.deepStrictEqual(events.notifyCompletions, []);
assert.deepStrictEqual(events.emailStates, []);
assert.equal(events.logs.some(({ message }) => /忽略过期的步骤 2 完成消息/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 完成消息/.test(message)), true);
});
@@ -1,4 +1,4 @@
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
@@ -35,7 +35,7 @@ test('platform verify module supports codex2api protocol callback exchange', asy
},
chrome: {},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completed.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -68,7 +68,7 @@ test('platform verify module supports codex2api protocol callback exchange', asy
]);
assert.deepStrictEqual(completed, [
{
step: 10,
step: 'platform-verify',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
verifiedStatus: 'OAuth 账号 flow@example.com 添加成功',
@@ -98,7 +98,7 @@ test('platform verify retries transient SUB2API oauth/token exchange errors befo
},
},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'sub2api',
getTabId: async () => 12,
@@ -166,7 +166,7 @@ test('platform verify retries transient SUB2API token_exchange_user_error before
},
},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTab: async () => {},
getPanelMode: () => 'sub2api',
getTabId: async () => 12,
@@ -1,4 +1,4 @@
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
@@ -17,7 +17,7 @@ function createDeps(overrides = {}) {
},
},
closeConflictingTabsForSource: async () => {},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completed.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -97,7 +97,7 @@ test('platform verify module submits CPA callback via management API first', asy
assert.equal(uiCalled, false);
assert.deepStrictEqual(completed, [
{
step: 10,
step: 'platform-verify',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
verifiedStatus: 'CPA API 回调提交成功',
@@ -317,7 +317,7 @@ test('platform verify module submits Plus visible step 13 to SUB2API via direct
assert.equal(exchangeCall.body.state, 'oauth-state');
assert.deepStrictEqual(createCall.body.group_ids, [5]);
assert.deepStrictEqual(completed, [{
step: 13,
step: 'platform-verify',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
verifiedStatus: 'SUB2API 已创建账号 #11',
+29 -54
View File
@@ -22,29 +22,22 @@ test('runtime-state module exposes a factory', () => {
assert.equal(typeof api?.createRuntimeStateHelpers, 'function');
});
test('runtime-state view derives canonical flow metadata from legacy step state', () => {
test('runtime-state view preserves canonical flow metadata from node state', () => {
const api = loadRuntimeStateApi();
const helpers = api.createRuntimeStateHelpers({
DEFAULT_ACTIVE_FLOW_ID: 'openai',
defaultStepStatuses: {
1: 'pending',
2: 'pending',
10: 'pending',
},
getStepDefinitionForState(step) {
return {
1: { id: 1, key: 'open-chatgpt' },
2: { id: 2, key: 'submit-signup-email' },
10: { id: 10, key: 'oauth-login' },
}[Number(step)] || null;
defaultNodeStatuses: {
'open-chatgpt': 'pending',
'submit-signup-email': 'pending',
'oauth-login': 'pending',
},
});
const view = helpers.buildStateView({
currentStep: 2,
stepStatuses: {
1: 'completed',
2: 'running',
currentNodeId: 'submit-signup-email',
nodeStatuses: {
'open-chatgpt': 'completed',
'submit-signup-email': 'running',
},
oauthUrl: 'https://auth.example.com/start',
plusCheckoutTabId: 88,
@@ -63,14 +56,9 @@ test('runtime-state view derives canonical flow metadata from legacy step state'
assert.equal(view.activeFlowId, 'openai');
assert.equal(view.currentNodeId, 'submit-signup-email');
assert.deepStrictEqual(view.legacyStepCompat, {
currentStep: 2,
stepStatuses: {
1: 'completed',
2: 'running',
10: 'pending',
},
});
assert.equal(Object.prototype.hasOwnProperty.call(view, 'legacyStepCompat'), false);
assert.equal(Object.prototype.hasOwnProperty.call(view, 'currentStep'), false);
assert.equal(Object.prototype.hasOwnProperty.call(view, 'stepStatuses'), false);
assert.deepStrictEqual(view.nodeStatuses, {
'open-chatgpt': 'completed',
'submit-signup-email': 'running',
@@ -93,35 +81,33 @@ test('runtime-state view derives canonical flow metadata from legacy step state'
});
});
test('runtime-state patch accepts nested flow updates while keeping legacy compatibility fields in sync', () => {
test('runtime-state patch accepts nested flow and node updates without legacy step state', () => {
const api = loadRuntimeStateApi();
const helpers = api.createRuntimeStateHelpers({
DEFAULT_ACTIVE_FLOW_ID: 'openai',
defaultStepStatuses: {
1: 'pending',
2: 'pending',
10: 'pending',
},
getStepDefinitionForState(step) {
return {
1: { id: 1, key: 'open-chatgpt' },
2: { id: 2, key: 'submit-signup-email' },
10: { id: 10, key: 'oauth-login' },
}[Number(step)] || null;
defaultNodeStatuses: {
'open-chatgpt': 'pending',
'submit-signup-email': 'pending',
'oauth-login': 'pending',
},
});
const patch = helpers.buildSessionStatePatch({
currentStep: 1,
stepStatuses: {
1: 'running',
2: 'pending',
10: 'pending',
currentNodeId: 'open-chatgpt',
nodeStatuses: {
'open-chatgpt': 'running',
'submit-signup-email': 'pending',
'oauth-login': 'pending',
},
oauthUrl: 'https://old.example.com/start',
}, {
runtimeState: {
activeRunId: 'run-001',
currentNodeId: 'oauth-login',
nodeStatuses: {
'open-chatgpt': 'completed',
'oauth-login': 'running',
},
flowState: {
openai: {
auth: {
@@ -132,13 +118,6 @@ test('runtime-state patch accepts nested flow updates while keeping legacy compa
},
},
},
legacyStepCompat: {
currentStep: 10,
stepStatuses: {
1: 'completed',
10: 'running',
},
},
},
});
@@ -147,12 +126,8 @@ test('runtime-state patch accepts nested flow updates while keeping legacy compa
assert.equal(patch.currentNodeId, 'oauth-login');
assert.equal(patch.oauthUrl, 'https://new.example.com/start');
assert.equal(patch.plusCheckoutTabId, 99);
assert.equal(patch.currentStep, 10);
assert.deepStrictEqual(patch.stepStatuses, {
1: 'completed',
2: 'pending',
10: 'running',
});
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'currentStep'), false);
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'stepStatuses'), false);
assert.deepStrictEqual(patch.nodeStatuses, {
'open-chatgpt': 'completed',
'submit-signup-email': 'pending',
+15 -15
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -20,7 +20,7 @@ test('step 2 completes with password step skipped when landing on email verifica
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -40,7 +40,7 @@ test('step 2 completes with password step skipped when landing on email verifica
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
step: 'submit-signup-email',
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
@@ -59,7 +59,7 @@ test('step 2 keeps password flow when landing on password page', async () => {
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -79,7 +79,7 @@ test('step 2 keeps password flow when landing on password page', async () => {
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
step: 'submit-signup-email',
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
@@ -110,7 +110,7 @@ test('step 2 uses phone activation when resolved signup method is phone', async
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -170,7 +170,7 @@ test('step 2 uses phone activation when resolved signup method is phone', async
]);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
step: 'submit-signup-email',
payload: {
accountIdentifierType: 'phone',
accountIdentifier: '66959916439',
@@ -200,7 +200,7 @@ test('step 2 reuses existing signup phone activation without acquiring a new num
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -263,7 +263,7 @@ test('step 2 submits manual signup phone without acquiring a number', async () =
const executor = step2Api.createStep2Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -310,7 +310,7 @@ test('step 2 submits manual signup phone without acquiring a number', async () =
]);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
step: 'submit-signup-email',
payload: {
accountIdentifierType: 'phone',
accountIdentifier: '+446700000002',
@@ -338,7 +338,7 @@ test('step 2 stops with an explicit error instead of silently skipping 3/4/5 on
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -382,7 +382,7 @@ test('step 2 does not force auth-entry retry on logged-out chatgpt home when con
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
@@ -414,7 +414,7 @@ test('step 2 does not force auth-entry retry on logged-out chatgpt home when con
assert.deepStrictEqual(sentPayloads, [{ email: 'user@example.com' }]);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
step: 'submit-signup-email',
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
@@ -450,7 +450,7 @@ test('step 2 waits for the existing signup tab to settle before probing the entr
},
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {
@@ -499,7 +499,7 @@ test('step 2 waits for the existing signup tab to settle before probing the entr
assert.equal(logs.some((item) => item.meta.step === 2 && item.meta.stepKey === 'signup-entry'), true);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
step: 'submit-signup-email',
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
+77 -70
View File
@@ -4,6 +4,19 @@ const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
const OPENAI_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',
];
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
@@ -48,113 +61,107 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('skipStep cascades from step 1 to step 5 when downstream steps are pending', async () => {
function createApi() {
const bundle = [
extractFunction('isStepDoneStatus'),
extractFunction('skipStep'),
extractFunction('skipNode'),
].join('\n');
const statuses = {
1: 'pending',
2: 'pending',
3: 'pending',
4: 'pending',
5: 'pending',
6: 'pending',
7: 'pending',
8: 'pending',
9: 'pending',
10: 'pending',
};
return new Function(`
const DEFAULT_STATE = { nodeStatuses: {} };
function getNodeIdsForState() {
return ${JSON.stringify(OPENAI_NODE_IDS)};
}
function normalizeStatusMapForNodes(statuses = {}) {
return { ...statuses };
}
${bundle}
return { skipNode };
`)();
}
test('skipNode cascades from open-chatgpt through signup profile when downstream nodes are pending', async () => {
const statuses = Object.fromEntries(OPENAI_NODE_IDS.map((nodeId) => [nodeId, 'pending']));
const events = {
setStepStatusCalls: [],
setNodeStatusCalls: [],
logs: [],
};
const api = new Function(`
const STEP_IDS = [1,2,3,4,5,6,7,8,9,10];
${bundle}
return { skipStep };
`)();
const api = createApi();
globalThis.ensureManualInteractionAllowed = async () => ({
stepStatuses: { ...statuses },
nodeStatuses: { ...statuses },
});
globalThis.getState = async () => ({
stepStatuses: { ...statuses },
nodeStatuses: { ...statuses },
});
globalThis.setStepStatus = async (step, status) => {
events.setStepStatusCalls.push({ step, status });
statuses[step] = status;
globalThis.setNodeStatus = async (nodeId, status) => {
events.setNodeStatusCalls.push({ nodeId, status });
statuses[nodeId] = status;
};
globalThis.addLog = async (message, level) => {
events.logs.push({ message, level });
};
const result = await api.skipStep(1);
const result = await api.skipNode('open-chatgpt');
assert.deepStrictEqual(result, { ok: true, step: 1, status: 'skipped' });
assert.deepStrictEqual(events.setStepStatusCalls, [
{ step: 1, status: 'skipped' },
{ step: 2, status: 'skipped' },
{ step: 3, status: 'skipped' },
{ step: 4, status: 'skipped' },
{ step: 5, status: 'skipped' },
assert.deepStrictEqual(result, { ok: true, nodeId: 'open-chatgpt', status: 'skipped' });
assert.deepStrictEqual(events.setNodeStatusCalls, [
{ nodeId: 'open-chatgpt', status: 'skipped' },
{ nodeId: 'submit-signup-email', status: 'skipped' },
{ nodeId: 'fill-password', status: 'skipped' },
{ nodeId: 'fetch-signup-code', status: 'skipped' },
{ nodeId: 'fill-profile', status: 'skipped' },
]);
assert.equal(events.logs[0]?.message, '步骤 1 已跳过');
assert.equal(events.logs[1]?.message, '步骤 1 已跳过,步骤 2、3、4、5 也已同时跳过。');
assert.equal(events.logs[0]?.message, '节点 open-chatgpt 已跳过');
assert.equal(
events.logs[1]?.message,
'节点 open-chatgpt 已跳过,节点 submit-signup-email、fill-password、fetch-signup-code、fill-profile 也已同时跳过。'
);
});
test('skipStep from step 1 skips only unfinished steps up to step 5', async () => {
const bundle = [
extractFunction('isStepDoneStatus'),
extractFunction('skipStep'),
].join('\n');
test('skipNode from open-chatgpt skips only unfinished linked signup nodes', async () => {
const statuses = {
1: 'pending',
2: 'completed',
3: 'running',
4: 'pending',
5: 'manual_completed',
6: 'pending',
7: 'pending',
8: 'pending',
9: 'pending',
10: 'pending',
'open-chatgpt': 'pending',
'submit-signup-email': 'completed',
'fill-password': 'running',
'fetch-signup-code': 'pending',
'fill-profile': 'manual_completed',
'wait-registration-success': 'pending',
'oauth-login': 'pending',
'fetch-login-code': 'pending',
'confirm-oauth': 'pending',
'platform-verify': 'pending',
};
const events = {
setStepStatusCalls: [],
setNodeStatusCalls: [],
logs: [],
};
const api = new Function(`
const STEP_IDS = [1,2,3,4,5,6,7,8,9,10];
${bundle}
return { skipStep };
`)();
const api = createApi();
globalThis.ensureManualInteractionAllowed = async () => ({
stepStatuses: { ...statuses },
nodeStatuses: { ...statuses },
});
globalThis.getState = async () => ({
stepStatuses: { ...statuses },
nodeStatuses: { ...statuses },
});
globalThis.setStepStatus = async (step, status) => {
events.setStepStatusCalls.push({ step, status });
statuses[step] = status;
globalThis.setNodeStatus = async (nodeId, status) => {
events.setNodeStatusCalls.push({ nodeId, status });
statuses[nodeId] = status;
};
globalThis.addLog = async (message, level) => {
events.logs.push({ message, level });
};
await api.skipStep(1);
await api.skipNode('open-chatgpt');
assert.deepStrictEqual(events.setStepStatusCalls, [
{ step: 1, status: 'skipped' },
{ step: 4, status: 'skipped' },
assert.deepStrictEqual(events.setNodeStatusCalls, [
{ nodeId: 'open-chatgpt', status: 'skipped' },
{ nodeId: 'fetch-signup-code', status: 'skipped' },
]);
assert.equal(events.logs.some(({ message }) => message === '步骤 1 已跳过,步骤 4 也已同时跳过。'), true);
assert.equal(
events.logs.some(({ message }) => (
message === '节点 open-chatgpt 已跳过,节点 fetch-signup-code 也已同时跳过。'
)),
true
);
});
+27 -28
View File
@@ -51,57 +51,56 @@ function extractFunction(name) {
return source.slice(start, end);
}
function createApi(events, lastStepId = 10) {
return new Function('events', 'lastStepId', `
function createApi(events, lastNodeId = 'platform-verify') {
return new Function('events', 'lastNodeId', `
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 };
return { nodeStatuses: {}, contributionMode: true };
}
function getLastStepIdForState() {
return lastStepId;
function getLastNodeIdForState() {
return lastNodeId;
}
async function setStepStatus(step, status) {
events.push({ type: 'status', step, status });
async function setNodeStatus(nodeId, status) {
events.push({ type: 'status', nodeId, status });
}
async function addLog(message, level) {
events.push({ type: 'log', message, level });
async function addLog(message, level, options = {}) {
events.push({ type: 'log', message, level, options });
}
async function appendManualAccountRunRecordIfNeeded() {
events.push({ type: 'manual-record' });
}
function notifyStepError(step, error) {
events.push({ type: 'error', step, error });
function notifyNodeError(nodeId, error) {
events.push({ type: 'error', nodeId, error });
}
function notifyStepComplete(step, payload) {
events.push({ type: 'notify', step, payload });
function notifyNodeComplete(nodeId, payload) {
events.push({ type: 'notify', nodeId, payload });
}
async function handleStepData(step, payload) {
events.push({ type: 'handle-start', step, payload });
async function handleNodeData(nodeId, payload) {
events.push({ type: 'handle-start', nodeId, payload });
await new Promise((resolve) => setTimeout(resolve, 25));
events.push({ type: 'handle-done', step });
events.push({ type: 'handle-done', nodeId });
}
async function appendAndBroadcastAccountRunRecord(status, state) {
events.push({ type: 'record', status, state });
}
${extractFunction('runCompletedStepSideEffects')}
${extractFunction('reportCompletedStepSideEffectError')}
${extractFunction('completeStepFromBackground')}
return { completeStepFromBackground };
`)(events, lastStepId);
${extractFunction('runCompletedNodeSideEffects')}
${extractFunction('reportCompletedNodeSideEffectError')}
${extractFunction('completeNodeFromBackground')}
return { completeNodeFromBackground };
`)(events, lastNodeId);
}
test('completeStepFromBackground releases final step before slow post-completion side effects', async () => {
test('completeNodeFromBackground releases final node before slow post-completion side effects', async () => {
const events = [];
const api = createApi(events, 10);
const api = createApi(events, 'platform-verify');
await api.completeStepFromBackground(10, { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' });
await api.completeNodeFromBackground('platform-verify', { 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);
@@ -115,11 +114,11 @@ test('completeStepFromBackground releases final step before slow post-completion
assert.equal(settledTypes.includes('record'), true);
});
test('completeStepFromBackground keeps non-final step data handling before completion signal', async () => {
test('completeNodeFromBackground keeps non-final node data handling before completion signal', async () => {
const events = [];
const api = createApi(events, 10);
const api = createApi(events, 'platform-verify');
await api.completeStepFromBackground(9, { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' });
await api.completeNodeFromBackground('confirm-oauth', { 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);
+10 -8
View File
@@ -2,18 +2,20 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background imports step registry and shared step definitions', () => {
test('background imports node registry and shared workflow definitions', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /background\/steps\/registry\.js/);
assert.match(source, /data\/step-definitions\.js/);
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
assert.match(source, /background\/workflow-engine\.js/);
assert.match(source, /MultiPageStepDefinitions\?\.getNodes/);
assert.match(source, /getStepRegistryForState\(state\)/);
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
assert.match(source, /plusPayPalStepRegistry/);
assert.match(source, /plusGoPayStepRegistry/);
assert.match(source, /normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\) === PLUS_PAYMENT_METHOD_GOPAY/);
assert.match(source, /activeStepRegistry\.executeStep\(step,\s*\{/);
assert.match(source, /buildNodeRegistry\(definitions/);
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
assert.match(source, /plusPayPalStepRegistry/);
assert.match(source, /plusGoPayStepRegistry/);
assert.match(source, /normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\) === PLUS_PAYMENT_METHOD_GOPAY/);
assert.match(source, /activeStepRegistry\.executeNode\(normalizedNodeId,\s*\{/);
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
assert.match(source, /background\/steps\/gopay-manual-confirm\.js/);
@@ -39,7 +39,8 @@ test('step 3 reuses existing generated password when rerunning the same email fl
assert.deepStrictEqual(events.passwordStates, ['Secret123!']);
assert.deepStrictEqual(events.messages, [
{
type: 'EXECUTE_STEP',
type: 'EXECUTE_NODE',
nodeId: 'fill-password',
step: 3,
source: 'background',
payload: {
@@ -109,7 +110,8 @@ test('step 3 supports phone-only signup identity when password page is present',
]);
assert.deepStrictEqual(events.messages, [
{
type: 'EXECUTE_STEP',
type: 'EXECUTE_NODE',
nodeId: 'fill-password',
step: 3,
source: 'background',
payload: {
+10 -10
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -23,7 +23,7 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
@@ -84,7 +84,7 @@ test('step 4 does not request a fresh code first for Cloudflare temp mail', asyn
update: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
@@ -135,7 +135,7 @@ test('step 4 checks iCloud session before polling iCloud mailbox', async () => {
update: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureIcloudMailSession: async () => {
icloudChecks += 1;
@@ -183,7 +183,7 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
update: async () => {},
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -224,7 +224,7 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
assert.deepStrictEqual(completions, [
{
step: 4,
step: 'fetch-signup-code',
payload: { skipProfileStep: true },
},
]);
@@ -244,7 +244,7 @@ test('step 4 phone signup branch uses SMS helper and does not poll mailbox', asy
update: async () => {},
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -296,7 +296,7 @@ test('step 4 phone signup branch uses SMS helper and does not poll mailbox', asy
assert.equal(Object.prototype.hasOwnProperty.call(phoneCalls[0].options, 'signupProfile'), true);
assert.deepStrictEqual(completions, [
{
step: 4,
step: 'fetch-signup-code',
payload: {
phoneVerification: true,
code: '',
@@ -321,7 +321,7 @@ test('step 4 phone signup email-verification handoff polls mailbox instead of co
update: async () => {},
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -403,7 +403,7 @@ test('step 4 prepare retries transport by recovering retry page without replayin
update: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
@@ -30,7 +30,8 @@ test('step 5 forwards generated profile data and relies on completion signal flo
{
source: 'signup-page',
message: {
type: 'EXECUTE_STEP',
type: 'EXECUTE_NODE',
nodeId: 'fill-profile',
step: 5,
source: 'background',
payload: {
+23 -23
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -17,7 +17,7 @@ test('step 6 waits for registration success and completes from background', asyn
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async (step) => {
completeNodeFromBackground: async (step) => {
events.completedSteps.push(step);
},
sleepWithStop: async (ms) => {
@@ -28,7 +28,7 @@ test('step 6 waits for registration success and completes from background', asyn
await executor.executeStep6();
assert.deepStrictEqual(events.waits, [20000]);
assert.deepStrictEqual(events.completedSteps, [6]);
assert.deepStrictEqual(events.completedSteps, ['wait-registration-success']);
assert.ok(events.logs.some(({ message }) => /等待 20 /.test(message)));
});
@@ -64,7 +64,7 @@ test('step 6 only clears cookies when cleanup switch is enabled', async () => {
const executor = api.createStep6Executor({
addLog: async () => {},
chrome: chromeApi,
completeStepFromBackground: async (step) => {
completeNodeFromBackground: async (step) => {
events.completedSteps.push(step);
},
sleepWithStop: async () => {},
@@ -77,7 +77,7 @@ test('step 6 only clears cookies when cleanup switch is enabled', async () => {
await executor.executeStep6({ step6CookieCleanupEnabled: true });
assert.deepStrictEqual(events.completedSteps, [6, 6]);
assert.deepStrictEqual(events.completedSteps, ['wait-registration-success', 'wait-registration-success']);
assert.deepStrictEqual(events.removedCookies, [
{
url: 'https://chatgpt.com/auth',
@@ -102,7 +102,7 @@ test('step 7 retries up to configured limit and then fails', async () => {
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {
completeNodeFromBackground: async () => {
events.completed += 1;
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -153,7 +153,7 @@ test('step 7 exits internal retry loop immediately when add-phone is detected',
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {
completeNodeFromBackground: async () => {
events.completed += 1;
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -201,7 +201,7 @@ test('step 7 hands direct add-phone to shared phone verification when enabled',
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completions.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -256,7 +256,7 @@ test('step 7 hands direct add-phone to shared phone verification when enabled',
]);
assert.deepStrictEqual(events.completions, [
{
step: 7,
step: 'oauth-login',
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
@@ -280,7 +280,7 @@ test('step 7 direct add-phone stays fatal when phone verification is disabled',
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {
completeNodeFromBackground: async () => {
events.completions += 1;
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -324,7 +324,7 @@ test('step 7 propagates fatal errors from shared add-phone verification', async
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {
completeNodeFromBackground: async () => {
events.completions += 1;
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -368,7 +368,7 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, options) => {
@@ -419,7 +419,7 @@ test('step 7 forwards direct OAuth consent skip metadata when completing', async
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completions.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -447,7 +447,7 @@ test('step 7 forwards direct OAuth consent skip metadata when completing', async
assert.deepStrictEqual(events.completions, [
{
step: 10,
step: 'oauth-login',
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
@@ -469,7 +469,7 @@ test('step 7 forwards phone login identity payload when account identifier is ph
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completions.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -529,7 +529,7 @@ test('step 7 forwards phone login identity payload when account identifier is ph
]);
assert.deepStrictEqual(events.completions, [
{
step: 7,
step: 'oauth-login',
payload: {
loginVerificationRequestedAt: 123456,
accountIdentifierType: 'phone',
@@ -558,7 +558,7 @@ test('step 7 keeps Plus email login even when phone sms runtime exists', async (
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({
@@ -629,7 +629,7 @@ test('step 7 keeps phone login after step 8 stores an unbound email for phone si
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ ...phoneSignupState }),
@@ -668,7 +668,7 @@ test('step 7 can infer phone login from an available phone signup configuration
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({
@@ -715,7 +715,7 @@ test('step 7 can start from a manually filled signup phone without completed ste
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completions.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
@@ -755,7 +755,7 @@ test('step 7 can start from a manually filled signup phone without completed ste
assert.equal(events.payloads[0].password, '');
assert.deepStrictEqual(events.completions, [
{
step: 7,
step: 'oauth-login',
payload: {
loginVerificationRequestedAt: 987654,
accountIdentifierType: 'phone',
@@ -783,7 +783,7 @@ test('step 7 stops immediately when management secret is missing', async () => {
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
@@ -827,7 +827,7 @@ test('step 7 stops immediately when management secret is invalid', async () => {
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
+10 -10
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -112,7 +112,7 @@ test('step 8 keeps phone-registered accounts on email-code flow when page is ema
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
calls.completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -190,7 +190,7 @@ test('step 8 routes only a real phone verification page through sms helper', asy
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
calls.completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -250,7 +250,7 @@ test('step 8 routes only a real phone verification page through sms helper', asy
]);
assert.deepStrictEqual(calls.completions, [
{
step: 8,
step: 'fetch-login-code',
payload: {
phoneVerification: true,
loginPhoneVerification: true,
@@ -770,7 +770,7 @@ test('step 8 completes directly when auth page is already on OAuth consent page'
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completeCalls.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -825,7 +825,7 @@ test('step 8 completes directly when auth page is already on OAuth consent page'
]);
assert.deepStrictEqual(events.completeCalls, [
{
step: 8,
step: 'fetch-login-code',
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
@@ -855,7 +855,7 @@ test('step 8 retries in-place when polling fails but auth page still stays on ve
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => {
events.ensureCalls += 1;
@@ -922,7 +922,7 @@ test('step 8 keeps resend cooldown timestamp across in-place retries to avoid re
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'user@example.com' }),
rerunStep7ForStep8Recovery: async () => {},
@@ -997,7 +997,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completeCalls.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
@@ -1046,7 +1046,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent
assert.equal(events.rerunStep7, 0);
assert.deepStrictEqual(events.completeCalls, [
{
step: 8,
step: 'fetch-login-code',
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
+58 -5
View File
@@ -48,11 +48,59 @@ function extractFunction(name) {
return source.slice(start, end);
}
const NODE_COMPAT_HELPERS = `
const workflowEngine = null;
const STEP_NODE_IDS = {
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'confirm-oauth',
10: 'platform-verify',
};
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
function getNodeIdsForState() {
return Object.values(STEP_NODE_IDS);
}
function getNodeIdByStepForState(step) {
return STEP_NODE_IDS[Number(step)] || '';
}
function getStepIdByNodeIdForState(nodeId) {
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
const nodeStatuses = {};
for (const [step, status] of Object.entries(stepStatuses || {})) {
const nodeId = getNodeIdByStepForState(step);
if (nodeId) nodeStatuses[nodeId] = status;
}
return nodeStatuses;
}
function normalizeStatusMapForNodes(statuses = {}, state = {}) {
return {
...DEFAULT_STATE.nodeStatuses,
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
...(state?.nodeStatuses || {}),
...(statuses || {}),
};
}
DEFAULT_STATE.nodeStatuses = projectStepStatusesToNodeStatuses(DEFAULT_STATE.stepStatuses || {});
`;
test('generic stopped record resolves to next unfinished step during execution gap', async () => {
const bundle = [
NODE_COMPAT_HELPERS,
extractFunction('isStepDoneStatus'),
extractFunction('getRunningNodeIds'),
extractFunction('getRunningSteps'),
extractFunction('inferStoppedRecordNode'),
extractFunction('inferStoppedRecordStep'),
extractFunction('resolveAccountRunRecordStatusForStop'),
extractFunction('extractStoppedNodeFromRecordStatus'),
extractFunction('extractStoppedStepFromRecordStatus'),
extractFunction('resolveAccountRunRecordReasonForStop'),
extractFunction('appendAndBroadcastAccountRunRecord'),
@@ -103,10 +151,9 @@ return {
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.resolveAccountRunRecordStatusForStop('stopped', state), 'node:oauth-login:stopped');
assert.equal(api.resolveAccountRunRecordReasonForStop('node:oauth-login:stopped', '流程已被用户停止。'), '节点 oauth-login 已被用户停止。');
assert.equal(
api.resolveAccountRunRecordReasonForStop('step2_stopped', '步骤 2 已使用邮箱,流程尚未完成。'),
'步骤 2 已停止:邮箱已设置,流程尚未完成。'
@@ -114,19 +161,23 @@ return {
await api.appendAndBroadcastAccountRunRecord('stopped', state, '流程已被用户停止。');
assert.deepStrictEqual(api.getCaptured(), {
status: 'step7_stopped',
status: 'node:oauth-login:stopped',
state,
reason: '步骤 7 已被用户停止。',
reason: '节点 oauth-login 已被用户停止。',
});
});
test('requestStop appends a stopped record for the next unfinished step when no step is running', async () => {
const bundle = [
NODE_COMPAT_HELPERS,
extractFunction('normalizeAutoRunSessionId'),
extractFunction('clearCurrentAutoRunSessionId'),
extractFunction('cleanupStep8NavigationListeners'),
extractFunction('rejectPendingStep8'),
extractFunction('isStepDoneStatus'),
extractFunction('getRunningNodeIds'),
extractFunction('getRunningSteps'),
extractFunction('inferStoppedRecordNode'),
extractFunction('inferStoppedRecordStep'),
extractFunction('requestStop'),
].join('\n');
@@ -148,6 +199,7 @@ 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 nodeWaiters = new Map();
const stepWaiters = new Map();
const appended = [];
const logs = [];
@@ -176,6 +228,7 @@ async function addLog(message, level) {
}
async function broadcastStopToContentScripts() {}
async function markRunningStepsStopped() {}
async function markRunningNodesStopped() {}
async function broadcastAutoRunStatus() {}
async function appendAndBroadcastAccountRunRecord(status, state, reason) {
appended.push({ status, state, reason });
+4 -4
View File
@@ -121,13 +121,13 @@ test('GoPay approve waits and retries slowly on Hubungkan linking page', () => {
});
test('background auto-run routes GoPay restart sentinel back to step 6', () => {
test('background auto-run routes GoPay restart sentinel back to checkout-create node', () => {
const backgroundSource = fs.readFileSync('background.js', 'utf8');
assert.match(backgroundSource, /isGoPayCheckoutRestartRequiredFailure/);
assert.match(backgroundSource, /GOPAY_RESTART_FROM_STEP6::/);
assert.match(backgroundSource, /step === 8 && isGoPayCheckoutRestartRequiredFailure\(err\)/);
assert.match(backgroundSource, /step = 6/);
assert.match(backgroundSource, /invalidateDownstreamAfterStepRestart\(5/);
assert.match(backgroundSource, /nodeId === 'paypal-approve' && isGoPayCheckoutRestartRequiredFailure\(err\)/);
assert.match(backgroundSource, /getNodeIndex\(await getState\(\), 'plus-checkout-create'\)/);
assert.match(backgroundSource, /invalidateDownstreamAfterAutoRunNodeRestart\(getPreviousNodeId\('plus-checkout-create'/);
});
test('GoPay approve gives PIN precedence over OTP on ambiguous second PIN pages', () => {
+7 -7
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
@@ -56,7 +56,7 @@ function createExecutor({
},
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.completed.push({ step, payload });
},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
@@ -259,7 +259,7 @@ test('PayPal approve keeps original combined email and password login path', asy
});
assert.equal(events.submittedPayloads.length, 1);
assert.deepEqual(events.completed.map((item) => item.step), [8]);
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
});
@@ -404,7 +404,7 @@ test('PayPal approve discovers an already open unregistered PayPal tab', async (
assert.deepEqual(events.updatedTabs, [{ tabId: 7, updateInfo: { active: true } }]);
assert.equal(events.logs.some(({ message }) => /发现 PayPal 页面/.test(message)), true);
assert.deepEqual(events.completed.map((item) => item.step), [8]);
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
});
@@ -440,7 +440,7 @@ test('PayPal approve discovers PayPal tabs through the locked automation window
assert.deepEqual(queries, [{}]);
assert.deepEqual(events.updatedTabs, [{ tabId: 9, updateInfo: { active: true } }]);
assert.deepEqual(events.completed.map((item) => item.step), [8]);
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
});
test('PayPal approve auto-detects split email then password pages', async () => {
@@ -464,7 +464,7 @@ test('PayPal approve auto-detects split email then password pages', async () =>
});
assert.equal(events.submittedPayloads.length, 2);
assert.deepEqual(events.completed.map((item) => item.step), [8]);
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
assert.equal(events.logs.some(({ message }) => /识别到密码页/.test(message)), true);
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
});
@@ -490,6 +490,6 @@ test('PayPal approve finishes when login redirects away from PayPal', async () =
});
assert.equal(events.submittedPayloads.length, 1);
assert.deepEqual(events.completed.map((item) => item.step), [8]);
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), false);
});
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -153,7 +153,7 @@ function createExecutorHarness({
getAllFrames: async () => frames,
},
},
completeStepFromBackground: async (step, payload) => events.completed.push({ step, payload }),
completeNodeFromBackground: async (step, payload) => events.completed.push({ step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId) => events.ensuredTabs.push({ source, tabId }),
fetch: fetchImpl,
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
@@ -237,7 +237,7 @@ test('Plus checkout billing uses the current checkout tab when step 6 did not re
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL' && entry.frameId === 0), true);
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS' && entry.frameId === 0), true);
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE' && entry.frameId === 0), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
assert.equal(events.states.some((updates) => updates.plusCheckoutTabId === checkoutTab.id), true);
assert.equal(events.logs.some((entry) => /当前已在 Plus Checkout 页面/.test(entry.message)), true);
});
@@ -297,7 +297,7 @@ test('Plus checkout billing sends the billing command to the iframe that contain
assert.equal(fillMessage.frameId, 8);
assert.equal(subscribeMessage.frameId, 0);
assert.equal(events.logs.some((entry) => /checkout iframe/.test(entry.message)), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('Plus checkout billing uses proxy exit country for GoPay address when available', async () => {
@@ -597,7 +597,7 @@ test('Plus checkout billing selects GoPay and waits for a GoPay redirect', async
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'ID');
assert.equal(subscribeMessage.message.payload.paymentMethod, 'gopay');
assert.equal(checkoutTab.url, 'https://gopay.co.id/payment/session');
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('Plus checkout billing still inspects a frame when ping readiness is stale', async () => {
@@ -625,7 +625,7 @@ test('Plus checkout billing still inspects a frame when ping readiness is stale'
const selectMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL');
assert.equal(selectMessage.frameId, 0);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('Plus checkout billing uses the autocomplete iframe for address suggestions when Stripe splits it out', async () => {
@@ -655,7 +655,7 @@ test('Plus checkout billing uses the autocomplete iframe for address suggestions
assert.equal(ensureAddressMessage.frameId, 8);
assert.equal(combinedFillMessage, undefined);
assert.equal(events.logs.some((entry) => /Google 地址推荐/.test(entry.message)), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('Plus checkout billing skips Google autocomplete when meiguodizhi returns a complete address', async () => {
@@ -713,7 +713,7 @@ test('Plus checkout billing skips Google autocomplete when meiguodizhi returns a
path: '/de-address',
method: 'refresh',
});
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('Plus checkout billing uses the detected checkout country before choosing an address seed', async () => {
@@ -940,7 +940,7 @@ test('GPC billing polls queue task, submits WhatsApp OTP then PIN, and waits unt
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_123' && state.gopayHelperTaskStatus === 'completed'), true);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:等待 WhatsApp OTP'), true);
assert.equal(events.logs.some((entry) => /whatsapp_otp_wait/.test(entry.message)), false);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
assert.ok(events.sleeps.includes(3000));
});
@@ -995,7 +995,7 @@ test('GPC billing auto mode only polls until completed without OTP or PIN submis
assert.equal(events.logs.some((entry) => /auto_otp_wait/.test(entry.message)), false);
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_auto' && state.gopayHelperTaskStatus === 'completed'), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
});
@@ -1266,7 +1266,7 @@ test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () =>
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_sms/otp')).options.body), {
otp: '654321',
});
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
@@ -1346,7 +1346,7 @@ test('GPC billing can read WhatsApp OTP from local helper when enabled', async (
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_wa/otp')).options.body), {
otp: '765432',
});
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
@@ -1481,7 +1481,7 @@ test('GPC billing helper mode requests newer OTP after invalid OTP error', async
assert.equal(helperUrls[1].searchParams.get('consume'), '1');
assert.ok(Number(helperUrls[1].searchParams.get('after_ms')) > 1710000000000);
assert.equal(events.logs.some((entry) => /OTP 校验失败/.test(entry.message)), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('GPC billing manual OTP wrong input opens next dialog only after previous one closes', async () => {
@@ -1558,7 +1558,7 @@ test('GPC billing manual OTP wrong input opens next dialog only after previous o
.filter((call) => call.url.endsWith('/api/gp/tasks/task_manual_retry/otp'))
.map((call) => JSON.parse(call.options.body));
assert.deepEqual(otpBodies, [{ otp: '111111' }, { otp: '222222' }]);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].step, 'plus-checkout-billing');
});
test('GPC billing manual OTP cancel stops task and ends current round', async () => {
+15 -15
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
@@ -204,7 +204,7 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
},
},
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.push({ type: 'complete', step, payload });
},
ensureContentScriptReadyOnTabUntilStopped: async () => {
@@ -267,7 +267,7 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
update: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
registerTab: async () => {},
sendTabMessageUntilStopped: async (_tabId, _source, message) => {
@@ -370,7 +370,7 @@ test('GPC manual checkout injects Plus script before reading ChatGPT session tok
remove: async (tabId) => events.push({ type: 'tab-remove', tabId }),
},
},
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -438,7 +438,7 @@ test('GPC manual checkout injects Plus script before reading ChatGPT session tok
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperRemoteStage, 'checkout_start');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, '');
assert.ok(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperOrderCreatedAt > 0);
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
});
@@ -456,7 +456,7 @@ test('GPC auto checkout only sends access token and API Key', async () => {
remove: async () => {},
},
},
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -507,7 +507,7 @@ test('GPC auto checkout only sends access token and API Key', async () => {
assert.equal(statePayload.gopayHelperTaskId, 'task_auto');
assert.equal(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
});
test('GPC auto checkout keeps running when balance payload omits auto mode permission', async () => {
@@ -523,7 +523,7 @@ test('GPC auto checkout keeps running when balance payload omits auto mode permi
remove: async () => {},
},
},
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -564,7 +564,7 @@ test('GPC auto checkout keeps running when balance payload omits auto mode permi
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.gopayHelperTaskId, 'task_auto_unknown_permission');
assert.equal(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
});
test('GPC auto checkout blocks API Keys without auto mode permission', async () => {
@@ -579,7 +579,7 @@ test('GPC auto checkout blocks API Keys without auto mode permission', async ()
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -623,7 +623,7 @@ test('GPC checkout blocks exhausted API Keys before creating task', async () =>
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -668,7 +668,7 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -720,7 +720,7 @@ test('GPC checkout surfaces unified queue API errors', async () => {
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
@@ -780,7 +780,7 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without helper phone');
@@ -821,7 +821,7 @@ test('GPC checkout rejects missing API Key before calling helper API', async ()
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without API Key');
+3 -3
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -12,7 +12,7 @@ test('Plus return confirm waits a fixed 20 seconds after return URL is detected'
addLog: async (message, level = 'info') => {
events.push({ type: 'log', message, level });
},
completeStepFromBackground: async (step, payload) => {
completeNodeFromBackground: async (step, payload) => {
events.push({ type: 'complete', step, payload });
},
getTabId: async (source) => (source === 'paypal-flow' ? 77 : null),
@@ -46,7 +46,7 @@ test('Plus return confirm waits a fixed 20 seconds after return URL is detected'
events.find((event) => event.type === 'complete'),
{
type: 'complete',
step: 9,
step: 'plus-checkout-return',
payload: { plusReturnUrl: 'https://chatgpt.com/backend-api/payments/success' },
}
);
+15 -1
View File
@@ -132,7 +132,10 @@ test('sidepanel settings refresh preserves rendered step progress', () => {
const bundle = [
extractFunction('isDoneStatus'),
extractFunction('getNodeStatuses'),
extractFunction('getStepStatuses'),
extractFunction('escapeCssValue'),
extractFunction('renderSingleNodeStatus'),
extractFunction('renderSingleStepStatus'),
extractFunction('renderStepStatuses'),
extractFunction('updateProgressCounter'),
@@ -148,13 +151,24 @@ const STATUS_ICONS = {
manual_completed: 'M',
skipped: 'K',
};
let latestState = { stepStatuses: { 1: 'completed', 2: 'running', 3: 'pending' } };
let latestState = { nodeStatuses: { 'open-chatgpt': 'completed', 'submit-signup-email': 'running', 'fill-password': 'pending' } };
let NODE_IDS = ['open-chatgpt', 'submit-signup-email', 'fill-password'];
let NODE_DEFAULT_STATUSES = { 'open-chatgpt': 'pending', 'submit-signup-email': 'pending', 'fill-password': 'pending' };
let STEP_IDS = [1, 2, 3];
let STEP_DEFAULT_STATUSES = { 1: 'pending', 2: 'pending', 3: 'pending' };
function getStepIdByNodeIdForCurrentMode(nodeId) {
return { 'open-chatgpt': 1, 'submit-signup-email': 2, 'fill-password': 3 }[nodeId] || 0;
}
const rows = new Map(STEP_IDS.map((step) => [step, { className: 'step-row' }]));
const statusEls = new Map(STEP_IDS.map((step) => [step, { textContent: '' }]));
const nodeRows = new Map(NODE_IDS.map((nodeId) => [nodeId, rows.get(getStepIdByNodeIdForCurrentMode(nodeId))]));
const nodeStatusEls = new Map(NODE_IDS.map((nodeId) => [nodeId, statusEls.get(getStepIdByNodeIdForCurrentMode(nodeId))]));
const document = {
querySelector(selector) {
const nodeMatch = selector.match(/data-node-id="([^"]+)"/);
if (nodeMatch) {
return selector.includes('step-status') ? nodeStatusEls.get(nodeMatch[1]) : nodeRows.get(nodeMatch[1]);
}
const match = selector.match(/data-step="(\\d+)"/);
const step = match ? Number(match[1]) : 0;
return selector.includes('step-status') ? statusEls.get(step) : rows.get(step);
@@ -200,7 +200,7 @@ test('sidepanel source wires runtime signup phone field to background sync messa
assert.match(sidepanelSource, /final \? 'SAVE_SIGNUP_PHONE' : 'SET_SIGNUP_PHONE_STATE'/);
assert.match(sidepanelSource, /message\.payload\.signupPhoneNumber !== undefined/);
assert.match(sidepanelSource, /await persistSignupPhoneInputForAction\(\);\s*await saveSettings/);
assert.match(sidepanelSource, /if \(shouldExecuteStep3WithSignupPhoneIdentity\(latestState\)\)[\s\S]*payload: \{ step \}/);
assert.match(sidepanelSource, /if \(shouldExecuteStep3WithSignupPhoneIdentity\(latestState\)\)[\s\S]*type:\s*'EXECUTE_NODE'[\s\S]*payload: \{ nodeId \}/);
assert.match(sidepanelSource, /async function handleSkipStep\(step\)[\s\S]*await persistCurrentSettingsForAction\(\);/);
assert.match(sidepanelSource, /inputSignupPhone\.addEventListener\('input'[\s\S]*signupPhoneInputDirty = true/);
});
+3
View File
@@ -53,4 +53,7 @@ test('shared source registry exposes canonical source, alias, detection, and rea
assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
assert.equal(registry.driverAcceptsCommand('openai-auth', 'submit-signup-email'), true);
assert.equal(registry.driverAcceptsCommand('content/platform-panel', 'platform-verify'), true);
assert.equal(registry.driverAcceptsCommand('openai-auth', 'platform-verify'), false);
});
+2 -2
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -240,7 +240,7 @@ test('step 8 escalates to rerun step 7 after too many local retry_without_step7
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => {
calls.ensureReady += 1;
+12 -4
View File
@@ -1,4 +1,4 @@
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const helperSource = fs.readFileSync('background.js', 'utf8');
@@ -78,7 +78,7 @@ 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'])),
nodeStatuses: {},
};
const STEP8_CLICK_RETRY_DELAY_MS = 500;
const STEP8_MAX_ROUNDS = 5;
@@ -134,6 +134,7 @@ const chrome = {
},
};
const nodeWaiters = new Map();
const stepWaiters = new Map();
let resumeWaiter = null;
@@ -141,6 +142,13 @@ function cancelPendingCommands() {}
function abortActiveIcloudRequests() {}
async function addLog() {}
async function broadcastStopToContentScripts() {}
function getRunningNodeIds() {
return [];
}
function inferStoppedRecordNode() {
return '';
}
async function markRunningNodesStopped() {}
async function markRunningStepsStopped() {}
async function broadcastAutoRunStatus() {}
async function appendAndBroadcastAccountRunRecord() {}
@@ -156,7 +164,7 @@ function isAutoRunScheduledState() {
function getStep8CallbackUrlFromNavigation() { return ''; }
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'no_effect'; }
async function completeStepFromBackground() {}
async function completeNodeFromBackground() {}
async function getTabId() {
return await new Promise((resolve) => {
resolveTabId = resolve;
@@ -218,7 +226,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
completeNodeFromBackground,
ensureStep8SignupPageReady,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
+9 -9
View File
@@ -1,4 +1,4 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
@@ -102,8 +102,8 @@ function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject(handler) { step8PendingReject = handler; }
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
async function completeNodeFromBackground(nodeId, payload) {
completePayload = { nodeId, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
@@ -120,7 +120,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
completeNodeFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
@@ -176,7 +176,7 @@ return {
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.equal(snapshot.hasPendingReject, false);
assert.deepEqual(snapshot.completePayload, {
step: 9,
nodeId: 'confirm-oauth',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
@@ -279,8 +279,8 @@ function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject() {}
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
async function completeNodeFromBackground(nodeId, payload) {
completePayload = { nodeId, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
@@ -297,7 +297,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
completeNodeFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
@@ -348,7 +348,7 @@ return {
assert.equal(snapshot.deferCalls >= 1, true);
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.deepEqual(snapshot.completePayload, {
step: 12,
nodeId: 'confirm-oauth',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
+5 -5
View File
@@ -1,4 +1,4 @@
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const step9ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
@@ -148,8 +148,8 @@ function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listen
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject(handler) { step8PendingReject = handler; }
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
async function completeNodeFromBackground(nodeId, payload) {
completePayload = { nodeId, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 200;
@@ -167,7 +167,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
completeNodeFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
@@ -226,7 +226,7 @@ return {
assert.strictEqual(snapshot.cleanupCalls >= 1, true, 'step9 should cleanup navigation listeners');
assert.strictEqual(snapshot.hasPendingReject, false, 'step9 should clear pending reject after completion');
assert.deepStrictEqual(snapshot.completePayload, {
step: 9,
nodeId: 'confirm-oauth',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
+6 -4
View File
@@ -51,8 +51,8 @@ function createSub2ApiPanelContext(fetchCalls = []) {
removeItem(key) { storage.delete(`session:${key}`); },
},
log() {},
reportComplete(step, payload) {
context.completed.push({ step, payload });
reportComplete(nodeId, payload) {
context.completed.push({ nodeId, payload });
},
reportReady() {},
reportError() {},
@@ -177,7 +177,8 @@ test('SUB2API step 10 uses the same proxy for code exchange and account creation
assert.equal(exchangeCall.body.proxy_id, 7);
assert.equal(createCall.body.proxy_id, 7);
assert.equal(createCall.body.group_ids[0], 5);
assert.equal(context.completed[0].step, 10);
assert.equal(context.completed[0].nodeId, 'platform-verify');
assert.equal(context.completed[0].payload.visibleStep, 10);
});
test('SUB2API panel accepts Plus platform verify step 13', async () => {
@@ -202,7 +203,8 @@ test('SUB2API panel accepts Plus platform verify step 13', async () => {
assert.equal(exchangeCall.body.code, 'callback-code');
assert.equal(createCall.body.group_ids[0], 5);
assert.equal(context.completed[0].step, 13);
assert.equal(context.completed[0].nodeId, 'platform-verify');
assert.equal(context.completed[0].payload.visibleStep, 13);
});
test('SUB2API step 1 omits proxy_id when default proxy is empty', async () => {
+88 -40
View File
@@ -1,13 +1,58 @@
const test = require('node:test');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background/verification-flow.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
const rawCreateVerificationFlowHelpers = api.createVerificationFlowHelpers.bind(api);
const TEST_STEP_NODE_IDS = Object.freeze({
4: 'fetch-signup-code',
7: 'oauth-login',
8: 'fetch-login-code',
11: 'fetch-login-code',
});
const TEST_NODE_STEP_IDS = Object.fromEntries(
Object.entries(TEST_STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)])
);
function getTestNodeIdByStepForState(step) {
return TEST_STEP_NODE_IDS[Number(step)] || '';
}
function getTestStepIdByNodeId(nodeId) {
return TEST_NODE_STEP_IDS[String(nodeId || '').trim()] || null;
}
function normalizeVerificationFlowTestOverrides(overrides = {}) {
const normalized = { ...overrides };
if (
typeof normalized.completeNodeFromBackground !== 'function'
&& typeof normalized.completeStepFromBackground === 'function'
) {
const completeStepFromBackground = normalized.completeStepFromBackground;
normalized.completeNodeFromBackground = async (nodeId, payload) => (
completeStepFromBackground(getTestStepIdByNodeId(nodeId), payload)
);
}
if (
typeof normalized.setNodeStatus !== 'function'
&& typeof normalized.setStepStatus === 'function'
) {
const setStepStatus = normalized.setStepStatus;
normalized.setNodeStatus = async (nodeId, status) => (
setStepStatus(getTestStepIdByNodeId(nodeId), status)
);
}
delete normalized.completeStepFromBackground;
delete normalized.setStepStatus;
return normalized;
}
function createVerificationFlowTestHelpers(overrides = {}) {
return api.createVerificationFlowHelpers({
return rawCreateVerificationFlowHelpers({
addLog: async () => {},
buildVerificationPollPayload: null,
chrome: {
@@ -18,8 +63,9 @@ function createVerificationFlowTestHelpers(overrides = {}) {
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER: 'cloudmail',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getNodeIdByStepForState: getTestNodeIdByStepForState,
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
@@ -36,14 +82,16 @@ function createVerificationFlowTestHelpers(overrides = {}) {
sendToContentScript: async () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
setNodeStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
...overrides,
...normalizeVerificationFlowTestOverrides(overrides),
});
}
api.createVerificationFlowHelpers = createVerificationFlowTestHelpers;
test('verification flow prefers injected verification poll payload builder when provided', () => {
const helpers = createVerificationFlowTestHelpers({
buildVerificationPollPayload: (step, state, overrides = {}) => ({
@@ -85,7 +133,7 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -184,7 +232,7 @@ test('verification flow only enables 2925 target email matching in receive mode'
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -233,7 +281,7 @@ test('verification flow runs beforeSubmit hook before filling the code', async (
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (_step, payload) => {
completeNodeFromBackground: async (_step, payload) => {
events.push(['complete', payload.code]);
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
@@ -299,7 +347,7 @@ test('verification flow skips 2925 mailbox preclear when using a fixed login mai
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -360,7 +408,7 @@ test('verification flow skips 2925 mailbox preclear when using a fixed signup ma
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -436,7 +484,7 @@ test('verification flow closes the tracked iCloud mail tab after a successful ve
throw new Error('should not use family cleanup when tracked tab exists');
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -498,7 +546,7 @@ test('verification flow completes step 8 and flags phone verification when add-p
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (_step, payload) => {
completeNodeFromBackground: async (_step, payload) => {
events.push(['complete', payload.code]);
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
@@ -568,7 +616,7 @@ test('verification flow keeps step 8 successful when code submit transport fails
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (_step, payload) => {
completeNodeFromBackground: async (_step, payload) => {
events.push(['complete', payload.code]);
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
@@ -642,7 +690,7 @@ test('verification flow treats manual step 8 add-phone confirmation as the same
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {
completeNodeFromBackground: async () => {
throw new Error('should not complete step 8');
},
confirmCustomVerificationStepBypassRequest: async () => ({
@@ -689,7 +737,7 @@ test('verification flow caps mail polling timeout to the remaining oauth budget'
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -753,7 +801,7 @@ test('verification flow keeps mail polling response timeout above minimum floor'
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -816,7 +864,7 @@ test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even w
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -881,7 +929,7 @@ test('verification flow can run a 2/3/15 2925 resend polling plan', async () =>
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -953,7 +1001,7 @@ test('verification flow uses full 2925 polling window after a rejected login cod
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1026,7 +1074,7 @@ test('verification flow keeps Hotmail request timestamp filtering on the first p
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 87654,
@@ -1081,7 +1129,7 @@ test('verification flow keeps fixed filter timestamp after step 4 resend', async
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: (_step, state) => Math.max(0, Number(state.signupVerificationRequestedAt || 0) - 15000),
@@ -1147,7 +1195,7 @@ test('verification flow uses configured signup resend count for step 4', async (
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1206,7 +1254,7 @@ test('verification flow uses configured login resend count for step 8', async ()
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1265,8 +1313,8 @@ test('verification flow can complete Plus visible login-code step with shared st
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completed.push({ step, payload });
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
@@ -1310,7 +1358,7 @@ test('verification flow can complete Plus visible login-code step with shared st
assert.deepStrictEqual(fillMessages.map((message) => message.step), [8]);
assert.deepStrictEqual(completed, [
{
step: 11,
nodeId: 'fetch-login-code',
payload: {
emailTimestamp: 456,
code: '654321',
@@ -1328,7 +1376,7 @@ test('verification flow waits during resend cooldown instead of tight-looping',
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1389,7 +1437,7 @@ test('verification flow clicks resend before waiting for the next LuckMail /code
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1459,7 +1507,7 @@ test('verification flow notifies onResendRequestedAt when resend is triggered',
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1532,7 +1580,7 @@ test('verification flow uses resilient signup-page transport when submitting ver
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1582,7 +1630,7 @@ test('verification flow does not replay step 8 code submit after transient auth-
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1647,7 +1695,7 @@ test('verification flow requests a new code immediately after Cloudflare Temp Em
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (_step, payload) => {
completeNodeFromBackground: async (_step, payload) => {
completed.push(payload);
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
@@ -1726,7 +1774,7 @@ test('verification flow forwards optional signup profile payload when submitting
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1787,8 +1835,8 @@ test('verification flow keeps combined signup profile skip reason when completin
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (step, payload) => {
completed.push({ step, payload });
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
@@ -1843,7 +1891,7 @@ test('verification flow keeps combined signup profile skip reason when completin
assert.equal(resilientCalls[0].payload.signupProfile.firstName, 'Ada');
assert.deepStrictEqual(completed, [
{
step: 4,
nodeId: 'fetch-signup-code',
payload: {
emailTimestamp: 123,
code: '654321',
@@ -1869,7 +1917,7 @@ test('verification flow treats retryable submit transport failure as success whe
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1919,7 +1967,7 @@ test('verification flow avoids resend storms when iCloud polling keeps hitting t
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -1982,7 +2030,7 @@ test('verification flow stops iCloud poll-only loop after repeated no-code round
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
@@ -2043,7 +2091,7 @@ test('verification flow derives iCloud polling response timeout from the configu
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
+3 -3
View File
@@ -1,4 +1,4 @@
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const helperSource = fs.readFileSync('background.js', 'utf8');
@@ -97,7 +97,7 @@ const helpers = self.MultiPageBackgroundVerificationFlow.createVerificationFlowH
addLog,
chrome: {},
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig,
getHotmailVerificationRequestTimestamp: () => 123,
@@ -164,7 +164,7 @@ const helpers = self.MultiPageBackgroundVerificationFlow.createVerificationFlowH
addLog,
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
completeStepFromBackground: async () => {},
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 123,