Chunk 2 of the disableBundledSkills GAP feature. Adds a conditional GAP check
that prescribes the `disableBundledSkills` lever — but only when the active
skill listing is measurably over budget (SKL's CA-SKL-002 overflow signal) and
the lever is un-pulled. It stays an opportunity, not noise.
Bundled skills (/code-review, /batch, /debug, /loop, /claude-api, …) live in the
CC binary, not on disk, so their exact cost is unmeasurable here — the finding
says so plainly, and frames the lever as zero-cost budget reclaim that leaves
the user's own skills untouched. CC 2.1.169+.
- Pure, exported bundledSkillsLeverFinding({leverPulled, aggregate}) → finding|null
(severity low, category token-efficiency, CA-GAP-NNN), wired into scan() via the
shared measureActiveSkillListing().
- Lever resolved via new isBundledSkillsDisabled(): env var + settings cascade
read directly, because discovery does NOT tag ~/.claude/settings.json (its
relPath lacks ".claude" when walked from the .claude root) — the dominant
user-scope location for this global preference would otherwise be missed.
- GAP scan() now reads HOME → existing GAP tests retrofitted to withHermeticHome
per the hermetic rule. Snapshots unchanged, contamination grep clean.
- 16 new tests (9 GAP, 7 lib). Suite 887 -> 903. README/CLAUDE.md document the
cross-scanner remediation; test counts synced. self-audit: PASS, configGrade
A 96, pluginGrade A 100, readme gate passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
181 lines
6.4 KiB
JavaScript
181 lines
6.4 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);
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
});
|