The >500-line check emitted HIGH severity with "Files over 500 lines significantly reduce Claude's adherence to instructions." CC 2.1.169 scaled the "too long" warning by context window, and the plugin's own configuration-best-practices.md:97 footnote already says raw line count is a Sonnet-era heuristic superseded by cache-prefix stability — so the absolute HIGH + universal adherence claim is now-wrong. - >500 lines: HIGH -> MEDIUM, reworded to token-cost-every-turn + smaller-context-model caveat + cache-prefix pointer; notes CC 2.1.169 scales the threshold by context window. - >200 lines: stays MEDIUM, dropped the absolute "optimal adherence" framing for the same token/context-window framing. Aligns the scanner with anti-patterns.md:7 (CA-CML-001 = medium) and configuration-best-practices.md:97. No snapshot impact (byte snapshots use a 24-line fixture CLAUDE.md). Full suite: 842/842 green (+5). self-audit PASS, A(100)/A(97).
163 lines
6 KiB
JavaScript
163 lines
6 KiB
JavaScript
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 { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
|
|
import { scan } from '../../scanners/claude-md-linter.mjs';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const FIXTURES = resolve(__dirname, '../fixtures');
|
|
|
|
describe('CML scanner — healthy project', () => {
|
|
let result;
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
const discovery = await discoverConfigFiles(resolve(FIXTURES, 'healthy-project'));
|
|
result = await scan(resolve(FIXTURES, 'healthy-project'), discovery);
|
|
});
|
|
|
|
it('returns status ok', () => {
|
|
assert.strictEqual(result.status, 'ok');
|
|
});
|
|
|
|
it('scans at least 1 file', () => {
|
|
assert.ok(result.files_scanned >= 1);
|
|
});
|
|
|
|
it('has scanner prefix CML', () => {
|
|
assert.strictEqual(result.scanner, 'CML');
|
|
});
|
|
|
|
it('has all severity count keys', () => {
|
|
for (const key of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
assert.ok(key in result.counts, `Missing count key: ${key}`);
|
|
}
|
|
});
|
|
|
|
it('finds no critical or high issues in healthy project', () => {
|
|
const serious = result.findings.filter(f => f.severity === 'critical' || f.severity === 'high');
|
|
assert.strictEqual(serious.length, 0, `Found serious issues: ${serious.map(f => f.title).join(', ')}`);
|
|
});
|
|
|
|
it('all finding IDs match CA-CML-NNN pattern', () => {
|
|
for (const f of result.findings) {
|
|
assert.match(f.id, /^CA-CML-\d{3}$/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('CML scanner — broken project', () => {
|
|
let result;
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
const discovery = await discoverConfigFiles(resolve(FIXTURES, 'broken-project'));
|
|
result = await scan(resolve(FIXTURES, 'broken-project'), discovery);
|
|
});
|
|
|
|
it('detects long CLAUDE.md (>200 lines)', () => {
|
|
const found = result.findings.some(f => f.title.includes('exceeds'));
|
|
assert.ok(found, 'Should detect oversized CLAUDE.md');
|
|
});
|
|
|
|
it('detects missing headings', () => {
|
|
const found = result.findings.some(f => f.title.includes('no markdown headings'));
|
|
assert.ok(found, 'Should detect lack of headings');
|
|
});
|
|
|
|
it('detects TODO markers', () => {
|
|
const found = result.findings.some(f => f.title.includes('TODO'));
|
|
assert.ok(found, 'Should detect TODO markers');
|
|
});
|
|
|
|
it('detects repeated content', () => {
|
|
const found = result.findings.some(f => f.title.includes('Repeated content'));
|
|
assert.ok(found, 'Should detect repeated lines');
|
|
});
|
|
});
|
|
|
|
describe('CML scanner — broken project: 200-tier stays MEDIUM (regression lock)', () => {
|
|
let result;
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
const discovery = await discoverConfigFiles(resolve(FIXTURES, 'broken-project'));
|
|
result = await scan(resolve(FIXTURES, 'broken-project'), discovery);
|
|
});
|
|
|
|
it('the >200 length finding is MEDIUM', () => {
|
|
const f = result.findings.find(x => /exceeds recommended 200/.test(x.title || ''));
|
|
assert.ok(f, 'expected the 200-line recommendation finding');
|
|
assert.strictEqual(f.severity, 'medium');
|
|
});
|
|
});
|
|
|
|
describe('CML scanner — large cascade (>500 lines): reframed, not absolute-adherence HIGH', () => {
|
|
// large-cascade/CLAUDE.md is 1024 lines. CC 2.1.169 scales the "too long"
|
|
// threshold by context window, and the plugin's own
|
|
// configuration-best-practices.md:97 footnote says raw line count is a
|
|
// Sonnet-era heuristic superseded by cache-prefix stability. So the absolute
|
|
// HIGH@500 + "significantly reduce adherence" claim is now-wrong.
|
|
let result;
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
const discovery = await discoverConfigFiles(resolve(FIXTURES, 'large-cascade'));
|
|
result = await scan(resolve(FIXTURES, 'large-cascade'), discovery);
|
|
});
|
|
|
|
it('flags the >500 finding as MEDIUM, not HIGH', () => {
|
|
const f = result.findings.find(x => /exceeds 500/.test(x.title || ''));
|
|
assert.ok(f, 'expected the >500 length finding');
|
|
assert.strictEqual(f.severity, 'medium');
|
|
});
|
|
|
|
it('drops the absolute "significantly reduce adherence" claim', () => {
|
|
const f = result.findings.find(x => /exceeds 500/.test(x.title || ''));
|
|
assert.doesNotMatch(String(f?.description || ''), /significantly reduce/i);
|
|
});
|
|
|
|
it('reframes toward token cost / context window / cache-prefix', () => {
|
|
const f = result.findings.find(x => /exceeds 500/.test(x.title || ''));
|
|
assert.match(
|
|
`${f?.description || ''} ${f?.recommendation || ''}`,
|
|
/every turn|context window|cache/i,
|
|
);
|
|
});
|
|
|
|
it('produces no HIGH-severity length finding', () => {
|
|
const highLen = result.findings.filter(f => f.severity === 'high' && /exceeds/.test(f.title || ''));
|
|
assert.strictEqual(highLen.length, 0, `unexpected HIGH length finding: ${highLen.map(f => f.title).join(', ')}`);
|
|
});
|
|
});
|
|
|
|
describe('CML scanner — empty project', () => {
|
|
let result;
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
const discovery = await discoverConfigFiles(resolve(FIXTURES, 'empty-project'));
|
|
result = await scan(resolve(FIXTURES, 'empty-project'), discovery);
|
|
});
|
|
|
|
it('detects missing CLAUDE.md', () => {
|
|
const found = result.findings.some(f => f.title.includes('No CLAUDE.md'));
|
|
assert.ok(found, 'Should report missing CLAUDE.md');
|
|
});
|
|
|
|
it('returns high severity for missing CLAUDE.md', () => {
|
|
const f = result.findings.find(f => f.title.includes('No CLAUDE.md'));
|
|
assert.strictEqual(f?.severity, 'high');
|
|
});
|
|
});
|
|
|
|
describe('CML scanner — minimal project', () => {
|
|
let result;
|
|
beforeEach(async () => {
|
|
resetCounter();
|
|
const discovery = await discoverConfigFiles(resolve(FIXTURES, 'minimal-project'));
|
|
result = await scan(resolve(FIXTURES, 'minimal-project'), discovery);
|
|
});
|
|
|
|
it('detects nearly empty CLAUDE.md', () => {
|
|
const found = result.findings.some(f => f.title.includes('nearly empty'));
|
|
assert.ok(found, 'Should detect nearly empty CLAUDE.md');
|
|
});
|
|
});
|