Files
go_bt/tests/aistudio_manual_key_sync.test.js

137 lines
7.8 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');
const { syncExistingAistudioApiKey } = require('../src/services/aistudio_api_key_service');
const { watchManualAistudioKey } = require('../src/services/aistudio_manual_key_watcher');
const dummyKey = `AIza${'A'.repeat(32)}`;
const projectId = 'manual-project-25180';
(async () => {
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } });
await page.route('https://aistudio.google.com/api-keys', route => route.fulfill({
status: 200,
contentType: 'text/html',
body: `
<button data-test-id="create-api-key-button">Create API key</button>
<table><tbody>
<tr role="row">
<td role="cell" class="cdk-column-Key">
<ms-api-key-key-string><button class="key-string-link">...AAAA</button></ms-api-key-key-string>
<div class="sub-text">Manual Gemini Key</div>
</td>
<td role="cell" class="cdk-column-Project">
<button>My Project 25180</button><div class="sub-text">${projectId}</div>
</td>
<td role="cell"><button aria-haspopup="menu"><span>more_vert</span></button></td>
</tr>
</tbody></table>
<script>
document.querySelector('.key-string-link').addEventListener('click', () => {
const dialog = document.createElement('div');
dialog.setAttribute('role', 'dialog');
dialog.style.cssText = 'position:fixed;left:200px;top:100px;width:600px;height:300px;background:white';
dialog.innerHTML = '<h2>API key details</h2><input readonly value="${dummyKey}">';
document.body.appendChild(dialog);
});
</script>
`,
}));
const result = await syncExistingAistudioApiKey(page, { email: 'manual@example.com' }, () => {}, {
timeoutMs: 10000,
});
assert.strictEqual(result.ok, true, '人工创建的线上 Key 应能被只读同步');
assert.strictEqual(result.data.apiKey, dummyKey, '应读到详情框中的完整 Key');
assert.strictEqual(result.data.keyName, 'Manual Gemini Key', '应保存列表行中的 Key 名称');
assert.strictEqual(result.data.projectName, 'My Project 25180', '应保存列表行中的项目名称');
await page.close();
const manualPage = await browser.newPage({ viewport: { width: 1200, height: 800 } });
await manualPage.route('https://aistudio.google.com/api-keys', route => route.fulfill({
status: 200,
contentType: 'text/html',
body: '<main><h1>API Keys</h1><button>Create API key</button></main>',
}));
await manualPage.goto('https://aistudio.google.com/api-keys');
const watchPromise = watchManualAistudioKey(browser, { email: 'manual@example.com' }, () => {}, {
timeoutMs: 10000,
pollMs: 100,
});
await manualPage.waitForTimeout(300);
await manualPage.evaluate(({ key }) => {
const dialog = document.createElement('div');
dialog.setAttribute('role', 'dialog');
dialog.style.cssText = 'position:fixed;left:200px;top:100px;width:600px;height:300px;background:white';
dialog.innerHTML = `<h2>API key details</h2><input readonly value="${key}"><p>projects/568908549937</p>`;
document.body.appendChild(dialog);
}, { key: dummyKey });
const watched = await watchPromise;
assert.strictEqual(watched.ok, true, '点击浏览器后应自动识别人工创建成功的 Key 详情');
assert.strictEqual(watched.data.apiKey, dummyKey, '自动监听应读取完整 Key');
const serviceSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'services', 'aistudio_api_key_service.js'), 'utf8');
const syncStart = serviceSource.indexOf('async function syncExistingAistudioApiKey');
const syncEnd = serviceSource.indexOf('\n/**', syncStart + 20);
const syncSource = serviceSource.slice(syncStart, syncEnd);
assert(!syncSource.includes('deleteAllKeys('), '人工同步流程不得删除线上 Key');
assert(!syncSource.includes('createKey('), '人工同步流程不得新建线上 Key');
const stepSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'pipeline', 'steps', 'aistudio_sync_key.js'), 'utf8');
assert(stepSource.includes("dbWrite('upsertAistudioApiKey'"), '同步成功必须写入 Key 数据库');
assert(stepSource.includes("dbWrite('addAistudioFlowLog'"), '同步成功必须追加 AI Studio 流程日志');
assert(stepSource.includes("'success'"), '人工同步日志必须标记为成功');
assert(stepSource.includes('aistudio_pending_project:'),
'人工同步成功后必须清除该邮箱遗留的待同步 project ID');
const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
assert(!html.includes('openAistudioKeysModal()'), '顶部总 API Keys 查看入口应移除');
assert(html.includes('openAistudioKeyModal(account)'), '账号列表行必须提供单邮箱查看入口');
assert(!html.includes('syncAistudioKey(account)'), '页面不应要求用户创建 Key 后再点一次“同步人工 Key”');
assert(app.includes("runTask('open_manual_aistudio'"),
'点击浏览器必须同时启动人工 AI Studio Key 自动监听');
assert(app.includes('aistudio-api-keys?email=${encodeURIComponent(account.email)}'),
'查看接口必须按当前邮箱查询,不能读取全部账号');
const runnerSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'workers', 'task_runner.js'), 'utf8');
assert(runnerSource.includes("taskType === 'aistudio_sync_key'"),
'人工 Key 同步必须有独立的浏览器恢复策略');
assert(runnerSource.includes('aistudioSyncBrowserRetry < 1'),
'比特浏览器被人工关闭/重开时应重连一次,但不能无限重试');
assert(runnerSource.includes('Target page, context or browser has been closed'),
'只应对 page/context/browser 被关闭的瞬时错误执行重连');
assert(runnerSource.includes('isClosedBrowserError(lastFailure.msg)'),
'登录服务将关闭错误转成普通步骤失败时,也必须触发一次重连');
assert(runnerSource.includes("case 'open_manual_aistudio':"),
'工作器必须在比特浏览器打开后持续监听人工 Key');
assert(runnerSource.includes("dbWrite('upsertAistudioApiKey'"),
'自动监听读到 Key 后必须直接写入当前邮箱数据库');
assert(runnerSource.includes('人工创建的 AI Studio API Key 已自动保存'),
'自动保存后必须把 AI Studio 流程日志标记为成功');
const manualOpenStart = runnerSource.indexOf("case 'open_manual_aistudio':");
const manualOpenEnd = runnerSource.indexOf("\n case '", manualOpenStart + 20);
const manualOpenSource = runnerSource.slice(manualOpenStart, manualOpenEnd);
assert(manualOpenSource.includes('aistudio_pending_project:'),
'浏览器人工监听保存成功后必须清除该邮箱遗留的待同步 project ID');
const operationsSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'routes', 'operations.js'), 'utf8');
assert(operationsSource.includes("taskType === 'open_manual_aistudio' && !isLocalRequest(req)"),
'能读取人工 Key 的浏览器监听任务必须只允许本机启动');
assert(operationsSource.includes('Promise.resolve(worker.terminate())'),
'自动保存完成后必须断开监听 Worker,但不能调用 browser.close() 关闭用户窗口');
console.log('aistudio_manual_key_sync.test.js: PASS');
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});