import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' /** * Тесты эвристики «человек на ВКС». Мокаем `node:child_process.exec` * (через него идёт `tasklist`), electron BrowserWindow (broadcast no-op) и * logger. resetModules + dynamic import в каждом тесте — чтобы сбросить * module-level кэш (`cachedActive`, `lastCheckAt`). */ type ExecCb = (err: Error | null, res?: { stdout: string }) => void const h = vi.hoisted(() => ({ // Текущая реализация exec для конкретного теста. execImpl: ((_cmd: string, _opts: unknown, cb: ExecCb) => cb(null, { stdout: '' })) as ( cmd: string, opts: unknown, cb: ExecCb ) => void, calls: 0, log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } })) vi.mock('node:child_process', () => ({ exec: (cmd: string, opts: unknown, cb: ExecCb) => { h.calls += 1 h.execImpl(cmd, opts, cb) } })) vi.mock('electron', () => ({ BrowserWindow: { getAllWindows: () => [] } })) vi.mock('./logger', () => ({ log: h.log })) /** CSV-строка tasklist для заданного набора .exe. */ function csv(...procs: string[]): string { return procs.map((p) => `"${p}","1234","Console","1","85,432 K"`).join('\r\n') } async function load(): Promise { return import('./meeting-detect') } beforeEach(() => { vi.resetModules() h.calls = 0 h.execImpl = (_cmd, _opts, cb) => cb(null, { stdout: '' }) h.log.info.mockClear() h.log.warn.mockClear() }) afterEach(() => { vi.restoreAllMocks() }) describe('isMeetingActive', () => { it('детектит zoom.exe', async () => { h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('zoom.exe') }) const { isMeetingActive } = await load() expect(await isMeetingActive()).toBe(true) }) it('детектит новые Teams (ms-teams.exe)', async () => { h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('explorer.exe', 'ms-teams.exe') }) const { isMeetingActive } = await load() expect(await isMeetingActive()).toBe(true) }) it('возвращает false когда ВКС-процессов нет', async () => { h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('explorer.exe', 'code.exe', 'chrome.exe') }) const { isMeetingActive } = await load() expect(await isMeetingActive()).toBe(false) }) it('кэширует результат в пределах CACHE_MS (exec вызывается один раз)', async () => { h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('discord.exe') }) const { isMeetingActive } = await load() await isMeetingActive() await isMeetingActive() expect(h.calls).toBe(1) }) it('при падении tasklist возвращает false и логирует warn', async () => { h.execImpl = (_c, _o, cb) => cb(new Error('ETIMEDOUT')) const { isMeetingActive } = await load() expect(await isMeetingActive()).toBe(false) expect(h.log.warn).toHaveBeenCalled() }) it('isMeetingActiveSync отражает последний известный результат', async () => { h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('webex.exe') }) const mod = await load() expect(mod.isMeetingActiveSync()).toBe(false) // до первого запроса await mod.isMeetingActive() expect(mod.isMeetingActiveSync()).toBe(true) }) it('на не-Windows возвращает false без вызова tasklist', async () => { const original = process.platform Object.defineProperty(process, 'platform', { value: 'linux' }) try { const { isMeetingActive } = await load() expect(await isMeetingActive()).toBe(false) expect(h.calls).toBe(0) } finally { Object.defineProperty(process, 'platform', { value: original }) } }) })