feat: add CPA plus session import strategy
This commit is contained in:
@@ -55,6 +55,7 @@ test('background account history settings are normalized independently from hotm
|
||||
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
|
||||
extractFunction('normalizeVerificationResendCount'),
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('normalizePlusAccountAccessStrategy'),
|
||||
extractFunction('normalizeGpcHelperPhoneMode'),
|
||||
extractFunction('normalizePhoneSmsProvider'),
|
||||
extractFunction('normalizePhoneSmsProviderOrder'),
|
||||
@@ -125,6 +126,10 @@ const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
|
||||
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
|
||||
const FIVE_SIM_COUNTRY_ID = 'vietnam';
|
||||
@@ -228,6 +233,7 @@ return {
|
||||
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', 'cpa_codex_session'), 'cpa_codex_session');
|
||||
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'unknown'), 'oauth');
|
||||
assert.equal(
|
||||
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gpc.qlhazycoder.top/api/checkout/start '),
|
||||
|
||||
@@ -48,13 +48,14 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('background auth chain set does not include SUB2API session import node', () => {
|
||||
test('background auth chain set does not include Plus session import nodes', () => {
|
||||
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/);
|
||||
assert.doesNotMatch(authChainBlock, /cpa-session-import/);
|
||||
});
|
||||
|
||||
const NODE_EXECUTE_COMPAT_HELPERS = `
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function createJsonResponse(payload, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => payload,
|
||||
};
|
||||
}
|
||||
|
||||
function loadCpaApiModule() {
|
||||
const source = fs.readFileSync('background/cpa-api.js', 'utf8');
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundCpaApi;`)({});
|
||||
}
|
||||
|
||||
function encodeBase64UrlJson(value) {
|
||||
return Buffer.from(JSON.stringify(value)).toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function createJwtToken(payload = {}) {
|
||||
return [
|
||||
encodeBase64UrlJson({ alg: 'HS256', typ: 'JWT' }),
|
||||
encodeBase64UrlJson(payload),
|
||||
'signature',
|
||||
].join('.');
|
||||
}
|
||||
|
||||
test('cpa api imports current ChatGPT session through management auth-files endpoint', async () => {
|
||||
const apiModule = loadCpaApiModule();
|
||||
const logs = [];
|
||||
const fetchCalls = [];
|
||||
const expiresAt = '2026-05-20T12:34:56.000Z';
|
||||
const accessToken = createJwtToken({
|
||||
exp: Math.floor(Date.parse(expiresAt) / 1000),
|
||||
email: 'jwt@example.com',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_123',
|
||||
chatgpt_plan_type: 'plus',
|
||||
chatgpt_user_id: 'user_123',
|
||||
},
|
||||
'https://api.openai.com/profile': {
|
||||
email: 'profile@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
const api = apiModule.createCpaApi({
|
||||
addLog: async (message, level = 'info', options = {}) => {
|
||||
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
|
||||
},
|
||||
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 || 'POST',
|
||||
headers: options.headers || {},
|
||||
body,
|
||||
});
|
||||
|
||||
if (parsed.pathname === '/v0/management/auth-files') {
|
||||
return createJsonResponse({});
|
||||
}
|
||||
|
||||
return createJsonResponse({ message: `unexpected path ${parsed.pathname}` }, 404);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await api.importCurrentChatGptSession({
|
||||
vpsUrl: 'https://cpa.example.com/management.html#/oauth',
|
||||
vpsPassword: 'management-key',
|
||||
session: {
|
||||
accessToken,
|
||||
expires: expiresAt,
|
||||
user: {
|
||||
email: 'flow@example.com',
|
||||
},
|
||||
account: {
|
||||
id: 'acct_123',
|
||||
planType: 'plus',
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
email: 'registration@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'identifier@example.com',
|
||||
}, {
|
||||
logLabel: '步骤 10',
|
||||
logOptions: { step: 10, stepKey: 'cpa-session-import' },
|
||||
});
|
||||
|
||||
const importCall = fetchCalls.find((call) => call.path === '/v0/management/auth-files');
|
||||
assert.ok(importCall, 'expected CPA auth-files import call');
|
||||
assert.equal(importCall.method, 'POST');
|
||||
assert.equal(importCall.headers.Authorization, 'Bearer management-key');
|
||||
assert.equal(importCall.headers['X-Management-Key'], 'management-key');
|
||||
assert.equal(
|
||||
decodeURIComponent(new URLSearchParams(importCall.search).get('name')),
|
||||
'codex-flow@example.com-plus.json'
|
||||
);
|
||||
assert.equal(importCall.body.type, 'codex');
|
||||
assert.equal(importCall.body.account_id, 'acct_123');
|
||||
assert.equal(importCall.body.chatgpt_account_id, 'acct_123');
|
||||
assert.equal(importCall.body.email, 'flow@example.com');
|
||||
assert.equal(importCall.body.plan_type, 'plus');
|
||||
assert.equal(importCall.body.chatgpt_plan_type, 'plus');
|
||||
assert.equal(importCall.body.access_token, accessToken);
|
||||
assert.equal(importCall.body.id_token_synthetic, true);
|
||||
assert.match(String(importCall.body.id_token || ''), /\.synthetic$/);
|
||||
assert.equal(importCall.body.expired, expiresAt);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(importCall.body, 'refresh_token'), false);
|
||||
assert.equal(result.verifiedStatus, 'CPA 会话导入完成:flow@example.com');
|
||||
assert.equal(
|
||||
logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('cpa api falls back to registration email when session has no readable email', () => {
|
||||
const apiModule = loadCpaApiModule();
|
||||
const accessToken = createJwtToken({
|
||||
exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_456',
|
||||
chatgpt_plan_type: 'plus',
|
||||
chatgpt_user_id: 'user_456',
|
||||
},
|
||||
});
|
||||
|
||||
const api = apiModule.createCpaApi();
|
||||
const result = api.buildCpaSessionAuthJson({
|
||||
session: {
|
||||
accessToken,
|
||||
user: {
|
||||
name: 'Flow User',
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
email: 'registration@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'identifier@example.com',
|
||||
});
|
||||
|
||||
assert.equal(result.email, 'registration@example.com');
|
||||
assert.equal(result.authJson.email, 'registration@example.com');
|
||||
assert.equal(result.fileName, 'codex-registration@example.com-plus.json');
|
||||
});
|
||||
|
||||
test('cpa api preserves provided id_token and refresh_token when available', () => {
|
||||
const apiModule = loadCpaApiModule();
|
||||
const accessToken = createJwtToken({
|
||||
exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_789',
|
||||
chatgpt_plan_type: 'plus',
|
||||
chatgpt_user_id: 'user_789',
|
||||
},
|
||||
});
|
||||
|
||||
const api = apiModule.createCpaApi();
|
||||
const result = api.buildCpaSessionAuthJson({
|
||||
session: {
|
||||
accessToken,
|
||||
user: {
|
||||
email: 'session@example.com',
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
id_token: 'provided-id-token',
|
||||
refresh_token: 'refresh-token-1',
|
||||
session_token: 'session-token-1',
|
||||
});
|
||||
|
||||
assert.equal(result.authJson.id_token, 'provided-id-token');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(result.authJson, 'id_token_synthetic'), false);
|
||||
assert.equal(result.authJson.refresh_token, 'refresh-token-1');
|
||||
assert.equal(result.authJson.session_token, 'session-token-1');
|
||||
assert.equal(result.hasRefreshToken, true);
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
function loadCpaSessionImportModule() {
|
||||
const source = fs.readFileSync('background/steps/cpa-session-import.js', 'utf8');
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundCpaSessionImport;`)({});
|
||||
}
|
||||
|
||||
test('CPA session import step reads current ChatGPT session and completes node', async () => {
|
||||
const moduleApi = loadCpaSessionImportModule();
|
||||
const completed = [];
|
||||
const logs = [];
|
||||
const ensureCalls = [];
|
||||
const sentMessages = [];
|
||||
const importedPayloads = [];
|
||||
|
||||
const executor = moduleApi.createCpaSessionImportExecutor({
|
||||
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 });
|
||||
},
|
||||
createCpaApi: () => ({
|
||||
importCurrentChatGptSession: async (state, options) => {
|
||||
importedPayloads.push({ state, options });
|
||||
return {
|
||||
verifiedStatus: 'CPA 会话导入完成:flow@example.com',
|
||||
cpaImportedFileName: 'codex-flow@example.com-plus.json',
|
||||
cpaImportedEmail: 'flow@example.com',
|
||||
};
|
||||
},
|
||||
}),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options = {}) => {
|
||||
ensureCalls.push({ source, tabId, options });
|
||||
},
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
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 () => {},
|
||||
});
|
||||
|
||||
await executor.executeCpaSessionImport({
|
||||
nodeId: 'cpa-session-import',
|
||||
visibleStep: 10,
|
||||
plusCheckoutTabId: 91,
|
||||
vpsUrl: 'https://cpa.example.com/management.html#/oauth',
|
||||
vpsPassword: 'management-key',
|
||||
});
|
||||
|
||||
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: 'cpa-session-import',
|
||||
payload: {
|
||||
verifiedStatus: 'CPA 会话导入完成:flow@example.com',
|
||||
cpaImportedFileName: 'codex-flow@example.com-plus.json',
|
||||
cpaImportedEmail: 'flow@example.com',
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
logs.some((entry) => entry.stepKey === 'cpa-session-import' && /ChatGPT/.test(entry.message)),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('CPA session import step falls back to an active ChatGPT tab when no checkout tab is tracked', async () => {
|
||||
const moduleApi = loadCpaSessionImportModule();
|
||||
const completed = [];
|
||||
const importedPayloads = [];
|
||||
const queryCalls = [];
|
||||
const registerCalls = [];
|
||||
|
||||
const sessionTab = {
|
||||
id: 77,
|
||||
url: 'https://chatgpt.com/?model=gpt-4o',
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
lastAccessed: 1234,
|
||||
};
|
||||
|
||||
const executor = moduleApi.createCpaSessionImportExecutor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async (tabId) => ({
|
||||
...sessionTab,
|
||||
id: tabId,
|
||||
}),
|
||||
query: async (queryInfo = {}) => {
|
||||
queryCalls.push(queryInfo);
|
||||
if (queryInfo.active && queryInfo.currentWindow) {
|
||||
return [sessionTab];
|
||||
}
|
||||
return [sessionTab];
|
||||
},
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completed.push({ nodeId, payload });
|
||||
},
|
||||
createCpaApi: () => ({
|
||||
importCurrentChatGptSession: async (state) => {
|
||||
importedPayloads.push(state);
|
||||
return {
|
||||
verifiedStatus: 'CPA 会话导入完成:fallback@example.com',
|
||||
};
|
||||
},
|
||||
}),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
registerTab: async (source, tabId) => {
|
||||
registerCalls.push({ source, tabId });
|
||||
},
|
||||
sendTabMessageUntilStopped: async () => ({
|
||||
session: {
|
||||
accessToken: 'session-access-token',
|
||||
user: {
|
||||
email: 'fallback@example.com',
|
||||
},
|
||||
},
|
||||
accessToken: 'session-access-token',
|
||||
}),
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabCompleteUntilStopped: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeCpaSessionImport({
|
||||
nodeId: 'cpa-session-import',
|
||||
visibleStep: 10,
|
||||
vpsUrl: 'https://cpa.example.com/management.html#/oauth',
|
||||
vpsPassword: 'management-key',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(queryCalls, [
|
||||
{ active: true, currentWindow: true },
|
||||
{},
|
||||
]);
|
||||
assert.deepStrictEqual(registerCalls, [{
|
||||
source: 'plus-checkout',
|
||||
tabId: 77,
|
||||
}]);
|
||||
assert.equal(importedPayloads.length, 1);
|
||||
assert.equal(importedPayloads[0].session.user.email, 'fallback@example.com');
|
||||
assert.equal(completed.length, 1);
|
||||
});
|
||||
|
||||
test('CPA session import step reports missing readable session tab when tracked tabs are unusable', async () => {
|
||||
const moduleApi = loadCpaSessionImportModule();
|
||||
let sendCalled = false;
|
||||
|
||||
const executor = moduleApi.createCpaSessionImportExecutor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async (tabId) => ({
|
||||
id: tabId,
|
||||
url: 'https://example.com/not-chatgpt',
|
||||
}),
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async () => {},
|
||||
createCpaApi: () => ({
|
||||
importCurrentChatGptSession: async () => ({}),
|
||||
}),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
getTabId: async () => 91,
|
||||
isTabAlive: async () => true,
|
||||
sendTabMessageUntilStopped: async () => {
|
||||
sendCalled = true;
|
||||
return {};
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForTabCompleteUntilStopped: async () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeCpaSessionImport({
|
||||
nodeId: 'cpa-session-import',
|
||||
visibleStep: 10,
|
||||
vpsUrl: 'https://cpa.example.com/management.html#/oauth',
|
||||
vpsPassword: 'management-key',
|
||||
}),
|
||||
/未找到可读取 ChatGPT 会话的标签页/
|
||||
);
|
||||
|
||||
assert.equal(sendCalled, false);
|
||||
});
|
||||
|
||||
test('background wires CPA session import executor into the workflow runtime', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/cpa-api\.js/);
|
||||
assert.match(source, /background\/steps\/cpa-session-import\.js/);
|
||||
assert.match(source, /'cpa-session-import': \(state\) => cpaSessionImportExecutor\.executeCpaSessionImport\(state\)/);
|
||||
assert.match(source, /'cpa-session-import',[\s\S]*'oauth-login'/);
|
||||
});
|
||||
@@ -59,14 +59,25 @@ const workflowEngine = null;
|
||||
const self = {
|
||||
MultiPageStepDefinitions: {
|
||||
getSteps(options = {}) {
|
||||
return String(options.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
|
||||
? [
|
||||
const strategy = String(options.plusAccountAccessStrategy || '').trim();
|
||||
if (strategy === 'sub2api_codex_session') {
|
||||
return [
|
||||
{ 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' },
|
||||
];
|
||||
}
|
||||
return strategy === 'cpa_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: 'cpa-session-import' },
|
||||
]
|
||||
: [
|
||||
{ id: 1, key: 'open-chatgpt' },
|
||||
@@ -96,17 +107,26 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
|
||||
}
|
||||
function normalizePlusAccountAccessStrategy(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'sub2api_codex_session'
|
||||
? 'sub2api_codex_session'
|
||||
: 'oauth';
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'sub2api_codex_session') {
|
||||
return 'sub2api_codex_session';
|
||||
}
|
||||
if (normalized === 'cpa_codex_session') {
|
||||
return 'cpa_codex_session';
|
||||
}
|
||||
return 'oauth';
|
||||
}
|
||||
function isPlusModeState(state = {}) {
|
||||
return Boolean(state?.plusModeEnabled);
|
||||
}
|
||||
function resolveCurrentFlowCapabilities(state = {}, options = {}) {
|
||||
const effectiveStrategy = String(options.panelMode || '').trim().toLowerCase() === 'sub2api'
|
||||
? normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy)
|
||||
: 'oauth';
|
||||
const normalizedPanelMode = String(options.panelMode || '').trim().toLowerCase();
|
||||
const requestedStrategy = normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy);
|
||||
const effectiveStrategy = normalizedPanelMode === 'sub2api'
|
||||
? (requestedStrategy === 'sub2api_codex_session' ? 'sub2api_codex_session' : 'oauth')
|
||||
: (normalizedPanelMode === 'cpa'
|
||||
? (requestedStrategy === 'cpa_codex_session' ? 'cpa_codex_session' : 'oauth')
|
||||
: 'oauth');
|
||||
return {
|
||||
effectivePanelMode: options.panelMode,
|
||||
effectivePlusAccountAccessStrategy: effectiveStrategy,
|
||||
@@ -159,6 +179,32 @@ test('background step resolution keeps SUB2API session tail only when the effect
|
||||
]);
|
||||
});
|
||||
|
||||
test('background step resolution keeps CPA session tail when the effective Plus target supports it', () => {
|
||||
const api = createHarness();
|
||||
const state = {
|
||||
activeFlowId: 'openai',
|
||||
flowId: 'openai',
|
||||
panelMode: 'cpa',
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
signupMethod: 'email',
|
||||
};
|
||||
|
||||
const resolvedState = api.buildResolvedStepDefinitionState(state);
|
||||
const nodeIds = api.getNodeIdsForState(state);
|
||||
|
||||
assert.equal(resolvedState.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.deepStrictEqual(nodeIds, [
|
||||
'open-chatgpt',
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
'plus-checkout-return',
|
||||
'cpa-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 = {
|
||||
|
||||
@@ -173,3 +173,33 @@ test('message router appends success record when SUB2API session import is the f
|
||||
assert.equal(appendCalls.length, 1);
|
||||
assert.equal(appendCalls[0][0], 'success');
|
||||
});
|
||||
|
||||
test('message router appends success record when CPA session import is the final Plus node', async () => {
|
||||
const { appendCalls, router } = createRouterWithFinalNode({
|
||||
finalNodeId: 'cpa-session-import',
|
||||
nodeIds: [
|
||||
'open-chatgpt',
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
'plus-checkout-return',
|
||||
'cpa-session-import',
|
||||
],
|
||||
nodeStepMap: {
|
||||
'plus-checkout-create': 6,
|
||||
'plus-checkout-billing': 7,
|
||||
'paypal-approve': 8,
|
||||
'plus-checkout-return': 9,
|
||||
'cpa-session-import': 10,
|
||||
},
|
||||
});
|
||||
|
||||
await router.handleMessage({
|
||||
type: 'NODE_COMPLETE',
|
||||
nodeId: 'cpa-session-import',
|
||||
payload: { nodeId: 'cpa-session-import' },
|
||||
}, {});
|
||||
|
||||
assert.equal(appendCalls.length, 1);
|
||||
assert.equal(appendCalls[0][0], 'success');
|
||||
});
|
||||
|
||||
@@ -111,6 +111,10 @@ const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState'];
|
||||
const LEGACY_AUTO_STEP_DELAY_KEYS = [];
|
||||
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = [];
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
function isPlainObjectValue(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
@@ -125,6 +129,7 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
|
||||
}
|
||||
${extractFunction('normalizePlusAccountAccessStrategy')}
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
@@ -601,5 +601,5 @@ 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'/);
|
||||
assert.match(source, /'sub2api-session-import',[\s\S]*'oauth-login'/);
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
});
|
||||
|
||||
test('flow capability registry preserves requested Plus account strategy while forcing OAuth on unsupported targets', () => {
|
||||
test('flow capability registry exposes editable Plus account access strategies for CPA', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
@@ -226,12 +226,36 @@ test('flow capability registry preserves requested Plus account strategy while f
|
||||
openaiIntegrationTargetId: 'cpa',
|
||||
signupMethod: 'email',
|
||||
plusModeEnabled: true,
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'cpa_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, true);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
test('flow capability registry forces OAuth when the current target only supports OAuth', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'openai',
|
||||
openaiIntegrationTargetId: 'codex2api',
|
||||
signupMethod: 'email',
|
||||
plusModeEnabled: true,
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'oauth');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'oauth');
|
||||
|
||||
@@ -99,3 +99,15 @@ test('settings schema can project canonical state into a read view without legac
|
||||
assert.equal(view.settingsSchemaVersion, 4);
|
||||
assert.equal(view.settingsState.activeFlowId, 'kiro');
|
||||
});
|
||||
|
||||
test('settings schema preserves CPA session strategy in canonical state and read view', () => {
|
||||
const { settingsSchema } = loadApis();
|
||||
const schema = settingsSchema.createSettingsSchema();
|
||||
const normalized = schema.normalizeSettingsState({
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
});
|
||||
const view = schema.buildSettingsView(normalized);
|
||||
|
||||
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
|
||||
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.match(stateUpdates[0].plusManualConfirmationMessage, /OAuth/);
|
||||
assert.equal(broadcasts.length, 1);
|
||||
assert.equal(broadcasts[0].plusManualConfirmationPending, true);
|
||||
assert.deepStrictEqual(
|
||||
@@ -59,7 +59,7 @@ test('GoPay manual confirm executor publishes pending manual confirmation state
|
||||
);
|
||||
assert.match(
|
||||
events.find((event) => event.type === 'log')?.message || '',
|
||||
/确认后继续OAuth 登录/
|
||||
/OAuth/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -100,9 +100,53 @@ test('GoPay manual confirm executor switches continuation copy to SUB2API sessio
|
||||
|
||||
assert.equal(stateUpdates.length, 1);
|
||||
assert.equal(stateUpdates[0].plusManualConfirmationStep, 9);
|
||||
assert.match(stateUpdates[0].plusManualConfirmationMessage, /导入当前 ChatGPT 会话到 SUB2API/);
|
||||
assert.match(stateUpdates[0].plusManualConfirmationMessage, /SUB2API/);
|
||||
assert.match(
|
||||
events.find((event) => event.type === 'log')?.message || '',
|
||||
/确认后继续导入当前 ChatGPT 会话到 SUB2API/
|
||||
/SUB2API/
|
||||
);
|
||||
});
|
||||
|
||||
test('GoPay manual confirm executor switches continuation copy to CPA session import when the effective tail is CPA 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',
|
||||
'cpa-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, /CPA/);
|
||||
assert.match(
|
||||
events.find((event) => event.type === 'log')?.message || '',
|
||||
/CPA/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -57,6 +57,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
@@ -165,6 +166,33 @@ test('sidepanel enables SUB2API session strategy selection when the current Plus
|
||||
assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
|
||||
});
|
||||
|
||||
test('sidepanel enables CPA session strategy selection when the current Plus target supports it', () => {
|
||||
const api = buildHarness(
|
||||
`{
|
||||
canShowPlusSettings: true,
|
||||
runtimeLocks: { plusModeEnabled: true },
|
||||
canEditPlusAccountAccessStrategy: true,
|
||||
availablePlusAccountAccessStrategies: ['oauth', 'cpa_codex_session'],
|
||||
effectivePanelMode: 'cpa',
|
||||
effectivePlusAccountAccessStrategy: 'cpa_codex_session',
|
||||
}`,
|
||||
`{
|
||||
activeFlowId: 'openai',
|
||||
panelMode: 'cpa',
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
}`
|
||||
);
|
||||
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.disabled, false);
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.value, 'cpa_codex_session');
|
||||
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'cpa_codex_session');
|
||||
assert.match(api.plusAccountAccessStrategyCaption.textContent, /CPA/);
|
||||
});
|
||||
|
||||
test('sidepanel rebuilds step definitions and workflow nodes with the effective Plus account strategy', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeSignupMethod'),
|
||||
@@ -195,6 +223,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
let currentPlusAccountAccessStrategy = 'oauth';
|
||||
let currentSignupMethod = 'email';
|
||||
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
|
||||
@@ -272,12 +301,20 @@ return {
|
||||
|
||||
test('background declares Plus account access strategy constants before precomputing session-tail step definitions', () => {
|
||||
const strategyConstantIndex = backgroundSource.indexOf("const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';");
|
||||
const cpaStrategyConstantIndex = backgroundSource.indexOf("const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';");
|
||||
const precomputedSessionStepsIndex = backgroundSource.indexOf('const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS =');
|
||||
const precomputedCpaSessionStepsIndex = backgroundSource.indexOf('const PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS =');
|
||||
|
||||
assert.ok(strategyConstantIndex >= 0, 'expected Plus account access strategy constant declaration');
|
||||
assert.ok(cpaStrategyConstantIndex >= 0, 'expected CPA Plus account access strategy constant declaration');
|
||||
assert.ok(precomputedSessionStepsIndex >= 0, 'expected precomputed SUB2API session step definitions');
|
||||
assert.ok(precomputedCpaSessionStepsIndex >= 0, 'expected precomputed CPA session step definitions');
|
||||
assert.ok(
|
||||
strategyConstantIndex < precomputedSessionStepsIndex,
|
||||
'strategy constant must be declared before background precomputes session-tail step definitions'
|
||||
);
|
||||
assert.ok(
|
||||
cpaStrategyConstantIndex < precomputedCpaSessionStepsIndex,
|
||||
'CPA strategy constant must be declared before background precomputes CPA session-tail step definitions'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -85,6 +85,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
|
||||
let currentSignupMethod = 'email';
|
||||
@@ -170,6 +171,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
${bundle}
|
||||
@@ -229,6 +231,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
${bundle}
|
||||
return {
|
||||
@@ -272,6 +275,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
|
||||
let currentSignupMethod = 'email';
|
||||
@@ -330,6 +334,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
|
||||
@@ -432,6 +437,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
|
||||
@@ -495,6 +501,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
@@ -577,6 +584,7 @@ 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 PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
@@ -745,6 +753,7 @@ test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED sta
|
||||
const api = new Function(`
|
||||
const events = [];
|
||||
let latestState = {
|
||||
activeFlowId: 'openai',
|
||||
plusManualConfirmationPending: true,
|
||||
plusManualConfirmationRequestId: 'gopay-request-1',
|
||||
plusManualConfirmationStep: 7,
|
||||
@@ -799,6 +808,11 @@ return { events, syncPlusManualConfirmationDialog };
|
||||
|
||||
test('sidepanel resolves pending GPC OTP with typed code', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeSignupMethod'),
|
||||
extractFunction('normalizePlusAccountAccessStrategy'),
|
||||
extractFunction('normalizePlusStrategyTargetId'),
|
||||
extractFunction('getPlusAccountAccessStrategyContinuationLabel'),
|
||||
extractFunction('resolvePlusManualContinuationActionLabelFromState'),
|
||||
extractLastFunction('openPlusManualConfirmationDialog'),
|
||||
extractLastFunction('syncPlusManualConfirmationDialog'),
|
||||
].join('\n');
|
||||
@@ -815,6 +829,14 @@ let latestState = {
|
||||
};
|
||||
let activePlusManualConfirmationRequestId = '';
|
||||
let plusManualConfirmationDialogInFlight = false;
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const SIGNUP_METHOD_EMAIL = 'email';
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
const DEFAULT_SIGNUP_METHOD = 'email';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = 'oauth';
|
||||
const sharedFormDialog = {
|
||||
async open(options) {
|
||||
events.push({ type: 'form', options });
|
||||
|
||||
@@ -335,6 +335,70 @@ test('Plus phone signup never switches to SUB2API session tail even if the reque
|
||||
assert.equal(stepKeys.includes('platform-verify'), true);
|
||||
});
|
||||
|
||||
test('Plus session strategy swaps the OAuth tail for a single CPA 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: 'cpa_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: 'cpa_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: 'cpa_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 === 'cpa-session-import');
|
||||
|
||||
assert.equal(stepKeys.at(-1), 'cpa-session-import', `${label} should end with CPA session import`);
|
||||
assert.equal(nodeIds.at(-1), 'cpa-session-import', `${label} node order should end with CPA session import`);
|
||||
forbiddenTailKeys.forEach((key) => {
|
||||
assert.equal(stepKeys.includes(key), false, `${label} should not keep ${key} in CPA session mode`);
|
||||
assert.equal(nodeIds.includes(key), false, `${label} nodes should not keep ${key} in CPA session mode`);
|
||||
});
|
||||
assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the CPA tail`);
|
||||
assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match CPA session import`);
|
||||
assert.deepStrictEqual(previousNode?.next, ['cpa-session-import'], `${label} previous node should link to CPA session import`);
|
||||
assert.deepStrictEqual(sessionImportNode?.next, [], `${label} CPA session import should be terminal`);
|
||||
});
|
||||
});
|
||||
|
||||
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