'use strict'; const assert = require('assert'); const { TaskBrowserLifecycle } = require('../src/services/task_browser_lifecycle'); function createHarness(initialBrowsers = []) { const order = []; const notifications = []; const rows = new Map(); let createCount = 0; const lifecycle = new TaskBrowserLifecycle({ getBrowserListSimple: async () => initialBrowsers, createBrowserFromAccount: async account => { createCount++; order.push(`create:${account.email}`); return { browserId: `created-${createCount}`, browserSeq: createCount }; }, getApi: () => ({ updateBrowserProxy: async ids => { order.push(`proxy:${ids[0]}`); return { success: true }; }, getAliveBrowserPids: async () => ({ success: true, data: {} }), }), closeBrowser: async id => { order.push(`close:${id}`); return { success: true }; }, deleteBrowsersBatch: async (ids, callback) => { order.push(`delete:${ids[0]}`); callback(ids[0], true, ''); return { successCount: 1, total: 1 }; }, parseAccountProxy: value => (value ? { proxyMethod: 2, proxyType: 'socks5', host: '127.0.0.1', port: '1080' } : { proxyMethod: 2, proxyType: 'noproxy' }), dbWrite: async (fn, args) => { if (fn === 'updateAccountBrowserId') { const previous = rows.get(args[0]) || { email: args[0] }; rows.set(args[0], { ...previous, browser_id: args[1] || '', browser_seq: args[2] ?? null }); order.push(`db:${args[0]}:${args[1] || 'clear'}`); return true; } if (fn === 'getAccount') return rows.get(args[0]) || null; throw new Error(`unexpected db call: ${fn}`); }, updateMessage: async () => {}, acquireRequestSlot: async () => { order.push('slot'); }, notify: message => notifications.push(message), }); return { lifecycle, order, notifications, rows, getCreateCount: () => createCount }; } (async () => { { const harness = createHarness([{ id: 'bit-existing', name: 'user@example.com', seq: 7 }]); const account = { email: 'user@example.com', browser_id: 'stale-id', proxy_ip: '127.0.0.1:1080' }; const result = await harness.lifecycle.run(account, async prepared => { harness.order.push(`work:${prepared.browser_id}`); return { ok: true }; }); assert.deepStrictEqual(result, { ok: true }); assert.strictEqual(harness.getCreateCount(), 0, '同邮箱比特窗口应复用,旧 ID 不应阻止重绑'); assert(harness.order.indexOf('proxy:bit-existing') < harness.order.indexOf('work:bit-existing')); assert(harness.order.indexOf('work:bit-existing') < harness.order.indexOf('delete:bit-existing')); assert.strictEqual(account.browser_id, '', '任务结束后应清空账号浏览器绑定'); assert.strictEqual(harness.rows.get(account.email).browser_id, ''); assert.deepStrictEqual(harness.notifications.map(item => item.action), ['prepared', 'released']); } { const harness = createHarness([]); const account = { email: 'new@example.com', proxy_ip: '' }; const first = await harness.lifecycle.run(account, async prepared => ({ __defer: true, browserId: prepared.browser_id, })); assert.strictEqual(first.__defer, true); assert.strictEqual(account.browser_id, 'created-1', '延迟重试仍属于同一账号任务,不应提前删除窗口'); assert.strictEqual(harness.order.filter(item => item.startsWith('delete:')).length, 0); const second = await harness.lifecycle.run(account, async prepared => ({ browserId: prepared.browser_id })); assert.strictEqual(second.browserId, 'created-1'); assert.strictEqual(harness.getCreateCount(), 1, '延迟重试应继续复用当前临时窗口'); assert.strictEqual(account.browser_id, ''); assert.strictEqual(harness.order.filter(item => item === 'delete:created-1').length, 1); } console.log('task_browser_lifecycle.test.js PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });