// ide-extension-discovery.test.mjs — discoverJetBrainsExtensions integration tests. // All fixtures built in-test via mkdtemp + mkdir — no committed filesystem state. import { describe, it, after } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, mkdir, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { discoverJetBrainsExtensions, getAndroidStudioBaseDir, } from '../../scanners/lib/ide-extension-discovery.mjs'; const TEST_PREFIX = 'llmsec-jbdisc-'; const created = []; async function buildFixtureBase(layout) { const base = await mkdtemp(join(tmpdir(), TEST_PREFIX)); created.push(base); for (const [product, plugins] of Object.entries(layout)) { for (const plug of plugins) { await mkdir(join(base, product, 'plugins', plug), { recursive: true }); } } return base; } describe('discoverJetBrainsExtensions — basic walk', () => { it('finds plugins under IntelliJIdea/plugins/', async () => { const base = await buildFixtureBase({ 'IntelliJIdea2024.3': ['com.example.one', 'com.example.two'], }); const res = await discoverJetBrainsExtensions({ rootsOverride: [base] }); assert.equal(res.extensions.length, 2); assert.ok(res.extensions.every(e => e.type === 'jetbrains')); assert.ok(res.extensions.some(e => e.name === 'com.example.one')); assert.equal(res.extensions[0].productDir, 'IntelliJIdea2024.3'); }); }); describe('discoverJetBrainsExtensions — Fleet excluded', () => { it('ignores Fleet directory', async () => { const base = await buildFixtureBase({ 'IntelliJIdea2024.3': ['com.example.good'], 'Fleet': ['com.example.fleet'], }); const res = await discoverJetBrainsExtensions({ rootsOverride: [base] }); assert.equal(res.extensions.length, 1); assert.equal(res.extensions[0].name, 'com.example.good'); assert.ok(res.extensions.every(e => !e.location.includes('Fleet'))); }); }); describe('discoverJetBrainsExtensions — Android Studio support', () => { it('discovers from AndroidStudio product dir', async () => { const base = await buildFixtureBase({ 'AndroidStudio2024.3.1': ['com.google.example'], }); const res = await discoverJetBrainsExtensions({ rootsOverride: [base] }); assert.equal(res.extensions.length, 1); assert.equal(res.extensions[0].productDir, 'AndroidStudio2024.3.1'); }); }); describe('discoverJetBrainsExtensions — multi-product', () => { it('walks all matching product dirs', async () => { const base = await buildFixtureBase({ 'IntelliJIdea2024.3': ['a'], 'PyCharm2024.3': ['b'], 'GoLand2024.3': ['c'], 'Toolbox': ['d'], // excluded }); const res = await discoverJetBrainsExtensions({ rootsOverride: [base] }); assert.equal(res.extensions.length, 3); assert.ok(!res.extensions.some(e => e.location.includes('Toolbox'))); }); }); describe('discoverJetBrainsExtensions — skip hidden + disabled_plugins.txt', () => { it('ignores dotfile plugins and disabled_plugins.txt', async () => { const base = await buildFixtureBase({ 'IntelliJIdea2024.3': ['.hidden', 'com.real', 'disabled_plugins.txt'], }); const res = await discoverJetBrainsExtensions({ rootsOverride: [base] }); assert.equal(res.extensions.length, 1); assert.equal(res.extensions[0].name, 'com.real'); }); }); describe('getAndroidStudioBaseDir — Linux path divergence', () => { it('is a path-producing function (no exception)', () => { // Actual return value is null or a path depending on host — just assert no throw. const v = getAndroidStudioBaseDir(); assert.ok(v === null || typeof v === 'string'); }); }); after(async () => { for (const r of created) await rm(r, { recursive: true, force: true }).catch(() => {}); });