const assert = require('assert'); const fs = require('fs'); const path = require('path'); const vm = require('vm'); const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8'); const htmlSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8'); const styleSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'style.css'), 'utf8'); const taskRunnerSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'workers', 'task_runner.js'), 'utf8'); const context = vm.createContext({ console, Set, Map, Date, JSON, Math, String, Number, Object, Array, RegExp, Promise, Uint8Array, setTimeout, clearTimeout, }); vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' }); (async () => { const app = context.__geminiApp(); app.$nextTick = callback => { if (callback) callback(); return Promise.resolve(); }; app.accounts = Array.from({ length: 1000 }, (_, index) => ({ email: `user${String(index).padStart(4, '0')}@example.com`, password: `password-${index}`, secret_key: 'JBSWY3DPEHPK3PXP', status: '未登录', })); app._accountByEmail = new Map(app.accounts.map(account => [account.email, account])); app._rebuildDashboardStats(); app.accounts[0].status = 'ChatGPT成功'; app.accounts[1].status = 'ChatGPT失败'; app.accounts[2].status = '失败'; app.accounts[3].status = '未注册成功已改密和2FA'; assert.strictEqual(app.tierCounts.level1['已登录'], 1, 'ChatGPT成功应归入已登录'); assert.strictEqual(app.tierCounts.level1['登录失败'], 3, 'ChatGPT失败、失败和新状态都不能归入已登录'); app.tierFilter.level1 = '已登录'; assert.strictEqual(app.filteredAccounts.length, 1, '已登录筛选不应包含 ChatGPT失败'); app.tierFilter.level1 = '登录失败'; assert.strictEqual(app.filteredAccounts.length, 3, '登录失败筛选应包含新状态和其他任务失败状态'); assert.strictEqual(app.statusClass('未注册成功已改密和2FA'), 'status-ChatGPT失败'); app.tierFilter.level1 = '全部'; app.accounts[0].status = '未登录'; app.accounts[1].status = '未登录'; app.accounts[2].status = '未登录'; app.accounts[3].status = '已充值提取'; assert.strictEqual(app.statusCounts['已充值提取'], 1, '已充值提取应计入独立状态统计'); assert.strictEqual(app.statusClass('已充值提取'), 'status-已充值提取'); app.accounts[3].status = '未登录'; const nowIso = new Date().toISOString(); const oldIso = new Date(Date.now() - (10 * 24 * 60 * 60 * 1000)).toISOString(); app.accounts[4].password_changed = 1; app.accounts[4].password_changed_at = nowIso; app.accounts[5].password_changed = 1; app.accounts[5].password_changed_at = oldIso; app.accounts[6].status = '改密成功'; assert.strictEqual(app._hasPasswordChanged(app.accounts[6]), true, '实时状态已改密成功时不能继续归入未改密'); app.setTierFilter(3, '未改密'); assert(!app.filteredAccounts.some(account => ['user0004@example.com', 'user0005@example.com', 'user0006@example.com'].includes(account.email)), '未改密筛选应排除所有已改密账号'); app.setTierFilter(3, '改密<7天'); assert.deepStrictEqual(Array.from(app.filteredAccounts, account => account.email), ['user0004@example.com'], '改密不足7天应有独立筛选结果'); app.setTierFilter(3, '改密≥7天'); assert.deepStrictEqual(Array.from(app.filteredAccounts, account => account.email), ['user0005@example.com'], '改密满7天应有独立筛选结果'); app.setTierFilter(3, '全部'); app.accounts[6].status = '未登录'; app.accounts[0].restriction_status = 'restricted'; app.accounts[0].status = '登录成功'; assert.strictEqual(app._isGcpRestricted(app.accounts[0]), true, '限制检查结果应计入 GCP 被限制'); assert.strictEqual(app._failureLabelForCode('GCP_RESTRICTED', ''), 'GCP被限制'); assert.strictEqual(app.tierCounts.level2['GCP被限制'], 1, '组合任务旧数据应出现在 GCP 仪表盘'); app.tierFilter.level2 = 'GCP被限制'; assert.strictEqual(app._tier2Accounts.length, 1, 'GCP 仪表盘应能筛选组合任务结果'); app.tierFilter.level2 = '全部'; app._showTaskStartFeedback('change_proxy', app.accounts); assert.strictEqual(app.taskProgress.total, 1000, '任务启动时应立即显示总进度'); const etaNow = Date.now(); app.taskProgress = { current: 25, total: 100, startedAt: etaNow - 25000 }; app.taskClockNow = etaNow; assert.strictEqual(app.taskProgressPercent, 25, '应实时计算任务完成百分比'); assert(app.taskEtaText.includes('预计结束'), '完成首个账号后应显示预计结束时间'); app.taskProgress = { current: 2, total: 100, startedAt: etaNow - 25000 }; assert(app.taskEtaText.includes('采样中'), '并发任务前几个结果不应产生失真的预计时间'); app._showTaskStartFeedback('change_proxy', app.accounts); app._activeTaskType = 'change_proxy'; app._setActiveTaskEmails(app.accounts.map(account => account.email)); app.isRunning = true; assert.strictEqual(app.getLiveMessage(app.accounts[0]), '等待切换 IP...', '未开始账号应按需显示等待状态'); app._queueAccountUpdate({ email: 'user0000@example.com', message: '✅ 切换 IP 成功,浏览器代理已更新', }); app._flushAccountUpdates(); assert.strictEqual(app.accountLogs['user0000@example.com'].message, '✅ 切换 IP 成功,浏览器代理已更新'); assert.strictEqual(app.accountLogs['user0000@example.com'].type, 'success'); assert.strictEqual(app.accountTotalPages, 10); assert.strictEqual(app.pagedAccounts.length, 100, '默认只应渲染 100 个账号'); assert.strictEqual(app.pagedAccounts[0].email, 'user0000@example.com'); app.accountPage = 10; assert.strictEqual(app.pagedAccounts.length, 100); assert.strictEqual(app.pagedAccounts[0].email, 'user0900@example.com'); app.selectedEmails = new Set(['user0000@example.com']); app.accountPage = 2; app.selectCurrentPage(); assert.strictEqual(app.selectedCount, 101, '选中当前页应保留已有选择并加入本页账号'); assert(app.selectedEmails.has('user0100@example.com')); assert(app.selectedEmails.has('user0199@example.com')); assert(!app.selectedEmails.has('user0200@example.com')); app.deselectCurrentPage(); assert.strictEqual(app.selectedCount, 1, '取消当前页选择应保留其他页已选账号'); assert(app.selectedEmails.has('user0000@example.com')); assert(!app.selectedEmails.has('user0100@example.com')); assert(!app.selectedEmails.has('user0199@example.com')); let computedTotpCount = 0; app._computeTotp = async () => { computedTotpCount++; return '123456'; }; await app.updateTotpCodes(); assert.strictEqual(computedTotpCount, 100, 'TOTP 只应计算当前页账号'); assert.strictEqual(Object.keys(app.totpCodes).length, 100); app.searchText = 'user0999'; app.resetAccountPage(); assert.strictEqual(app.filteredAccounts.length, 1); assert.strictEqual(app.accountCurrentPage, 1); assert.strictEqual(app.pagedAccounts[0].email, 'user0999@example.com'); app.searchText = ''; app.accounts[10].browser_id = 'browser-live'; app.accounts[10].browser_seq = null; context.fetch = async () => ({ json: async () => ({ success: true, data: [{ id: 'browser-live', name: 'Browser Live', seq: 88 }] }), }); await app.loadBrowsers(); assert.strictEqual(app.accounts[10].browser_seq, '88', '浏览器列表同步完成后应立即补齐当前页面的显示编号'); app._browserById = new Map([['browser-1', { id: 'browser-1', seq: 42, name: 'Browser 42' }]]); assert.strictEqual(app.getBrowserSeq('browser-1'), '42'); assert.strictEqual(app.getBrowserSeq('missing-browser', '77'), '77', '实时列表缺失时应显示数据库保存的浏览器编号'); app._pendingAccountUpdates = new Map([[ 'user0500@example.com', { email: 'user0500@example.com', status: '登录成功', message: '完成', password_changed: 1, password_changed_at: nowIso, }, ]]); app._flushAccountUpdates(); assert.strictEqual(app._accountByEmail.get('user0500@example.com').status, '登录成功'); assert.strictEqual(app._accountByEmail.get('user0500@example.com').password_changed, 1, '改密标记应通过实时更新进入页面'); assert.strictEqual( app.dashboardStats.tierCounts.level1['已登录'], app.tierCounts.level1['已登录'], '实时更新后应刷新一次仪表盘快照', ); const accountLogsIdentity = app.accountLogs; app.addLog('[user0500@example.com] ✅ 完成'); assert.strictEqual(app.accountLogs, accountLogsIdentity, '高频账号日志不应复制整个日志对象'); assert.strictEqual(app.accountLogs['user0500@example.com'].message, '✅ 完成'); for (let index = 0; index < 600; index++) app.addLog(`batch log ${index}`); app._flushLogs(); assert.strictEqual(app.logs.length, 500, '日志 DOM 数据应保持固定上限'); assert(htmlSource.includes('x-for="account in pagedAccounts"'), '账号表格必须使用分页数据源'); assert(htmlSource.includes('@click="selectCurrentPage()"'), '分页栏应提供选中当前页按钮'); assert(htmlSource.includes('@click="deselectCurrentPage()"'), '分页栏应提供取消当前页选择按钮'); assert(htmlSource.includes('>取消当前页选择'), '分页栏应明确显示取消当前页选择'); assert(htmlSource.includes('task-progress-strip'), '运行任务时必须渲染统一进度条'); assert(!htmlSource.includes('x-for="account in filteredAccounts"'), '账号表格不应再一次渲染全部筛选结果'); assert(htmlSource.includes('dashboardStats.tierCounts'), '仪表盘卡片应读取共享统计快照'); assert(!/x-text="tierCounts\./.test(htmlSource), '每个仪表盘卡片不应各自重新扫描账号列表'); assert(htmlSource.includes('@click="runDeleteBrowsers()"'), '操作栏应显示独立的删除浏览器按钮'); assert(taskRunnerSource.includes('successfullyDeletedIds.has(acc.browser_id)'), '只有窗口删除成功后才能清空浏览器编号'); assert.strictEqual((htmlSource.match(/