Refactor workflow auto-run to node graph
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 === '步骤 7:GPC 任务状态:等待 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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' },
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user