// vsix-sandbox.test.mjs — Tests for the VSIX sandbox wrapper and worker. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { buildSandboxProfile, buildBwrapArgs, buildSandboxedWorker, runVsixWorker, runPluginWorker, DEFAULT_VSIX_WORKER_PATH, DEFAULT_JETBRAINS_WORKER_PATH, __testing, } from '../../scanners/lib/vsix-sandbox.mjs'; describe('vsix-sandbox — buildSandboxProfile', () => { it('returns null on non-darwin', () => { if (process.platform === 'darwin') return; // Not applicable here. const profile = buildSandboxProfile('/tmp'); assert.equal(profile, null); }); it('returns a valid profile string on macOS when sandbox-exec exists', () => { if (process.platform !== 'darwin') return; const has = spawnSync('which', ['sandbox-exec'], { encoding: 'utf8' }); if (has.status !== 0) return; const profile = buildSandboxProfile('/tmp'); assert.ok(profile, 'expected profile string on macOS'); assert.match(profile, /\(version 1\)/); assert.match(profile, /\(deny file-write\*\)/); assert.match(profile, /\(allow file-write\* \(subpath /); }); }); describe('vsix-sandbox — buildBwrapArgs', () => { it('returns null on non-linux', () => { if (process.platform === 'linux') return; const args = buildBwrapArgs('/tmp', ['/bin/true']); assert.equal(args, null); }); }); describe('vsix-sandbox — buildSandboxedWorker', () => { it('returns sandbox-exec on macOS, bwrap on Linux, or null fallback', () => { const { cmd, args, sandbox } = buildSandboxedWorker('/tmp', ['--url', 'https://x', '--tmpdir', '/tmp']); assert.ok(cmd); assert.ok(Array.isArray(args)); if (process.platform === 'darwin') { const has = spawnSync('which', ['sandbox-exec'], { encoding: 'utf8' }); if (has.status === 0) { assert.equal(sandbox, 'sandbox-exec'); assert.equal(cmd, 'sandbox-exec'); assert.equal(args[0], '-p'); } } else if (process.platform === 'linux') { // Could be 'bwrap' or null depending on availability — both are valid. assert.ok(sandbox === 'bwrap' || sandbox === null); } else { assert.equal(sandbox, null); assert.equal(cmd, 'node'); } }); it('always includes the worker path and forwarded args', () => { const { args } = buildSandboxedWorker('/tmp', ['--url', 'https://example/', '--tmpdir', '/tmp']); const joined = args.join(' '); assert.match(joined, /vsix-fetch-worker\.mjs/); assert.match(joined, /--url https:\/\/example\//); assert.match(joined, /--tmpdir \/tmp/); }); it('honors explicit workerPath parameter (Step 10 generalization)', () => { const custom = '/path/to/other-worker.mjs'; const { args } = buildSandboxedWorker('/tmp', ['--foo'], custom); const joined = args.join(' '); // Must contain the custom path, and NOT the default VSIX worker. assert.ok(joined.includes(custom), `expected custom worker in args: ${joined}`); assert.ok(!joined.includes('vsix-fetch-worker.mjs'), `custom worker should have replaced default: ${joined}`); assert.match(joined, /--foo/); }); it('defaults to DEFAULT_VSIX_WORKER_PATH when workerPath omitted', () => { const { args } = buildSandboxedWorker('/tmp', ['--url', 'https://x/', '--tmpdir', '/tmp']); assert.ok(args.some((a) => a === DEFAULT_VSIX_WORKER_PATH), `expected DEFAULT_VSIX_WORKER_PATH in args: ${args.join(' ')}`); }); it('exports DEFAULT_JETBRAINS_WORKER_PATH (Step 10 constant for Step 12)', () => { assert.ok(typeof DEFAULT_JETBRAINS_WORKER_PATH === 'string'); assert.match(DEFAULT_JETBRAINS_WORKER_PATH, /jetbrains-fetch-worker\.mjs$/); }); }); describe('vsix-sandbox — runPluginWorker (generalized runner)', () => { it('is exported as a function', () => { assert.equal(typeof runPluginWorker, 'function'); }); }); describe('vsix-sandbox — runVsixWorker (live worker, no network)', () => { it('handles non-HTTPS URL: worker exits with ok:false and a fetch error', async () => { const dir = await mkdtemp(join(tmpdir(), 'llm-sec-vsix-test-')); try { const { ok, payload, sandbox } = await runVsixWorker('http://example.com/foo.vsix', dir); assert.equal(ok, false); assert.ok(payload.error, 'expected error message'); assert.match(payload.error, /fetch failed|HTTPS|unsupported/i); // Sandbox may be 'sandbox-exec', 'bwrap', or null on Windows. All valid. assert.ok(sandbox === 'sandbox-exec' || sandbox === 'bwrap' || sandbox === null); } finally { await rm(dir, { recursive: true, force: true }); } }); it('handles unsupported URL kind: worker exits with ok:false', async () => { const dir = await mkdtemp(join(tmpdir(), 'llm-sec-vsix-test-')); try { const { ok, payload } = await runVsixWorker('https://example.com/random.zip', dir); assert.equal(ok, false); assert.match(payload.error, /unsupported URL|fetch failed/i); } finally { await rm(dir, { recursive: true, force: true }); } }); it('rejects when no --url or --tmpdir is provided (worker arg validation)', async () => { // Construct a minimal direct worker call without any args. const { spawn } = await import('node:child_process'); const child = spawn('node', [__testing.WORKER_PATH], { stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; child.stdout.on('data', (c) => { out += c.toString('utf8'); }); const code = await new Promise((resolve) => child.on('close', resolve)); assert.equal(code, 1); const parsed = JSON.parse(out.trim()); assert.equal(parsed.ok, false); assert.match(parsed.error, /missing --url or --tmpdir/); }); });