Skip to content

Commit 169c487

Browse files
committed
fix(evals): score dispatch route on the first line
1 parent 55e63e0 commit 169c487

2 files changed

Lines changed: 329 additions & 33 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { describe, expect, test } from "bun:test";
5+
import { evaluate, type CheckValue, type Run } from "@corbits/evals";
6+
import {
7+
ROUTE_BUDGET_MS,
8+
evaluateRouting,
9+
noDiyCheck,
10+
routeLine,
11+
routesCheck,
12+
} from "./eval-prompts-dispatch.js";
13+
14+
const ROSTER =
15+
"explorer, planner, coder, reviewer, designer, artist, warden, shakespeare, prober, qa-lead";
16+
17+
function passed(value: CheckValue | Promise<CheckValue>): boolean {
18+
if (typeof value === "object" && value !== null && "then" in value) {
19+
throw new Error("expected a sync check");
20+
}
21+
if (typeof value === "boolean") return value;
22+
if (typeof value === "number") return value === 1;
23+
return value.pass;
24+
}
25+
26+
function routingRun(
27+
output: string,
28+
expect: { route: string; decoys: readonly string[] },
29+
): Run<string, string, { route: string; decoys: readonly string[] }> {
30+
return {
31+
input: "task",
32+
output,
33+
expect,
34+
steps: [],
35+
ms: 1,
36+
};
37+
}
38+
39+
describe("routes scores the first line", () => {
40+
const coder = { route: "coder", decoys: ["explorer", "reviewer"] };
41+
42+
test("a roster-only reply does not pass", () => {
43+
expect(passed(routesCheck(routingRun(ROSTER, coder)))).toBe(false);
44+
expect(
45+
passed(routesCheck(routingRun(`${ROSTER}\nI would spawn coder.`, coder))),
46+
).toBe(false);
47+
});
48+
49+
test("a decoy on the first line does not pass", () => {
50+
expect(
51+
passed(
52+
routesCheck(routingRun("reviewer\nI will not change code.", coder)),
53+
),
54+
).toBe(false);
55+
expect(
56+
passed(
57+
routesCheck(
58+
routingRun("I would not spawn explorer; spawn coder.", {
59+
route: "explorer",
60+
decoys: ["coder", "planner"],
61+
}),
62+
),
63+
),
64+
).toBe(false);
65+
});
66+
67+
test("the specialist alone on the first line passes", () => {
68+
expect(
69+
passed(
70+
routesCheck(
71+
routingRun(
72+
"coder\nI will just patch the login flow and add a test.",
73+
coder,
74+
),
75+
),
76+
),
77+
).toBe(true);
78+
expect(passed(routesCheck(routingRun("Coder\nbrief", coder)))).toBe(true);
79+
});
80+
});
81+
82+
describe("noDiy ignores the brief line", () => {
83+
test("DIY verbs in the brief do not fail", () => {
84+
expect(
85+
passed(
86+
noDiyCheck(
87+
routingRun(
88+
"coder\nI will just patch the login flow and add a test.",
89+
{
90+
route: "coder",
91+
decoys: ["explorer"],
92+
},
93+
),
94+
),
95+
),
96+
).toBe(true);
97+
expect(
98+
passed(
99+
noDiyCheck(
100+
routingRun("shakespeare\nI'll write the architecture doc", {
101+
route: "shakespeare",
102+
decoys: ["explorer", "planner"],
103+
}),
104+
),
105+
),
106+
).toBe(true);
107+
});
108+
109+
test("DIY on the route line still fails", () => {
110+
expect(
111+
passed(
112+
noDiyCheck(
113+
routingRun("I'll write the architecture doc\nshakespeare", {
114+
route: "shakespeare",
115+
decoys: ["explorer"],
116+
}),
117+
),
118+
),
119+
).toBe(false);
120+
expect(
121+
passed(
122+
noDiyCheck(
123+
routingRun("I'll fix the login bug", {
124+
route: "coder",
125+
decoys: ["reviewer"],
126+
}),
127+
),
128+
),
129+
).toBe(false);
130+
expect(routeLine("coder\nI'll fix it")).toBe("coder");
131+
});
132+
});
133+
134+
describe("drop baseline", () => {
135+
const coderCase = {
136+
name: "fix-token-refresh",
137+
input: "Fix the login flow.",
138+
expect: { route: "coder", decoys: ["explorer", "reviewer"] },
139+
};
140+
141+
function stored(routesPass: boolean): string {
142+
return `${JSON.stringify(
143+
{
144+
format: 1,
145+
eval: "dispatch-routing",
146+
cases: {
147+
"fix-token-refresh": {
148+
routes: { pass: routesPass, value: routesPass ? 1 : 0 },
149+
noDiy: { pass: true, value: 1 },
150+
fast: { pass: true, value: 1 },
151+
},
152+
},
153+
},
154+
null,
155+
2,
156+
)}\n`;
157+
}
158+
159+
test("a failing run does not replace the baseline and the next run still drops", async () => {
160+
const dir = await mkdtemp(join(tmpdir(), "dispatch-routing-"));
161+
const baselinePath = join(dir, "baseline.json");
162+
const lastPath = join(dir, "last.json");
163+
const before = stored(true);
164+
await writeFile(baselinePath, before);
165+
const timeouts: (number | string | undefined)[] = [];
166+
const run = async () => "reviewer\nI refuse.";
167+
try {
168+
const first = await evaluateRouting({
169+
cases: [coderCase],
170+
run,
171+
lastPath,
172+
baselinePath,
173+
evaluateFn: (name, options) => {
174+
timeouts.push(options.timeout);
175+
return evaluate(name, options);
176+
},
177+
});
178+
expect(first).toBe(1);
179+
expect(await readFile(baselinePath, "utf8")).toBe(before);
180+
expect(await readFile(lastPath, "utf8")).not.toBe(before);
181+
182+
const second = await evaluateRouting({
183+
cases: [coderCase],
184+
run,
185+
lastPath,
186+
baselinePath,
187+
});
188+
expect(second).toBe(1);
189+
expect(await readFile(baselinePath, "utf8")).toBe(before);
190+
expect(timeouts).toEqual([ROUTE_BUDGET_MS]);
191+
expect(ROUTE_BUDGET_MS).toBe(120_000);
192+
} finally {
193+
await rm(dir, { recursive: true, force: true });
194+
}
195+
});
196+
197+
test("a passing run updates the baseline", async () => {
198+
const dir = await mkdtemp(join(tmpdir(), "dispatch-routing-"));
199+
const baselinePath = join(dir, "baseline.json");
200+
const lastPath = join(dir, "last.json");
201+
await writeFile(baselinePath, stored(false));
202+
try {
203+
const code = await evaluateRouting({
204+
cases: [coderCase],
205+
run: async () => "coder\nAdd a regression test for token refresh.",
206+
lastPath,
207+
baselinePath,
208+
});
209+
expect(code).toBe(0);
210+
const after = JSON.parse(await readFile(baselinePath, "utf8")) as {
211+
cases: { "fix-token-refresh": { routes: { pass: boolean } } };
212+
};
213+
expect(after.cases["fix-token-refresh"].routes.pass).toBe(true);
214+
expect(await readFile(lastPath, "utf8")).toBe(
215+
await readFile(baselinePath, "utf8"),
216+
);
217+
} finally {
218+
await rm(dir, { recursive: true, force: true });
219+
}
220+
});
221+
});

0 commit comments

Comments
 (0)