137 lines
7.8 KiB
JavaScript
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;
|
|
});
|