fix(scanners): the fix engine downgraded a near-miss on xhigh to high (C2)
`fix-engine.mjs` carried its own copy of the valid `effortLevel` list, and that copy had gone stale on `xhigh` (CC 2.1.154's top Opus tier) while `settings-validator.mjs` had all five. The nearest-match "fix" therefore corrected `xhig` — and `XHIGH` — to `high`: the tool silently changed the tier the user asked for, in the one code path whose whole job is to write the corrected value back to disk. Fixed by sharing the validator's table instead of aligning the copy, so the two cannot drift again. Guard asserts the blanket invariant — a one-character near-miss on EVERY valid tier corrects back to that same tier — rather than pinning the one level that happened to be missing; verified red against the original stale array. Suite 1488/0. Frozen v5.0.0 snapshots untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P6Rh59Mtj4uYrdYMCYZJE
This commit is contained in:
parent
c76dc537ce
commit
3d6ddb273c
3 changed files with 52 additions and 4 deletions
|
|
@ -9,6 +9,7 @@ import { dirname } from 'node:path';
|
|||
import { parseJson, parseFrontmatter } from './lib/yaml-parser.mjs';
|
||||
import { createBackup } from './lib/backup.mjs';
|
||||
import { runAllScanners } from './scan-orchestrator.mjs';
|
||||
import { VALID_EFFORT_LEVELS as SETTINGS_EFFORT_LEVELS } from './settings-validator.mjs';
|
||||
|
||||
/**
|
||||
* Fix type constants.
|
||||
|
|
@ -22,8 +23,8 @@ const FIX_TYPES = {
|
|||
FILE_RENAME: 'file-rename',
|
||||
};
|
||||
|
||||
/** Valid effortLevel values for nearest-match */
|
||||
const VALID_EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
|
||||
/** Valid effortLevel values for nearest-match — the validator's list, not a copy. */
|
||||
const VALID_EFFORT_LEVELS = [...SETTINGS_EFFORT_LEVELS];
|
||||
|
||||
/**
|
||||
* Plan fixes from a scanner envelope.
|
||||
|
|
|
|||
|
|
@ -71,8 +71,11 @@ const TYPE_CHECKS = new Map([
|
|||
['wheelScrollAccelerationEnabled', 'boolean'],
|
||||
]);
|
||||
|
||||
/** Valid effortLevel values (CC 2.1.154 added 'xhigh' as the Opus-4.8 top tier) */
|
||||
const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
||||
/** Valid effortLevel values (CC 2.1.154 added 'xhigh' as the Opus-4.8 top tier).
|
||||
* Exported because the fix engine's nearest-match needs the SAME list: a second
|
||||
* copy there had gone stale on `xhigh` and quietly corrected near-misses on the
|
||||
* top tier down to `high` (C2). One table, no drift. */
|
||||
export const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
||||
|
||||
/** v5 M6: warn when additionalDirectories grows beyond this — each entry adds
|
||||
* a project root to walks/discovery, inflating per-turn cost and confusing scope. */
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
|
|||
import { runAllScanners } from '../../scanners/scan-orchestrator.mjs';
|
||||
import { planFixes, applyFixes, verifyFixes, FIX_TYPES } from '../../scanners/fix-engine.mjs';
|
||||
import { parseJson, parseFrontmatter } from '../../scanners/lib/yaml-parser.mjs';
|
||||
import { VALID_EFFORT_LEVELS } from '../../scanners/settings-validator.mjs';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const FIXTURES = resolve(__dirname, '../fixtures');
|
||||
|
|
@ -271,6 +272,49 @@ describe('applyFixes on tmp copy', () => {
|
|||
assert.ok(newStat.isFile());
|
||||
});
|
||||
|
||||
// C2 — the nearest-match table for effortLevel dropped `xhigh` (CC 2.1.154),
|
||||
// so the engine "corrected" a near-miss on the top Opus tier down to `high`:
|
||||
// a silent downgrade of the very setting the user asked for. Behaviour-level
|
||||
// (scan → plan → apply), because the helper itself is not exported.
|
||||
async function fixEffortLevel(dir, raw) {
|
||||
const settingsPath = join(dir, '.claude', 'settings.json');
|
||||
const before = parseJson(await readFile(settingsPath, 'utf-8'));
|
||||
before.effortLevel = raw;
|
||||
await writeFile(settingsPath, `${JSON.stringify(before, null, 2)}\n`, 'utf-8');
|
||||
|
||||
resetCounter();
|
||||
const env = await runAllScanners(dir);
|
||||
const { fixes } = planFixes(env);
|
||||
const effortFix = fixes.filter(f => f.key === 'effortLevel');
|
||||
assert.strictEqual(effortFix.length, 1, `"${raw}" must be planned as exactly one effortLevel fix`);
|
||||
await applyFixes(effortFix, { dryRun: false, backupDir: dir });
|
||||
|
||||
return parseJson(await readFile(settingsPath, 'utf-8')).effortLevel;
|
||||
}
|
||||
|
||||
// Blanket invariant, not just the level that happened to be missing: for
|
||||
// EVERY valid tier, a one-character near-miss must correct back to that same
|
||||
// tier. A test pinned to `xhig` alone would go green again if a future edit
|
||||
// dropped a different level from the table.
|
||||
it('corrects a near-miss on every valid tier back to that tier (C2)', async () => {
|
||||
for (const level of VALID_EFFORT_LEVELS) {
|
||||
const nearMiss = level.slice(0, -1);
|
||||
assert.strictEqual(
|
||||
await fixEffortLevel(tmpDir, nearMiss),
|
||||
level,
|
||||
`"${nearMiss}" must correct to "${level}" — any other target silently changes the tier the user asked for`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('corrects a case variant of "xhigh" to xhigh, not high (C2)', async () => {
|
||||
assert.strictEqual(
|
||||
await fixEffortLevel(tmpDir, 'XHIGH'),
|
||||
'xhigh',
|
||||
'the user named a real tier in the wrong case; the fix must keep the tier',
|
||||
);
|
||||
});
|
||||
|
||||
it('validates JSON output after fix', async () => {
|
||||
const { fixes } = planFixes(envelope);
|
||||
const jsonFixes = fixes.filter(f => f.file.endsWith('.json'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue