'use strict' /** * Tests for apps/desktop/electron/venv-blocker-scan.ts * * Run with: npx vitest run electron/venv-blocker-scan.test.ts * (from apps/desktop; wired into npm test:desktop:platforms) */ import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { describe, it } from 'vitest' import { formatBlockerMessage, formatProbeFailedMessage, parseVenvBlockerScanOutput, resolveVenvPython, scanVenvBlockers, stopSafeVenvBlockers } from './venv-blocker-scan' // --------------------------------------------------------------------------- // resolveVenvPython // --------------------------------------------------------------------------- describe('resolveVenvPython', () => { it('returns a real path when a temp venv python file exists', () => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-vt-')) try { const scriptsDir = process.platform === 'win32' ? 'Scripts' : 'bin' const pythonName = process.platform === 'win32' ? 'python.exe' : 'python3' const dir = path.join(sandbox, 'venv', scriptsDir) fs.mkdirSync(dir, { recursive: true }) const pyPath = path.join(dir, pythonName) fs.writeFileSync(pyPath, '', { mode: 0o755 }) assert.equal(resolveVenvPython(sandbox), pyPath) } finally { fs.rmSync(sandbox, { recursive: true, force: true }) } }) it('returns null for non-existent venv', () => { assert.equal(resolveVenvPython('/nonexistent'), null) }) }) // --------------------------------------------------------------------------- // formatBlockerMessage / formatProbeFailedMessage // --------------------------------------------------------------------------- describe('formatBlockerMessage', () => { it('includes PID, name, cmdline, remote-client warning, and retry suggestion', () => { const msg = formatBlockerMessage({ blocked: true, processes: [{ pid: 101, name: 'python.exe', cmdline: 'serve --host 10.0.0.1', kind: 'other', safeToStop: false }] }) assert.ok(msg.includes('PID 101')) assert.ok(msg.includes('python.exe')) assert.ok(msg.includes('serve')) assert.ok(msg.includes('remote backend')) assert.ok(msg.includes('retry')) assert.ok(!msg.includes('force-venv')) }) }) describe('formatProbeFailedMessage', () => { it('suggests retry and hermes update', () => { const msg = formatProbeFailedMessage() assert.ok(msg.includes('hermes update')) assert.ok(msg.includes('retry')) }) it('distinguishes a timeout from a confirmed blocker', () => { const msg = formatProbeFailedMessage('timed out after 60 seconds') assert.ok(msg.includes('timed out after 60 seconds')) assert.ok(msg.includes('no blocking process was confirmed')) }) }) // --------------------------------------------------------------------------- // parseVenvBlockerScanOutput — pure function // --------------------------------------------------------------------------- describe('parseVenvBlockerScanOutput', () => { const ok = (over: any = {}) => JSON.stringify({ ok: true, blocked: false, processes: [], ...over }) it('valid clear', () => { const o = parseVenvBlockerScanOutput(ok()) assert.equal(o.kind, 'clear') }) it('valid blocked', () => { const o = parseVenvBlockerScanOutput( ok({ blocked: true, processes: [{ pid: 1, name: 'p', cmdline: 'c' }] }) ) assert.equal(o.kind, 'blocked') }) // Contract fixture (#98336/#98350): the scanner reports exemption // diagnostics (counts + sanitized evidence) alongside the authoritative // blocked/processes fields. The consumer must tolerate those fields today // and must keep enforcing blocked/processes consistency — a future parser // change that either chokes on the diagnostics or silently reinterprets // an exemption as a blocker breaks this fixture. it('tolerates exemption diagnostics while enforcing blocked/processes consistency', () => { const clear = parseVenvBlockerScanOutput( ok({ pausable_gateways: 2, deferred_backends: 1, deferred_backend_evidence: [{ pid: 78, purpose: 'serve', port: 9119 }] }) ) assert.equal(clear.kind, 'clear') const blocked = parseVenvBlockerScanOutput( ok({ blocked: true, processes: [{ pid: 79, name: 'python.exe', cmdline: 'c' }], pausable_gateways: 1, deferred_backends: 1, deferred_backend_evidence: [{ pid: 78, purpose: 'serve', port: 9119 }] }) ) assert.equal(blocked.kind, 'blocked') if (blocked.kind !== 'blocked') { return } assert.deepEqual( blocked.result.processes.map(p => p.pid), [79] ) }) it('classifies Python http.server blockers as safe local previews with a human label', () => { const o = parseVenvBlockerScanOutput( ok({ blocked: true, processes: [ { pid: 47484, name: 'python.exe', cmdline: 'C:\\Hermes\\venv\\Scripts\\python.exe -m http.server 8766 --directory C', kind: 'local-preview', safeToStop: true, label: 'Example Preview', port: 8766, createTime: 1722798000.25 } ] }) ) assert.equal(o.kind, 'blocked') if (o.kind !== 'blocked') { return } assert.deepEqual(o.result.processes[0], { pid: 47484, name: 'python.exe', cmdline: 'C:\\Hermes\\venv\\Scripts\\python.exe -m http.server 8766 --directory C', kind: 'local-preview', safeToStop: true, label: 'Example Preview', port: 8766, createTime: 1722798000.25 }) }) it('does not trust a truncated http.server command line without scanner identity metadata', () => { const o = parseVenvBlockerScanOutput( ok({ blocked: true, processes: [ { pid: 47484, name: 'python.exe', cmdline: 'python.exe -m http.server 8766 --directory C' } ] }) ) assert.equal(o.kind, 'blocked') if (o.kind !== 'blocked') { return } assert.equal(o.result.processes[0]?.kind, 'other') assert.equal(o.result.processes[0]?.safeToStop, false) }) it('never marks an arbitrary Python process safe to stop', () => { const o = parseVenvBlockerScanOutput( ok({ blocked: true, processes: [{ pid: 9, name: 'python.exe', cmdline: 'python.exe important-script.py' }] }) ) assert.equal(o.kind, 'blocked') if (o.kind !== 'blocked') { return } assert.equal(o.result.processes[0]?.kind, 'other') assert.equal(o.result.processes[0]?.safeToStop, false) }) it('malformed JSON', () => { assert.equal(parseVenvBlockerScanOutput('not json').kind, 'probe-failure') }) it('ok=false is rejected', () => { assert.equal( parseVenvBlockerScanOutput(JSON.stringify({ ok: false, blocked: false, processes: [] })).kind, 'probe-failure' ) }) it('blocked must be boolean', () => { assert.equal(parseVenvBlockerScanOutput(ok({ blocked: 'false' })).kind, 'probe-failure') }) it('blocked=true with empty processes rejected', () => { assert.equal(parseVenvBlockerScanOutput(ok({ blocked: true, processes: [] })).kind, 'probe-failure') }) it('blocked=false with non-empty processes rejected', () => { assert.equal( parseVenvBlockerScanOutput(ok({ processes: [{ pid: 1, name: 'p', cmdline: 'c' }] })).kind, 'probe-failure' ) }) it('process pid must be positive integer', () => { assert.equal( parseVenvBlockerScanOutput(ok({ blocked: true, processes: [{ pid: 0, name: 'p', cmdline: 'c' }] })).kind, 'probe-failure' ) }) it('process name must be non-empty string', () => { assert.equal( parseVenvBlockerScanOutput(ok({ blocked: true, processes: [{ pid: 1, name: '', cmdline: 'c' }] })).kind, 'probe-failure' ) }) it('process missing cmdline is rejected', () => { assert.equal( parseVenvBlockerScanOutput(ok({ blocked: true, processes: [{ pid: 1, name: 'p' }] })).kind, 'probe-failure' ) }) }) // --------------------------------------------------------------------------- // scanVenvBlockers — subprocess with injection // --------------------------------------------------------------------------- describe('scanVenvBlockers', () => { const stubVenv = () => '/fake/venv/python.exe' const okJson = JSON.stringify({ ok: true, blocked: false, processes: [] }) const blockedJson = JSON.stringify({ ok: true, blocked: true, processes: [{ pid: 1, name: 'p', cmdline: 'c' }] }) function execReturn(json: string): any { return (async (...args: any[]) => ({ stdout: json, stderr: '' })) as any } function execThrow(status: number, stderr: string): any { return (async (...args: any[]) => { const e: any = new Error() e.status = status e.stderr = Buffer.from(stderr) throw e }) as any } function execTimeout(): any { return (async (...args: any[]) => { const e: any = new Error() e.killed = true e.signal = 'SIGTERM' throw e }) as any } it('clear scan returns clear', async () => { assert.equal((await scanVenvBlockers('/r', execReturn(okJson), stubVenv)).kind, 'clear') }) it('blocked scan returns blocked', async () => { assert.equal((await scanVenvBlockers('/r', execReturn(blockedJson), stubVenv)).kind, 'blocked') }) it('non-zero exit is probe-failure', async () => { const o = await scanVenvBlockers('/r', execThrow(2, 'ModuleNotFoundError'), stubVenv) assert.equal(o.kind, 'probe-failure') }) it('reports a timed-out subprocess explicitly', async () => { const o = await scanVenvBlockers('/r', execTimeout(), stubVenv) assert.deepEqual(o, { kind: 'probe-failure', error: 'timed out after 60 seconds' }) }) it('missing venv python is probe-failure', async () => { const o = await scanVenvBlockers('/r', execReturn(okJson), () => null) assert.equal(o.kind, 'probe-failure') }) it('malformed subprocess output is probe-failure', async () => { const o = await scanVenvBlockers('/r', execReturn('bad json'), stubVenv) assert.equal(o.kind, 'probe-failure') }) it('calls subprocess with correct args, cwd and timeout', async () => { const calls: any[] = [] const spy = (async (cmd: string, args: string[], opts: any) => { calls.push({ cmd, args, cwd: opts.cwd, timeout: opts.timeout }) return { stdout: okJson, stderr: '' } }) as any await scanVenvBlockers('/update/root', spy, stubVenv) assert.equal(calls.length, 1) const c = calls[0] assert.ok(c.cmd.endsWith('python.exe')) assert.deepEqual(c.args, ['-m', 'hermes_cli._scan_venv_blockers']) assert.equal(c.cwd, '/update/root') assert.equal(c.timeout, 60_000) }) }) describe('stopSafeVenvBlockers', () => { it('stops only blockers explicitly classified as safe local previews', async () => { const calls: Array<{ command: string; args: string[] }> = [] const exec = (async (command: string, args: string[]) => { calls.push({ command, args }) return { stdout: '', stderr: '' } }) as any const outcome = await stopSafeVenvBlockers( '/update/root', { blocked: true, processes: [ { pid: 47484, name: 'python.exe', cmdline: 'python.exe -m http.server 8766 --directory C:\\preview', kind: 'local-preview', safeToStop: true, label: 'preview', port: 8766, createTime: 1722798000.25 }, { pid: 99, name: 'python.exe', cmdline: 'python.exe important-script.py', kind: 'other', safeToStop: false } ] }, exec, () => 'C:\\Hermes\\venv\\Scripts\\python.exe' ) assert.deepEqual(calls, [ { command: 'C:\\Hermes\\venv\\Scripts\\python.exe', args: ['-m', 'hermes_cli._scan_venv_blockers', '--terminate-safe', '47484', '1722798000.25'] } ]) assert.deepEqual(outcome, { stopped: [47484], failed: [] }) }) })