ktg-plugin-marketplace/plugins/llm-security/tests/scanners/dashboard.test.mjs

294 lines
9.8 KiB
JavaScript

// 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}`);
}
}
});
});