config-audit/tests/scanners/accurate-tokens.test.mjs
Kjell Tore Guttormsen 325182ddc9 test: isolate HOME in all CLI-spawning tests (close leak class)
Follow-up to the posture-grade-stability fix in 66433fe. Audited every
test that spawns a CLI and found more of the same class: tests running
HOME-scoped scanners (SKL/COL) or the CLAUDE.md cascade against the
developer's real ~/.claude instead of an isolated HOME.

Fixed (env: hermeticEnv()):
- posture.test.mjs        — runs full posture (SKL/COL/cascade); twin of
                            the posture-grade-stability leak, masked only
                            because its asserts are structural/relative
- drift-cli.test.mjs      — ACTIVE bug: the CLI wrote baselines into the
                            real ~/.claude during the run (pollution); now
                            isolated, and afterEach cleanup wrapped in
                            withHermeticHome so it looks in the same HOME
- token-hotspots-cli.test.mjs — scan-orchestrator run executes SKL/COL on
                            real HOME; TOK reads the HOME cascade
- accurate-tokens.test.mjs — TOK reads the HOME cascade (kept the
                            ANTHROPIC_API_KEY deletion)

Proven safe, left as-is (no HOME-scoped scan affecting assertions, no
HOME writes): post-edit-verify.test.mjs (fast-path early-returns only),
fix-cli.test.mjs (output byte-identical real vs empty HOME — fixable
findings are project-local HKV/RUL/SET, never SKL/COL),
lint-default-output (caller already uses withHermeticHome).

Suite 875/875, no snapshot drift. No test regressed under isolation,
confirming none had a hidden real-HOME dependency.

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

218 lines
7.1 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { hermeticEnv } from '../helpers/hermetic-home.mjs';
const exec = promisify(execFile);
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const REPO = resolve(__dirname, '../..');
const CLI = resolve(REPO, 'scanners/token-hotspots-cli.mjs');
const TOKENIZER_MODULE = resolve(REPO, 'scanners/lib/tokenizer-api.mjs');
const FIXTURE = resolve(REPO, 'tests/fixtures/marketplace-large');
describe('--accurate-tokens (no API key)', () => {
it('skips API calibration and reports calibration.skipped === "no-api-key"', async () => {
// Isolate HOME (TOK reads the CLAUDE.md cascade) while dropping the API key.
const env = hermeticEnv();
delete env.ANTHROPIC_API_KEY;
const { stdout, stderr } = await exec(
'node',
[CLI, FIXTURE, '--json', '--accurate-tokens'],
{ timeout: 30000, cwd: REPO, env },
);
const json = JSON.parse(stdout);
assert.equal(json.calibration?.skipped, 'no-api-key');
assert.match(stderr, /ANTHROPIC_API_KEY not set/i);
});
it('does not include calibration field when --accurate-tokens absent', async () => {
const { stdout } = await exec('node', [CLI, FIXTURE, '--json'], {
timeout: 30000,
cwd: REPO,
env: hermeticEnv(),
});
const json = JSON.parse(stdout);
assert.equal(json.calibration, undefined);
});
});
describe('tokenizer-api.mjs — key masking', () => {
it('masks API key in error messages to first 8 chars + "..."', async () => {
const tokenizerApi = await import(TOKENIZER_MODULE);
const fakeKey = 'sk-ant-FAKEKEY-1234567890';
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
const err = new Error('network failure');
throw err;
};
let threw = null;
try {
await tokenizerApi.callCountTokensApi('hello', fakeKey, { maxRetries: 0 });
} catch (e) {
threw = e;
} finally {
globalThis.fetch = originalFetch;
}
assert.ok(threw, 'expected an error to be thrown');
assert.ok(
!threw.message.includes('FAKEKEY-1234567890'),
`key must NOT appear unmasked in error message; got: ${threw.message}`,
);
assert.ok(
threw.message.includes('sk-ant-F'),
`error must mention masked key prefix sk-ant-F...; got: ${threw.message}`,
);
});
it('does NOT include response body in thrown errors on non-429 HTTP failure', async () => {
const tokenizerApi = await import(TOKENIZER_MODULE);
const fakeKey = 'sk-ant-LEAKYBODY-9999';
const echoBody = `{"error": "invalid api key sk-ant-LEAKYBODY-9999"}`;
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => ({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: async () => echoBody,
json: async () => JSON.parse(echoBody),
});
let threw = null;
try {
await tokenizerApi.callCountTokensApi('hi', fakeKey, { maxRetries: 0 });
} catch (e) {
threw = e;
} finally {
globalThis.fetch = originalFetch;
}
assert.ok(threw);
assert.ok(
!threw.message.includes('LEAKYBODY-9999'),
`body must NOT echo back into thrown message; got: ${threw.message}`,
);
assert.match(threw.message, /401/);
});
it('uses AbortController with a 5-second timeout', async () => {
const tokenizerApi = await import(TOKENIZER_MODULE);
const fakeKey = 'sk-ant-TIMEOUTKEY-0000';
let capturedSignal = null;
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_url, init) => {
capturedSignal = init?.signal;
return {
ok: true,
status: 200,
statusText: 'OK',
json: async () => ({ input_tokens: 42 }),
};
};
try {
const result = await tokenizerApi.callCountTokensApi('hi', fakeKey, { maxRetries: 0 });
assert.equal(result.input_tokens, 42);
assert.ok(capturedSignal, 'fetch must be called with an AbortController signal');
assert.ok(typeof capturedSignal.aborted === 'boolean');
} finally {
globalThis.fetch = originalFetch;
}
});
it('retries on 429 with exponential backoff (max 3 retries)', async () => {
const tokenizerApi = await import(TOKENIZER_MODULE);
const fakeKey = 'sk-ant-RETRYKEY-0000';
let calls = 0;
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
calls++;
if (calls <= 2) {
return {
ok: false,
status: 429,
statusText: 'Too Many Requests',
text: async () => '',
json: async () => ({}),
};
}
return {
ok: true,
status: 200,
statusText: 'OK',
json: async () => ({ input_tokens: 100 }),
};
};
try {
const result = await tokenizerApi.callCountTokensApi('hello', fakeKey, {
maxRetries: 3,
backoffBaseMs: 1,
});
assert.equal(result.input_tokens, 100);
assert.equal(calls, 3, 'expected 2 retries before success on third call');
} finally {
globalThis.fetch = originalFetch;
}
});
it('sends required headers: x-api-key, anthropic-version, content-type', async () => {
const tokenizerApi = await import(TOKENIZER_MODULE);
const fakeKey = 'sk-ant-HEADERTEST-0000';
let capturedInit = null;
const originalFetch = globalThis.fetch;
globalThis.fetch = async (_url, init) => {
capturedInit = init;
return {
ok: true,
status: 200,
statusText: 'OK',
json: async () => ({ input_tokens: 10 }),
};
};
try {
await tokenizerApi.callCountTokensApi('hi', fakeKey, { maxRetries: 0 });
const headers = capturedInit?.headers || {};
assert.equal(headers['x-api-key'], fakeKey);
assert.equal(headers['anthropic-version'], '2023-06-01');
assert.equal(headers['content-type'], 'application/json');
} finally {
globalThis.fetch = originalFetch;
}
});
});
describe('--accurate-tokens (mocked fetch — happy path)', () => {
it('returns input_tokens from mocked fetch response', async () => {
// Note: the v5 plan specified `mock.method(tokenizerApi, ...)` but ESM
// read-only bindings make that pattern unusable. We mock at the
// globalThis.fetch boundary instead, which is the actual external
// dependency and gives equivalent coverage. Subprocess CLI integration
// can't carry the mock across processes, so unit-level fetch mock + the
// no-key subprocess test are the two coverage points.
const tokenizerApi = await import(TOKENIZER_MODULE);
const fakeKey = 'sk-ant-MOCKED-0000';
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => ({
ok: true,
status: 200,
statusText: 'OK',
json: async () => ({ input_tokens: 4200 }),
});
try {
const result = await tokenizerApi.callCountTokensApi('hello world', fakeKey, { maxRetries: 0 });
assert.equal(result.input_tokens, 4200);
} finally {
globalThis.fetch = originalFetch;
}
});
});