import { afterEach, beforeEach, describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { resolveConnectSelector, FREE_TIER_SELECTOR } from '../src/devin-connect-models.js'; const SAVED_CONNECT_TOKEN = process.env.DEVIN_CONNECT_TOKEN; const SAVED_WINDSURF_API_KEY = process.env.WINDSURF_API_KEY; beforeEach(() => { delete process.env.DEVIN_CONNECT_TOKEN; delete process.env.WINDSURF_API_KEY; }); afterEach(() => { if (SAVED_CONNECT_TOKEN === undefined) delete process.env.DEVIN_CONNECT_TOKEN; else process.env.DEVIN_CONNECT_TOKEN = SAVED_CONNECT_TOKEN; if (SAVED_WINDSURF_API_KEY === undefined) delete process.env.WINDSURF_API_KEY; else process.env.WINDSURF_API_KEY = SAVED_WINDSURF_API_KEY; }); describe('resolveConnectSelector', () => { it('resolves the free-tier swe selector (dash and dot forms)', () => { assert.deepEqual(resolveConnectSelector('swe-1-6-slow'), { selector: 'swe-1-6-slow', mapped: true }); assert.deepEqual(resolveConnectSelector('swe-1.6-slow'), { selector: 'swe-1-6-slow', mapped: true }); }); it('maps swe-2 _lookup aliases onto catalog selectors', () => { // Same names models.js _lookup accepts. Without these SELECTOR_MAP rows, // DEVIN_CONNECT=1 + STRICT_MODEL=1 400s them even though resolveModel succeeds. const cases = [ ['swe-2', 'swe-2-medium'], ['swe-2.0', 'swe-2-medium'], ['swe-2-0', 'swe-2-medium'], ['swe2', 'swe-2-medium'], ['swe-2-medium', 'swe-2-medium'], ['swe-2-high', 'swe-2-high'], ['swe-2-max', 'swe-2-max'], ['swe-2.0-medium', 'swe-2-medium'], ['swe-2.0-high', 'swe-2-high'], ['swe-2.0-max', 'swe-2-max'], ]; for (const [name, selector] of cases) { assert.deepEqual(resolveConnectSelector(name), { selector, mapped: true }, name); } }); it('maps the dashed swe-2.0 effort spellings onto their swe-2 tiers', () => { // Live-catalog probe (2026-10-25): `swe-2-0-medium` / `swe-2-0-high` / // `swe-2-0-max` were the ONLY swe-2 spellings missing from SELECTOR_MAP — // they resolved to {selector:null, mapped:false} against a live catalog and // to the swe-1-6-slow degrade target against the frozen snapshot, while the // unsuffixed `swe-2-0` row and the dotted `swe-2.0-{medium,high,max}` rows // worked. Same model family, four spellings, one of them silently free-tier. // Targets are verified present in src/data/devin-catalog-snapshot.json. const cases = [ ['swe-2-0-medium', 'swe-2-medium'], ['swe-2-0-high', 'swe-2-high'], ['swe-2-0-max', 'swe-2-max'], ]; for (const [name, selector] of cases) { assert.deepEqual(resolveConnectSelector(name), { selector, mapped: true }, name); } }); it('maps the compact gpt5-6-luna spelling (no dash after gpt)', () => { // Issue #244 carried `gpt5.6-luna`, which the map covers; the sibling // compact-dash spelling `gpt5-6-luna` was not covered and missed every // lookup, so a client asking for Luna got the free-tier degrade. The target // (gpt-5-6-luna-medium) is a real snapshot selector; keep all four // spellings in lockstep, the same way the gpt-5.5 family is kept. const cases = [ ['gpt-5.6-luna', 'gpt-5-6-luna-medium'], ['gpt-5-6-luna', 'gpt-5-6-luna-medium'], ['gpt5.6-luna', 'gpt-5-6-luna-medium'], ['gpt5-6-luna', 'gpt-5-6-luna-medium'], ]; for (const [name, selector] of cases) { assert.deepEqual(resolveConnectSelector(name), { selector, mapped: true }, name); } }); it('maps claude friendly names to their captured upstream selectors', () => { assert.equal(resolveConnectSelector('claude-opus-4.8').selector, 'claude-opus-4-8-medium'); // Dashed bare form is a real catalog selector (base model) → resolves to itself; // the dotted family alias keeps the curated -thinking default. Both are MAPPED: // O1 (evidence/ClientSandboxes.md §5.3) is this alias working as designed, so the // escape hatch is the explicit dashed spelling, not an alias change. assert.deepEqual(resolveConnectSelector('claude-sonnet-4-6'), { selector: 'claude-sonnet-4-6', mapped: true }); assert.deepEqual(resolveConnectSelector('claude-sonnet-4.6'), { selector: 'claude-sonnet-4-6-thinking', mapped: true }); assert.equal(resolveConnectSelector('claude-sonnet-4.5').selector, 'MODEL_PRIVATE_2'); assert.equal(resolveConnectSelector('claude-haiku-4-5').selector, 'MODEL_PRIVATE_11'); }); it('keeps the base sonnet model reachable under its explicit spelling (O1)', () => { // O1 (evidence/ClientSandboxes.md §5.3): omp asked for the dotted alias and got // the thinking variant — that is the documented SELECTOR_MAP default, NOT a bug. // The base (non-thinking) model is a real catalog selector, so the escape hatch // must keep working: an explicit dashed spelling resolves to itself, mapped:true, // and is a DIFFERENT upstream selector from the dotted alias. const base = resolveConnectSelector('claude-sonnet-4-6'); const dotted = resolveConnectSelector('claude-sonnet-4.6'); assert.deepEqual(base, { selector: 'claude-sonnet-4-6', mapped: true }); assert.deepEqual(dotted, { selector: 'claude-sonnet-4-6-thinking', mapped: true }); assert.notEqual(base.selector, dotted.selector, 'the two spellings intentionally select different variants'); }); it('maps gpt and gemini families', () => { assert.equal(resolveConnectSelector('gpt-5-2-high').selector, 'MODEL_GPT_5_2_HIGH'); assert.equal(resolveConnectSelector('gemini-3-flash').selector, 'MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM'); }); it('is case-insensitive and strips a provider prefix', () => { assert.equal(resolveConnectSelector('Claude-Opus-4.8').selector, 'claude-opus-4-8-medium'); assert.equal(resolveConnectSelector('anthropic/claude-sonnet-4-5').selector, 'MODEL_PRIVATE_2'); }); it('strips a hyphenated provider prefix (x-ai/, z-ai/) like a plain one', () => { // S3 (alias matrix 2026-10-25): the strip rule was `/^[a-z]+\//`, which cannot // match a hyphenated provider segment. Every `x-ai/...` spelling therefore // missed every lookup and resolved to {selector:null, mapped:false} on a live // catalog — a 400 model_not_found for a model the account can run — while // `xai/…` and `openai/…` resolved. 313 probed rows across 48 families. assert.deepEqual(resolveConnectSelector('x-ai/grok-4-5-low'), { selector: 'grok-4-5-low', mapped: true }); assert.deepEqual(resolveConnectSelector('z-ai/glm-5.1'), { selector: 'glm-5-2', mapped: true }); // Control: a plain prefix keeps working, and the strip stays SINGLE — in // `x/y/glm-5.1` the second segment is part of the name, not another provider. assert.deepEqual(resolveConnectSelector('xai/grok-4-6'), { selector: 'grok-4-6-medium', mapped: true }); assert.deepEqual(resolveConnectSelector('x/y/glm-5.1'), { selector: FREE_TIER_SELECTOR, mapped: false }); }); it('resolves compact gpt5.x / gemini3.x spellings through the version separator', () => { // S4: only `gpt5.6-luna` carried a compact row (#244); every sibling shape // resolved to nothing. The class is normalized rather than row-by-row, // because v3.2.5 makes the map the ONLY alias entry point and speculative // rows are forbidden. assert.deepEqual(resolveConnectSelector('gpt5.5'), { selector: 'gpt-5-5-low', mapped: true }); assert.deepEqual(resolveConnectSelector('gemini3.5-flash'), { selector: 'gemini-3-5-flash-medium', mapped: true }); assert.deepEqual(resolveConnectSelector('gpt5.4'), { selector: 'gpt-5-4-medium', mapped: true }); assert.deepEqual(resolveConnectSelector('gpt5.4-mini'), { selector: 'gpt-5-4-mini-medium', mapped: true }); // Already-mapped compact spellings keep their target. assert.deepEqual(resolveConnectSelector('gpt5.6-luna'), { selector: 'gpt-5-6-luna-medium', mapped: true }); assert.deepEqual(resolveConnectSelector('gpt5-6-luna'), { selector: 'gpt-5-6-luna-medium', mapped: true }); // `claude5` is a FAMILY shorthand, not a compact version. Splitting it would // produce the nonexistent `claude-5` and silently degrade an alias that works // today, so the normalization requires the `.` after the major digit. assert.deepEqual(resolveConnectSelector('claude5'), { selector: 'claude-sonnet-5-medium', mapped: true }); // Case-insensitively too: the exact lowercase key is shielded by the raw map // lookup, so only the uppercased spelling exercises the normalization step. // A rule without the `.` rewrites `Claude5` into `claude-5` and degrades it. assert.deepEqual(resolveConnectSelector('Claude5'), { selector: 'claude-sonnet-5-medium', mapped: true }); }); it('resolves enum selectors regardless of case or a provider prefix', () => { // S5: the enum passthrough tested `/^MODEL_[A-Z0-9_]+$/` against the RAW name // while the normalized fallthrough lowercases — so a lowercased or prefixed // enum matched neither path (96 probed rows). The existence check is what // keeps a bogus MODEL_* from being written raw to #21. assert.deepEqual(resolveConnectSelector('openai/MODEL_GPT_5_2_LOW'), { selector: 'MODEL_GPT_5_2_LOW', mapped: true }); assert.deepEqual(resolveConnectSelector('model_gpt_5_2_low'), { selector: 'MODEL_GPT_5_2_LOW', mapped: true }); assert.deepEqual(resolveConnectSelector('MODEL_GPT_5_2_LOW'), { selector: 'MODEL_GPT_5_2_LOW', mapped: true }); // An unknown enum still fails closed: it is never passed through raw. assert.deepEqual(resolveConnectSelector('openai/model_gpt_9_9_ultra'), { selector: FREE_TIER_SELECTOR, mapped: false }); }); it('resolves the bare grok-4-5 and inkling family names to their -medium tier', () => { // Family residue from the alias matrix: these two bare names 400'd because the // catalog exposes only tier selectors and the map had no row — while the // sibling families grok-4-6 / grok-4-7 carry exactly such a row. The bare // family alias defaulting to -medium is the map's existing convention // (claude-fable-5-1, deepseek-v4-flash, gemini-3-7-flash, ...). Targets are // real snapshot selectors. assert.deepEqual(resolveConnectSelector('grok-4-5'), { selector: 'grok-4-5-medium', mapped: true }); assert.deepEqual(resolveConnectSelector('inkling'), { selector: 'inkling-medium', mapped: true }); }); it('pins the known prefix asymmetry for a dotted family alias (P2-1)', () => { // KNOWN ASYMMETRY, pinned deliberately. The alias map is consulted on the RAW // name before any normalization, so a dotted MAP KEY can carry a curated // target its prefixed spelling never reaches — the prefix then changes which // upstream model runs. `claude-sonnet-4.6` is such a key (curated -thinking); // `anthropic/claude-sonnet-4.6` misses the raw lookup, normalizes to // `claude-sonnet-4-6` (the base catalog selector), and runs the base model. // The dialect comment therefore must not claim "the prefix never changes the // model". Fixing the asymmetry is out of scope here; this test makes any // drift in either direction a deliberate change. assert.equal(resolveConnectSelector('claude-sonnet-4.6').selector, 'claude-sonnet-4-6-thinking'); assert.equal(resolveConnectSelector('anthropic/claude-sonnet-4.6').selector, 'claude-sonnet-4-6'); }); it('passes enum-form selectors through verbatim', () => { assert.deepEqual(resolveConnectSelector('MODEL_CLAUDE_4_5_OPUS_THINKING'), { selector: 'MODEL_CLAUDE_4_5_OPUS_THINKING', mapped: true }); }); it('degrades unknown names to the free-tier selector (mapped:false)', () => { assert.deepEqual(resolveConnectSelector('gpt-9-ultra'), { selector: FREE_TIER_SELECTOR, mapped: false }); assert.deepEqual(resolveConnectSelector(''), { selector: FREE_TIER_SELECTOR, mapped: false }); assert.deepEqual(resolveConnectSelector(null), { selector: FREE_TIER_SELECTOR, mapped: false }); }); it('resolves a dotted dash-form that normalizes to a real catalog selector', () => { // Regression: "gpt-5.5-medium" → norm "gpt-5-5-medium" IS a catalog selector // but not a SELECTOR_MAP alias. Before the norm→catalog check it silently // degraded to the free tier (mapped:false); it must now resolve mapped:true. const r = resolveConnectSelector('gpt-5.5-medium'); assert.equal(r.selector, 'gpt-5-5-medium'); assert.equal(r.mapped, true); }); }); describe('resolveConnectSelector — live catalog (audit 2026-07-12 snapshot staleness)', () => { // The committed snapshot never live-synced, so selectors the upstream added // after it was captured (proven on a live account 2026-07-12: qwen-3, glm-5, // kimi-k2.5, deepseek-v3, minimax-*) were absent from CATALOG_SELECTORS and // got mapped:false → 400'd by the strict gate despite being runnable. The live // catalog set (populated from GetCliModelConfigs) becomes authoritative after // every active contributor has a successful sync; the snapshot remains the // cold-start and partial-sync fallback. const STALE = ['qwen-3', 'glm-5', 'kimi-k2.5', 'deepseek-v3']; it('cold start (no live sync): a genuinely-runnable-but-unsnapshotted selector is mapped:false', async () => { // Fresh import so _liveSelectors starts empty (module-level Set). const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-a`); assert.equal(m.hasLiveCatalog(), false, 'no live sync yet'); // qwen-3 is not in the frozen snapshot → without live catalog it degrades. assert.equal(m.resolveConnectSelector('qwen-3').mapped, false); }); it('after live sync: the same selectors resolve mapped:true to themselves', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-b`); // Simulate a GetCliModelConfigs sync feeding the decoded catalog shape. m.setLiveCatalogSelectors(STALE.map(s => ({ selector: s }))); assert.equal(m.hasLiveCatalog(), true); for (const s of STALE) { const r = m.resolveConnectSelector(s); assert.equal(r.mapped, true, `${s} must be recognized after live sync`); assert.equal(r.selector, s, `${s} resolves to itself`); } }); it('live sync recognizes the canonical selector but does NOT pass a family alias through raw (ultracode fix 2026-07-12)', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-c`); // Real regression shape: -medium is the upstream-accepted selector; // the bare family name is its alias and is NOT in the hand-maintained // SELECTOR_MAP. (Use a future family so this guard stays valid as the map // grows; gpt-5.6-sol is mapped now.) m.setLiveCatalogSelectors([{ selector: 'gpt-7-future-medium', alias: 'gpt-7-future' }]); // The canonical full selector IS recognized from the live catalog. assert.equal(m.resolveConnectSelector('gpt-7-future-medium').mapped, true); // The family alias must NOT resolve mapped:true off the live set — folding it // in made the gateway pass the bare family form to upstream #21 → UPSTREAM_INTERNAL // (burns the account). Aliases are the SELECTOR_MAP's job; an unknown one fails // closed (degrades to free / 400 under strict), it does NOT pass through raw. const r = m.resolveConnectSelector('gpt-7-future'); assert.equal(r.mapped, false, 'family alias not in SELECTOR_MAP must NOT be treated as a runnable selector'); assert.notEqual(r.selector, 'gpt-7-future', 'must never emit the bare family alias as the upstream selector'); }); it('a known SELECTOR_MAP alias still resolves correctly (regression guard)', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-c2`); m.setLiveCatalogSelectors([{ selector: 'glm-5-2', alias: 'glm-5.2' }]); // glm-5.2 is in the hand-maintained SELECTOR_MAP → still resolves to glm-5-2, // independent of the (now removed) alias fold. assert.equal(m.resolveConnectSelector('glm-5-2').mapped, true); assert.equal(m.resolveConnectSelector('glm-5.2').selector, 'glm-5-2'); }); it('resolves the dashed glm-5-1 spelling onto the same selector as glm-5.1', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-glm51`); // Live catalog in which the only GLM row is glm-5-2 — the shape a real account // reports. Before this fix the map carried `glm-5.1` but NOT `glm-5-1`, so the // dashed spelling missed every map lookup, was absent from the live set, and the // authoritative catalog left no degrade target → {selector:null, mapped:false}, // which the handler answers with 400 model_not_found. Dotted worked, dashed did // not, for the same model. m.setLiveCatalogSelectors([{ selector: 'glm-5-2' }]); assert.deepEqual(m.resolveConnectSelector('glm-5-1'), { selector: 'glm-5-2', mapped: true }, 'dashed glm-5-1 must resolve to the live glm-5-2 selector, not fail model_not_found'); assert.deepEqual(m.resolveConnectSelector('glm-5.1'), { selector: 'glm-5-2', mapped: true }, 'dotted glm-5.1 keeps resolving to the same selector'); }); it('retires a snapshot alias when its target is absent from a complete live catalog', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-c3`); m.setLiveCatalogSelectors([ { selector: 'claude-opus-4-8-medium', alias: 'claude-opus-4.8' }, ]); assert.equal(m.resolveConnectSelector('claude-opus-4.8').mapped, true, 'precondition: an alias targeting a live selector remains valid'); const retired = m.resolveConnectSelector('gpt-5.5', { warnOnFallback: false }); // #258 residual: this used to degrade to FREE_TIER_SELECTOR. On a complete // live sync that selector is not callable either (it is absent from the live // catalog and 502s as "quota exhausted"), so "retired" must mean RETIRED — // null forces the handler to answer model_not_found instead of routing the // caller to a dead model and reporting its quota error as if it were theirs. assert.deepEqual(retired, { selector: null, mapped: false }, 'chat preflight must not keep a snapshot-only alias routable after complete live sync, ' + 'and must not redirect it to a selector the live catalog proved absent'); }); it('keeps snapshot aliases during an explicitly incomplete mixed-pool sync', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-partial`); m.setLiveCatalogSelectors([ { selector: 'claude-opus-4-8-medium' }, ], { includeSnapshotFallback: true }); assert.deepEqual( m.resolveConnectSelector('gpt-5.5', { warnOnFallback: false }), { selector: 'gpt-5-5-low', mapped: true }, 'an account without LKG rows still contributes the frozen snapshot fallback', ); }); it('keeps snapshot aliases for the process env-token fallback beside live rows', async () => { const savedToken = process.env.DEVIN_CONNECT_TOKEN; const savedKey = process.env.WINDSURF_API_KEY; const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-env-token`); try { delete process.env.WINDSURF_API_KEY; process.env.DEVIN_CONNECT_TOKEN = 'env-token-fallback-test'; m.setLiveCatalogSelectors([{ selector: 'claude-opus-4-8-medium' }]); assert.deepEqual( m.resolveConnectSelector('gpt-5.5', { warnOnFallback: false }), { selector: 'gpt-5-5-low', mapped: true }, ); } finally { if (savedToken === undefined) delete process.env.DEVIN_CONNECT_TOKEN; else process.env.DEVIN_CONNECT_TOKEN = savedToken; if (savedKey === undefined) delete process.env.WINDSURF_API_KEY; else process.env.WINDSURF_API_KEY = savedKey; } }); it('keeps catalog-independent synthetic selectors valid after live sync', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-synthetic`); m.setLiveCatalogSelectors([{ selector: 'claude-opus-4-8-medium' }]); assert.deepEqual( m.resolveConnectSelector('subagent-default'), { selector: 'subagent-default', mapped: true }, ); }); it('a bad/empty sync never blanks out a good live set', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-d`); m.setLiveCatalogSelectors([{ selector: 'qwen-3' }]); assert.equal(m.resolveConnectSelector('qwen-3').mapped, true); m.setLiveCatalogSelectors([]); // empty → no-op m.setLiveCatalogSelectors(null); // garbage → no-op assert.equal(m.resolveConnectSelector('qwen-3').mapped, true, 'prior good set survives a bad sync'); }); it('normalizes string-only seam rows into the readable live catalog', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-strings`); m.setLiveCatalogSelectors([' qwen-3 ', '']); assert.deepEqual(m.getLiveCatalog(), [{ selector: 'qwen-3' }]); assert.deepEqual(m.resolveConnectSelector('qwen-3'), { selector: 'qwen-3', mapped: true }); }); it('normalizes object selectors without dropping their catalog metadata', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-objects`); m.setLiveCatalogSelectors([ { selector: ' qwen-3 ', label: 'Qwen 3', provider: 'alibaba' }, ]); assert.deepEqual(m.getLiveCatalog(), [ { selector: 'qwen-3', label: 'Qwen 3', provider: 'alibaba' }, ]); assert.deepEqual(m.resolveConnectSelector('qwen-3'), { selector: 'qwen-3', mapped: true }); }); it('genuine junk still degrades even with a live catalog present', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-e`); m.setLiveCatalogSelectors([{ selector: 'qwen-3' }]); assert.equal(m.resolveConnectSelector('totally-fake-model-xyz').mapped, false); }); it('resolves the new alias spellings against an authoritative live catalog (P3-d)', async () => { // The S3/S4/S5 spellings must work once a sync lands, not only against the // frozen snapshot: a live set retires the snapshot, so a target missing from // it is a 400. The live set is set EXPLICITLY to exactly the targets these // spellings need, so a resolution that still leaned on the snapshot (or on a // map target the account cannot reach) would fail here. const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-p3d`); m.setLiveCatalogSelectors([ { selector: 'grok-4-5-low', provider: 'xai' }, { selector: 'grok-4-5-medium', provider: 'xai' }, { selector: 'glm-5-2', provider: 'zhipu' }, { selector: 'gpt-5-5-low', provider: 'openai' }, { selector: 'gemini-3-5-flash-medium', provider: 'google' }, { selector: 'inkling-medium', provider: 'thinking_machines' }, { selector: 'MODEL_GPT_5_2_LOW', provider: 'openai' }, ]); assert.equal(m.hasLiveCatalog(), true); const cases = [ ['x-ai/grok-4-5-low', 'grok-4-5-low'], ['grok-4-5', 'grok-4-5-medium'], ['z-ai/glm-5.1', 'glm-5-2'], ['gpt5.5', 'gpt-5-5-low'], ['gemini3.5-flash', 'gemini-3-5-flash-medium'], ['inkling', 'inkling-medium'], ['openai/MODEL_GPT_5_2_LOW', 'MODEL_GPT_5_2_LOW'], ]; for (const [name, selector] of cases) { assert.deepEqual(m.resolveConnectSelector(name, { warnOnFallback: false }), { selector, mapped: true }, name); } }); }); describe('resolveConnectSelector — live free fallback (#258 residual)', () => { // #258 residual: the static `swe-1-6-slow` degrade target is absent from a // synced catalog and answers with "Your daily usage quota has been exhausted", // so an unmapped name surfaced as a FAKE QUOTA ERROR. A degrade target has to // be callable, and the caller that holds live pricing data says which are. it('refuses the frozen fallback once a live catalog proves it absent', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-fb1`); m.setLiveCatalogSelectors([{ selector: 'swe-1-7' }, { selector: 'glm-5-2' }]); assert.deepEqual( m.resolveConnectSelector('totally-fake-model-xyz', { warnOnFallback: false }), { selector: null, mapped: false }, 'no callable target: the caller must answer model_not_found, not route to a dead selector', ); assert.deepEqual( m.resolveConnectSelector('', { warnOnFallback: false }), { selector: null, mapped: false }, 'the omitted-model path has the same constraint', ); }); it('degrades to a live free selector supplied by the caller', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-fb2`); m.setLiveCatalogSelectors([{ selector: 'swe-1-7' }, { selector: 'glm-5-2' }]); assert.deepEqual( m.resolveConnectSelector('totally-fake-model-xyz', { warnOnFallback: false, freeFallback: ['glm-5-2', 'swe-1-7'], }), { selector: 'glm-5-2', mapped: false }, 'caller order is the preference order', ); }); it('skips a free selector the catalog does not expose and takes the next', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-fb3`); m.setLiveCatalogSelectors([{ selector: 'swe-1-7' }]); assert.deepEqual( m.resolveConnectSelector('totally-fake-model-xyz', { warnOnFallback: false, // swe-2-medium is free on a promo deployment but this catalog does not // carry it — routing there would be the same dead-target defect. freeFallback: ['swe-2-medium', 'swe-1-7'], }), { selector: 'swe-1-7', mapped: false }, ); }); it('a live free selector outranks the static floor on cold start', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-fb4`); assert.equal(m.hasLiveCatalog(), false); assert.deepEqual( m.resolveConnectSelector('totally-fake-model-xyz', { warnOnFallback: false, freeFallback: ['swe-1-7'], }), { selector: 'swe-1-7', mapped: false }, ); }); it('keeps the static floor while no live catalog has arrived', async () => { const m = await import(`../src/devin-connect-models.js?fresh=${Date.now()}-fb5`); assert.deepEqual( m.resolveConnectSelector('totally-fake-model-xyz', { warnOnFallback: false }), { selector: 'swe-1-6-slow', mapped: false }, 'cold start: nothing has contradicted the documented floor yet, so it applies', ); }); });