Skip to content

Commit bdb8243

Browse files
committed
test(engine): cover the prediction-metrics split in the engine's OWN suite
codecov reported this file at 35.84% under the `engine` flag while the root vitest suite showed it fully covered. Both were right: @loopover/engine uploads coverage from its own node:test suite (packages/loopover-engine/test/**), and that suite had no test for this file at all — the vitest one lands under `backend` and does not lift the engine flag. So the aggregation and the renderer get tests where the engine actually measures itself, including the property the split exists for: every sample `collectMinerPredictionMetrics` reports appears verbatim in what `renderMinerPredictionMetrics` emits, so the JSON snapshot and the Prometheus scrape cannot report different numbers. Also pins the escaping, which is a correctness rule rather than cosmetics: a conclusion is DATA, so a quote, backslash, or newline inside one must not forge a second series.
1 parent e4ea2a7 commit bdb8243

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
4+
import {
5+
MINER_PREDICTIONS_TOTAL,
6+
MINER_PREDICTION_CORRECT_TOTAL,
7+
MINER_PREDICTION_INCORRECT_TOTAL,
8+
collectMinerPredictionMetrics,
9+
renderMinerPredictionMetrics,
10+
type MinerPredictionMetricRow,
11+
} from "../dist/index.js";
12+
13+
// #9523: the miner's prediction-calibration metrics, now split into an aggregation
14+
// (`collectMinerPredictionMetrics`) and a text renderer that formats it. The split exists so the miner's
15+
// `loopover_miner_get_metrics_snapshot` MCP tool can return the SAME families the Prometheus scrape emits --
16+
// a second summing pass for the JSON surface would be a second definition of what each counter means, free
17+
// to drift from what a scrape reports.
18+
19+
const ROWS: MinerPredictionMetricRow[] = [
20+
{ conclusion: "merge", correct: true },
21+
{ conclusion: "merge", correct: false },
22+
{ conclusion: "close", correct: true },
23+
{ conclusion: "hold" },
24+
];
25+
26+
test("collect: counts every prediction by conclusion, in sorted order", () => {
27+
const families = collectMinerPredictionMetrics(ROWS);
28+
const totals = families.find((family) => family.name === MINER_PREDICTIONS_TOTAL);
29+
assert.ok(totals);
30+
// Sorted so the surface is deterministic across runs.
31+
assert.deepEqual(
32+
totals.samples.map((sample) => sample.labels?.conclusion),
33+
["close", "hold", "merge"],
34+
);
35+
assert.deepEqual(
36+
totals.samples.map((sample) => sample.value),
37+
[1, 1, 2],
38+
);
39+
});
40+
41+
test("collect: correct/incorrect only move for rows carrying a RESOLVED outcome", () => {
42+
const families = collectMinerPredictionMetrics(ROWS);
43+
// The `hold` row has no `correct` field: it counts toward predictions_total only, so the surface stays
44+
// meaningful before outcome-pairing exists and grows once it does.
45+
assert.equal(families.find((family) => family.name === MINER_PREDICTION_CORRECT_TOTAL)?.samples[0]?.value, 2);
46+
assert.equal(families.find((family) => family.name === MINER_PREDICTION_INCORRECT_TOTAL)?.samples[0]?.value, 1);
47+
});
48+
49+
test("collect: emits all three counters for an EMPTY ledger, so the surface is well-formed from day one", () => {
50+
const families = collectMinerPredictionMetrics([]);
51+
assert.deepEqual(families.map((family) => family.name), [
52+
MINER_PREDICTIONS_TOTAL,
53+
MINER_PREDICTION_CORRECT_TOTAL,
54+
MINER_PREDICTION_INCORRECT_TOTAL,
55+
]);
56+
// No conclusions seen yet, so the labelled counter has no series -- but it still declares itself.
57+
assert.deepEqual(families[0]?.samples, []);
58+
assert.equal(families[1]?.samples[0]?.value, 0);
59+
assert.equal(families[2]?.samples[0]?.value, 0);
60+
});
61+
62+
test("collect: treats a null `correct` as unresolved, not as incorrect", () => {
63+
const families = collectMinerPredictionMetrics([{ conclusion: "merge", correct: null }]);
64+
assert.equal(families.find((family) => family.name === MINER_PREDICTION_CORRECT_TOTAL)?.samples[0]?.value, 0);
65+
assert.equal(families.find((family) => family.name === MINER_PREDICTION_INCORRECT_TOTAL)?.samples[0]?.value, 0);
66+
});
67+
68+
test("render: formats the collected families as Prometheus text exposition", () => {
69+
const text = renderMinerPredictionMetrics(ROWS);
70+
assert.match(text, new RegExp(`# HELP ${MINER_PREDICTIONS_TOTAL} `));
71+
assert.match(text, new RegExp(`# TYPE ${MINER_PREDICTIONS_TOTAL} counter`));
72+
assert.match(text, new RegExp(`${MINER_PREDICTIONS_TOTAL}\\{conclusion="merge"\\} 2`));
73+
assert.match(text, new RegExp(`${MINER_PREDICTION_CORRECT_TOTAL} 2`));
74+
assert.match(text, new RegExp(`${MINER_PREDICTION_INCORRECT_TOTAL} 1`));
75+
assert.ok(text.endsWith("\n"), "the document is newline-terminated");
76+
});
77+
78+
test("render: an unlabelled counter emits no braces at all", () => {
79+
const text = renderMinerPredictionMetrics([]);
80+
assert.match(text, new RegExp(`\\n${MINER_PREDICTION_CORRECT_TOTAL} 0\\n`));
81+
});
82+
83+
test("render: escapes a hostile conclusion so it cannot break the exposition line", () => {
84+
// A conclusion is data, not a literal: a quote, a backslash, or a newline in it must not forge a series.
85+
const text = renderMinerPredictionMetrics([{ conclusion: 'we"ird\\back\nline' }]);
86+
assert.match(text, /conclusion="we\\"ird\\\\back\\nline"/);
87+
// Exactly one sample line for that counter -- the injected newline did not split it into two.
88+
const sampleLines = text.split("\n").filter((line) => line.startsWith(`${MINER_PREDICTIONS_TOTAL}{`));
89+
assert.equal(sampleLines.length, 1);
90+
});
91+
92+
test("render and collect agree: every collected sample appears in the rendered text", () => {
93+
// The whole point of the split -- the scrape and the JSON snapshot cannot report different numbers.
94+
const families = collectMinerPredictionMetrics(ROWS);
95+
const text = renderMinerPredictionMetrics(ROWS);
96+
for (const family of families) {
97+
for (const sample of family.samples) {
98+
const labels = sample.labels ? `{conclusion="${sample.labels.conclusion}"}` : "";
99+
assert.ok(text.includes(`${family.name}${labels} ${sample.value}`), `${family.name}${labels} should be rendered`);
100+
}
101+
}
102+
});

0 commit comments

Comments
 (0)