Add Plus SUB2API session access strategy
This commit is contained in:
@@ -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_token,accessToken 过期后无法自动续期',
|
||||
}],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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'/);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -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' } };
|
||||
|
||||
@@ -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>');
|
||||
|
||||
Reference in New Issue
Block a user