diegosouzapw--omniroute
651 行
22 KiB
TypeScript
651 行
22 KiB
TypeScript
/**
|
|
* Unit tests for Auto-Combo Engine (Phase 5)
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { calculateFactors, calculateScore, DEFAULT_WEIGHTS, validateWeights } from "../scoring";
|
|
import type { ProviderCandidate, ScoringWeights } from "../scoring";
|
|
import {
|
|
getTaskFitness,
|
|
getTaskFitnessWithSource,
|
|
getTaskTypes,
|
|
getModelsDevTierFitness,
|
|
invalidateFitnessCache,
|
|
setUserFitnessOverride,
|
|
clearUserFitnessOverride,
|
|
} from "../taskFitness";
|
|
import { SelfHealingManager } from "../selfHealing";
|
|
import { MODE_PACKS, getModePack, getModePackNames } from "../modePacks";
|
|
import { getStrategy } from "../routerStrategy";
|
|
import type { RoutingContext } from "../routerStrategy";
|
|
|
|
describe("Scoring", () => {
|
|
const candidate: ProviderCandidate = {
|
|
provider: "anthropic",
|
|
model: "claude-sonnet",
|
|
quotaRemaining: 80,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 3,
|
|
p95LatencyMs: 1200,
|
|
latencyStdDev: 120,
|
|
errorRate: 0.02,
|
|
};
|
|
|
|
it("should calculate a score between 0 and 1", () => {
|
|
const pool: ProviderCandidate[] = [
|
|
candidate,
|
|
{
|
|
...candidate,
|
|
provider: "google",
|
|
model: "gemini-pro",
|
|
costPer1MTokens: 6,
|
|
p95LatencyMs: 1800,
|
|
latencyStdDev: 300,
|
|
quotaRemaining: 70,
|
|
},
|
|
];
|
|
const factors = calculateFactors(candidate, pool, "coding", getTaskFitness);
|
|
const score = calculateScore(factors, DEFAULT_WEIGHTS);
|
|
expect(score).toBeGreaterThan(0);
|
|
expect(score).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
it("OPEN circuit breaker should reduce score", () => {
|
|
const unhealthyCandidate: ProviderCandidate = { ...candidate, circuitBreakerState: "OPEN" };
|
|
const pool: ProviderCandidate[] = [candidate, unhealthyCandidate];
|
|
|
|
const healthyFactors = calculateFactors(candidate, pool, "coding", getTaskFitness);
|
|
const unhealthyFactors = calculateFactors(unhealthyCandidate, pool, "coding", getTaskFitness);
|
|
|
|
const healthy = calculateScore(healthyFactors, DEFAULT_WEIGHTS);
|
|
const unhealthy = calculateScore(unhealthyFactors, DEFAULT_WEIGHTS);
|
|
expect(healthy).toBeGreaterThan(unhealthy);
|
|
});
|
|
|
|
it("should validate weights sum to 1.0", () => {
|
|
expect(validateWeights(DEFAULT_WEIGHTS)).toBe(true);
|
|
expect(validateWeights({ ...DEFAULT_WEIGHTS, quota: 0.5 })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Task Fitness", () => {
|
|
it("should return fitness score for known model+task", () => {
|
|
const score = getTaskFitness("claude-sonnet", "coding");
|
|
expect(score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it("should return 0.5 default for unknown model", () => {
|
|
const score = getTaskFitness("totally-unknown-model", "coding");
|
|
expect(score).toBe(0.5);
|
|
});
|
|
|
|
it("should list all task types", () => {
|
|
const types = getTaskTypes();
|
|
expect(types).toContain("coding");
|
|
expect(types).toContain("review");
|
|
expect(types).toContain("planning");
|
|
expect(types.length).toBeGreaterThanOrEqual(6);
|
|
});
|
|
|
|
it("should boost wildcard patterns", () => {
|
|
const coderScore = getTaskFitness("some-coder-model", "coding");
|
|
const normalScore = getTaskFitness("some-random-model", "coding");
|
|
expect(coderScore).toBeGreaterThan(normalScore);
|
|
});
|
|
|
|
describe("-free alias resolution (#4517)", () => {
|
|
beforeEach(() => invalidateFitnessCache());
|
|
|
|
it("returns the base model's arena_elo when given a -free variant", async () => {
|
|
// The fix: getTaskFitnessWithSource strips a trailing "-free" suffix
|
|
// and re-queries arena_elo with the base id. We seed an arena_elo
|
|
// row directly via the DB module, look up the free variant, and
|
|
// assert the alias path returns the base score with source
|
|
// "arena_elo_free_alias".
|
|
const baseId = "alias-base-test-4517";
|
|
const freeId = "alias-base-test-4517-free";
|
|
const { upsertModelIntelligence, deleteModelIntelligence } =
|
|
await import("../../../../src/lib/db/modelIntelligence.ts");
|
|
// Seed arena_elo on the base id only — no row exists for the free id.
|
|
upsertModelIntelligence({
|
|
model: baseId,
|
|
source: "arena_elo",
|
|
category: "coding",
|
|
score: 0.42,
|
|
eloRaw: 1500,
|
|
confidence: "high",
|
|
expiresAt: null,
|
|
});
|
|
invalidateFitnessCache();
|
|
try {
|
|
const result = getTaskFitnessWithSource(freeId, "coding");
|
|
// Without the fix: result.source would be "wildcard_boost" (0.5 default).
|
|
// With the fix: result.source is "arena_elo_free_alias" with score 0.42.
|
|
expect(result.score).toBeCloseTo(0.42, 5);
|
|
expect(result.source).toBe("arena_elo_free_alias");
|
|
} finally {
|
|
deleteModelIntelligence(baseId, "arena_elo", "coding");
|
|
invalidateFitnessCache();
|
|
}
|
|
});
|
|
|
|
it("does not strip -free when arena_elo is present on the literal model id", () => {
|
|
// If both "foo-free" and "foo" have arena_elo rows, the literal "foo-free"
|
|
// wins (we never go through the alias path). This protects future
|
|
// benchmark uploads that specifically tag free tiers.
|
|
setUserFitnessOverride("foo-free", "coding", 0.91);
|
|
const result = getTaskFitnessWithSource("foo-free", "coding");
|
|
expect(result.score).toBe(0.91);
|
|
expect(result.source).toBe("user_override");
|
|
clearUserFitnessOverride("foo-free", "coding");
|
|
invalidateFitnessCache();
|
|
});
|
|
|
|
it("ignores -free suffix only at the end of the model id", () => {
|
|
// "free-something" must NOT be treated as a free alias of "free-something-free"
|
|
// — the suffix must be at the end. "mimo-free-edition" is left alone.
|
|
// We just confirm no exception is thrown and the lookup returns a number.
|
|
const score = getTaskFitness("mimo-free-edition", "coding");
|
|
expect(typeof score).toBe("number");
|
|
expect(score).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Self-Healing", () => {
|
|
let healer: SelfHealingManager;
|
|
|
|
beforeEach(() => {
|
|
healer = new SelfHealingManager();
|
|
});
|
|
|
|
it("should exclude provider with low score", () => {
|
|
const result = healer.evaluate("bad-provider", 0.1, "CLOSED");
|
|
expect(result.excluded).toBe(true);
|
|
expect(result.reason).toContain("below threshold");
|
|
});
|
|
|
|
it("should keep healthy providers", () => {
|
|
const result = healer.evaluate("good-provider", 0.8, "CLOSED");
|
|
expect(result.excluded).toBe(false);
|
|
});
|
|
|
|
it("should auto-exclude OPEN circuit breakers", () => {
|
|
const result = healer.evaluate("broken-provider", 0.8, "OPEN");
|
|
expect(result.excluded).toBe(true);
|
|
});
|
|
|
|
it("should detect incident mode when >50% providers are OPEN", () => {
|
|
healer.updateIncidentMode(["OPEN", "OPEN", "CLOSED"]);
|
|
expect(healer.isInIncidentMode()).toBe(true);
|
|
});
|
|
|
|
it("should not be in incident mode when most are CLOSED", () => {
|
|
healer.updateIncidentMode(["CLOSED", "CLOSED", "OPEN"]);
|
|
expect(healer.isInIncidentMode()).toBe(false);
|
|
});
|
|
|
|
it("should track exclusion count", () => {
|
|
healer.evaluate("p1", 0.1, "CLOSED");
|
|
healer.evaluate("p2", 0.1, "CLOSED");
|
|
const status = healer.getStatus();
|
|
expect(status.exclusionCount).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("Mode Packs", () => {
|
|
it("should have 5 mode packs", () => {
|
|
// #4235 Phase B added reliability-first (for the `:reliable` tier).
|
|
expect(getModePackNames()).toHaveLength(5);
|
|
expect(getModePackNames()).toContain("reliability-first");
|
|
});
|
|
|
|
it("reliability-first should prioritize health and stability", () => {
|
|
const pack = MODE_PACKS["reliability-first"];
|
|
expect(pack.health).toBeGreaterThan(pack.latencyInv);
|
|
expect(pack.stability).toBeGreaterThan(pack.costInv);
|
|
});
|
|
|
|
it("all mode pack weights should sum to 1.0", () => {
|
|
for (const name of getModePackNames()) {
|
|
const pack = getModePack(name);
|
|
if (pack) {
|
|
const sum = Object.values(pack).reduce((a, b) => a + b, 0);
|
|
expect(Math.abs(sum - 1.0)).toBeLessThan(0.001);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("ship-fast should prioritize latency", () => {
|
|
const pack = MODE_PACKS["ship-fast"];
|
|
expect(pack.latencyInv).toBeGreaterThan(pack.costInv);
|
|
});
|
|
|
|
it("cost-saver should prioritize cost", () => {
|
|
const pack = MODE_PACKS["cost-saver"];
|
|
expect(pack.costInv).toBeGreaterThan(pack.latencyInv);
|
|
});
|
|
|
|
it("quality-first should prioritize task fit", () => {
|
|
const pack = MODE_PACKS["quality-first"];
|
|
expect(pack.taskFit).toBeGreaterThan(pack.costInv);
|
|
});
|
|
|
|
it("undefined pack should return undefined", () => {
|
|
expect(getModePack("nonexistent")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("SLA-aware Strategy", () => {
|
|
const pool: ProviderCandidate[] = [
|
|
{
|
|
provider: "fast-flaky",
|
|
model: "fast-model",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 2,
|
|
p95LatencyMs: 800,
|
|
latencyStdDev: 200,
|
|
errorRate: 0.2,
|
|
},
|
|
{
|
|
provider: "steady",
|
|
model: "steady-model",
|
|
quotaRemaining: 80,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 6,
|
|
p95LatencyMs: 1400,
|
|
latencyStdDev: 100,
|
|
errorRate: 0.01,
|
|
},
|
|
{
|
|
provider: "cheap-slow",
|
|
model: "cheap-model",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 0.2,
|
|
p95LatencyMs: 3500,
|
|
latencyStdDev: 150,
|
|
errorRate: 0.01,
|
|
},
|
|
];
|
|
|
|
it("should prefer candidates that satisfy latency and error-rate SLOs", () => {
|
|
const strategy = getStrategy("sla-aware");
|
|
const result = strategy.select(pool, {
|
|
taskType: "coding",
|
|
sla: {
|
|
targetP95Ms: 2000,
|
|
maxErrorRate: 0.05,
|
|
maxCostPer1MTokens: 10,
|
|
},
|
|
});
|
|
|
|
expect(result.strategy).toBe("sla-aware");
|
|
expect(result.provider).toBe("steady");
|
|
expect(result.reason).toContain("p95=1400ms/2000ms");
|
|
});
|
|
|
|
it("should support the sla alias and soft-fallback when no candidate satisfies all SLOs", () => {
|
|
const strategy = getStrategy("sla");
|
|
const result = strategy.select(pool, {
|
|
taskType: "coding",
|
|
sla: {
|
|
targetP95Ms: 500,
|
|
maxErrorRate: 0.005,
|
|
maxCostPer1MTokens: 1,
|
|
hardConstraints: true,
|
|
},
|
|
});
|
|
|
|
expect(result.strategy).toBe("sla-aware");
|
|
expect(result.candidatesConsidered).toBe(3);
|
|
expect(result.reason).toContain("no candidate met all SLA constraints");
|
|
});
|
|
|
|
it("should use pure score ranking in soft mode even when a compliant candidate exists", () => {
|
|
const strategy = getStrategy("sla-aware");
|
|
const softPool: ProviderCandidate[] = [
|
|
{
|
|
provider: "slightly-over-error",
|
|
model: "fast-reliable-enough",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 1,
|
|
p95LatencyMs: 500,
|
|
latencyStdDev: 10,
|
|
errorRate: 0.06,
|
|
},
|
|
{
|
|
provider: "compliant-but-risky",
|
|
model: "threshold-model",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "HALF_OPEN",
|
|
costPer1MTokens: 5,
|
|
p95LatencyMs: 2_000,
|
|
latencyStdDev: 1_000,
|
|
errorRate: 0.05,
|
|
},
|
|
];
|
|
|
|
const result = strategy.select(softPool, {
|
|
taskType: "coding",
|
|
sla: {
|
|
targetP95Ms: 2_000,
|
|
maxErrorRate: 0.05,
|
|
maxCostPer1MTokens: 5,
|
|
},
|
|
});
|
|
|
|
expect(result.provider).toBe("slightly-over-error");
|
|
expect(result.reason).not.toContain("no candidate met all SLA constraints");
|
|
});
|
|
|
|
it("should prefer compliant candidates before score when hard constraints are enabled", () => {
|
|
const strategy = getStrategy("sla-aware");
|
|
const hardPool: ProviderCandidate[] = [
|
|
{
|
|
provider: "slightly-over-error",
|
|
model: "fast-reliable-enough",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 1,
|
|
p95LatencyMs: 500,
|
|
latencyStdDev: 10,
|
|
errorRate: 0.06,
|
|
},
|
|
{
|
|
provider: "compliant-but-risky",
|
|
model: "threshold-model",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "HALF_OPEN",
|
|
costPer1MTokens: 5,
|
|
p95LatencyMs: 2_000,
|
|
latencyStdDev: 1_000,
|
|
errorRate: 0.05,
|
|
},
|
|
];
|
|
|
|
const result = strategy.select(hardPool, {
|
|
taskType: "coding",
|
|
sla: {
|
|
targetP95Ms: 2_000,
|
|
maxErrorRate: 0.05,
|
|
maxCostPer1MTokens: 5,
|
|
hardConstraints: true,
|
|
},
|
|
});
|
|
|
|
expect(result.provider).toBe("compliant-but-risky");
|
|
});
|
|
|
|
it("should give full SLO-factor credit to candidates exactly at configured thresholds", () => {
|
|
const strategy = getStrategy("sla-aware");
|
|
const result = strategy.select(
|
|
[
|
|
{
|
|
provider: "threshold-provider",
|
|
model: "threshold-model",
|
|
quotaRemaining: 100,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 5,
|
|
p95LatencyMs: 1_000,
|
|
latencyStdDev: 50,
|
|
errorRate: 0.1,
|
|
},
|
|
],
|
|
{
|
|
taskType: "coding",
|
|
sla: {
|
|
targetP95Ms: 1_000,
|
|
maxErrorRate: 0.1,
|
|
maxCostPer1MTokens: 5,
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(result.finalScore).toBeGreaterThan(0.9);
|
|
});
|
|
});
|
|
|
|
describe("LKGP Strategy", () => {
|
|
const pool: ProviderCandidate[] = [
|
|
{
|
|
provider: "anthropic",
|
|
model: "claude-sonnet",
|
|
quotaRemaining: 80,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 3,
|
|
p95LatencyMs: 1200,
|
|
latencyStdDev: 120,
|
|
errorRate: 0.02,
|
|
},
|
|
{
|
|
provider: "openai",
|
|
model: "gpt-4o",
|
|
quotaRemaining: 90,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: 5,
|
|
p95LatencyMs: 800,
|
|
latencyStdDev: 80,
|
|
errorRate: 0.01,
|
|
},
|
|
];
|
|
|
|
it("should fall back to rules strategy when lkgpEnabled is false", () => {
|
|
const context: RoutingContext = {
|
|
taskType: "coding",
|
|
lastKnownGoodProvider: "anthropic",
|
|
lkgpEnabled: false,
|
|
};
|
|
const lkgpStrategy = getStrategy("lkgp");
|
|
const rulesStrategy = getStrategy("rules");
|
|
|
|
const lkgpResult = lkgpStrategy.select(pool, context);
|
|
const rulesResult = rulesStrategy.select(pool, context);
|
|
|
|
expect(lkgpResult.strategy).toBe("rules");
|
|
expect(lkgpResult.provider).toBe(rulesResult.provider);
|
|
});
|
|
|
|
it("should use LKGP provider when lkgpEnabled is true", () => {
|
|
const context: RoutingContext = {
|
|
taskType: "coding",
|
|
lastKnownGoodProvider: "anthropic",
|
|
lkgpEnabled: true,
|
|
};
|
|
const lkgpStrategy = getStrategy("lkgp");
|
|
const result = lkgpStrategy.select(pool, context);
|
|
|
|
expect(result.strategy).toBe("lkgp");
|
|
expect(result.provider).toBe("anthropic");
|
|
});
|
|
|
|
it("should use LKGP provider when lkgpEnabled is undefined (default)", () => {
|
|
const context: RoutingContext = {
|
|
taskType: "coding",
|
|
lastKnownGoodProvider: "openai",
|
|
};
|
|
const lkgpStrategy = getStrategy("lkgp");
|
|
const result = lkgpStrategy.select(pool, context);
|
|
|
|
expect(result.strategy).toBe("lkgp");
|
|
expect(result.provider).toBe("openai");
|
|
});
|
|
});
|
|
|
|
describe("Task Fitness Resolution Chain", () => {
|
|
it("getTaskFitness should return static table score for known models", () => {
|
|
const score = getTaskFitness("claude-sonnet", "coding");
|
|
expect(score).toBe(0.95);
|
|
});
|
|
|
|
it("getTaskFitness should return 0.5 for unknown models with no wildcard match", () => {
|
|
const score = getTaskFitness("unknown-model-xyz", "coding");
|
|
expect(score).toBe(0.5);
|
|
});
|
|
|
|
it("getTaskFitness should apply wildcard boosts for model name patterns", () => {
|
|
const score = getTaskFitness("deepseek-coder-v2", "coding");
|
|
expect(score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it("getTaskFitness should apply thinking wildcard for planning tasks", () => {
|
|
const score = getTaskFitness("some-thinking-model", "planning");
|
|
expect(score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it("getTaskFitnessWithSource should return source='fitness_table' for known static models", () => {
|
|
const result = getTaskFitnessWithSource("claude-sonnet", "coding");
|
|
expect(result).toEqual({ score: 0.95, source: "fitness_table" });
|
|
});
|
|
|
|
it("getTaskFitnessWithSource should return source='wildcard_boost' for wildcard-matched models", () => {
|
|
const result = getTaskFitnessWithSource("fast-model", "coding");
|
|
expect(result).toEqual({ score: expect.any(Number), source: "wildcard_boost" });
|
|
});
|
|
|
|
it("getTaskTypes should return task types without 'default'", () => {
|
|
const types = getTaskTypes();
|
|
expect(types).toContain("coding");
|
|
expect(types).toContain("review");
|
|
expect(types).toContain("planning");
|
|
expect(types).not.toContain("default");
|
|
});
|
|
|
|
it("unknown models should return 0.5 (default) when no DB or static entry exists", () => {
|
|
const score = getTaskFitness("completely-unknown-model-xyz-999", "coding");
|
|
expect(score).toBe(0.5);
|
|
});
|
|
|
|
it("wildcard boosts still work for models containing 'coder'", () => {
|
|
const score = getTaskFitness("my-coder-pro", "coding");
|
|
// Base 0.5 + coder boost 0.15 + code boost 0.1 = 0.75
|
|
// "coder" contains "code", so both wildcard patterns match
|
|
expect(score).toBe(0.75);
|
|
});
|
|
|
|
it("wildcard boosts still work for models containing 'thinking'", () => {
|
|
const score = getTaskFitness("my-thinking-model", "planning");
|
|
// Base 0.5 + thinking boost 0.1 = 0.6
|
|
expect(score).toBe(0.6);
|
|
});
|
|
|
|
it("wildcard boosts still work for models containing 'thinking' for analysis tasks", () => {
|
|
const score = getTaskFitness("my-thinking-model", "analysis");
|
|
// Base 0.5 + thinking boost 0.1 = 0.6
|
|
expect(score).toBe(0.6);
|
|
});
|
|
|
|
it("wildcard boosts for 'code' pattern apply to coding tasks", () => {
|
|
const score = getTaskFitness("my-code-generator", "coding");
|
|
// Base 0.5 + code boost 0.1 = 0.6
|
|
expect(score).toBe(0.6);
|
|
});
|
|
|
|
it("wildcard boosts for 'fast' pattern apply to coding tasks", () => {
|
|
const score = getTaskFitness("my-fast-model", "coding");
|
|
// Base 0.5 + fast boost 0.05 = 0.55
|
|
expect(score).toBe(0.55);
|
|
});
|
|
|
|
it("getTaskFitnessWithSource returns 'wildcard_boost' for pattern-matched unknown models", () => {
|
|
const result = getTaskFitnessWithSource("my-coder-pro", "coding");
|
|
expect(result.source).toBe("wildcard_boost");
|
|
expect(result.score).toBeGreaterThan(0.5);
|
|
});
|
|
|
|
it("getTaskFitnessWithSource returns 'fitness_table' for statically known models", () => {
|
|
const result = getTaskFitnessWithSource("claude-sonnet", "review");
|
|
expect(result.source).toBe("fitness_table");
|
|
expect(result.score).toBe(0.92);
|
|
});
|
|
|
|
it("getTaskFitnessWithSource returns 'wildcard_boost' with 0.5 for unknown models with no pattern", () => {
|
|
const result = getTaskFitnessWithSource("totally-random-xyz", "coding");
|
|
expect(result.source).toBe("wildcard_boost");
|
|
expect(result.score).toBe(0.5);
|
|
});
|
|
});
|
|
|
|
describe("Task Fitness DB Resolution Chain", () => {
|
|
// These tests verify that when DB is available, the resolution chain
|
|
// (user_override → arena_elo → models_dev_tier → static → wildcard)
|
|
// works correctly. Since the DB module is loaded lazily via require(),
|
|
// these tests cover the cases where DB is NOT available (graceful fallback).
|
|
|
|
it("falls back to static FITNESS_TABLE when DB is not initialized", () => {
|
|
// In the test environment, DB is typically not initialized,
|
|
// so getTaskFitness should fall through to the static table
|
|
const score = getTaskFitness("claude-sonnet", "coding");
|
|
// Static table has claude-sonnet → 0.95 for coding
|
|
expect(score).toBe(0.95);
|
|
});
|
|
|
|
it("falls back to static FITNESS_TABLE for review task type", () => {
|
|
const score = getTaskFitness("claude-opus", "review");
|
|
// Static table has claude-opus → 0.95 for review
|
|
expect(score).toBe(0.95);
|
|
});
|
|
|
|
it("falls back to wildcard boosts when no static entry exists and DB unavailable", () => {
|
|
// "coder-unknown" has no static entry but matches "coder" wildcard
|
|
const score = getTaskFitness("coder-unknown", "coding");
|
|
expect(score).toBeGreaterThan(0.5);
|
|
expect(score).toBeLessThanOrEqual(1.0);
|
|
});
|
|
|
|
it("getModelsDevTierFitness returns null when DB is not initialized", () => {
|
|
// Without a running DB, this should return null gracefully
|
|
const score = getModelsDevTierFitness("claude-sonnet", "coding");
|
|
// Either null (no capabilities data) or a number from DB if DB happens to be up
|
|
if (score !== null) {
|
|
expect(score).toBeGreaterThanOrEqual(0);
|
|
expect(score).toBeLessThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
it("invalidateFitnessCache does not throw", () => {
|
|
expect(() => invalidateFitnessCache()).not.toThrow();
|
|
});
|
|
|
|
it("resolution chain: static table takes priority over wildcard for known models", () => {
|
|
// "claude-sonnet" is in the static table with coding=0.95
|
|
// It does NOT match "coder" wildcard because the static table is checked first
|
|
const score = getTaskFitness("claude-sonnet", "coding");
|
|
expect(score).toBe(0.95); // From static table, NOT wildcard
|
|
});
|
|
|
|
it("getTaskFitnessWithSource identifies fitness_table as source for known models", () => {
|
|
const model = "claude-sonnet";
|
|
const category = "coding";
|
|
|
|
const result = getTaskFitnessWithSource(model, category);
|
|
expect(result.source).toBe("fitness_table");
|
|
expect(result.score).toBe(0.95);
|
|
});
|
|
|
|
it("case insensitivity: model names are lowercased before lookup", () => {
|
|
const upperScore = getTaskFitness("CLAUDE-SONNET", "coding");
|
|
const lowerScore = getTaskFitness("claude-sonnet", "coding");
|
|
expect(upperScore).toBe(lowerScore);
|
|
});
|
|
|
|
it("case insensitivity: task types are lowercased before lookup", () => {
|
|
const upperScore = getTaskFitness("claude-sonnet", "CODING");
|
|
const lowerScore = getTaskFitness("claude-sonnet", "coding");
|
|
expect(upperScore).toBe(lowerScore);
|
|
});
|
|
});
|