config-audit/tests/lib/skill-listing-budget.test.mjs
Kjell Tore Guttormsen 0a631e3061 refactor(skl): extract skill-listing budget to shared lib (single source of truth)
Chunk 1 of the disableBundledSkills GAP feature. Moves the per-description cap,
aggregate budget constants, calibration note, and the enumerate-and-measure step
out of skill-listing-scanner into scanners/lib/skill-listing-budget.mjs — so SKL
(diagnoses overflow) and the upcoming GAP check (prescribes disableBundledSkills)
consume one budget definition instead of two divergent copies.

- New lib: assessSkillListingBudget (pure aggregate math) + measureActiveSkillListing
  (HOME-scoped enumerate-and-measure wrapper).
- SKL delegates measurement; all finding strings kept byte-identical. 18/18 SKL
  tests pass unchanged → behavior-neutral refactor.
- 12 new lib unit tests pin the budget contract. Suite 875 -> 887.
- README badge + CLAUDE.md test counts synced (self-audit --check-readme: passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 21:23:01 +02:00

100 lines
3.5 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 {
assessSkillListingBudget,
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);
});
});