// ai-bom.test.mjs — Tests for AI-BOM generator (CycloneDX 1.6) import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { discoverComponents, buildAIBOM } from '../../scanners/lib/bom-builder.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const GRADE_A_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-a-project'); const GRADE_F_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-f-project'); const PLUGIN_ROOT = resolve(__dirname, '../..'); // --------------------------------------------------------------------------- // CycloneDX structure tests // --------------------------------------------------------------------------- describe('ai-bom: buildAIBOM structure', () => { it('produces valid CycloneDX 1.6 envelope', () => { const bom = buildAIBOM([]); assert.equal(bom.bomFormat, 'CycloneDX'); assert.equal(bom.specVersion, '1.6'); assert.equal(bom.version, 1); }); it('has metadata with timestamp and tools', () => { const bom = buildAIBOM([]); assert.ok(bom.metadata.timestamp.match(/^\d{4}-\d{2}-\d{2}T/)); assert.equal(bom.metadata.tools[0].name, 'ai-bom-generator'); assert.equal(bom.metadata.tools[0].vendor, 'llm-security'); }); it('includes project metadata from arguments', () => { const bom = buildAIBOM([], { name: 'test-project', version: '1.2.3' }); assert.equal(bom.metadata.component.name, 'test-project'); assert.equal(bom.metadata.component.version, '1.2.3'); }); it('defaults project name to unknown', () => { const bom = buildAIBOM([]); assert.equal(bom.metadata.component.name, 'unknown'); assert.equal(bom.metadata.component.version, '0.0.0'); }); it('has components and dependencies arrays', () => { const bom = buildAIBOM([]); assert.ok(Array.isArray(bom.components)); assert.ok(Array.isArray(bom.dependencies)); }); it('passes through components array', () => { const components = [ { type: 'machine-learning-model', name: 'opus', 'bom-ref': 'model-opus' }, ]; const bom = buildAIBOM(components); assert.equal(bom.components.length, 1); assert.equal(bom.components[0].name, 'opus'); }); }); // --------------------------------------------------------------------------- // Component discovery tests // --------------------------------------------------------------------------- describe('ai-bom: discoverComponents', () => { it('discovers hooks from grade-a fixture', async () => { const components = await discoverComponents(GRADE_A_FIXTURE); const hooks = components.filter(c => c.name.startsWith('hook:')); assert.ok(hooks.length >= 4, `Expected >= 4 hooks, got ${hooks.length}`); }); it('discovers knowledge files from plugin root', async () => { const components = await discoverComponents(PLUGIN_ROOT); const knowledge = components.filter(c => c.name.startsWith('knowledge:')); assert.ok(knowledge.length >= 5, `Expected >= 5 knowledge files, got ${knowledge.length}`); }); it('returns empty array for minimal project', async () => { const components = await discoverComponents(GRADE_F_FIXTURE); // Grade-f has no hooks, no MCP, no knowledge assert.ok(Array.isArray(components)); }); it('all components have type and name', async () => { const components = await discoverComponents(PLUGIN_ROOT); for (const c of components) { assert.ok(c.type, `Component missing type: ${JSON.stringify(c)}`); assert.ok(c.name, `Component missing name: ${JSON.stringify(c)}`); assert.ok(c['bom-ref'], `Component missing bom-ref: ${JSON.stringify(c)}`); } }); it('component types are valid CycloneDX types', async () => { const validTypes = new Set(['machine-learning-model', 'framework', 'library', 'data', 'application']); const components = await discoverComponents(PLUGIN_ROOT); for (const c of components) { assert.ok(validTypes.has(c.type), `Invalid component type: ${c.type} for ${c.name}`); } }); }); // --------------------------------------------------------------------------- // Integration: discover + build // --------------------------------------------------------------------------- describe('ai-bom: end-to-end', () => { it('produces valid CycloneDX BOM from real project', async () => { const components = await discoverComponents(PLUGIN_ROOT); const bom = buildAIBOM(components, { name: 'llm-security', version: '6.0.0' }); assert.equal(bom.bomFormat, 'CycloneDX'); assert.equal(bom.specVersion, '1.6'); assert.ok(bom.components.length > 0, 'Expected at least one component'); assert.equal(bom.metadata.component.name, 'llm-security'); }); it('empty project produces valid BOM with zero components', async () => { const components = await discoverComponents(GRADE_F_FIXTURE); const bom = buildAIBOM(components); assert.equal(bom.bomFormat, 'CycloneDX'); assert.ok(Array.isArray(bom.components)); }); });