ktg-plugin-marketplace/plugins/config-audit/tests/scanners/token-hotspots.test.mjs
Kjell Tore Guttormsen 34669d596c feat(config-audit): TOK consumes readActiveConfig (v5 F1)
Removes the v4 'void readActiveConfig' placeholder and wires the
active-config snapshot into the TOK scanner.

Per-turn behavior changes:
- Each enabled MCP server becomes its own hotspot entry (richer than
  the parent .mcp.json file alone)
- total_estimated_tokens now includes MCP server cost
- result.activeConfig exposes a small summary
  (claudeMdEstimatedTokens, mcpServerCount, pluginCount, skillCount)

Failures of readActiveConfig are non-fatal — the scanner falls back
to the discovery-only path used in v4.

Tests: +3 cases on the new tok-active-config fixture
(.mcp.json with 2 servers, CLAUDE.md, plugin skeleton).
2026-05-01 06:27:34 +02:00

173 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { scan } from '../../scanners/token-hotspots.mjs';
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const FIXTURES = resolve(__dirname, '../fixtures');
async function fixtureDiscovery(name) {
return discoverConfigFiles(resolve(FIXTURES, name));
}
async function runScanner(fixtureName) {
resetCounter();
const path = resolve(FIXTURES, fixtureName);
const discovery = await fixtureDiscovery(fixtureName);
return scan(path, discovery);
}
describe('TOK scanner — healthy-project', () => {
let result;
beforeEach(async () => {
result = await runScanner('healthy-project');
});
it('returns status ok', () => {
assert.equal(result.status, 'ok');
});
it('reports scanner prefix TOK', () => {
assert.equal(result.scanner, 'TOK');
});
it('finding IDs match CA-TOK-NNN pattern', () => {
for (const f of result.findings) {
assert.match(f.id, /^CA-TOK-\d{3}$/);
}
});
it('exposes total_estimated_tokens as a number', () => {
assert.equal(typeof result.total_estimated_tokens, 'number');
assert.ok(result.total_estimated_tokens >= 0);
});
});
describe('TOK scanner — opus-47/cache-breaking', () => {
let result;
beforeEach(async () => {
result = await runScanner('opus-47/cache-breaking');
});
it('flags CA-TOK-001 (cache-breaking volatile top)', () => {
const f = result.findings.find(x => x.id === 'CA-TOK-001');
assert.ok(f, 'expected a CA-TOK-001 finding for cache-breaking fixture');
});
it('CA-TOK-001 severity is medium or low', () => {
const f = result.findings.find(x => x.id === 'CA-TOK-001');
assert.ok(['medium', 'low'].includes(f.severity), `unexpected severity ${f.severity}`);
});
});
describe('TOK scanner — opus-47/redundant-tools', () => {
let result;
beforeEach(async () => {
result = await runScanner('opus-47/redundant-tools');
});
it('emits at least one CA-TOK-002 finding (redundant tool/permission)', () => {
const has002 = result.findings.some(f => /^CA-TOK-002$/.test(f.id) || f.title?.toLowerCase().includes('redundant'));
assert.ok(has002, 'expected a CA-TOK-002 finding for redundant-tools fixture');
});
});
describe('TOK scanner — opus-47/deep-imports', () => {
let result;
beforeEach(async () => {
result = await runScanner('opus-47/deep-imports');
});
it('emits at least one CA-TOK-003 finding (deep @import chain)', () => {
const has003 = result.findings.some(f => /^CA-TOK-003$/.test(f.id) || f.title?.toLowerCase().includes('import'));
assert.ok(has003, 'expected a CA-TOK-003 finding for deep-imports fixture');
});
});
describe('TOK scanner — opus-47/sonnet-era', () => {
let result;
beforeEach(async () => {
result = await runScanner('opus-47/sonnet-era');
});
it('emits no findings above info severity', () => {
const nonInfo = result.findings.filter(f => f.severity !== 'info');
assert.equal(nonInfo.length, 0, `expected only info findings, got: ${nonInfo.map(f => f.id + '=' + f.severity).join(', ')}`);
});
});
describe('TOK scanner — marketplace scale ordering', () => {
it('total_estimated_tokens strictly increases across small → medium → large', async () => {
const small = await runScanner('marketplace-small');
const medium = await runScanner('marketplace-medium');
const large = await runScanner('marketplace-large');
assert.ok(small.total_estimated_tokens < medium.total_estimated_tokens,
`expected small (${small.total_estimated_tokens}) < medium (${medium.total_estimated_tokens})`);
assert.ok(medium.total_estimated_tokens < large.total_estimated_tokens,
`expected medium (${medium.total_estimated_tokens}) < large (${large.total_estimated_tokens})`);
});
});
describe('TOK scanner — readActiveConfig integration (v5 F1)', () => {
let result;
beforeEach(async () => {
result = await runScanner('tok-active-config');
});
it('exposes activeConfig summary on the result (proves readActiveConfig was called)', () => {
assert.ok(result.activeConfig, 'expected result.activeConfig to be set');
assert.equal(typeof result.activeConfig.claudeMdEstimatedTokens, 'number');
assert.ok(result.activeConfig.claudeMdEstimatedTokens > 0,
`expected claudeMd cascade > 0 tokens, got ${result.activeConfig.claudeMdEstimatedTokens}`);
});
it('hotspots include at least one MCP-source entry', () => {
const hasMcp = result.hotspots.some(h => /mcp/i.test(h.source));
assert.ok(hasMcp,
`expected hotspots to include an MCP source; got: ${result.hotspots.map(h => h.source).join(', ')}`);
});
it('total_estimated_tokens exceeds the minimal sonnet-era baseline', async () => {
// sonnet-era has no .mcp.json — the activeConfig MCP entries from this
// fixture should push its total above sonnet-era's even when both fixtures
// share the user's ambient cascade/plugin state.
const baseline = await runScanner('opus-47/sonnet-era');
assert.ok(result.total_estimated_tokens > baseline.total_estimated_tokens,
`expected ${result.total_estimated_tokens} > ${baseline.total_estimated_tokens}`);
});
});
describe('TOK scanner — hotspots contract', () => {
let result;
beforeEach(async () => {
result = await runScanner('marketplace-large');
});
it('every finding has a non-empty recommendation', () => {
for (const f of result.findings) {
assert.ok(f.recommendation, `finding ${f.id} missing recommendation`);
assert.ok(String(f.recommendation).length > 0, `finding ${f.id} has empty recommendation`);
}
});
it('exposes a hotspots array of length 310', () => {
assert.ok(Array.isArray(result.hotspots), 'expected result.hotspots to be an array');
assert.ok(result.hotspots.length >= 3, `expected ≥3 hotspots, got ${result.hotspots.length}`);
assert.ok(result.hotspots.length <= 10, `expected ≤10 hotspots, got ${result.hotspots.length}`);
});
it('every hotspot exposes source/estimated_tokens/rank/recommendations', () => {
for (const h of result.hotspots) {
assert.ok(typeof h.source === 'string' && h.source.length > 0, 'hotspot.source missing');
assert.equal(typeof h.estimated_tokens, 'number', 'hotspot.estimated_tokens not a number');
assert.equal(typeof h.rank, 'number', 'hotspot.rank not a number');
assert.ok(Array.isArray(h.recommendations), 'hotspot.recommendations not an array');
assert.ok(h.recommendations.length >= 1 && h.recommendations.length <= 3,
`hotspot.recommendations length should be 13, got ${h.recommendations.length}`);
}
});
});