Read the script
// Smoke tests for dispatch-counter.mjs. Run: node --test hooks/
// Tests drive the CLI end-to-end with CLAUDE_CONFIG_DIR pointed at a temp
// dir, so no exports or refactors of the script are needed.
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "dispatch-counter.mjs");
function run(args, configDir, stdin, extraEnv, cwd) {
return execFileSync(process.execPath, [SCRIPT, ...args].filter(Boolean), {
// CLAUDE_CODE_SUBAGENT_MODEL, CLAUDE_CODE_SUBAGENT_MODEL_FORCE and
// CLAUDE_CODE_EFFORT_LEVEL are blanked by default so a developer's own
// overrides cannot leak into the hermetic tests; set any via extraEnv.
env: { ...process.env, CLAUDE_CONFIG_DIR: configDir, CLAUDE_CODE_SUBAGENT_MODEL: "", CLAUDE_CODE_SUBAGENT_MODEL_FORCE: "", CLAUDE_CODE_EFFORT_LEVEL: "", ...(extraEnv ?? {}) },
// Default the working directory to the temp config dir, never the directory
// the suite happens to run from: the hook reads <cwd>/.claude/settings*.json
// for the session effort, so an ambient cwd would let a real settings file
// two levels up decide a test outcome. Tests that need a specific project
// cascade pass their own cwd.
cwd: cwd ?? configDir,
input: stdin ?? "",
encoding: "utf-8",
});
}
function freshConfigDir() {
return mkdtempSync(join(tmpdir(), "mr-test-"));
}
function writeLog(configDir, entries) {
const dir = join(configDir, "model-routing");
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "dispatches.jsonl"), entries.map((e) => JSON.stringify(e)).join("\n") + "\n");
}
test("report with no log explains itself instead of printing nothing", () => {
const cfg = freshConfigDir();
try {
const out = run(["report"], cfg);
assert.match(out, /No dispatches logged/);
assert.match(out, /PostToolUse hook/);
} finally { rmSync(cfg, { recursive: true, force: true }); }
});
test("stats with no log prints a no-data marker", () => {
const cfg = freshConfigDir();
try {
assert.equal(run(["stats"], cfg), "routed-down: no data (7d)");
} finally { rmSync(cfg, { recursive: true, force: true }); }
});
test("report groups by tier and never ranks unknown models", () => {
const cfg = freshConfigDir();
const now = Date.now();
writeLog(cfg, [
// sonnet from an opus session: routed down.
{ ts: now, agent: "general-purpose", model: "sonnet", session: "claude-opus-4-8" },
// opus from an opus session: at tier, not down.
{ ts: now, agent: "model-routing:implementer", model: "opus", session: "claude-opus-4-8" },
// future model family: tier unknown - must NOT count as routed down.
{ ts: now, agent: "general-purpose", model: "zephyr-1", session: "claude-opus-4-8" },
]);
try {
const out = run(["report"], cfg);
// Unknown-tier entries are excluded from the denominator - one exotic
// model must not drag the routed-down share down.
assert.match(out, /1 of 2 comparable dispatches \(50%\) ran on a lower tier/);
assert.match(out, /1 not tier-comparable excluded/);
// Unknown-model rows land in their own section - honest unknown,
// not silently counted as routed down or at-tier.
assert.match(out, /Not tier-comparable[\s\S]*general-purpose \(model=zephyr-1\)/);
assert.match(out, /Ran at the session tier[\s\S]*implementer \(model=opus\)/);
} finally { rmSync(cfg, { recursive: true, force: true }); }
});
test("bare pinned agents classify by their frontmatter pin", () => {
const cfg = freshConfigDir();
const now = Date.now();
writeLog(cfg, [
// implementer pins sonnet: a bare dispatch from an opus session ran
// sonnet, so it is routed down even without an explicit model param.
{ ts: now, agent: "model-routing:implementer", model: null, session: "claude-opus-4-8" },
// reviewer pins opus: bare on an opus session stays at the session tier.
{ ts: now, agent: "model-routing:reviewer", model: null, session: "claude-opus-4-8" },
]);
try {
const out = run(["report"], cfg);
assert.match(out, /1 of 2 dispatches \(50%\) ran on a lower tier/);
assert.match(out, /Ran cheaper[\s\S]*implementer \(pin=sonnet\)/);
assert.match(out, /Ran at the session tier[\s\S]*reviewer \(pin=opus\)/);
} finally { rmSync(cfg, { recursive: true, force: true }); }
});
test("a curated foreign agent pin classifies like a bundled one, not a leak", () => {
const cfg = freshConfigDir();
const now = Date.now();
// codex:codex-rescue pins sonnet in its own frontmatter (openai-codex
// plugin) - this plugin cannot see that file, but FOREIGN_AGENT_PINS
// records it by hand because it was the exact agent causing false leak
// warnings before this test existed. A bare dispatch on an opus session
// should route down on the strength of that known pin, the same as a
// bundled agent, and must not count toward "tier leaks" at all.
writeLog(cfg, [
{ ts: now, agent: "codex:codex-rescue", model: null, session: "claude-opus-5" },
// A genuinely unpinned type, for contrast - this one SHOULD leak.
{ ts: now, agent: "general-purpose", model: null, session: "claude-opus-5" },
]);
try {
const out = run(["report"], cfg);
assert.match(out, /1 of 2 dispatches \(50%\) ran on a lower tier/);
assert.match(out, /Ran cheaper[\s\S]*codex:codex-rescue \(pin=sonnet\)/);
assert.match(out, /Tier leaks: 1 of 1 dispatches on agent types with no MODEL pin this plugin knows \(100%\)/);
} finally { rmSync(cfg, { recursive: true, force: true }); }
});
test("a foreign-pinned agent dispatched below its own pin is still called out", () => {
const cfg = freshConfigDir();
const no