Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "postil-cli"
version = "0.9.15"
version = "0.9.16"
edition = "2024"
description = "Postil: a low-noise AI review gate. Silent on clean PRs, hard gate on real risk."
license = "Apache-2.0"
Expand Down
104 changes: 103 additions & 1 deletion bench/src/scorer-eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SCORER_CASE_EXEC_TIMEOUT_MS,
SCORER_CASE_HARNESS_ALLOWANCE_MS,
SCORER_MAX_CASE_MS,
SCORER_PROXY_UPSTREAM_TIMEOUT_MS,
SCORER_REASON_SCHEMA_PATTERN,
SCORER_DIAGNOSTIC_RESPONSE_LIMIT,
TRUE_FINDING_CASES,
Expand Down Expand Up @@ -1117,6 +1118,54 @@ describe("scorer proxy and isolated runtime", () => {
}
});

for (const phase of ["scorer", "adjudication"] as const) {
test(`latches ${phase} admission timeout before a retry can dispatch upstream`, async () => {
let dispatches = 0;
const upstream = createServer(async (req: IncomingMessage, res: ServerResponse) => {
await requestBody(req);
dispatches++;
if (dispatches === 1) return;
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ usage: { prompt_tokens: 3, completion_tokens: 2, cost: 0.001 } }));
});
const upstreamBase = await listen(upstream);
const proxy = await startScorerProxy(
fixture("clean-docs-only"), "falseFinding", upstreamBase, crypto.randomUUID(), 100,
);
const request = (body: object) => fetch(`${proxy.baseUrl}/chat/completions`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
try {
const first = await request(phase === "scorer" ? scorerRequest() : adjudicationRequest());
expect(first.status).toBe(504);
await first.text();
for (const body of [scorerRequest(), adjudicationRequest()]) {
const retry = await request(body);
expect(retry.status).toBe(400);
expect(await retry.json()).toEqual({ error: "qualification admission already failed after an upstream timeout" });
}
expect(dispatches).toBe(1);
expect(proxy.attempts).toHaveLength(1);
expect(proxy.attempts[0]).toMatchObject({
phase, outcome: "timedOut", costUsd: null, costProviderDecimal: null,
usageValid: false, usagePresent: false, httpStatus: null,
});
const diagnostics = scorerCaseDiagnostics({
child: { exitCode: 1, stderr: "", timedOut: false }, attempts: proxy.attempts,
});
expect(diagnostics.failureSignals).toContain("upstreamTimeout");
expect(diagnostics.responses[0]).toMatchObject({
exactCost: "unavailable", accountingIssues: ["responseUnavailable"],
});
} finally {
await proxy.close();
upstream.closeAllConnections();
if (upstream.listening) await close(upstream);
}
});
}

test("aborts an in-flight upstream request before proxy teardown waits", async () => {
let markUpstreamStarted: (() => void) | undefined;
const upstreamStarted = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -1908,6 +1957,24 @@ describe("aggregate", () => {
});
});

test("admits a 30-second maximum without relaxing percentile limits", () => {
expect(SCORER_MAX_CASE_MS).toBe(30_000);
expect(SCORER_PROXY_UPSTREAM_TIMEOUT_MS).toBe(30_000);
expect(SCORER_CASE_EXEC_TIMEOUT_MS).toBe(65_000);
const cases = qualificationCases(3);
cases[0]!.durationMs = 30_000;
expect(aggregate("scorer/model", cases, 3)).toMatchObject({
passed: true, maxDurationMs: 30_000, p50DurationMs: 1000, p95DurationMs: 1000,
});
cases[0]!.durationMs = 30_001;
expect(aggregate("scorer/model", cases, 3).admissionFailures)
.toContain("max latency 30001ms exceeds 30000ms");
for (const entry of cases) entry.durationMs = 10_001;
const failures = aggregate("scorer/model", cases, 3).admissionFailures;
expect(failures).toContain("p50 latency 10001ms exceeds 5000ms");
expect(failures).toContain("p95 latency 10001ms exceeds 10000ms");
});

