config-audit/tests/lib/skill-listing-budget.test.mjs
Kjell Tore Guttormsen 2082b7d112 feat(skl,cml): --context-window calibration, advisory when unknown (v5.11 B8) [skip-docs]
SKL-002 (skill-listing budget) and CML char-budget now calibrate to a
resolved context window instead of always anchoring at 200k:

- resolveContextWindow(): --context-window <n> calibrates; 'auto' keeps the
  conservative 200k anchor but marks advisory (model→window probing deferred
  to B8b); no flag → 200k anchor, byte-identical to pre-B8 default.
- scaleForWindow(): linear off the 200k anchor (identity at the anchor).
- SKL + CML each keep an untouched default branch (window===200k && !advisory)
  for byte-stability and a calibrated branch; advisory downgrades the budget
  finding from a breach (low/medium) to info.
- Flag wired through scan-orchestrator + posture; runAllScanners resolves once
  and threads { contextWindow } to scanners (others ignore the 3rd arg).
- CPS intentionally excluded: it has no window-anchored budget (fixed
  150-line volatility heuristic), so there is nothing to calibrate.

15 new tests; e2e CLI verified (1M suppresses SKL-002, auto → info, default
unchanged); full suite 1279 green; snapshots byte-stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:44:52 +02:00

195 lines
7.1 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 } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import {
assessSkillListingBudget,
envFlag,
isBundledSkillsDisabled,
DESCRIPTION_CAP,
AGGREGATE_BUDGET_TOKENS,
CONTEXT_WINDOW_ANCHOR,
LARGE_CONTEXT_WINDOW,
LARGE_CONTEXT_BUDGET_TOKENS,
withCommas,
BUDGET_CALIBRATION_NOTE,
} from '../../scanners/lib/skill-listing-budget.mjs';
// This lib is the single source of truth for the skill-listing budget that both
// the SKL scanner (diagnoses overflow) and the GAP scanner (recommends the
// disableBundledSkills lever) consume. The constants and aggregate math here
// must match what SKL shipped in v5.2.0 — these tests pin that contract.
describe('skill-listing-budget — constants', () => {
it('DESCRIPTION_CAP is the verified 1536-char listing cap (CC 2.1.105)', () => {
assert.equal(DESCRIPTION_CAP, 1536);
});
it('AGGREGATE_BUDGET_TOKENS is 2% of the conservative 200k anchor', () => {
assert.equal(CONTEXT_WINDOW_ANCHOR, 200_000);
assert.equal(AGGREGATE_BUDGET_TOKENS, 4000);
});
it('LARGE_CONTEXT_BUDGET_TOKENS is 2% of the 1M window', () => {
assert.equal(LARGE_CONTEXT_WINDOW, 1_000_000);
assert.equal(LARGE_CONTEXT_BUDGET_TOKENS, 20000);
});
});
describe('withCommas', () => {
it('inserts thousands separators', () => {
assert.equal(withCommas(1_000_000), '1,000,000');
assert.equal(withCommas(4000), '4,000');
assert.equal(withCommas(999), '999');
assert.equal(withCommas(0), '0');
});
});
describe('BUDGET_CALIBRATION_NOTE', () => {
it('discloses the 1M-context scaling and flags this as an estimate, not telemetry', () => {
assert.match(BUDGET_CALIBRATION_NOTE, /1,000,000/);
assert.match(BUDGET_CALIBRATION_NOTE, /20,000/);
assert.match(BUDGET_CALIBRATION_NOTE, /estimate/i);
});
});
describe('assessSkillListingBudget — aggregate math', () => {
it('sums each description length capped at the 1536 cap', () => {
// 1000 + 1000 + min(5000,1536) = 3536
const r = assessSkillListingBudget([1000, 1000, 5000]);
assert.equal(r.aggregateChars, 3536);
});
it('scanned counts every entry, including empty descriptions', () => {
const r = assessSkillListingBudget([0, 0, 100]);
assert.equal(r.scanned, 3);
assert.equal(r.aggregateChars, 100);
});
it('aggregateTokens mirrors estimateTokens markdown (ceil chars/4)', () => {
const r = assessSkillListingBudget([16]); // 16 chars -> 4 tokens
assert.equal(r.aggregateTokens, 4);
});
it('is NOT over budget exactly at the budget (16×1000 chars -> 4000 tok)', () => {
const r = assessSkillListingBudget(Array(16).fill(1000));
assert.equal(r.aggregateChars, 16000);
assert.equal(r.aggregateTokens, 4000);
assert.equal(r.overBudget, false);
assert.equal(r.overBy, 0);
});
it('is over budget one skill past the budget (17×1000 chars -> 4250 tok)', () => {
const r = assessSkillListingBudget(Array(17).fill(1000));
assert.equal(r.aggregateTokens, 4250);
assert.equal(r.overBudget, true);
assert.equal(r.overBy, 250);
});
it('exposes budgetTokens and handles the empty case', () => {
const r = assessSkillListingBudget([]);
assert.equal(r.budgetTokens, AGGREGATE_BUDGET_TOKENS);
assert.equal(r.scanned, 0);
assert.equal(r.aggregateChars, 0);
assert.equal(r.aggregateTokens, 0);
assert.equal(r.overBudget, false);
assert.equal(r.overBy, 0);
});
it('treats negative or non-finite lengths as zero contribution', () => {
const r = assessSkillListingBudget([-5, NaN, 100]);
assert.equal(r.aggregateChars, 100);
assert.equal(r.scanned, 3);
});
it('accepts a calibrated budget (B8): 17×1000 chars is within a 20,000-tok 1M budget', () => {
const r = assessSkillListingBudget(Array(17).fill(1000), 20_000);
assert.equal(r.aggregateTokens, 4250);
assert.equal(r.budgetTokens, 20_000, 'reports the calibrated budget, not the 200k default');
assert.equal(r.overBudget, false, 'within the relaxed 1M budget');
assert.equal(r.overBy, 0);
});
it('the budget argument defaults to the 200k anchor (byte-stable for existing callers)', () => {
const r = assessSkillListingBudget(Array(17).fill(1000));
assert.equal(r.budgetTokens, AGGREGATE_BUDGET_TOKENS);
assert.equal(r.overBudget, true);
});
});
describe('envFlag', () => {
it('treats 1/true/yes/on as set', () => {
for (const v of ['1', 'true', 'TRUE', 'yes', 'on', ' 1 ']) {
assert.equal(envFlag(v), true, `expected ${JSON.stringify(v)} → true`);
}
});
it('treats null/empty/0/false/no/off as un-set', () => {
for (const v of [undefined, null, '', '0', 'false', 'no', 'off', ' ']) {
assert.equal(envFlag(v), false, `expected ${JSON.stringify(v)} → false`);
}
});
});
describe('isBundledSkillsDisabled — lever cascade', () => {
async function withHome(fn) {
const home = await mkdtemp(join(tmpdir(), 'config-audit-lever-home-'));
const originalHome = process.env.HOME;
const originalEnv = process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS;
process.env.HOME = home;
delete process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS;
try {
return await fn(home);
} finally {
process.env.HOME = originalHome;
if (originalEnv === undefined) delete process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS;
else process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS = originalEnv;
await rm(home, { recursive: true, force: true });
}
}
async function writeSettings(dir, obj) {
await mkdir(dir, { recursive: true });
await writeFile(join(dir, 'settings.json'), JSON.stringify(obj));
}
it('is false on a clean HOME with no env and no settings', async () => {
await withHome(async () => {
assert.equal(await isBundledSkillsDisabled(), false);
});
});
it('is true when the env var is set', async () => {
await withHome(async () => {
process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS = '1';
assert.equal(await isBundledSkillsDisabled(), true);
});
});
it('is true when user ~/.claude/settings.json sets it (the location discovery misses)', async () => {
await withHome(async (home) => {
await writeSettings(join(home, '.claude'), { disableBundledSkills: true });
assert.equal(await isBundledSkillsDisabled(), true);
});
});
it('is true when project .claude/settings.json sets it', async () => {
await withHome(async () => {
const project = await mkdtemp(join(tmpdir(), 'config-audit-lever-proj-'));
try {
await writeSettings(join(project, '.claude'), { disableBundledSkills: true });
assert.equal(await isBundledSkillsDisabled(project), true);
} finally {
await rm(project, { recursive: true, force: true });
}
});
});
it('is false when the setting is present but not strictly true', async () => {
await withHome(async (home) => {
await writeSettings(join(home, '.claude'), { disableBundledSkills: false });
assert.equal(await isBundledSkillsDisabled(), false);
});
});
});