Files
FlowPilot/tests/auto-run-step6-restart.test.js
T
2026-05-19 13:29:18 +00:00

1006 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
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'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
extractFunction('isGpcCheckoutRestartRequiredFailure'),
extractFunction('isPlusCheckoutRestartStep'),
extractFunction('isPlusCheckoutRestartRequiredFailure'),
extractFunction('getLatestLogTimestamp'),
extractFunction('buildAutoRunNodeIdleRestartError'),
extractFunction('isAutoRunStepIdleRestartError'),
extractFunction('startAutoRunNodeIdleLogWatchdog'),
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getPostStep6AutoRestartDecision'),
NODE_COMPAT_HELPERS,
extractFunction('getAutoRunWorkflowNodeIds'),
extractFunction('runAutoSequenceFromNode'),
extractFunction('runAutoSequenceFromNodeGraph'),
].join('\n');
const defaultStepDefinitions = {
1: { key: 'open-signup' },
2: { key: 'prepare-email' },
3: { key: 'fill-password' },
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'oauth-login' },
8: { key: 'fetch-login-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
const PHONE_IDENTITY_STATE_KEYS = [
'phoneNumber',
'signupPhoneNumber',
'signupPhoneActivation',
'signupPhoneCompletedActivation',
'signupPhoneVerificationRequestedAt',
'signupPhoneVerificationPurpose',
'accountIdentifierType',
'accountIdentifier',
];
function createDownstreamResetHarness(stepKey = '') {
return new Function(`
function getStepExecutionKeyForState() {
return ${JSON.stringify(stepKey)};
}
${extractFunction('getDownstreamStateResets')}
return { getDownstreamStateResets };
`)();
}
test('downstream restarts after account creation preserve phone signup identity fields', () => {
const numericResetHarness = createDownstreamResetHarness('');
const stepKeyResetHarnesses = [
createDownstreamResetHarness('oauth-login'),
createDownstreamResetHarness('fetch-login-code'),
createDownstreamResetHarness('confirm-oauth'),
];
const phoneState = {
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneActivation: { activationId: 'active', phoneNumber: '+447780579093' },
signupPhoneCompletedActivation: { activationId: 'done', phoneNumber: '+447780579093' },
};
for (const step of [5, 6, 7, 8, 9]) {
const resets = numericResetHarness.getDownstreamStateResets(step, phoneState);
for (const key of PHONE_IDENTITY_STATE_KEYS) {
assert.equal(
Object.prototype.hasOwnProperty.call(resets, key),
false,
`step ${step} reset must not clear ${key}`
);
}
}
for (const harness of stepKeyResetHarnesses) {
const resets = harness.getDownstreamStateResets(10, phoneState);
for (const key of PHONE_IDENTITY_STATE_KEYS) {
assert.equal(
Object.prototype.hasOwnProperty.call(resets, key),
false,
`${key} must not be cleared by step-key reset`
);
}
}
});
function createHarness(options = {}) {
const {
startStep = 7,
failureStep = 10,
failureBudget = 1,
failureMessage = '认证失败: Request failed with status code 502',
authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
customState = {},
stepDefinitions = defaultStepDefinitions,
stepIds = Object.keys(stepDefinitions).map(Number).sort((a, b) => a - b),
lastStepId = Math.max(...stepIds),
finalOAuthChainStartStep = 7,
idleLogTimeoutMs = 300000,
idleLogCheckIntervalMs = 5000,
hangStep = 0,
hangBudget = 0,
} = options;
return new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0 };
const LAST_STEP_ID = ${JSON.stringify(lastStepId)};
const FINAL_OAUTH_CHAIN_START_STEP = ${JSON.stringify(finalOAuthChainStartStep)};
const SIGNUP_METHOD_PHONE = 'phone';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = ${JSON.stringify(idleLogTimeoutMs)};
const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = ${JSON.stringify(idleLogCheckIntervalMs)};
const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;
const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
update: async () => {},
},
};
let remainingFailures = ${JSON.stringify(failureBudget)};
let remainingHangs = ${JSON.stringify(hangBudget)};
const events = {
steps: [],
logs: [],
invalidations: [],
cancellations: [],
stopBroadcasts: 0,
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level, timestamp: Date.now() });
}
async function ensureAutoEmailReady() {}
async function ensureResolvedSignupMethodForRun() { return 'email'; }
async function broadcastAutoRunStatus() {}
async function getState() {
return {
stepStatuses: { 3: 'completed' },
mailProvider: '163',
logs: events.logs,
...${JSON.stringify(customState)},
};
}
function getStepIdsForState() {
return ${JSON.stringify(stepIds)};
}
function getStepDefinitionForState(step) {
const map = ${JSON.stringify(stepDefinitions)};
return map[Number(step)] || null;
}
function getStepExecutionKeyForState(step, state = {}) {
return String(getStepDefinitionForState(step, state)?.key || '').trim();
}
function isStopError(error) {
return (error?.message || String(error || '')) === '流程已被用户停止。';
}
function isStepDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === ${JSON.stringify(hangStep)} && remainingHangs > 0) {
remainingHangs -= 1;
return new Promise(() => {});
}
if (step === ${JSON.stringify(failureStep)} && remainingFailures > 0) {
remainingFailures -= 1;
throw new Error(${JSON.stringify(failureMessage)});
}
}
async function getTabId() {
return 1;
}
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
events.invalidations.push({ step, options });
}
function cancelPendingCommands(reason = '') {
events.cancellations.push(reason);
}
async function broadcastStopToContentScripts() {
events.stopBroadcasts += 1;
}
function getLoginAuthStateLabel(state) {
return state || 'unknown';
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === PLUS_PAYMENT_METHOD_GPC_HELPER ? PLUS_PAYMENT_METHOD_GPC_HELPER : normalized;
}
function isPhoneSmsPlatformRateLimitFailure(error) {
const message = getErrorMessage(error);
return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message);
}
async function getLoginAuthStateFromContent() {
return ${JSON.stringify(authState)};
}
${bundle}
return {
async run() {
await runAutoSequenceFromStep(${JSON.stringify(startStep)}, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return events;
},
async runAndCaptureError() {
try {
await runAutoSequenceFromStep(${JSON.stringify(startStep)}, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return null;
} catch (error) {
return { error, events };
}
},
};
`)();
}
test('auto-run keeps restarting from step 7 after post-login failures without a hard cap', async () => {
const harness = createHarness({
failureStep: 10,
failureBudget: 6,
failureMessage: '认证失败: Request failed with status code 502',
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
});
const events = await harness.run();
assert.equal(events.invalidations.length, 6);
assert.deepStrictEqual(
events.steps,
[
7, 8, 9, 10,
7, 8, 9, 10,
7, 8, 9, 10,
7, 8, 9, 10,
7, 8, 9, 10,
7, 8, 9, 10,
7, 8, 9, 10,
]
);
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
});
test('auto-run restarts the current step after five minutes without new logs', async () => {
const harness = createHarness({
startStep: 10,
failureStep: 0,
hangStep: 10,
hangBudget: 1,
idleLogTimeoutMs: 20,
idleLogCheckIntervalMs: 5,
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [10, 10]);
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)));
});
test('auto-run applies the idle-log restart watchdog to early steps too', async () => {
const harness = createHarness({
startStep: 2,
failureStep: 0,
hangStep: 2,
hangBudget: 1,
idleLogTimeoutMs: 20,
idleLogCheckIntervalMs: 5,
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [2, 2, 4, 5, 6, 7, 8, 9, 10]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [1]);
assert.equal(events.cancellations.length, 1);
assert.equal(events.stopBroadcasts, 1);
});
test('auto-run stops current-step idle restarts after the retry cap', async () => {
const harness = createHarness({
startStep: 10,
failureStep: 0,
hangStep: 10,
hangBudget: 4,
idleLogTimeoutMs: 20,
idleLogCheckIntervalMs: 5,
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
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)));
});
test('auto-run stops restarting once add-phone is detected', async () => {
const harness = createHarness({
failureStep: 7,
failureBudget: 1,
failureMessage: '当前页面已进入手机号页面。URL: https://auth.openai.com/add-phone',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7]);
assert.ok(result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
});
test('auto-run stops restarting on generic phone-page failure messages even without add-phone url', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 1,
failureMessage: '步骤 8:当前认证页进入手机号页面,当前流程无法继续自动授权。',
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run does not restart step 7 when phone verification exhausted replacement attempts in add-phone flow', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 1,
failureMessage: 'Step 9: phone verification did not succeed after 3 number replacements. Last reason: sms_timeout_after_resend.',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run post-login restart decision does not treat 5sim rate limit on add-phone page as add-phone fatal', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 1,
failureMessage: 'FIVE_SIM_RATE_LIMIT::5sim 购买接口触发限流,请稍后再试:印度 (India): rate limit。',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 1,
failureMessage: '流程已被用户停止。',
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
});
const result = await harness.runAndCaptureError();
assert.equal(result?.error?.message, '流程已被用户停止。');
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run restarts from confirm-oauth step after transient step10 token_exchange_user_error', async () => {
const harness = createHarness({
failureStep: 10,
failureBudget: 1,
failureMessage: 'token exchange failed: status 400, body: { "error": { "message": "Invalid request. Please try again later.", "type": "invalid_request_error", "param": null, "code": "token_exchange_user_error" } }',
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
customState: {
panelMode: 'sub2api',
stepStatuses: { 3: 'completed' },
stepsVersion: 'ultra2.0',
visibleStep: 10,
accountContributionEnabled: false,
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [7, 8, 9, 10, 9, 10]);
assert.equal(events.invalidations.length, 1);
assert.deepStrictEqual(events.invalidations[0], {
step: 8,
options: {
logLabel: '节点 platform-verify 报错后准备回到 confirm-oauth 重试(第 1 次重开)',
},
});
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 () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 10,
failureStep: 10,
failureBudget: 1,
failureMessage: '步骤 10:判断失败后已重试 2 次,仍未成功。最后原因:点击登录入口后仍未进入手机号/邮箱/密码/验证码页。',
authState: { state: 'entry_page', url: 'https://auth.openai.com/log-in' },
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [10, 10, 11, 12, 13]);
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 () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
for (const scenario of [
{
failureStep: 11,
failureMessage: '步骤 11:无法获取新的登录验证码。',
expectedSteps: [10, 11, 10, 11, 12, 13],
},
{
failureStep: 12,
failureMessage: '步骤 12:长时间未进入 OAuth 同意页,无法定位“继续”按钮。',
expectedSteps: [10, 11, 12, 10, 11, 12, 13],
},
]) {
const harness = createHarness({
startStep: 10,
failureStep: scenario.failureStep,
failureBudget: 1,
failureMessage: scenario.failureMessage,
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, scenario.expectedSteps);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [7]);
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
}
});
test('auto-run restarts Plus/GPC transient platform verify exchange failures from confirm-oauth step 12', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 10,
failureStep: 13,
failureBudget: 1,
failureMessage: 'token exchange failed at https://auth.openai.com/oauth/token: token_exchange_user_error: Invalid request. Please try again later.',
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed', 6: 'completed', 7: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
panelMode: 'sub2api',
},
});
const events = await harness.run();
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 }) => /节点 platform-verify.*回到节点 confirm-oauth 重新开始授权流程/.test(message)));
});
test('auto-run restarts Plus checkout from step 6 when checkout creation fails', async () => {
const plusPaypalSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
8: { key: 'paypal-approve' },
9: { key: 'plus-checkout-return' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 6,
failureBudget: 1,
failureMessage: '步骤 6:创建 Plus Checkout 失败:checkout request failed',
stepDefinitions: plusPaypalSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
},
});
const events = await harness.run();
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 }) => /回到节点 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 () => {
const plusPaypalSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
8: { key: 'paypal-approve' },
9: { key: 'plus-checkout-return' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: '步骤 7:账单地址 iframe 无法注入,请重新创建 checkout。',
stepDefinitions: plusPaypalSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
},
});
const events = await harness.run();
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 }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 2,
failureMessage: 'GPC API 请求超时(>30 秒):https://gpc.qlhazycoder.top/api/gp/tasks/task_stalled',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(
events.steps,
[6, 7, 6, 7, 6, 7, 10, 11, 12, 13]
);
assert.deepStrictEqual(
events.invalidations.map((entry) => entry.step),
[5, 5]
);
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 () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GOPAY已经绑了订阅,需要手动解绑',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
});
test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 6,
failureBudget: 1,
failureMessage: '步骤 6GPC 模式获取 accessToken 失败。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
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 }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GPC 任务状态超过 60 秒无进展(已创建),请重新创建任务。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
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 }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
});
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 4,
failureMessage: 'GPC_TASK_ENDED::GPC task status stalled, recreate the task.',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(
events.steps,
[6, 7, 6, 7, 6, 7, 6, 7, 6, 7, 10, 11, 12, 13]
);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5, 5, 5, 5]);
assert.ok(events.logs.some(({ message }) => / 4 /.test(message)));
});
test('auto-run does not restart GPC checkout when Plus account has no free-trial eligibility', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(IDR 299000),当前账号没有免费试用资格,已跳过支付提交。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.deepStrictEqual(result.events.steps, [6, 7]);
assert.equal(result.events.invalidations.length, 0);
});
test('auto-run does not restart GPC checkout when account already has a ChatGPT subscription', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::该账号已经开通过ChatGPT订阅套餐,不能重复订阅。(checkout_order',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.deepStrictEqual(result.events.steps, [6, 7]);
assert.equal(result.events.invalidations.length, 0);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
});
test('auto-run does not reroute SUB2API session import failures into OAuth restarts', async () => {
const plusSessionSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
8: { key: 'paypal-approve' },
9: { key: 'plus-checkout-return' },
10: { key: 'sub2api-session-import' },
};
const harness = createHarness({
startStep: 10,
failureStep: 10,
failureBudget: 1,
failureMessage: '步骤 10:当前页面未读取到有效的 ChatGPT session。',
stepDefinitions: plusSessionSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: {
3: 'completed',
6: 'completed',
7: 'completed',
8: 'completed',
9: 'completed',
},
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
panelMode: 'sub2api',
plusAccountAccessStrategy: 'sub2api_codex_session',
},
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.match(result.error.message, /未读取到有效的 ChatGPT session/);
assert.deepStrictEqual(result.events.steps, [10]);
assert.equal(result.events.invalidations.length, 0);
assert.ok(!result.events.logs.some(({ message }) => /回到节点 oauth-login|回到节点 confirm-oauth|重新开始授权流程/.test(message)));
});