File size: 4,245 Bytes
2b64d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | // #103 (denvey): allowlist/blocklist must auto-inherit between a base
// model and its `-thinking` reasoning variant.
//
// Pre-fix UX bug: the dashboard surfaces base names (e.g.
// `claude-opus-4.6`); a user who carefully allowlists that name still
// gets a 403 the moment a request resolves to `claude-opus-4.6-thinking`,
// with no obvious connection to anything they configured. Same trap on
// the blocklist side: an operator who blocks the base would be surprised
// to see -thinking slip past.
//
// Other suffixes (-fast, -1m, -low/medium/high/xhigh, -mini, -nano,
// -codex, -max-*) are intentionally NOT inherited — those represent
// distinct entitlements (different context window, latency tier,
// pricing, or model architecture).
import { after, describe, test } from 'node:test';
import assert from 'node:assert/strict';
import {
getModelAccessConfig,
isModelAllowed,
setModelAccessList,
setModelAccessMode,
} from '../src/dashboard/model-access.js';
const original = getModelAccessConfig();
after(() => {
setModelAccessMode(original.mode);
setModelAccessList(original.list);
});
describe('isModelAllowed thinking-variant inheritance (#103)', () => {
test('allowlist: base entry implies the -thinking sibling', () => {
setModelAccessMode('allowlist');
setModelAccessList(['claude-opus-4.6']);
assert.equal(isModelAllowed('claude-opus-4.6').allowed, true);
assert.equal(isModelAllowed('claude-opus-4.6-thinking').allowed, true,
'allowlisting the base must auto-allow the -thinking sibling');
});
test('allowlist: -thinking entry implies the base sibling', () => {
setModelAccessMode('allowlist');
setModelAccessList(['claude-sonnet-4.6-thinking']);
assert.equal(isModelAllowed('claude-sonnet-4.6-thinking').allowed, true);
assert.equal(isModelAllowed('claude-sonnet-4.6').allowed, true);
});
test('allowlist: unrelated suffixes (-fast / -1m / -high) are NOT inherited', () => {
setModelAccessMode('allowlist');
setModelAccessList(['claude-opus-4.6']);
// These represent distinct entitlements (context window, tier,
// pricing) — they must remain individually gated.
assert.equal(isModelAllowed('claude-opus-4.6-fast').allowed, false);
assert.equal(isModelAllowed('claude-opus-4.6-1m').allowed, false);
assert.equal(isModelAllowed('claude-opus-4.6-high').allowed, false);
});
test('allowlist: empty list rejects everything (including -thinking)', () => {
setModelAccessMode('allowlist');
setModelAccessList([]);
assert.equal(isModelAllowed('claude-opus-4.6').allowed, false);
assert.equal(isModelAllowed('claude-opus-4.6-thinking').allowed, false);
});
test('blocklist: base entry also blocks the -thinking sibling', () => {
setModelAccessMode('blocklist');
setModelAccessList(['claude-opus-4.6']);
const baseRes = isModelAllowed('claude-opus-4.6');
const thinkRes = isModelAllowed('claude-opus-4.6-thinking');
assert.equal(baseRes.allowed, false);
assert.equal(thinkRes.allowed, false,
'blocking the base must auto-block the -thinking sibling');
assert.match(thinkRes.reason || '', /-thinking|claude-opus-4\.6/);
});
test('blocklist: -thinking entry also blocks the base sibling', () => {
setModelAccessMode('blocklist');
setModelAccessList(['claude-sonnet-4.6-thinking']);
assert.equal(isModelAllowed('claude-sonnet-4.6-thinking').allowed, false);
assert.equal(isModelAllowed('claude-sonnet-4.6').allowed, false);
});
test('blocklist: unrelated suffixes pass when only the base is blocked', () => {
setModelAccessMode('blocklist');
setModelAccessList(['claude-opus-4.6']);
assert.equal(isModelAllowed('claude-opus-4.6-fast').allowed, true);
assert.equal(isModelAllowed('claude-opus-4.6-mini').allowed, true);
assert.equal(isModelAllowed('claude-opus-4.6-codex').allowed, true);
});
test('mode=all bypasses inheritance entirely (everything allowed)', () => {
setModelAccessMode('all');
setModelAccessList(['claude-opus-4.6']); // list ignored in 'all' mode
assert.equal(isModelAllowed('claude-opus-4.6-thinking').allowed, true);
assert.equal(isModelAllowed('any-other-model').allowed, true);
});
});
|