Completes the deferred B8 half. `--context-window auto` now probes the configured model and calibrates SKL/CML budgets to its real window instead of always falling back to the conservative advisory anchor. - lib/context-window.mjs: pure modelToContextWindow() maps known 1M-tier model IDs (Fable 5, Opus 4.8/4.7/4.6, Sonnet 4.6 — verified June 2026 — plus the explicit [1m] tier tag, dated/provider-prefixed IDs, and the opus/sonnet/fable aliases) to the 1M window; unknown/unconfirmed -> null (caller keeps the conservative anchor). resolveContextWindow() auto branch now probes opts.model: recognized -> auto-probed (not advisory); unknown or unpinned -> auto-unresolved (advisory, pre-B8b behavior). - lib/active-model.mjs (new): resolveActiveModel() reads the model the way Claude Code resolves it — shell ANTHROPIC_MODEL override, then settings cascade local > project > user. Injectable env, hermetic under test HOME. - scan-orchestrator: resolves the active model only when the flag is `auto` and threads it into resolveContextWindow; posture inherits via runAllScanners. Default (no flag) and explicit --context-window <n> paths ignore the model and stay byte-stable; frozen v5.0.0 + SC-5 snapshots untouched. TDD: 17 new tests (context-window mapping/probe + active-model cascade). Suite 1296 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
4 KiB
JavaScript
96 lines
4 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { resolveActiveModel } from '../../scanners/lib/active-model.mjs';
|
|
|
|
// B8b — resolveActiveModel reads the configured model so `--context-window auto`
|
|
// can probe it. Source precedence mirrors Claude Code's own resolution: the shell
|
|
// ANTHROPIC_MODEL override wins, otherwise the settings cascade (local > project >
|
|
// user). Fully file/env-injectable so it is hermetic under the test HOME.
|
|
|
|
async function withTempHome(fn) {
|
|
const home = await mkdtemp(join(tmpdir(), 'config-audit-model-home-'));
|
|
try {
|
|
return await fn(home);
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function withTempProject(fn) {
|
|
const project = await mkdtemp(join(tmpdir(), 'config-audit-model-proj-'));
|
|
try {
|
|
return await fn(project);
|
|
} finally {
|
|
await rm(project, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function writeSettings(dir, file, obj) {
|
|
await mkdir(dir, { recursive: true });
|
|
await writeFile(join(dir, file), JSON.stringify(obj));
|
|
}
|
|
|
|
describe('resolveActiveModel — env override (ANTHROPIC_MODEL)', () => {
|
|
it('returns the shell ANTHROPIC_MODEL when set, ignoring settings', async () => {
|
|
await withTempHome(async (home) => {
|
|
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
|
|
const env = { HOME: home, ANTHROPIC_MODEL: 'claude-opus-4-8[1m]' };
|
|
assert.equal(await resolveActiveModel(null, { env }), 'claude-opus-4-8[1m]');
|
|
});
|
|
});
|
|
|
|
it('an empty ANTHROPIC_MODEL is treated as unset (falls through to the cascade)', async () => {
|
|
await withTempHome(async (home) => {
|
|
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
|
|
const env = { HOME: home, ANTHROPIC_MODEL: ' ' };
|
|
assert.equal(await resolveActiveModel(null, { env }), 'claude-sonnet-4-6');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('resolveActiveModel — settings cascade (local > project > user)', () => {
|
|
it('reads the model from user ~/.claude/settings.json', async () => {
|
|
await withTempHome(async (home) => {
|
|
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-opus-4-8' });
|
|
assert.equal(await resolveActiveModel(null, { env: { HOME: home } }), 'claude-opus-4-8');
|
|
});
|
|
});
|
|
|
|
it('project settings.json overrides user settings', async () => {
|
|
await withTempHome(async (home) => {
|
|
await writeSettings(join(home, '.claude'), 'settings.json', { model: 'claude-opus-4-8' });
|
|
await withTempProject(async (project) => {
|
|
await writeSettings(join(project, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
|
|
assert.equal(await resolveActiveModel(project, { env: { HOME: home } }), 'claude-sonnet-4-6');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('settings.local.json overrides project settings (local wins)', async () => {
|
|
await withTempHome(async (home) => {
|
|
await withTempProject(async (project) => {
|
|
await writeSettings(join(project, '.claude'), 'settings.json', { model: 'claude-sonnet-4-6' });
|
|
await writeSettings(join(project, '.claude'), 'settings.local.json', { model: 'claude-opus-4-8[1m]' });
|
|
assert.equal(await resolveActiveModel(project, { env: { HOME: home } }), 'claude-opus-4-8[1m]');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('resolveActiveModel — nothing configured', () => {
|
|
it('returns null on a clean HOME with no model anywhere (honest advisory fallback)', async () => {
|
|
await withTempHome(async (home) => {
|
|
assert.equal(await resolveActiveModel(null, { env: { HOME: home } }), null);
|
|
});
|
|
});
|
|
|
|
it('ignores a non-string model field', async () => {
|
|
await withTempHome(async (home) => {
|
|
await writeSettings(join(home, '.claude'), 'settings.json', { model: 123 });
|
|
assert.equal(await resolveActiveModel(null, { env: { HOME: home } }), null);
|
|
});
|
|
});
|
|
});
|