Files
go_bt/tests/recharge_ui.test.js
T

325 lines
16 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.
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
(async () => {
const root = path.join(__dirname, '..');
const appSource = fs.readFileSync(path.join(root, 'public', 'app.js'), 'utf8');
const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8');
const styles = fs.readFileSync(path.join(root, 'public', 'style.css'), 'utf8');
const operations = fs.readFileSync(path.join(root, 'src', 'routes', 'operations.js'), 'utf8');
const accountsRoute = fs.readFileSync(path.join(root, 'src', 'routes', 'accounts.js'), 'utf8');
const runner = fs.readFileSync(path.join(root, 'src', 'workers', 'task_runner.js'), 'utf8');
const rechargeService = fs.readFileSync(path.join(root, 'src', 'services', 'recharge_task_service.js'), 'utf8');
assert.match(
html,
/runOpenAIGetAuth\(\)[\s\S]{0,200}runOpenAIQueryPlan\(\)[\s\S]{0,200}openRechargeModal\(\)/,
'查询套餐按钮应位于提取授权JSON右侧',
);
assert.match(html, /<th>ChatGPT<\/th>\s*<th>是否已充值<\/th>\s*<th>授权JSON<\/th>\s*<th>AC凭证<\/th>\s*<th>Cli_AC凭证<\/th>\s*<th>账号套餐<\/th>/);
assert.match(html, /x-model="rechargeCredentialForm\.keyId"/);
assert.match(html, /type="password"[^>]*x-model="rechargeCredentialForm\.secret"/);
assert.match(styles, /input\[type="text"\],\s*input\[type="password"\],\s*input\[type="number"\]/,
'API Secret 密码框应与 Key ID 使用同一套输入框样式');
assert.match(html, /x-model="rechargeCardKeys"/);
assert.match(html, /完整 ChatGPT Session(结构化 JSON)/);
assert.match(operations, /taskType === 'card_pay_recharge' && !isLocalRequest\(req\)/);
assert.match(operations, /hasOwnProperty\.call\(params, 'confirmDuplicate'\)[\s\S]{0,100}params\.confirmDuplicate !== false/);
assert.match(runner, /latest\.chatgpt_session/);
assert.match(runner, /sessionText:\s*latest\.chatgpt_session/);
assert.match(runner, /confirmDuplicate:\s*false/);
assert.match(runner, /parseInt\(params && params\.threadCount, 10\) \|\| 2/,
'未指定充值并发时服务端应默认使用 2');
assert.match(runner, /Math\.min\(requestedRechargeConcurrency, 5\)/,
'卡付充值应限制为最多 5 路并发');
assert.match(runner, /const rechargeStartSpacingMs\s*=\s*1000/,
'卡付充值请求应相隔 1 秒发起');
assert.match(runner, /const rechargeQueryConcurrency\s*=\s*2/,
'卡付任务查询应与提交并发分离并限制为 2 路');
assert.match(html, /id="recharge-concurrency"[\s\S]{0,180}min="1" max="5"/,
'充值弹窗应提供 1 到 5 的独立并发设置');
assert.match(runner, /type:\s*'progress',\s*current:\s*0,\s*total:\s*accounts\.length/,
'充值开始时应立即推送 0/总数进度');
assert.match(runner, /onPrepared:[\s\S]{0,800}onConfirmed:[\s\S]{0,800}onPoll:/,
'充值应逐阶段推送预创建、确认和轮询进度');
assert.match(runner, /stagePrefix[\s\S]{0,300}开始处理/,
'每个账号应实时打印开始处理信息');
assert.match(runner, /maxRechargeAttempts\s*=\s*3/,
'单个账号充值最多应尝试三次');
assert.match(runner, /const rechargeOperationId\s*=\s*operationMap\.get\(emailKey\)[\s\S]{0,100}createRechargeOperationId\(email, cardKey, false\)/,
'每个账号应复用认领阶段生成的稳定充值操作号');
assert.match(runner, /operationId:\s*attemptOffset\s*>\s*0[\s\S]{0,180}rechargeOperationId/,
'只有远端任务明确失败后才应使用新的尝试幂等号');
assert.match(runner, /使用同一卡密重试/,
'重试进度应明确提示使用同一卡密');
assert.match(rechargeService, /error\s*&&\s*error\.retryable\s*===\s*true/,
'修改型充值请求的瞬时HTTP错误必须保留为待确认');
assert.match(runner, /rechargeNeedsManualConfirmation\(error, remoteTaskId\)/,
'最终失败状态必须结合远端任务号判断是否等待人工确认');
assert.match(operations, /message:\s*account\s*\?\s*\(account\.message/,
'充值结论应通过 SSE 同步到通用处理结论');
assert.match(runner, /publishRecharge\([\s\S]{0,180}'等待已确认'/,
'结果待确认时应转为等待人工确认状态');
assert.match(operations, /const nextStatus\s*=\s*remoteTaskId\s*\?\s*'充值中'\s*:\s*'充值失败'/,
'任务中断时已提交任务应保持充值中,未提交账号才标记失败');
assert.match(accountsRoute, /router\.put\('\/:email\/recharge-status'/,
'应提供本机人工修改充值状态接口');
assert.match(html, /<option value="账号本身已有会员">账号本身已有会员<\/option>/,
'充值状态原地下拉框应包含账号本身已有会员');
assert.match(appSource, /rechargeStatusOptions:[^\n]*'账号本身已有会员'/,
'组合筛选的充值状态应包含账号本身已有会员');
assert.match(accountsRoute, /allowedStatuses[^\n]*'账号本身已有会员'/,
'服务端应允许人工保存账号本身已有会员');
assert.match(styles, /\.status-账号本身已有会员\s*\{/,
'账号本身已有会员应有独立状态样式');
assert.match(accountsRoute, /router\.get\('\/:email\/recharge-card'/,
'应提供本机按需查看最近充值卡密的接口');
assert.match(accountsRoute, /recharge_card_key,[\s\S]{0,180}has_recharge_card/,
'账号列表只能返回是否有卡密,不能返回卡密明文');
assert.match(html, /openRechargeCard\(account\)[\s\S]{0,80}查看卡密/,
'充值状态旁应提供查看卡密按钮');
assert.match(html, /showRechargeCardModal/,
'应提供本次充值卡密查看弹窗');
assert.match(html, /充值完成时间:[\s\S]{0,180}rechargeCardCompletedAt/,
'查看卡密弹窗应显示本次充值完成时间');
assert.match(appSource, /CURRENT_TIMESTAMP 保存 UTC[\s\S]{0,500}timeZone:\s*'Asia\/Shanghai'/,
'数据库时间应按UTC解析并强制显示为北京时间');
assert.match(accountsRoute, /rechargeStatus:[\s\S]{0,120}rechargedAt:/,
'卡密接口应同时返回充值状态和完成时间');
assert.match(html, /@dblclick\.stop="startInlineEditRechargeStatus\(account\.email\)"/);
assert.match(html, /inlineEditRechargeStatusEmail === account\.email[\s\S]{0,700}<select class="status-select"/,
'充值状态应使用与状态列一致的原地下拉框');
for (const status of ['未充值', '充值中', '等待已确认', '充值成功', '充值失败']) {
assert.match(html, new RegExp(`<option value="${status}">${status}<\\/option>`));
}
assert.match(html, /quickSetRechargeStatus\(account\.email, \$event\.target\.value\)/);
assert.doesNotMatch(appSource, /请输入充值状态/,
'充值状态不应继续使用手工输入提示框');
assert.match(appSource, /startsWith\('等待已确认'\)/);
assert.match(styles, /\.status-等待已确认\s*\{/);
assert.doesNotMatch(runner, /openai_auth_json[^\n]*card_pay_recharge/);
let fetchImpl = async () => ({
ok: true,
json: async () => ({ success: true, data: { configured: true, keyIdMasked: 'unit••••mask' } }),
});
let latestEventSource = null;
let promptValue = '等待已确认';
class TestEventSource {
constructor(url) {
this.url = url;
latestEventSource = this;
}
close() { this.closed = true; }
}
const context = vm.createContext({
console, Set, Map, Date, JSON, Math, String, Number, Object, Array, RegExp, Promise,
setTimeout, clearTimeout,
fetch: (...args) => fetchImpl(...args),
confirm: () => true,
prompt: () => promptValue,
EventSource: TestEventSource,
});
vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' });
const app = context.__geminiApp();
assert.match(app.formatDate('2026-08-22 05:51:48'), /13:51:48/,
'SQLite UTC时间 05:51:48 应显示为北京时间 13:51:48');
assert.match(app.formatDate('2026-08-22T05:51:48Z'), /13:51:48/,
'标准UTC时间也应显示为北京时间');
assert.strictEqual(app.formatDate('invalid-time'), 'invalid-time');
const logs = [];
app.addLog = (message, type) => logs.push({ message, type });
app.accounts = [
{ email: 'first@example.com' },
{ email: 'second@example.com' },
];
app._accountByEmail = new Map(app.accounts.map(account => [account.email, account]));
app.selectedEmails = new Set(app.accounts.map(account => account.email));
app.rechargeCredentialMeta = { configured: true, keyIdMasked: 'unit••••mask', updatedAt: null };
app.loadRechargeCredentialMeta = async () => {};
await app.openRechargeModal();
assert.strictEqual(app.showRechargeModal, true);
assert.strictEqual(app.rechargeConcurrency, 2, '充值并发默认值应为 2');
app.rechargeConcurrency = 99;
assert.strictEqual(app.normalizedRechargeConcurrency, 5, '充值并发不得超过 5');
app.rechargeConcurrency = 0;
assert.strictEqual(app.normalizedRechargeConcurrency, 1, '充值并发不得小于 1');
app.rechargeConcurrency = 'invalid';
assert.strictEqual(app.normalizedRechargeConcurrency, 2, '无效充值并发应恢复默认值 2');
app.rechargeConcurrency = 2;
assert.deepStrictEqual(
Array.from(app.rechargeAccounts, row => row.email),
['first@example.com', 'second@example.com'],
);
app.rechargeCardKeys = 'CARD-ONE\n\nCARD-TWO';
assert.strictEqual(app.rechargeInputError, '卡密中间不能有空行');
app.rechargeCardKeys = 'CARD-ONE\nCARD-ONE';
assert.strictEqual(app.rechargeInputError, '卡密不能重复');
app.rechargeCardKeys = 'CARD-ONE\nCARD-TWO\n';
assert.strictEqual(app.rechargeInputError, '');
let submitted = null;
app.runTask = async (taskType, params, accounts) => {
submitted = { taskType, params, accounts };
return true;
};
await app.confirmRecharge();
assert.strictEqual(submitted.taskType, 'card_pay_recharge');
assert.strictEqual(submitted.params.threadCount, 2);
assert.deepStrictEqual(
JSON.parse(JSON.stringify(submitted.params.assignments)),
[
{ email: 'first@example.com', cardKey: 'CARD-ONE' },
{ email: 'second@example.com', cardKey: 'CARD-TWO' },
],
);
assert.strictEqual(Object.prototype.hasOwnProperty.call(submitted.params, 'accessToken'), false);
assert.strictEqual(Object.prototype.hasOwnProperty.call(submitted.params, 'session'), false);
assert.deepStrictEqual(
JSON.parse(JSON.stringify(submitted.accounts)),
[{ email: 'first@example.com' }, { email: 'second@example.com' }],
'充值任务只应把邮箱传给 Worker,不发送完整账号记录',
);
assert.strictEqual(app.showRechargeModal, false);
assert.strictEqual(app.rechargeCardKeys, '');
let request = null;
fetchImpl = async (url, options) => {
request = { url, options };
return {
ok: true,
json: async () => ({
success: true,
data: { configured: true, keyIdMasked: 'abcd••••wxyz', updatedAt: '2026-08-21T00:00:00Z' },
}),
};
};
app.rechargeCredentialForm = { keyId: ' api-key-id ', secret: 'unit-secret' };
await app.saveRechargeCredentials();
assert.strictEqual(request.url, '/api/recharge-credentials');
assert.strictEqual(request.options.method, 'PUT');
assert.deepStrictEqual(JSON.parse(request.options.body), { keyId: 'api-key-id', secret: 'unit-secret' });
assert.deepStrictEqual(
JSON.parse(JSON.stringify(app.rechargeCredentialForm)),
{ keyId: '', secret: '' },
'保存后必须立即清空前端凭据表单',
);
assert.strictEqual(Object.values(app.settings).includes('unit-secret'), false);
const account = app.accounts[0];
account.has_recharge_card = true;
fetchImpl = async (url, options) => {
request = { url, options };
return {
ok: true,
status: 200,
json: async () => ({
success: true,
data: {
cardKey: 'CARD-USED-THIS-TIME',
rechargeStatus: '充值成功',
rechargedAt: '2026-08-21T00:00:00Z',
},
}),
};
};
await app.openRechargeCard(account);
assert.strictEqual(request.url, '/api/accounts/first%40example.com/recharge-card');
assert.strictEqual(request.options.cache, 'no-store');
assert.strictEqual(app.showRechargeCardModal, true);
assert.strictEqual(app.rechargeCardText, 'CARD-USED-THIS-TIME');
assert.strictEqual(app.rechargeCardCompletedAt, '2026-08-21T00:00:00Z');
app.closeRechargeCard();
assert.strictEqual(app.rechargeCardText, '');
assert.strictEqual(app.rechargeCardCompletedAt, '');
fetchImpl = async (url, options) => {
request = { url, options };
return {
ok: true,
json: async () => ({
success: true,
data: { ...account, recharge_status: '等待已确认', recharge_message: '人工修改为等待已确认' },
}),
};
};
app._rebuildDashboardStats = () => {};
app.editRechargeStatus(account);
assert.strictEqual(app.inlineEditRechargeStatusEmail, account.email);
await app.quickSetRechargeStatus(account.email, '等待已确认');
assert.strictEqual(request.url, '/api/accounts/first%40example.com/recharge-status');
assert.strictEqual(request.options.method, 'PUT');
assert.deepStrictEqual(JSON.parse(request.options.body), { status: '等待已确认' });
assert.strictEqual(account.recharge_status, '等待已确认');
app._pendingAccountUpdates = new Map([[account.email, {
message: '充值成功',
recharge_status: '充值成功',
recharge_task_id: '123',
recharge_message: '充值成功',
recharged_at: '2026-08-21T00:00:00Z',
has_recharge_card: true,
}]]);
app._flushAccountUpdates();
assert.strictEqual(account.recharge_status, '充值成功');
assert.strictEqual(account.recharge_task_id, '123');
assert.strictEqual(account.message, '充值成功');
assert.strictEqual(account.has_recharge_card, true);
app.isRunning = false;
account.recharge_status = '充值失败';
account.recharge_message = '开放接口未查到卡密记录,请确认卡密及所属线路';
account.message = account.recharge_message;
account.failure_label = '功能失败';
app.accountLogs = {};
assert.strictEqual(
app.getLiveMessage(account),
'开放接口未查到卡密记录,请确认卡密及所属线路',
'充值完成后处理结论应显示完整充值原因,不能压缩为通用失败标签',
);
app.loadBrowsers = async () => {};
app.loadAccounts = async () => {};
app.isRunning = true;
app.currentTaskId = 'task-error';
app._activeTaskType = 'card_pay_recharge';
app._setActiveTaskEmails([account.email]);
app.startSSEListener('task-error');
latestEventSource.onmessage({
data: JSON.stringify({
type: 'done',
result: { taskType: 'card_pay_recharge', success: false, error: '手动停止' },
}),
});
assert.strictEqual(logs[logs.length - 1].message, '⚠️ 任务已停止或异常结束');
assert.strictEqual(logs[logs.length - 1].type, 'warn');
let stopRequest = null;
fetchImpl = async (url, options) => {
stopRequest = { url, options };
return { ok: true, json: async () => ({ success: true }) };
};
app.isRunning = true;
app.currentTaskId = 'task-soft-stop';
app._activeTaskType = 'card_pay_recharge';
app.startSSEListener('task-soft-stop');
const softStopEventSource = latestEventSource;
await app.stopTask();
assert.strictEqual(stopRequest.url, '/api/operations/stop/task-soft-stop');
assert.strictEqual(app.isRunning, true, '充值软停止时应等待 Worker 的 done 事件');
assert.strictEqual(app.currentTaskId, 'task-soft-stop');
assert.strictEqual(softStopEventSource.closed, undefined, '充值软停止时不得提前关闭 SSE');
assert.strictEqual(logs[logs.length - 1].message, '⏳ 正在安全停止充值任务,请等待当前请求完成...');
console.log('recharge_ui.test.js: PASS');
})().catch(error => {
console.error(error);
process.exitCode = 1;
});