'use strict'; const assert = require('assert'); const fs = require('fs'); const path = require('path'); const { runImportBrowserBatch, normalizeConcurrency, DEFAULT_MAX_CONCURRENCY, } = require('../src/services/import_browser_batch_service'); const { _private: { RateLimiter } } = require('../src/pipeline/browser_lifecycle'); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function waitUntil(predicate, timeoutMs = 1000) { const startedAt = Date.now(); while (!predicate()) { if (Date.now() - startedAt > timeoutMs) throw new Error('等待条件超时'); await sleep(5); } } (async () => { assert.strictEqual(normalizeConcurrency(30, 200), 30); assert.strictEqual(normalizeConcurrency(999, 200), DEFAULT_MAX_CONCURRENCY); assert.strictEqual(normalizeConcurrency(0, 200), 1); assert.strictEqual(normalizeConcurrency(30, 4), 4); const accounts = Array.from({ length: 8 }, (_, i) => ({ email: `user${i}@example.com` })); let releaseGate; const gate = new Promise(resolve => { releaseGate = resolve; }); let active = 0; let maxActive = 0; let started = 0; const progress = []; const batchPromise = runImportBrowserBatch(accounts, { concurrency: 3, acquireRequestSlot: async () => {}, processAccount: async () => { started += 1; active += 1; maxActive = Math.max(maxActive, active); await gate; active -= 1; return { action: 'created' }; }, onProgress: ({ completed }) => progress.push(completed), }); await waitUntil(() => started === 3); assert.strictEqual(maxActive, 3, '应同时启动三个账号,而不是逐个串行'); releaseGate(); const batchResult = await batchPromise; assert.strictEqual(batchResult.completed, accounts.length); assert.deepStrictEqual(progress, [1, 2, 3, 4, 5, 6, 7, 8]); const processed = []; const errors = []; let acquired = 0; const isolated = await runImportBrowserBatch(accounts, { concurrency: 4, needsRequestSlot: (_account, index) => index % 2 === 0, acquireRequestSlot: async () => { acquired += 1; }, processAccount: async (account, index) => { processed.push(account.email); if (index === 2) throw new Error('单号失败'); return { action: 'created' }; }, onAccountError: (error, account) => errors.push(`${account.email}:${error.message}`), }); assert.strictEqual(processed.length, accounts.length, '单号异常后其余账号仍应继续'); assert.strictEqual(isolated.results.filter(item => item && item.status === 'rejected').length, 1); assert.strictEqual(errors.length, 1); assert.strictEqual(acquired, 4); const limiter = new RateLimiter(50); const releasedAt = []; await Promise.all(Array.from({ length: 5 }, () => limiter.acquire().then(() => releasedAt.push(Date.now())))); for (let i = 1; i < releasedAt.length; i += 1) { assert.ok(releasedAt[i] - releasedAt[i - 1] >= 12, '限速器不应突发放行整批请求'); } const taskRunnerSource = fs.readFileSync( path.join(__dirname, '../src/workers/task_runner.js'), 'utf8' ); const importStart = taskRunnerSource.indexOf("case 'import_setup'"); const importEnd = taskRunnerSource.indexOf('\n default:', importStart); const importSection = taskRunnerSource.slice(importStart, importEnd); const disabledAt = importSection.indexOf('导入后自动创建比特浏览器已停用'); const firstBreakAt = importSection.indexOf('break;'); const legacyBrowserBatchAt = importSection.indexOf('runImportBrowserBatch(readyAccounts'); assert.ok(disabledAt >= 0, '导入初始化任务必须明确停用浏览器创建'); assert.ok(firstBreakAt > disabledAt, '停用提示后应立即结束导入初始化任务'); assert.ok(legacyBrowserBatchAt === -1 || firstBreakAt < legacyBrowserBatchAt, '旧浏览器批处理代码不得再被执行'); const appSource = fs.readFileSync(path.join(__dirname, '../public/app.js'), 'utf8'); assert.ok(!appSource.includes('_runImportSetup('), '前端导入成功后不得启动浏览器初始化任务'); console.log('import_browser_batch_service.test.js PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });