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
100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
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);
|
||
});
|
||
});
|