Files
FlowPilot/tests/background-sub2api-session-import.test.js
T
2026-05-19 12:40:03 +08:00

479 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const test = require('node:test');
function createJsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
text: async () => JSON.stringify(payload),
};
}
function loadSub2ApiApiModule() {
const source = fs.readFileSync('background/sub2api-api.js', 'utf8');
return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiApi;`)({});
}
function loadSub2ApiSessionImportModule() {
const source = fs.readFileSync('background/steps/sub2api-session-import.js', 'utf8');
return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiSessionImport;`)({});
}
test('sub2api api imports current ChatGPT session through codex-session endpoint', async () => {
const apiModule = loadSub2ApiApiModule();
const fetchCalls = [];
const logs = [];
const importExpiresAt = Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000);
const api = apiModule.createSub2ApiApi({
addLog: async (message, level = 'info', options = {}) => {
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
normalizeSub2ApiUrl: (value) => value,
DEFAULT_SUB2API_GROUP_NAME: 'codex',
fetchImpl: async (url, options = {}) => {
const parsed = new URL(url);
const body = options.body ? JSON.parse(options.body) : null;
fetchCalls.push({ path: parsed.pathname, search: parsed.search, method: options.method || 'GET', body });
if (parsed.pathname === '/api/v1/auth/login') {
return createJsonResponse({
code: 0,
data: {
access_token: 'admin-token',
},
});
}
if (parsed.pathname === '/api/v1/admin/groups/all') {
return createJsonResponse({
code: 0,
data: [
{ id: 5, name: 'codex', platform: 'openai' },
],
});
}
if (parsed.pathname === '/api/v1/admin/proxies/all') {
return createJsonResponse({
code: 0,
data: [{
id: 7,
name: 'shadowrocket',
protocol: 'socks5',
host: '127.0.0.1',
port: 1080,
status: 'active',
}],
});
}
if (parsed.pathname === '/api/v1/admin/accounts/import/codex-session') {
const parsedContent = JSON.parse(body.content);
assert.equal(parsedContent.accessToken, 'access-token-from-state');
assert.equal(parsedContent.user?.email, 'flow@example.com');
assert.equal(body.priority, 3);
assert.equal(body.proxy_id, 7);
assert.deepStrictEqual(body.group_ids, [5]);
assert.equal(body.auto_pause_on_expired, true);
assert.equal(body.update_existing, true);
assert.equal(body.expires_at, importExpiresAt);
return createJsonResponse({
code: 0,
data: {
total: 1,
created: 0,
updated: 1,
skipped: 0,
failed: 0,
warnings: [{
index: 1,
message: '未包含 refresh_tokenaccessToken 过期后无法自动续期',
}],
},
});
}
return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
},
});
const result = await api.importCurrentChatGptSession({
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
sub2apiDefaultProxyName: 'shadowrocket',
sub2apiAccountPriority: 3,
session: {
accessToken: 'access-token-from-session',
expires: '2026-05-20T12:34:56.000Z',
user: {
email: 'flow@example.com',
},
},
accessToken: 'access-token-from-state',
}, {
logLabel: '步骤 10',
logOptions: { step: 10, stepKey: 'sub2api-session-import' },
timeoutMs: 120000,
});
const importCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts/import/codex-session');
assert.ok(importCall, 'expected codex-session import call');
assert.equal(result.verifiedStatus, 'SUB2API 会话导入完成:新建 0,更新 1,跳过 0,失败 0');
assert.equal(result.sub2apiImportUpdated, 1);
assert.equal(
logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
true
);
});
test('session import step reads current ChatGPT session and completes node', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const completed = [];
const logs = [];
const ensureCalls = [];
const sentMessages = [];
const importedPayloads = [];
const executor = moduleApi.createSub2ApiSessionImportExecutor({
addLog: async (message, level = 'info', options = {}) => {
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
chrome: {
tabs: {
get: async (tabId) => ({
id: tabId,
url: 'https://chatgpt.com/?model=gpt-4o',
}),
update: async () => {},
},
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
createSub2ApiApi: () => ({
importCurrentChatGptSession: async (state, options) => {
importedPayloads.push({ state, options });
return {
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
sub2apiImportCreated: 1,
sub2apiImportUpdated: 0,
sub2apiImportSkipped: 0,
sub2apiImportFailed: 0,
};
},
}),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options = {}) => {
ensureCalls.push({ source, tabId, options });
},
getTabId: async () => null,
isTabAlive: async () => false,
normalizeSub2ApiUrl: (value) => value,
registerTab: async () => {},
sendTabMessageUntilStopped: async (tabId, source, message) => {
sentMessages.push({ tabId, source, message });
return {
session: {
accessToken: 'session-access-token',
expires: '2026-05-20T12:34:56.000Z',
user: {
email: 'flow@example.com',
},
},
accessToken: 'session-access-token',
};
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabCompleteUntilStopped: async () => {},
DEFAULT_SUB2API_GROUP_NAME: 'codex',
});
await executor.executeSub2ApiSessionImport({
nodeId: 'sub2api-session-import',
visibleStep: 10,
plusCheckoutTabId: 91,
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
});
assert.equal(ensureCalls.length, 1);
assert.equal(ensureCalls[0].source, 'plus-checkout');
assert.deepStrictEqual(ensureCalls[0].options.inject, [
'content/utils.js',
'content/operation-delay.js',
'content/plus-checkout.js',
]);
assert.deepStrictEqual(sentMessages, [{
tabId: 91,
source: 'plus-checkout',
message: {
type: 'PLUS_CHECKOUT_GET_STATE',
source: 'background',
payload: {
includeSession: true,
includeAccessToken: true,
},
},
}]);
assert.equal(importedPayloads.length, 1);
assert.equal(importedPayloads[0].state.accessToken, 'session-access-token');
assert.equal(importedPayloads[0].state.session.user.email, 'flow@example.com');
assert.equal(completed.length, 1);
assert.deepStrictEqual(completed[0], {
nodeId: 'sub2api-session-import',
payload: {
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
sub2apiImportCreated: 1,
sub2apiImportUpdated: 0,
sub2apiImportSkipped: 0,
sub2apiImportFailed: 0,
},
});
assert.equal(
logs.some((entry) => entry.stepKey === 'sub2api-session-import' && /读取当前 ChatGPT 登录会话/.test(entry.message)),
true
);
});
test('session import step falls back to an active ChatGPT tab when no checkout tab is tracked', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const completed = [];
const importedPayloads = [];
const queryCalls = [];
const registerCalls = [];
const sentMessages = [];
const sessionTab = {
id: 77,
url: 'https://chatgpt.com/?model=gpt-4o',
active: true,
currentWindow: true,
lastAccessed: 1234,
};
const executor = moduleApi.createSub2ApiSessionImportExecutor({
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 });
},
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 () => {},
getTabId: async () => null,
isTabAlive: async () => false,
normalizeSub2ApiUrl: (value) => value,
registerTab: async (source, tabId) => {
registerCalls.push({ source, tabId });
},
sendTabMessageUntilStopped: async (tabId, source, message) => {
sentMessages.push({ tabId, source, message });
return {
session: {
accessToken: 'session-access-token',
user: {
email: 'fallback@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,
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
});
assert.deepStrictEqual(queryCalls, [
{ active: true, currentWindow: true },
{},
]);
assert.deepStrictEqual(registerCalls, [{
source: 'plus-checkout',
tabId: 77,
}]);
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].tabId, 77);
assert.equal(importedPayloads.length, 1);
assert.equal(importedPayloads[0].state.session.user.email, 'fallback@example.com');
assert.equal(completed.length, 1);
});
test('session import step ignores unusable tracked tabs and prefers a real ChatGPT tab from open tabs', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const registerCalls = [];
const sentMessages = [];
const tabsById = {
91: {
id: 91,
url: 'https://www.paypal.com/checkoutnow',
active: false,
currentWindow: false,
lastAccessed: 10,
},
101: {
id: 101,
url: 'https://platform.openai.com/settings/profile',
active: true,
currentWindow: true,
lastAccessed: 50,
},
203: {
id: 203,
url: 'https://chatgpt.com/c/abc123',
active: false,
currentWindow: false,
lastAccessed: 40,
},
};
const executor = moduleApi.createSub2ApiSessionImportExecutor({
addLog: async () => {},
chrome: {
tabs: {
get: async (tabId) => tabsById[tabId] || null,
query: async (queryInfo = {}) => {
if (queryInfo.active && queryInfo.currentWindow) {
return [tabsById[101]];
}
return [tabsById[101], tabsById[203]];
},
update: async () => {},
},
},
completeNodeFromBackground: async () => {},
createSub2ApiApi: () => ({
importCurrentChatGptSession: async () => ({
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
}),
}),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
getTabId: async () => 91,
isTabAlive: async () => true,
normalizeSub2ApiUrl: (value) => value,
registerTab: async (source, tabId) => {
registerCalls.push({ source, tabId });
},
sendTabMessageUntilStopped: async (tabId, source, message) => {
sentMessages.push({ tabId, source, message });
return {
session: {
accessToken: 'session-access-token',
user: {
email: 'best-match@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,
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
});
assert.deepStrictEqual(registerCalls, [{
source: 'plus-checkout',
tabId: 203,
}]);
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].tabId, 203);
});
test('session import step reports missing readable session tab when tracked tabs are unusable', 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 会话的标签页/
);
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'/);
});