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>
195 lines
7.1 KiB
JavaScript
195 lines
7.1 KiB
JavaScript
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);
|
||
});
|
||
});
|
||
});
|