test("reports a timed-out scorer case without double-counting a structured failure", () => {
const cases = qualificationCases(1);
cases[0] = result({
Expand Down Expand Up @@ -1957,7 +2024,7 @@ describe("aggregate", () => {
c.findingPublished = true;
c.passed = false;
}
cases[0]!.durationMs = 20_001;
cases[0]!.durationMs = SCORER_MAX_CASE_MS + 1;
cases[1]!.costUsd = null;
const aggregateResult = aggregate("scorer/model", cases, 5);
expect(aggregateResult.passed).toBe(false);
Expand Down Expand Up @@ -2082,6 +2149,41 @@ describe("qualification utilities", () => {
});

describe("formatReport", () => {
test("reports estimated timeout cost as unknown until exact accounting is complete", () => {
const cases = qualificationCases(1);
const reportFor = (entries: ScorerEvalCase[]): ScorerEvalReport => ({
generatedAt: "2026-07-11T00:00:00.000Z",
qualificationSourceSha: "a".repeat(40), cliBinarySha256: "b".repeat(64),
apiBase: "https://example.test/v1", upstreamProvider: "test-provider",
upstreamProviderRoute: "test-provider/route", ...scorerReportContract(),
repeats: 1, completedCases: entries.length, totalCases: entries.length,
matrixComplete: true, passed: false,
models: [aggregate("scorer/model", entries, 1)], cases: entries,
});
const complete = reportFor(cases);
expect(complete.models[0]!.pricingKnown).toBe(true);
expect(complete.models[0]!.passed).toBe(true);
expect(formatReport(complete)).toContain("$0.000100");
expect(formatReport(complete)).toContain("Observed provider cost: $0.0012 (complete accounting)");
for (const accounting of [
{ costProviderDecimal: null, usageAccountingComplete: false },
{ costProviderDecimal: null, usageAccountingComplete: true },
{ costProviderDecimal: "0.0001", usageAccountingComplete: false },
]) {
const entries = [...cases];
entries[0] = { ...entries[0]!, ...accounting, timedOut: true, passed: false, costUsd: 0.001 };
const report = reportFor(entries);
expect(report.models[0]!.pricingKnown).toBe(false);
expect(report.models[0]!.admissionFailures).toContain("pricing missing for one or more cases");
expect(report.models[0]!.passed).toBe(false);
const text = formatReport(report);
expect(text.split("\n").find((line) => line.startsWith("scorer/model"))).toContain("unknown");
expect(text).toContain("Observed provider cost: incomplete accounting");
expect(text).not.toContain("(complete accounting)");
}
});


test("prints comparable scorer metrics", () => {
const report: ScorerEvalReport = {
generatedAt: "2026-07-11T00:00:00.000Z",
Expand Down
14 changes: 12 additions & 2 deletions bench/src/scorer-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const DEFAULT_QUALIFICATION_REPEATS = 5;
export const SCORER_REASON_MAX_BYTES = 240;
export const SCORER_MAX_P50_MS = 5_000;
export const SCORER_MAX_P95_MS = 10_000;
export const SCORER_MAX_CASE_MS = 20_000;
export const SCORER_MAX_CASE_MS = 30_000;
export const SCORER_CASE_HARNESS_ALLOWANCE_MS = 5_000;
export const SCORER_CASE_EXEC_TIMEOUT_MS =
2 * SCORER_MAX_CASE_MS + SCORER_CASE_HARNESS_ALLOWANCE_MS;
Expand Down Expand Up @@ -1846,6 +1846,7 @@ export async function startScorerProxy(
}> = [];
const upstreamControllers = new Set<AbortController>();
let upstreamOrdinal = 0;
let admissionTimedOut = false;
let closing = false;
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== "POST" || req.url !== "/chat/completions") {
Expand Down Expand Up @@ -2014,12 +2015,18 @@ export async function startScorerProxy(
return;
}

if (admissionTimedOut) {
res.writeHead(400, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "qualification admission already failed after an upstream timeout" }));
return;
}
const ordinal = ++upstreamOrdinal;
const controller = new AbortController();
upstreamControllers.add(controller);
let deadlineExceeded = false;
const timeout = setTimeout(() => {
deadlineExceeded = true;
admissionTimedOut = true;
controller.abort();
}, upstreamTimeoutMs);
const startedAt = performance.now();
Expand Down Expand Up @@ -2571,7 +2578,9 @@ export function aggregate(
}
}
}
const pricingKnown = costs.length === cases.length && cases.length > 0;
const pricingKnown = costs.length === cases.length && cases.length > 0 &&
cases.every((item) => item.usageAccountingComplete === true &&
typeof item.costProviderDecimal === "string");
if (!pricingKnown) admissionFailures.push("pricing missing for one or more cases");
if (structuralPass && p50DurationMs > SCORER_MAX_P50_MS) {
admissionFailures.push(`p50 latency ${p50DurationMs.toFixed(0)}ms exceeds ${SCORER_MAX_P50_MS}ms`);
Expand Down Expand Up @@ -2639,6 +2648,7 @@ export function formatReport(report: ScorerEvalReport): string {
const exactCosts = report.cases.map((item) => item.costProviderDecimal);
if (
exactCosts.length > 0 &&
report.cases.every((item) => item.usageAccountingComplete === true) &&
exactCosts.every((cost): cost is string => typeof cost === "string")
) {
lines.push(
Expand Down
Loading