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); }); });