@@ -2,7 +2,15 @@ import { mkdtempSync, rmSync } from "node:fs";
22import { tmpdir } from "node:os" ;
33import { join } from "node:path" ;
44import { afterEach , describe , expect , it , vi } from "vitest" ;
5- import { initPredictionLedger } from "../../packages/loopover-miner/lib/prediction-ledger.js" ;
5+ import {
6+ initEventLedger ,
7+ resolveEventLedgerDbPath ,
8+ } from "../../packages/loopover-miner/lib/event-ledger.js" ;
9+ import type { EventLedger , LedgerEntry } from "../../packages/loopover-miner/lib/event-ledger.js" ;
10+ import {
11+ initPredictionLedger ,
12+ resolvePredictionLedgerDbPath ,
13+ } from "../../packages/loopover-miner/lib/prediction-ledger.js" ;
614import {
715 collectPredictionMetricRows ,
816 runMetrics ,
@@ -20,24 +28,64 @@ function tempLedger(): PredictionLedger {
2028 return ledger ;
2129}
2230
23- function tempDbPath ( ) {
31+ function tempEventLedger ( ) : EventLedger {
32+ const root = mkdtempSync ( join ( tmpdir ( ) , "loopover-miner-metrics-cli-evt-" ) ) ;
33+ roots . push ( root ) ;
34+ const ledger = initEventLedger ( join ( root , "event-ledger.sqlite3" ) ) ;
35+ ledgers . push ( ledger ) ;
36+ return ledger ;
37+ }
38+
39+ function tempDir ( ) {
2440 const root = mkdtempSync ( join ( tmpdir ( ) , "loopover-miner-metrics-cli-" ) ) ;
2541 roots . push ( root ) ;
26- return join ( root , "prediction-ledger.sqlite3" ) ;
42+ return root ;
43+ }
44+
45+ /** Env pointing both local stores at a fresh temp config dir (mirrors miner-calibration-cli.test.ts). */
46+ function tempConfigEnv ( ) : Record < string , string | undefined > {
47+ return { LOOPOVER_MINER_CONFIG_DIR : tempDir ( ) } ;
2748}
2849
2950function appendPrediction ( ledger : PredictionLedger , targetId : number , conclusion : string ) {
3051 ledger . appendPrediction ( { repoFullName : "acme/widgets" , targetId, conclusion, pack : "gittensor" , engineVersion : "0.2.0" } ) ;
3152}
3253
54+ function seedPredictionEnv ( env : Record < string , string | undefined > , targetId : number , conclusion : string ) {
55+ const store = initPredictionLedger ( resolvePredictionLedgerDbPath ( env ) ) ;
56+ store . appendPrediction ( { repoFullName : "acme/widgets" , targetId, conclusion, pack : "gittensor" , engineVersion : "0.2.0" } ) ;
57+ store . close ( ) ;
58+ }
59+
60+ function seedOutcomeEnv ( env : Record < string , string | undefined > , prNumber : number , decision : string ) {
61+ const ledger = initEventLedger ( resolveEventLedgerDbPath ( env ) ) ;
62+ ledger . appendEvent ( { type : "pr_outcome" , repoFullName : "acme/widgets" , payload : { prNumber, decision } } ) ;
63+ ledger . close ( ) ;
64+ }
65+
66+ // Build a raw `pr_outcome` ledger row (or a deliberately malformed one) for the in-process join, exactly as
67+ // toOutcomeRecords would read it off the event ledger.
68+ let seq = 0 ;
69+ function outcomeEvent ( prNumber : unknown , decision : unknown , repoFullName : string | null = "acme/widgets" ) : LedgerEntry {
70+ seq += 1 ;
71+ return {
72+ id : seq ,
73+ seq,
74+ type : "pr_outcome" ,
75+ repoFullName,
76+ payload : { prNumber, decision } as Record < string , unknown > ,
77+ createdAt : new Date ( seq * 1000 ) . toISOString ( ) ,
78+ } ;
79+ }
80+
3381afterEach ( ( ) => {
3482 for ( const ledger of ledgers . splice ( 0 ) ) ledger . close ( ) ;
3583 vi . restoreAllMocks ( ) ;
3684 for ( const root of roots . splice ( 0 ) ) rmSync ( root , { recursive : true , force : true } ) ;
3785} ) ;
3886
3987describe ( "loopover-miner metrics CLI (#4838)" , ( ) => {
40- it ( "collectPredictionMetricRows projects ledger rows onto the renderer's conclusion-only shape " , ( ) => {
88+ it ( "collectPredictionMetricRows leaves `correct` unset when no outcome events are supplied " , ( ) => {
4189 const ledger = tempLedger ( ) ;
4290 appendPrediction ( ledger , 1 , "merge" ) ;
4391 appendPrediction ( ledger , 2 , "close" ) ;
@@ -51,34 +99,41 @@ describe("loopover-miner metrics CLI (#4838)", () => {
5199 appendPrediction ( ledger , 3 , "merge" ) ;
52100
53101 const log = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => undefined ) ;
54- expect ( runMetrics ( [ ] , { initPredictionLedger : ( ) => ledger } ) ) . toBe ( 0 ) ;
102+ // Inject an empty event ledger: no realized outcomes, so the correct/incorrect counters stay zero.
103+ expect ( runMetrics ( [ ] , { initPredictionLedger : ( ) => ledger , initEventLedger : ( ) => tempEventLedger ( ) } ) ) . toBe ( 0 ) ;
55104
56105 const text = String ( log . mock . calls [ 0 ] ?. [ 0 ] ) ;
57106 expect ( text ) . toContain ( "# TYPE loopover_miner_predictions_total counter" ) ;
58107 // Series are emitted in sorted conclusion order, so "close" precedes "merge".
59108 expect ( text ) . toContain ( 'loopover_miner_predictions_total{conclusion="close"} 1' ) ;
60109 expect ( text ) . toContain ( 'loopover_miner_predictions_total{conclusion="merge"} 2' ) ;
61- // No outcome-join exists yet , so both the correct and incorrect counters stay zero.
110+ // No realized outcomes joined , so both the correct and incorrect counters stay zero.
62111 expect ( text ) . toContain ( "loopover_miner_prediction_correct_total 0" ) ;
63112 expect ( text ) . toContain ( "loopover_miner_prediction_incorrect_total 0" ) ;
64113 // The output is a single, once-terminated document (no doubled trailing blank line).
65114 expect ( text . endsWith ( "\n" ) ) . toBe ( false ) ;
66115 } ) ;
67116
68- it ( "runMetrics opens and closes its own default ledger when none is injected" , ( ) => {
69- const dbPath = tempDbPath ( ) ;
70- const seed = initPredictionLedger ( dbPath ) ;
117+ it ( "runMetrics opens and closes its own default ledgers when none are injected" , ( ) => {
118+ const dir = tempDir ( ) ;
119+ const predictionDbPath = join ( dir , "prediction-ledger.sqlite3" ) ;
120+ const eventDbPath = join ( dir , "event-ledger.sqlite3" ) ;
121+ const seed = initPredictionLedger ( predictionDbPath ) ;
71122 appendPrediction ( seed , 1 , "hold" ) ;
72123 seed . close ( ) ;
73124
74- const prev = process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB ;
75- process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB = dbPath ;
125+ const prevPrediction = process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB ;
126+ const prevEvent = process . env . LOOPOVER_MINER_EVENT_LEDGER_DB ;
127+ process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB = predictionDbPath ;
128+ process . env . LOOPOVER_MINER_EVENT_LEDGER_DB = eventDbPath ;
76129 const log = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => undefined ) ;
77130 try {
78131 expect ( runMetrics ( [ ] ) ) . toBe ( 0 ) ;
79132 } finally {
80- if ( prev === undefined ) delete process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB ;
81- else process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB = prev ;
133+ if ( prevPrediction === undefined ) delete process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB ;
134+ else process . env . LOOPOVER_MINER_PREDICTION_LEDGER_DB = prevPrediction ;
135+ if ( prevEvent === undefined ) delete process . env . LOOPOVER_MINER_EVENT_LEDGER_DB ;
136+ else process . env . LOOPOVER_MINER_EVENT_LEDGER_DB = prevEvent ;
82137 }
83138 expect ( String ( log . mock . calls [ 0 ] ?. [ 0 ] ) ) . toContain ( 'loopover_miner_predictions_total{conclusion="hold"} 1' ) ;
84139 } ) ;
@@ -122,4 +177,53 @@ describe("loopover-miner metrics CLI (#4838)", () => {
122177 ) . toBe ( 2 ) ;
123178 expect ( error ) . toHaveBeenCalledWith ( "prediction-ledger-unavailable" ) ;
124179 } ) ;
180+
181+ describe ( "outcome-join (#8315)" , ( ) => {
182+ it ( "resolves each prediction's `correct` against the realized pr_outcome via the calibration join" , ( ) => {
183+ const ledger = tempLedger ( ) ;
184+ appendPrediction ( ledger , 1 , "merge" ) ; // realized merged -> correct
185+ appendPrediction ( ledger , 2 , "merge" ) ; // realized closed -> incorrect
186+ appendPrediction ( ledger , 3 , "close" ) ; // realized closed -> correct
187+ appendPrediction ( ledger , 4 , "close" ) ; // realized merged -> incorrect
188+ appendPrediction ( ledger , 5 , "hold" ) ; // hold has no realized counterpart -> unresolved
189+ appendPrediction ( ledger , 6 , "merge" ) ; // no outcome yet (pending) -> unresolved
190+ appendPrediction ( ledger , 7 , "merge" ) ; // outcome present but unclassifiable -> unresolved
191+
192+ const events : LedgerEntry [ ] = [
193+ outcomeEvent ( 1 , "merged" ) ,
194+ outcomeEvent ( 2 , "closed" ) ,
195+ outcomeEvent ( 3 , "closed" ) ,
196+ outcomeEvent ( 4 , "merged" ) ,
197+ outcomeEvent ( 5 , "merged" ) ,
198+ // no event for prediction 6 (still pending)
199+ outcomeEvent ( 7 , "reopened" ) , // a well-formed pr_outcome whose decision is neither merged nor closed
200+ outcomeEvent ( "not-a-number" , "merged" ) , // malformed: non-integer prNumber -> skipped by toOutcomeRecords
201+ ] ;
202+
203+ expect ( collectPredictionMetricRows ( ledger , events ) ) . toEqual ( [
204+ { conclusion : "merge" , correct : true } ,
205+ { conclusion : "merge" , correct : false } ,
206+ { conclusion : "close" , correct : true } ,
207+ { conclusion : "close" , correct : false } ,
208+ { conclusion : "hold" } ,
209+ { conclusion : "merge" } ,
210+ { conclusion : "merge" } ,
211+ ] ) ;
212+ } ) ;
213+
214+ it ( "runMetrics opens the event ledger by env path and moves the correct/incorrect counters" , ( ) => {
215+ const env = tempConfigEnv ( ) ;
216+ seedPredictionEnv ( env , 1 , "merge" ) ;
217+ seedPredictionEnv ( env , 2 , "close" ) ;
218+ seedOutcomeEnv ( env , 1 , "merged" ) ; // merge predicted, merged realized -> correct
219+ seedOutcomeEnv ( env , 2 , "merged" ) ; // close predicted, merged realized -> incorrect
220+
221+ const log = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => undefined ) ;
222+ expect ( runMetrics ( [ ] , { env } ) ) . toBe ( 0 ) ;
223+
224+ const text = String ( log . mock . calls [ 0 ] ?. [ 0 ] ) ;
225+ expect ( text ) . toContain ( "loopover_miner_prediction_correct_total 1" ) ;
226+ expect ( text ) . toContain ( "loopover_miner_prediction_incorrect_total 1" ) ;
227+ } ) ;
228+ } ) ;
125229} ) ;
0 commit comments