CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | /**
* Tests for the task → model router in src/lib/ai-client.ts.
*
* Routing policy under test:
* - Only the light, human-reviewed tasks (commit-message, issue-triage,
* pr-triage, label-suggest) may run on Haiku.
* - Everything that writes or judges code — and any unknown task —
* defaults to Sonnet.
* - AI_FORCE_SONNET=1 is an instant kill-switch that forces Sonnet for
* every task, read from the environment at call time.
*/
import { describe, it, expect, afterEach } from "bun:test";
import {
modelForTask,
MODEL_SONNET,
MODEL_HAIKU,
type AiTask,
} from "../lib/ai-client";
const ORIGINAL_FORCE_SONNET = process.env.AI_FORCE_SONNET;
afterEach(() => {
// Restore whatever the environment had before the suite ran.
if (ORIGINAL_FORCE_SONNET === undefined) {
delete process.env.AI_FORCE_SONNET;
} else {
process.env.AI_FORCE_SONNET = ORIGINAL_FORCE_SONNET;
}
});
// ──────────────────────────── Haiku allowlist ────────────────────────────
describe("modelForTask — Haiku allowlist", () => {
const haikuTasks: AiTask[] = [
"commit-message",
"issue-triage",
"pr-triage",
"label-suggest",
];
for (const task of haikuTasks) {
it(`routes ${task} to Haiku`, () => {
delete process.env.AI_FORCE_SONNET;
expect(modelForTask(task)).toBe(MODEL_HAIKU);
});
}
});
// ──────────────────────────── Sonnet defaults ────────────────────────────
describe("modelForTask — code-critical and doc tasks stay on Sonnet", () => {
const sonnetTasks: AiTask[] = [
"code-review",
"code-completion",
"spec-to-pr",
"ci-heal",
"pr-summary",
"changelog",
];
for (const task of sonnetTasks) {
it(`routes ${task} to Sonnet`, () => {
delete process.env.AI_FORCE_SONNET;
expect(modelForTask(task)).toBe(MODEL_SONNET);
});
}
it("defaults unknown tasks to Sonnet (never silently downgrades)", () => {
delete process.env.AI_FORCE_SONNET;
// Simulate a typo'd / future task name slipping past the type system.
expect(modelForTask("some-future-task" as AiTask)).toBe(MODEL_SONNET);
expect(modelForTask("" as AiTask)).toBe(MODEL_SONNET);
});
});
// ──────────────────────────── kill-switch ────────────────────────────
describe("modelForTask — AI_FORCE_SONNET kill-switch", () => {
const allTasks: AiTask[] = [
"commit-message",
"issue-triage",
"pr-triage",
"label-suggest",
"code-review",
"code-completion",
"spec-to-pr",
"ci-heal",
"pr-summary",
"changelog",
];
it("forces Sonnet for every task when AI_FORCE_SONNET=1", () => {
process.env.AI_FORCE_SONNET = "1";
for (const task of allTasks) {
expect(modelForTask(task)).toBe(MODEL_SONNET);
}
});
it("is read at call time — flipping the env var takes effect immediately", () => {
delete process.env.AI_FORCE_SONNET;
expect(modelForTask("commit-message")).toBe(MODEL_HAIKU);
process.env.AI_FORCE_SONNET = "1";
expect(modelForTask("commit-message")).toBe(MODEL_SONNET);
delete process.env.AI_FORCE_SONNET;
expect(modelForTask("commit-message")).toBe(MODEL_HAIKU);
});
it("only the exact value \"1\" activates the kill-switch", () => {
process.env.AI_FORCE_SONNET = "0";
expect(modelForTask("commit-message")).toBe(MODEL_HAIKU);
process.env.AI_FORCE_SONNET = "";
expect(modelForTask("commit-message")).toBe(MODEL_HAIKU);
});
});
|