// dashboard.test.mjs — Tests for the cross-project dashboard aggregator // Tests discovery, aggregation, caching, and grade calculation. // Uses posture-scan fixtures as known projects. import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { resolve, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { writeFile, mkdir, rm, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { resetCounter } from '../../scanners/lib/output.mjs'; // Import functions under test import { discoverProjects, aggregate } from '../../scanners/dashboard-aggregator.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const FIXTURES = resolve(__dirname, '../fixtures/posture-scan'); const GRADE_A_FIXTURE = resolve(FIXTURES, 'grade-a-project'); const GRADE_F_FIXTURE = resolve(FIXTURES, 'grade-f-project'); // --------------------------------------------------------------------------- // Discovery tests // --------------------------------------------------------------------------- describe('dashboard-aggregator: discoverProjects', () => { it('finds projects with CLAUDE.md marker', async () => { // The fixtures themselves have CLAUDE.md — use parent as search root with depth 2 const projects = await discoverProjects({ maxDepth: 2, extraPaths: [GRADE_A_FIXTURE], }); assert.ok(projects.includes(GRADE_A_FIXTURE), 'Should find grade-a fixture via extraPaths'); }); it('finds projects with .claude-plugin marker', async () => { const projects = await discoverProjects({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], }); assert.ok(projects.includes(GRADE_A_FIXTURE)); }); it('deduplicates project paths', async () => { const projects = await discoverProjects({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE, GRADE_A_FIXTURE, GRADE_A_FIXTURE], }); const count = projects.filter(p => p === GRADE_A_FIXTURE).length; assert.equal(count, 1, 'Should deduplicate'); }); it('returns sorted paths', async () => { const projects = await discoverProjects({ maxDepth: 0, extraPaths: [GRADE_F_FIXTURE, GRADE_A_FIXTURE], }); const filtered = projects.filter(p => p.includes('posture-scan')); for (let i = 1; i < filtered.length; i++) { assert.ok(filtered[i] >= filtered[i - 1], 'Should be sorted'); } }); it('handles non-existent extra paths gracefully', async () => { const projects = await discoverProjects({ maxDepth: 0, extraPaths: ['/nonexistent/path/that/does/not/exist'], }); assert.ok(Array.isArray(projects)); }); }); // --------------------------------------------------------------------------- // Aggregation tests // --------------------------------------------------------------------------- describe('dashboard-aggregator: aggregate', () => { let tmpCacheDir; beforeEach(async () => { resetCounter(); }); it('scans known fixtures and returns structured result', async () => { const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE, GRADE_F_FIXTURE], useCache: false, }); // Meta assert.equal(result.meta.scanner, 'dashboard-aggregator'); assert.ok(result.meta.version); assert.ok(result.meta.timestamp); assert.equal(result.meta.from_cache, false); // Machine assert.ok(result.machine.grade, 'Should have machine grade'); assert.ok(result.machine.projects_scanned >= 2, `Expected >=2 projects, got ${result.machine.projects_scanned}`); // Projects array assert.ok(Array.isArray(result.projects)); assert.ok(result.projects.length >= 2); // Each project has required fields for (const p of result.projects) { assert.ok(p.path, 'Project should have path'); assert.ok(p.display_name, 'Project should have display_name'); assert.ok(p.grade, 'Project should have grade'); assert.ok(typeof p.risk_score === 'number', 'risk_score should be number'); assert.ok(typeof p.findings_count === 'number', 'findings_count should be number'); assert.ok(p.counts, 'Project should have counts'); } }); it('machine grade is weakest link', async () => { const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE, GRADE_F_FIXTURE], useCache: false, }); // grade-f-project should drag machine grade down const fProject = result.projects.find(p => p.path === GRADE_F_FIXTURE); assert.ok(fProject, 'Should find grade-f fixture in results'); assert.equal(fProject.grade, 'F', 'Grade F fixture should get F'); // Machine grade should be F (weakest link) assert.equal(result.machine.grade, 'F', 'Machine grade should be F (weakest link)'); }); it('identifies worst category per project', async () => { const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_F_FIXTURE], useCache: false, }); const fProject = result.projects.find(p => p.path === GRADE_F_FIXTURE); assert.ok(fProject); assert.ok(fProject.worst_category, 'Should identify worst category'); assert.equal(fProject.worst_status, 'FAIL', 'Worst status should be FAIL for grade-f'); }); it('aggregates finding counts across projects', async () => { const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE, GRADE_F_FIXTURE], useCache: false, }); assert.ok(typeof result.machine.total_findings === 'number'); assert.ok(typeof result.machine.counts.critical === 'number'); assert.ok(typeof result.machine.counts.high === 'number'); // Total should match sum of per-project const sumFindings = result.projects.reduce((s, p) => s + p.findings_count, 0); assert.equal(result.machine.total_findings, sumFindings); }); it('includes duration_ms per project', async () => { const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: false, }); for (const p of result.projects) { assert.ok(typeof p.duration_ms === 'number', 'Should have duration_ms'); } }); it('records errors for invalid projects', async () => { // Create a fake project that will fail to scan properly const tmpDir = join(tmpdir(), `dashboard-test-err-${Date.now()}`); await mkdir(tmpDir, { recursive: true }); await writeFile(join(tmpDir, 'CLAUDE.md'), '# Test\n'); // Create a .claude dir with malformed settings.json to trigger issues // (posture-scanner should still succeed but with low grade) try { const result = await aggregate({ maxDepth: 0, extraPaths: [tmpDir], useCache: false, }); assert.ok(Array.isArray(result.errors)); // The project should either be in projects or errors const found = result.projects.some(p => p.path === tmpDir) || result.errors.some(e => e.path === tmpDir); assert.ok(found, 'Tmp project should appear in results or errors'); } finally { await rm(tmpDir, { recursive: true, force: true }); } }); }); // --------------------------------------------------------------------------- // Caching tests // --------------------------------------------------------------------------- describe('dashboard-aggregator: caching', () => { it('returns from_cache: true when cache is fresh', async () => { // First run: force fresh to populate cache const result1 = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: false, }); assert.equal(result1.meta.from_cache, false); // Second run: should use cache (freshly written) const result2 = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: true, stalenessMs: 60000, }); assert.equal(result2.meta.from_cache, true); }); it('bypasses cache with useCache: false', async () => { // Populate cache await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: true, stalenessMs: 60000, }); // Force fresh scan const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: false, }); assert.equal(result.meta.from_cache, false); }); it('rescans when cache is stale', async () => { // Populate cache await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: true, }); // Use 0ms staleness threshold — everything is stale const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: true, stalenessMs: 0, }); assert.equal(result.meta.from_cache, false); }); }); // --------------------------------------------------------------------------- // Grade comparison tests // --------------------------------------------------------------------------- describe('dashboard-aggregator: grade logic', () => { it('grade-a only yields machine grade A', async () => { resetCounter(); const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: false, }); const aProject = result.projects.find(p => p.path === GRADE_A_FIXTURE); if (aProject && aProject.grade === 'A') { // If only Grade A projects, machine grade should be A const allA = result.projects.every(p => p.grade === 'A'); if (allA) { assert.equal(result.machine.grade, 'A'); } } }); it('display_name uses tilde for home-relative paths', async () => { const result = await aggregate({ maxDepth: 0, extraPaths: [GRADE_A_FIXTURE], useCache: false, }); for (const p of result.projects) { if (p.path.startsWith(process.env.HOME || '/Users')) { assert.ok(p.display_name.startsWith('~/'), `Expected ~/... got ${p.display_name}`); } } }); });