Build minimal LLM eval harness scaffold (B5) — fixtures recorded separately
Land the B5 harness skeleton: fixture loader (strict JSONL contract with meta header + ordered turn records + assertions tail), replay-client that swaps in for pi-ai's streamSimple by replaying recorded assistant turns, structural assertion judge (tool_called / no_tool_called / message_contains_substring / tool_call_arg_matches / trace_max_turns), an eval-runner that sums the token ledger and produces an EvalResult, a vitest driver (tests/eval/harness.test.ts) that discovers *.jsonl fixtures, and a record-fixture CLI that writes fixture meta headers. Hard contracts: - No fallback: replay-client and fixture-loader throw on exhaustion / malformed input rather than synthesizing defaults. - No mock: types reuse @mariozechner/pi-ai AgentMessage / ToolCall verbatim; shadow types are forbidden. - No fake fixture data: tests/eval/fixtures/ ships only README + .gitkeep. Real traces are recorded by the user against live providers. package.json adds test:eval (vitest --passWithNoTests so an empty fixture dir is green) and eval:record (CLI entrypoint). electron-builder already excludes tests/ via its files: glob — fixtures never enter the shipped .asar.
This commit is contained in:
1 parent
76d9031a41
commit
e7f879bed0
12 files changed
+931
No files matched your search
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* B5 eval harness — driver spec.
|
||||
*
|
||||
* This spec discovers every `*.jsonl` fixture under `tests/eval/fixtures/`
|
||||
* and runs it through the assertion judge. If there are zero fixtures, the
|
||||
* spec file has zero tests — vitest is invoked with `--passWithNoTests`
|
||||
* from the `test:eval` script so this is a green no-op until the user
|
||||
* records real fixtures.
|
||||
*
|
||||
* The harness does NOT synthesize placeholder fixtures under any
|
||||
* circumstance. An empty `fixtures/` directory is a real state — the tier
|
||||
* is simply not yet exercising anything — and the green run reflects that
|
||||
* honestly.
|
||||
*/
|
||||
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadFixture } from "./fixture-loader.js";
|
||||
import { runEval } from "./eval-runner.js";
|
||||
|
||||
const FIXTURE_DIR = join(__dirname, "fixtures");
|
||||
|
||||
async function listFixtures(): Promise<string[]> {
|
||||
const entries = await readdir(FIXTURE_DIR, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
||||
.map((entry) => join(FIXTURE_DIR, entry.name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
const fixturePaths = await listFixtures();
|
||||
|
||||
describe.each(fixturePaths)("eval fixture %s", (fixturePath) => {
|
||||
it("passes all assertions", async () => {
|
||||
const fixture = await loadFixture(fixturePath);
|
||||
const result = runEval(fixture);
|
||||
if (!result.passed) {
|
||||
const failures = result.assertions
|
||||
.filter((assertion) => !assertion.passed)
|
||||
.map((assertion) => `- ${assertion.assertion.kind}: ${assertion.reason ?? "<no reason>"}`)
|
||||
.join("\n");
|
||||
throw new Error(
|
||||
`fixture ${result.fixtureId} failed:\n${failures}\ntoken ledger: ${JSON.stringify(result.tokenLedger)}`,
|
||||
);
|
||||
}
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user