Add Plus SUB2API session access strategy

This commit is contained in:
QLHazyCoder
2026-05-19 06:14:35 +08:00
parent a3c36e6f15
commit de17884a30
25 changed files with 2380 additions and 49 deletions
+42 -3
View File
@@ -173,8 +173,8 @@ const defaultStepDefinitions = {
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'auth-login' },
8: { key: 'auth-email-code' },
7: { key: 'oauth-login' },
8: { key: 'fetch-login-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
@@ -407,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 }) => /回到节点 auth-login 重新开始授权流程/.test(message)));
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
});
test('auto-run restarts the current step after five minutes without new logs', async () => {
@@ -964,3 +964,42 @@ test('auto-run does not restart GPC checkout when account already has a ChatGPT
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)));
});
@@ -227,6 +227,8 @@ return {
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gpc-helper'), 'gpc-helper');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal');
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'sub2api_codex_session'), 'sub2api_codex_session');
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'unknown'), 'oauth');
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
'https://gpc.qlhazycoder.top'
@@ -48,6 +48,15 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('background auth chain set does not include SUB2API session import node', () => {
const authChainStart = source.indexOf('const AUTH_CHAIN_NODE_IDS = new Set([');
const authChainEnd = source.indexOf(']);', authChainStart);
const authChainBlock = source.slice(authChainStart, authChainEnd);
assert.ok(authChainStart >= 0, 'expected AUTH_CHAIN_NODE_IDS block to exist');
assert.doesNotMatch(authChainBlock, /sub2api-session-import/);
});
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 = {
@@ -0,0 +1,185 @@
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);
}
function createHarness() {
return new Function(`
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const workflowEngine = null;
const self = {
MultiPageStepDefinitions: {
getSteps(options = {}) {
return String(options.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
? [
{ id: 1, key: 'open-chatgpt' },
{ id: 2, key: 'plus-checkout-create' },
{ id: 3, key: 'plus-checkout-billing' },
{ id: 4, key: 'paypal-approve' },
{ id: 5, key: 'plus-checkout-return' },
{ id: 6, key: 'sub2api-session-import' },
]
: [
{ id: 1, key: 'open-chatgpt' },
{ id: 2, key: 'plus-checkout-create' },
{ id: 3, key: 'plus-checkout-billing' },
{ id: 4, key: 'paypal-approve' },
{ id: 5, key: 'plus-checkout-return' },
{ id: 6, key: 'oauth-login' },
{ id: 7, key: 'fetch-login-code' },
{ id: 8, key: 'confirm-oauth' },
{ id: 9, key: 'platform-verify' },
];
},
getNodes(options = {}) {
return this.getSteps(options).map((definition) => ({
nodeId: String(definition.key || '').trim(),
displayOrder: Number(definition.id) || 0,
}));
},
},
};
function normalizeSignupMethod(value = '') {
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
}
function normalizePlusAccountAccessStrategy(value = '') {
return String(value || '').trim().toLowerCase() === 'sub2api_codex_session'
? 'sub2api_codex_session'
: 'oauth';
}
function isPlusModeState(state = {}) {
return Boolean(state?.plusModeEnabled);
}
function resolveCurrentFlowCapabilities(state = {}, options = {}) {
const effectiveStrategy = String(options.panelMode || '').trim().toLowerCase() === 'sub2api'
? normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy)
: 'oauth';
return {
effectivePanelMode: options.panelMode,
effectivePlusAccountAccessStrategy: effectiveStrategy,
effectiveSignupMethod: 'email',
stepDefinitionOptions: {
activeFlowId: 'openai',
panelMode: options.panelMode,
plusModeEnabled: Boolean(state.plusModeEnabled),
plusPaymentMethod: normalizePlusPaymentMethod(state.plusPaymentMethod),
plusAccountAccessStrategy: effectiveStrategy,
signupMethod: 'email',
},
};
}
${extractFunction('getSignupMethodForStepDefinitions')}
${extractFunction('buildResolvedStepDefinitionState')}
${extractFunction('getStepDefinitionsForState')}
${extractFunction('getNodeDefinitionsForState')}
${extractFunction('getNodeIdsForState')}
return {
buildResolvedStepDefinitionState,
getNodeIdsForState,
};
`)();
}
test('background step resolution keeps SUB2API session tail only when the effective Plus target supports it', () => {
const api = createHarness();
const state = {
activeFlowId: 'openai',
flowId: 'openai',
panelMode: 'sub2api',
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
};
const resolvedState = api.buildResolvedStepDefinitionState(state);
const nodeIds = api.getNodeIdsForState(state);
assert.equal(resolvedState.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.deepStrictEqual(nodeIds, [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'sub2api-session-import',
]);
});
test('background step resolution falls back to OAuth tail when the requested session strategy is not effective for the current panel mode', () => {
const api = createHarness();
const state = {
activeFlowId: 'openai',
flowId: 'openai',
panelMode: 'cpa',
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
};
const resolvedState = api.buildResolvedStepDefinitionState(state);
const nodeIds = api.getNodeIdsForState(state);
assert.equal(resolvedState.plusAccountAccessStrategy, 'oauth');
assert.equal(nodeIds.includes('sub2api-session-import'), false);
assert.deepStrictEqual(nodeIds.slice(-4), [
'oauth-login',
'fetch-login-code',
'confirm-oauth',
'platform-verify',
]);
});
@@ -297,6 +297,299 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
assert.equal(logs.length, 0);
});
test('SAVE_SETTING rebuilds Plus node statuses when the account access strategy changes', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
const broadcasts = [];
let state = {
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'oauth',
oauthUrl: 'https://oauth.example/current',
localhostUrl: 'http://localhost:38080/callback',
oauthFlowDeadlineAt: Date.now() + 60000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
cpaOAuthState: 'cpa-state',
cpaManagementOrigin: 'https://cpa.example.com',
sub2apiSessionId: 'sub-session',
sub2apiOAuthState: 'sub-oauth-state',
sub2apiGroupId: 'group-id',
sub2apiGroupIds: ['group-id'],
sub2apiDraftName: 'draft-name',
sub2apiProxyId: 'proxy-id',
codex2apiSessionId: 'codex-session',
codex2apiOAuthState: 'codex-oauth-state',
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'gopay-req',
plusManualConfirmationStep: 9,
plusManualConfirmationMethod: 'gopay',
plusManualConfirmationTitle: 'GoPay 订阅确认',
plusManualConfirmationMessage: '完成后继续 OAuth 登录。',
currentNodeId: 'confirm-oauth',
nodeStatuses: {
'open-chatgpt': 'completed',
'plus-checkout-create': 'completed',
'plus-checkout-billing': 'completed',
'paypal-approve': 'completed',
'plus-checkout-return': 'completed',
'oauth-login': 'completed',
'fetch-login-code': 'completed',
'post-login-phone-verification': 'completed',
'confirm-oauth': 'running',
'platform-verify': 'pending',
},
};
const router = api.createMessageRouter({
addLog: async () => {},
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'plusAccountAccessStrategy')
? { plusAccountAccessStrategy: input.plusAccountAccessStrategy }
: {},
broadcastDataUpdate: (payload) => broadcasts.push(payload),
getNodeIdsForState: (nextState = {}) => (
String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
? [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'sub2api-session-import',
]
: [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'oauth-login',
'fetch-login-code',
'post-login-phone-verification',
'confirm-oauth',
'platform-verify',
]
),
getState: async () => ({ ...state }),
getStepIdsForState: () => [],
setPersistentSettings: async (updates) => ({ ...updates }),
setState: async (updates) => {
state = { ...state, ...updates };
},
});
const response = await router.handleMessage({
type: 'SAVE_SETTING',
payload: {
plusAccountAccessStrategy: 'sub2api_codex_session',
},
});
assert.equal(response.ok, true);
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(state.currentNodeId, '');
assert.equal(state.oauthUrl, null);
assert.equal(state.localhostUrl, null);
assert.equal(state.oauthFlowDeadlineAt, null);
assert.equal(state.oauthFlowDeadlineSourceUrl, null);
assert.equal(state.cpaOAuthState, null);
assert.equal(state.cpaManagementOrigin, null);
assert.equal(state.sub2apiSessionId, null);
assert.equal(state.sub2apiOAuthState, null);
assert.equal(state.sub2apiGroupId, null);
assert.deepStrictEqual(state.sub2apiGroupIds, []);
assert.equal(state.sub2apiDraftName, null);
assert.equal(state.sub2apiProxyId, null);
assert.equal(state.codex2apiSessionId, null);
assert.equal(state.codex2apiOAuthState, null);
assert.equal(state.plusManualConfirmationPending, false);
assert.equal(state.plusManualConfirmationRequestId, '');
assert.equal(state.plusManualConfirmationStep, 0);
assert.equal(state.plusManualConfirmationMethod, '');
assert.equal(state.plusManualConfirmationTitle, '');
assert.equal(state.plusManualConfirmationMessage, '');
assert.deepStrictEqual(state.nodeStatuses, {
'open-chatgpt': 'pending',
'plus-checkout-create': 'pending',
'plus-checkout-billing': 'pending',
'paypal-approve': 'pending',
'plus-checkout-return': 'pending',
'sub2api-session-import': 'pending',
});
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'oauth-login'), false);
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'platform-verify'), false);
assert.deepStrictEqual(broadcasts.at(-1), {
plusAccountAccessStrategy: 'sub2api_codex_session',
oauthUrl: null,
localhostUrl: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
cpaOAuthState: null,
cpaManagementOrigin: null,
sub2apiSessionId: null,
sub2apiOAuthState: null,
sub2apiGroupId: null,
sub2apiGroupIds: [],
sub2apiDraftName: null,
sub2apiProxyId: null,
codex2apiSessionId: null,
codex2apiOAuthState: null,
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: '',
plusManualConfirmationStep: 0,
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
nodeStatuses: {
'open-chatgpt': 'pending',
'plus-checkout-create': 'pending',
'plus-checkout-billing': 'pending',
'paypal-approve': 'pending',
'plus-checkout-return': 'pending',
'sub2api-session-import': 'pending',
},
currentNodeId: '',
});
});
test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effective strategy back to OAuth', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
const broadcasts = [];
let state = {
panelMode: 'sub2api',
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
oauthUrl: 'https://oauth.example/current',
localhostUrl: 'http://localhost:38080/callback',
sub2apiSessionId: 'sub-session',
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'gopay-req',
plusManualConfirmationStep: 9,
plusManualConfirmationMethod: 'gopay',
plusManualConfirmationTitle: 'GoPay 订阅确认',
plusManualConfirmationMessage: '完成后继续导入当前 ChatGPT 会话到 SUB2API。',
currentNodeId: 'sub2api-session-import',
nodeStatuses: {
'open-chatgpt': 'completed',
'plus-checkout-create': 'completed',
'plus-checkout-billing': 'completed',
'paypal-approve': 'completed',
'plus-checkout-return': 'completed',
'sub2api-session-import': 'running',
},
};
const router = api.createMessageRouter({
addLog: async () => {},
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'panelMode')
? { panelMode: input.panelMode }
: {},
broadcastDataUpdate: (payload) => broadcasts.push(payload),
getNodeIdsForState: (nextState = {}) => (
String(nextState.panelMode || '').trim() === 'sub2api'
&& String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
? [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'sub2api-session-import',
]
: [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'oauth-login',
'fetch-login-code',
'post-login-phone-verification',
'confirm-oauth',
'platform-verify',
]
),
getState: async () => ({ ...state }),
getStepIdsForState: () => [],
setPersistentSettings: async (updates) => ({ ...updates }),
setState: async (updates) => {
state = { ...state, ...updates };
},
});
const response = await router.handleMessage({
type: 'SAVE_SETTING',
payload: {
panelMode: 'cpa',
},
});
assert.equal(response.ok, true);
assert.equal(state.panelMode, 'cpa');
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(state.currentNodeId, '');
assert.equal(state.oauthUrl, null);
assert.equal(state.localhostUrl, null);
assert.equal(state.sub2apiSessionId, null);
assert.equal(state.plusManualConfirmationPending, false);
assert.equal(state.plusManualConfirmationMessage, '');
assert.deepStrictEqual(state.nodeStatuses, {
'open-chatgpt': 'pending',
'plus-checkout-create': 'pending',
'plus-checkout-billing': 'pending',
'paypal-approve': 'pending',
'plus-checkout-return': 'pending',
'oauth-login': 'pending',
'fetch-login-code': 'pending',
'post-login-phone-verification': 'pending',
'confirm-oauth': 'pending',
'platform-verify': 'pending',
});
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'sub2api-session-import'), false);
assert.deepStrictEqual(broadcasts.at(-1), {
panelMode: 'cpa',
signupMethod: 'email',
oauthUrl: null,
localhostUrl: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
cpaOAuthState: null,
cpaManagementOrigin: null,
sub2apiSessionId: null,
sub2apiOAuthState: null,
sub2apiGroupId: null,
sub2apiGroupIds: [],
sub2apiDraftName: null,
sub2apiProxyId: null,
codex2apiSessionId: null,
codex2apiOAuthState: null,
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: '',
plusManualConfirmationStep: 0,
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
nodeStatuses: {
'open-chatgpt': 'pending',
'plus-checkout-create': 'pending',
'plus-checkout-billing': 'pending',
'paypal-approve': 'pending',
'plus-checkout-return': 'pending',
'oauth-login': 'pending',
'fetch-login-code': 'pending',
'post-login-phone-verification': 'pending',
'confirm-oauth': 'pending',
'platform-verify': 'pending',
},
currentNodeId: '',
});
});
test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
@@ -6,8 +6,25 @@ const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
function createRouterWithFinalNode(options = {}) {
const finalNodeId = String(options.finalNodeId || 'platform-verify').trim();
const nodeIds = Array.isArray(options.nodeIds) ? options.nodeIds.slice() : [
'open-chatgpt',
'oauth-login',
'fetch-login-code',
'confirm-oauth',
finalNodeId,
];
const nodeStepMap = {
'oauth-login': 10,
'fetch-login-code': 11,
'confirm-oauth': 12,
'platform-verify': 13,
'sub2api-session-import': 10,
...(options.nodeStepMap || {}),
};
const appendCalls = [];
const router = api.createMessageRouter({
addLog: async () => {},
appendAccountRunRecord: async (...args) => {
@@ -43,12 +60,15 @@ test('message router appends success record on Plus final step instead of hard-c
getCurrentLuckmailPurchase: () => null,
getPendingAutoRunTimerPlan: () => null,
getSourceLabel: () => '',
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],
getState: async () => ({ plusModeEnabled: true, nodeStatuses: { [finalNodeId]: 'pending' } }),
getNodeIdsForState: () => nodeIds.slice(),
getStepIdByNodeIdForState: (nodeId) => nodeStepMap[nodeId] || 0,
getLastStepIdForState: () => Math.max(...Object.values(nodeStepMap)),
getStepDefinitionForState: (step) => ({
id: step,
key: Object.entries(nodeStepMap).find(([, mappedStep]) => mappedStep === step)?.[0] || finalNodeId,
}),
getStepIdsForState: () => Object.values(nodeStepMap),
getTabId: async () => null,
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
@@ -100,8 +120,56 @@ test('message router appends success record on Plus final step instead of hard-c
verifyHotmailAccount: async () => {},
});
return {
appendCalls,
router,
};
}
test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
const { appendCalls, router } = createRouterWithFinalNode({
finalNodeId: 'platform-verify',
nodeIds: ['open-chatgpt', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
nodeStepMap: {
'oauth-login': 10,
'fetch-login-code': 11,
'confirm-oauth': 12,
'platform-verify': 13,
},
});
await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'platform-verify', payload: { nodeId: 'platform-verify' } }, {});
assert.equal(appendCalls.length, 1);
assert.equal(appendCalls[0][0], 'success');
});
test('message router appends success record when SUB2API session import is the final Plus node', async () => {
const { appendCalls, router } = createRouterWithFinalNode({
finalNodeId: 'sub2api-session-import',
nodeIds: [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
'sub2api-session-import',
],
nodeStepMap: {
'plus-checkout-create': 6,
'plus-checkout-billing': 7,
'paypal-approve': 8,
'plus-checkout-return': 9,
'sub2api-session-import': 10,
},
});
await router.handleMessage({
type: 'NODE_COMPLETE',
nodeId: 'sub2api-session-import',
payload: { nodeId: 'sub2api-session-import' },
}, {});
assert.equal(appendCalls.length, 1);
assert.equal(appendCalls[0][0], 'success');
});
@@ -80,6 +80,7 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'phoneSignupReloginAfterBindEmailEnabled',
'plusModeEnabled',
'plusPaymentMethod',
'plusAccountAccessStrategy',
'mailProvider',
'ipProxyEnabled',
'ipProxyService',
@@ -95,6 +96,7 @@ const PERSISTED_SETTING_DEFAULTS = {
signupMethod: 'email',
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'oauth',
phoneVerificationEnabled: false,
mailProvider: '163',
ipProxyEnabled: false,
@@ -249,6 +251,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'oauth',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
@@ -276,6 +279,7 @@ test('buildPersistentSettingsPayload accepts schema-only input when requireKnown
assert.equal(payload.kiroRsKey, 'schema-only-key');
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 4);
assert.equal(payload.settingsState.flows.openai.plus.plusAccountAccessStrategy, 'oauth');
});
test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => {
@@ -300,6 +304,7 @@ function getRequestedKeys() {
assert.ok(api.getRequestedKeys().includes('settingsSchemaVersion'));
assert.ok(api.getRequestedKeys().includes('settingsState'));
assert.ok(api.getRequestedKeys().includes('plusAccountAccessStrategy'));
assert.equal(state.settingsSchemaVersion, 4);
assert.equal(state.settingsState.activeFlowId, 'openai');
});
@@ -350,6 +355,7 @@ const chrome = {
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
@@ -384,6 +390,7 @@ const chrome = {
assert.equal(state.ipProxyEnabled, true);
assert.equal(state.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(state.kiroRsKey, 'stored-key');
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(Object.prototype.hasOwnProperty.call(state, 'kiroRegion'), false);
assert.deepEqual(state.stepExecutionRangeByFlow.kiro, {
enabled: true,
@@ -459,6 +466,7 @@ function getRemovedKeys() {
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
@@ -486,6 +494,7 @@ function getRemovedKeys() {
assert.equal(persisted.kiroTargetId, 'kiro-rs');
assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(persisted.kiroRsKey, 'nested-only-key');
assert.equal(persisted.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(Object.prototype.hasOwnProperty.call(persisted, 'kiroRegion'), false);
assert.equal(persisted.settingsSchemaVersion, 4);
assert.equal(Object.prototype.hasOwnProperty.call(write, 'activeFlowId'), false);
@@ -494,6 +503,7 @@ function getRemovedKeys() {
assert.equal(Object.prototype.hasOwnProperty.call(write, 'kiroRegion'), false);
assert.equal(write.settingsSchemaVersion, 4);
assert.equal(write.settingsState.activeFlowId, 'kiro');
assert.equal(write.settingsState.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(write.settingsState.flows.kiro.targetId, 'kiro-rs');
assert.ok(api.getRemovedKeys().includes('panelMode'));
assert.ok(api.getRemovedKeys().includes('kiroRsUrl'));
@@ -0,0 +1,289 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
function createJsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
text: async () => JSON.stringify(payload),
};
}
function loadSub2ApiApiModule() {
const source = fs.readFileSync('background/sub2api-api.js', 'utf8');
return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiApi;`)({});
}
function loadSub2ApiSessionImportModule() {
const source = fs.readFileSync('background/steps/sub2api-session-import.js', 'utf8');
return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiSessionImport;`)({});
}
test('sub2api api imports current ChatGPT session through codex-session endpoint', async () => {
const apiModule = loadSub2ApiApiModule();
const fetchCalls = [];
const logs = [];
const importExpiresAt = Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000);
const api = apiModule.createSub2ApiApi({
addLog: async (message, level = 'info', options = {}) => {
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
normalizeSub2ApiUrl: (value) => value,
DEFAULT_SUB2API_GROUP_NAME: 'codex',
fetchImpl: async (url, options = {}) => {
const parsed = new URL(url);
const body = options.body ? JSON.parse(options.body) : null;
fetchCalls.push({ path: parsed.pathname, search: parsed.search, method: options.method || 'GET', body });
if (parsed.pathname === '/api/v1/auth/login') {
return createJsonResponse({
code: 0,
data: {
access_token: 'admin-token',
},
});
}
if (parsed.pathname === '/api/v1/admin/groups/all') {
return createJsonResponse({
code: 0,
data: [
{ id: 5, name: 'codex', platform: 'openai' },
],
});
}
if (parsed.pathname === '/api/v1/admin/proxies/all') {
return createJsonResponse({
code: 0,
data: [{
id: 7,
name: 'shadowrocket',
protocol: 'socks5',
host: '127.0.0.1',
port: 1080,
status: 'active',
}],
});
}
if (parsed.pathname === '/api/v1/admin/accounts/import/codex-session') {
const parsedContent = JSON.parse(body.content);
assert.equal(parsedContent.accessToken, 'access-token-from-state');
assert.equal(parsedContent.user?.email, 'flow@example.com');
assert.equal(body.priority, 3);
assert.equal(body.proxy_id, 7);
assert.deepStrictEqual(body.group_ids, [5]);
assert.equal(body.auto_pause_on_expired, true);
assert.equal(body.update_existing, true);
assert.equal(body.expires_at, importExpiresAt);
return createJsonResponse({
code: 0,
data: {
total: 1,
created: 0,
updated: 1,
skipped: 0,
failed: 0,
warnings: [{
index: 1,
message: '未包含 refresh_tokenaccessToken 过期后无法自动续期',
}],
},
});
}
return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
},
});
const result = await api.importCurrentChatGptSession({
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiDefaultProxyName: 'shadowrocket',
sub2apiAccountPriority: 3,
session: {
accessToken: 'access-token-from-session',
expires: '2026-05-20T12:34:56.000Z',
user: {
email: 'flow@example.com',
},
},
accessToken: 'access-token-from-state',
}, {
logLabel: '步骤 10',
logOptions: { step: 10, stepKey: 'sub2api-session-import' },
timeoutMs: 120000,
});
const importCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts/import/codex-session');
assert.ok(importCall, 'expected codex-session import call');
assert.equal(result.verifiedStatus, 'SUB2API 会话导入完成:新建 0,更新 1,跳过 0,失败 0');
assert.equal(result.sub2apiImportUpdated, 1);
assert.equal(
logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
true
);
});
test('session import step reads current ChatGPT session and completes node', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const completed = [];
const logs = [];
const ensureCalls = [];
const sentMessages = [];
const importedPayloads = [];
const executor = moduleApi.createSub2ApiSessionImportExecutor({
addLog: async (message, level = 'info', options = {}) => {
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
chrome: {
tabs: {
get: async (tabId) => ({
id: tabId,
url: 'https://chatgpt.com/?model=gpt-4o',
}),
update: async () => {},
},
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
createSub2ApiApi: () => ({
importCurrentChatGptSession: async (state, options) => {
importedPayloads.push({ state, options });
return {
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
sub2apiImportCreated: 1,
sub2apiImportUpdated: 0,
sub2apiImportSkipped: 0,
sub2apiImportFailed: 0,
};
},
}),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options = {}) => {
ensureCalls.push({ source, tabId, options });
},
getTabId: async () => null,
isTabAlive: async () => false,
normalizeSub2ApiUrl: (value) => value,
registerTab: async () => {},
sendTabMessageUntilStopped: async (tabId, source, message) => {
sentMessages.push({ tabId, source, message });
return {
session: {
accessToken: 'session-access-token',
expires: '2026-05-20T12:34:56.000Z',
user: {
email: 'flow@example.com',
},
},
accessToken: 'session-access-token',
};
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabCompleteUntilStopped: async () => {},
DEFAULT_SUB2API_GROUP_NAME: 'codex',
});
await executor.executeSub2ApiSessionImport({
nodeId: 'sub2api-session-import',
visibleStep: 10,
plusCheckoutTabId: 91,
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
});
assert.equal(ensureCalls.length, 1);
assert.equal(ensureCalls[0].source, 'plus-checkout');
assert.deepStrictEqual(ensureCalls[0].options.inject, [
'content/utils.js',
'content/operation-delay.js',
'content/plus-checkout.js',
]);
assert.deepStrictEqual(sentMessages, [{
tabId: 91,
source: 'plus-checkout',
message: {
type: 'PLUS_CHECKOUT_GET_STATE',
source: 'background',
payload: {
includeSession: true,
includeAccessToken: true,
},
},
}]);
assert.equal(importedPayloads.length, 1);
assert.equal(importedPayloads[0].state.accessToken, 'session-access-token');
assert.equal(importedPayloads[0].state.session.user.email, 'flow@example.com');
assert.equal(completed.length, 1);
assert.deepStrictEqual(completed[0], {
nodeId: 'sub2api-session-import',
payload: {
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
sub2apiImportCreated: 1,
sub2apiImportUpdated: 0,
sub2apiImportSkipped: 0,
sub2apiImportFailed: 0,
},
});
assert.equal(
logs.some((entry) => entry.stepKey === 'sub2api-session-import' && /读取当前 ChatGPT 登录会话/.test(entry.message)),
true
);
});
test('session import step rejects unsupported non-chatgpt tabs before reading session', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
let sendCalled = false;
const executor = moduleApi.createSub2ApiSessionImportExecutor({
addLog: async () => {},
chrome: {
tabs: {
get: async (tabId) => ({
id: tabId,
url: 'https://example.com/not-chatgpt',
}),
update: async () => {},
},
},
completeNodeFromBackground: async () => {},
createSub2ApiApi: () => ({
importCurrentChatGptSession: async () => ({}),
}),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
getTabId: async () => 91,
isTabAlive: async () => true,
normalizeSub2ApiUrl: (value) => value,
sendTabMessageUntilStopped: async () => {
sendCalled = true;
return {};
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executeSub2ApiSessionImport({
nodeId: 'sub2api-session-import',
visibleStep: 10,
}),
/当前标签页不在 ChatGPT \/ OpenAI 页面/
);
assert.equal(sendCalled, false);
});
test('background wires sub2api session import executor into the workflow runtime', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /background\/steps\/sub2api-session-import\.js/);
assert.match(source, /'sub2api-session-import': \(state\) => sub2ApiSessionImportExecutor\.executeSub2ApiSessionImport\(state\)/);
assert.match(source, /'sub2api-session-import',\s*\n\s*'oauth-login'/);
});
+45
View File
@@ -191,3 +191,48 @@ test('flow capability registry normalizes unsupported mode switches back to the
]
);
});
test('flow capability registry exposes editable Plus account access strategies for SUB2API', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry();
const capabilityState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'openai',
openaiIntegrationTargetId: 'sub2api',
signupMethod: 'email',
plusModeEnabled: true,
plusAccountAccessStrategy: 'sub2api_codex_session',
},
});
assert.deepEqual(
capabilityState.availablePlusAccountAccessStrategies,
['oauth', 'sub2api_codex_session']
);
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, true);
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
});
test('flow capability registry preserves requested Plus account strategy while forcing OAuth on unsupported targets', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry();
const capabilityState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'openai',
openaiIntegrationTargetId: 'cpa',
signupMethod: 'email',
plusModeEnabled: true,
plusAccountAccessStrategy: 'sub2api_codex_session',
},
});
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'oauth');
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'oauth');
});
@@ -34,6 +34,10 @@ test('flow registry exposes canonical flow and target metadata', () => {
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
['cpa', 'sub2api', 'codex2api']
);
assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
['row-plus-mode', 'row-plus-payment-method', 'row-plus-account-access-strategy']
);
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
});
@@ -48,6 +52,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
ipProxyEnabled: true,
ipProxyService: '711proxy',
customPassword: 'SharedSecret123!',
plusAccountAccessStrategy: 'sub2api_codex_session',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'secret-key',
stepExecutionRangeByFlow: {
@@ -61,6 +66,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
assert.equal(normalized.services.proxy.enabled, true);
assert.equal(normalized.services.account.customPassword, 'SharedSecret123!');
assert.equal(normalized.flows.openai.integrationTargetId, 'sub2api');
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(normalized.flows.kiro.targetId, 'kiro-rs');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
@@ -79,6 +85,7 @@ test('settings schema can project canonical state into a read view without legac
kiroTargetId: 'kiro-rs',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'key-123',
plusAccountAccessStrategy: 'sub2api_codex_session',
});
const view = schema.buildSettingsView(normalized);
@@ -88,6 +95,7 @@ test('settings schema can project canonical state into a read view without legac
assert.equal(view.panelMode, 'cpa');
assert.equal(view.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(view.kiroRsKey, 'key-123');
assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(view.settingsSchemaVersion, 4);
assert.equal(view.settingsState.activeFlowId, 'kiro');
});
+62 -2
View File
@@ -6,7 +6,7 @@ const source = fs.readFileSync('background/steps/gopay-manual-confirm.js', 'utf8
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundGoPayManualConfirm;`)(globalScope);
test('GoPay manual confirm executor publishes pending manual confirmation state for step 7', async () => {
test('GoPay manual confirm executor publishes pending manual confirmation state for OAuth continuation', async () => {
const stateUpdates = [];
const broadcasts = [];
const events = [];
@@ -24,6 +24,13 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
},
},
},
getNodeIdsForState: () => [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'gopay-subscription-confirm',
'oauth-login',
],
getTabId: async () => 42,
isTabAlive: async () => true,
registerTab: async () => {},
@@ -32,17 +39,70 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
},
});
await executor.executeGoPayManualConfirm({ plusCheckoutTabId: 42, plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session' });
await executor.executeGoPayManualConfirm({
nodeId: 'gopay-subscription-confirm',
plusCheckoutTabId: 42,
plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session',
});
assert.equal(stateUpdates.length, 1);
assert.equal(stateUpdates[0].plusManualConfirmationPending, true);
assert.equal(stateUpdates[0].plusManualConfirmationMethod, 'gopay');
assert.equal(stateUpdates[0].plusManualConfirmationStep, 7);
assert.match(String(stateUpdates[0].plusManualConfirmationRequestId || ''), /^gopay-/);
assert.match(stateUpdates[0].plusManualConfirmationMessage, /OAuth 登录/);
assert.equal(broadcasts.length, 1);
assert.equal(broadcasts[0].plusManualConfirmationPending, true);
assert.deepStrictEqual(
events.find((event) => event.type === 'tab-update'),
{ type: 'tab-update', tabId: 42, payload: { active: true } }
);
assert.match(
events.find((event) => event.type === 'log')?.message || '',
/确认后继续OAuth 登录/
);
});
test('GoPay manual confirm executor switches continuation copy to SUB2API session import when the effective tail is session-based', async () => {
const stateUpdates = [];
const events = [];
const executor = api.createGoPayManualConfirmExecutor({
addLog: async (message, level = 'info') => {
events.push({ type: 'log', message, level });
},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
update: async () => {},
},
},
getNodeIdsForState: () => [
'open-chatgpt',
'plus-checkout-create',
'plus-checkout-billing',
'gopay-subscription-confirm',
'sub2api-session-import',
],
getTabId: async () => 42,
isTabAlive: async () => true,
registerTab: async () => {},
setState: async (payload) => {
stateUpdates.push(payload);
},
});
await executor.executeGoPayManualConfirm({
nodeId: 'gopay-subscription-confirm',
visibleStep: 9,
plusCheckoutTabId: 42,
plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session',
});
assert.equal(stateUpdates.length, 1);
assert.equal(stateUpdates[0].plusManualConfirmationStep, 9);
assert.match(stateUpdates[0].plusManualConfirmationMessage, /导入当前 ChatGPT 会话到 SUB2API/);
assert.match(
events.find((event) => event.type === 'log')?.message || '',
/确认后继续导入当前 ChatGPT 会话到 SUB2API/
);
});
+270
View File
@@ -0,0 +1,270 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => sidepanelSource.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 < sidepanelSource.length; i += 1) {
const ch = sidepanelSource[i];
if (ch === '(') parenDepth += 1;
if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) signatureEnded = true;
}
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 < sidepanelSource.length; end += 1) {
const ch = sidepanelSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
function buildHarness(capabilityStateSource, stateSource) {
return new Function(`
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
${extractFunction('normalizePlusAccountAccessStrategy')}
${extractFunction('getRequestedPlusAccountAccessStrategy')}
${extractFunction('updatePlusModeUI')}
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
}
function getSelectedPlusPaymentMethod() {
return normalizePlusPaymentMethod(selectPlusPaymentMethod.value || latestState?.plusPaymentMethod || currentPlusPaymentMethod || 'paypal');
}
function normalizeGpcHelperPhoneModeValue(value = '') {
return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
}
function normalizeGpcOtpChannelValue(value = '') {
return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp';
}
function isGpcAutoModePermissionDenied() {
return false;
}
function getSelectedPanelMode() {
return latestState?.panelMode || 'cpa';
}
function resolveCurrentSidepanelCapabilities() {
return ${capabilityStateSource};
}
let latestState = ${stateSource};
let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = latestState.plusAccountAccessStrategy || 'oauth';
const inputPlusModeEnabled = { checked: true };
const rowPlusMode = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowPlusAccountAccessStrategy = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: 'none' } };
const selectPlusPaymentMethod = { value: latestState.plusPaymentMethod || 'paypal', style: { display: 'none' } };
const selectPlusAccountAccessStrategy = {
value: latestState.plusAccountAccessStrategy || 'oauth',
disabled: false,
dataset: { requestedValue: latestState.plusAccountAccessStrategy || 'oauth' },
setAttribute(name, value) {
this[name] = value;
},
};
const plusPaymentMethodCaption = { textContent: '' };
const plusAccountAccessStrategyCaption = { textContent: '' };
return {
getRequestedPlusAccountAccessStrategy,
plusAccountAccessStrategyCaption,
rowPlusAccountAccessStrategy,
selectPlusAccountAccessStrategy,
updatePlusModeUI,
};
`)();
}
test('sidepanel keeps requested Plus account strategy while OAuth-only targets force the effective value', () => {
const api = buildHarness(
`{
canShowPlusSettings: true,
runtimeLocks: { plusModeEnabled: true },
canEditPlusAccountAccessStrategy: false,
effectivePlusAccountAccessStrategy: 'oauth',
}`,
`{
activeFlowId: 'openai',
panelMode: 'cpa',
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
}`
);
api.updatePlusModeUI();
assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
assert.equal(api.selectPlusAccountAccessStrategy.disabled, true);
assert.equal(api.selectPlusAccountAccessStrategy.dataset.requestedValue, 'sub2api_codex_session');
assert.equal(api.selectPlusAccountAccessStrategy.value, 'oauth');
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
assert.match(api.plusAccountAccessStrategyCaption.textContent, /OAuth/);
});
test('sidepanel enables SUB2API session strategy selection when the current Plus target supports it', () => {
const api = buildHarness(
`{
canShowPlusSettings: true,
runtimeLocks: { plusModeEnabled: true },
canEditPlusAccountAccessStrategy: true,
effectivePlusAccountAccessStrategy: 'sub2api_codex_session',
}`,
`{
activeFlowId: 'openai',
panelMode: 'sub2api',
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
}`
);
api.updatePlusModeUI();
assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
assert.equal(api.selectPlusAccountAccessStrategy.disabled, false);
assert.equal(api.selectPlusAccountAccessStrategy.value, 'sub2api_codex_session');
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
});
test('sidepanel rebuilds step definitions and workflow nodes with the effective Plus account strategy', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getStepDefinitionsForMode'),
extractFunction('getWorkflowNodesForMode'),
extractFunction('rebuildStepDefinitionState'),
extractFunction('syncStepDefinitionsForMode'),
].join('\n');
const api = new Function(`
const calls = [];
const window = {
MultiPageStepDefinitions: {
getSteps(options) {
calls.push({ type: 'getSteps', options });
return [{ id: 10, order: 100, key: 'sub2api-session-import', title: 'session-import' }];
},
getNodes(options) {
calls.push({ type: 'getNodes', options });
return [{ nodeId: 'sub2api-session-import', displayOrder: 10, next: [] }];
},
},
};
let latestState = { activeFlowId: 'openai' };
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
let currentPlusAccountAccessStrategy = 'oauth';
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
let currentStepDefinitionFlowId = 'openai';
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = [];
let workflowNodes = [];
let STEP_IDS = [];
let STEP_DEFAULT_STATUSES = {};
let SKIPPABLE_STEPS = new Set();
let NODE_IDS = [];
let NODE_DEFAULT_STATUSES = {};
function getSelectedPlusPaymentMethod() {
return currentPlusPaymentMethod;
}
function renderStepsList() {}
${bundle}
return {
calls,
syncStepDefinitionsForMode,
snapshot() {
return {
currentPlusAccountAccessStrategy,
workflowNodes,
stepDefinitions,
STEP_IDS,
NODE_IDS,
};
},
};
`)();
api.syncStepDefinitionsForMode(true, {
activeFlowId: 'openai',
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
render: true,
});
const snapshot = api.snapshot();
assert.equal(snapshot.currentPlusAccountAccessStrategy, 'sub2api_codex_session');
assert.deepStrictEqual(snapshot.STEP_IDS, [10]);
assert.deepStrictEqual(snapshot.NODE_IDS, ['sub2api-session-import']);
assert.equal(snapshot.stepDefinitions[0]?.key, 'sub2api-session-import');
assert.equal(snapshot.workflowNodes[0]?.nodeId, 'sub2api-session-import');
assert.deepStrictEqual(api.calls, [
{
type: 'getSteps',
options: {
activeFlowId: 'openai',
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
},
},
{
type: 'getNodes',
options: {
activeFlowId: 'openai',
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
},
},
]);
});
+48 -2
View File
@@ -65,6 +65,7 @@ test('sidepanel step definitions keep the selected Plus payment method', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getStepDefinitionsForMode'),
extractFunction('rebuildStepDefinitionState'),
extractFunction('syncStepDefinitionsForMode'),
@@ -82,6 +83,10 @@ const window = {
};
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
@@ -107,7 +112,7 @@ return {
assert.deepEqual(api.getStepIds(), [7]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
});
@@ -141,7 +146,9 @@ test('sidepanel applies restored signup method when rebuilding shared step defin
test('sidepanel Plus UI hides PayPal account selector while GoPay is selected', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -156,10 +163,14 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
const api = new Function(`
let latestState = { plusPaymentMethod: 'gopay' };
let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gopay', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const rowPayPalAccount = { style: { display: '' } };
${bundle}
return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
@@ -178,7 +189,9 @@ return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -206,6 +219,7 @@ const window = {
},
};
let latestState = { plusPaymentMethod: 'paypal' };
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const rowPlusMode = { style: { display: '' } };
const selectPlusPaymentMethod = { value: 'paypal', style: { display: '' } };
@@ -213,6 +227,9 @@ const rowPlusPaymentMethod = { style: { display: '' } };
const rowPayPalAccount = { style: { display: '' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
${bundle}
return {
rowPlusMode,
@@ -235,6 +252,7 @@ test('sidepanel step definitions keep GPC helper mode distinct', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getStepDefinitionsForMode'),
extractFunction('rebuildStepDefinitionState'),
extractFunction('syncStepDefinitionsForMode'),
@@ -252,6 +270,10 @@ const window = {
};
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
@@ -277,14 +299,16 @@ return {
assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
});
test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -299,10 +323,14 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperAutoModeEnabled: true };
let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
@@ -380,7 +408,9 @@ return {
test('sidepanel keeps selected GPC auto mode when API Key has no auto permission', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -395,10 +425,14 @@ test('sidepanel keeps selected GPC auto mode when API Key has no auto permission
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
@@ -432,7 +466,9 @@ return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, r
test('sidepanel keeps selected GPC auto mode when persisted permission survives stop refresh', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -452,10 +488,14 @@ let latestState = {
gopayHelperBalancePayload: { auto_mode_enabled: true },
};
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
@@ -513,7 +553,9 @@ return {
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizePlusAccountAccessStrategy'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('getRequestedPlusAccountAccessStrategy'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('normalizeGpcAutoModePermissionValue'),
@@ -528,10 +570,14 @@ test('sidepanel keeps selected GPC auto mode before permission has been queried'
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: null };
let currentPlusPaymentMethod = 'gpc-helper';
let currentPlusAccountAccessStrategy = 'oauth';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
+81
View File
@@ -254,6 +254,87 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
});
test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
const source = fs.readFileSync('data/step-definitions.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
const forbiddenTailKeys = [
'oauth-login',
'fetch-login-code',
'post-login-phone-verification',
'confirm-oauth',
'platform-verify',
];
[
{
label: 'paypal',
options: {
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'plus-checkout-return',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
{
label: 'gopay',
options: {
plusModeEnabled: true,
plusPaymentMethod: 'gopay',
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'gopay-subscription-confirm',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
},
{
label: 'gpc-helper',
options: {
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
plusAccountAccessStrategy: 'sub2api_codex_session',
},
previousNodeId: 'plus-checkout-billing',
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
},
].forEach(({ label, options, previousNodeId, expectedStepIds }) => {
const steps = api.getSteps(options);
const nodes = api.getNodes(options);
const stepKeys = steps.map((step) => step.key);
const nodeIds = nodes.map((node) => node.nodeId);
const previousNode = nodes.find((node) => node.nodeId === previousNodeId);
const sessionImportNode = nodes.find((node) => node.nodeId === 'sub2api-session-import');
assert.equal(stepKeys.at(-1), 'sub2api-session-import', `${label} should end with session import`);
assert.equal(nodeIds.at(-1), 'sub2api-session-import', `${label} node order should end with session import`);
forbiddenTailKeys.forEach((key) => {
assert.equal(stepKeys.includes(key), false, `${label} should not keep ${key} in session mode`);
assert.equal(nodeIds.includes(key), false, `${label} nodes should not keep ${key} in session mode`);
});
assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the new tail`);
assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match session import`);
assert.deepStrictEqual(previousNode?.next, ['sub2api-session-import'], `${label} previous node should link to session import`);
assert.deepStrictEqual(sessionImportNode?.next, [], `${label} session import should be terminal`);
});
});
test('Plus phone signup never switches to SUB2API session tail even if the requested strategy is session import', () => {
const source = fs.readFileSync('data/step-definitions.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
const steps = api.getSteps({
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
signupMethod: 'phone',
plusAccountAccessStrategy: 'sub2api_codex_session',
});
const stepKeys = steps.map((step) => step.key);
assert.equal(stepKeys.includes('sub2api-session-import'), false);
assert.equal(stepKeys.includes('oauth-login'), true);
assert.equal(stepKeys.includes('platform-verify'), true);
});
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const definitionsIndex = html.indexOf('<script src="../data/step-definitions.js"></script>');