package main import ( "strings" "testing" "textmachine/backend/internal/pipeline" "textmachine/backend/internal/store" ) // render_observability_test.go pins the two counters the engine had always computed but no surface // printed (found by the coldrun-b readiness pass): the pack-19 voice flagger and the pack-20 banknote // channel. Both are pure observability — no gate, no snapshot fold, no verdict — so each test also // pins the SILENCE case: a run without the channel keeps its byte-identical output. func TestRenderQualityPrintsVoiceAxesWithDenominators(t *testing.T) { var b strings.Builder q := &pipeline.QualityReport{ VoiceFlags: 2, VoiceReplies: 40, VoiceAttributed: 31, VoicePairRegister: 5, SpoilerLeaks: 1, VoiceCheckVersion: "voice-v1", } if err := renderQuality(&b, q); err != nil { t.Fatal(err) } out := b.String() // The count alone is unreadable — 2 of 31 attributed replies is a different fact from 2 of 3. for _, want := range []string{"VOICE (axes A–C", "flags=2", "attributed=31", "replies=40", "axis-D pair-register=5", "spoiler-leaks=1", "rules=voice-v1"} { if !strings.Contains(out, want) { t.Fatalf("the voice line must carry %q, got:\n%s", want, out) } } } func TestRenderQualityVoiceLinePrintsZeroWhenTheGateRan(t *testing.T) { // «Not measured» and «measured, clean» are different answers (coldrun-a §7 reported the first as // the second). The version string is the discriminator: it is set iff gates.voice was on. var b strings.Builder if err := renderQuality(&b, &pipeline.QualityReport{VoiceCheckVersion: "voice-v1"}); err != nil { t.Fatal(err) } if !strings.Contains(b.String(), "flags=0 over attributed=0 of replies=0") { t.Fatalf("a gate that ran and found nothing must say so with its denominators:\n%s", b.String()) } } func TestRenderQualityStaysSilentWithoutTheVoiceGate(t *testing.T) { var b strings.Builder if err := renderQuality(&b, &pipeline.QualityReport{TotalUnits: 3}); err != nil { t.Fatal(err) } if strings.Contains(b.String(), "VOICE") { t.Fatalf("a book whose voice gate never ran must not grow a voice line:\n%s", b.String()) } } func TestRenderReportPrintsBanknoteCoverageNotJustTheTotal(t *testing.T) { // Chunk coverage is the signal a grand total hides: 12 lines can be 12 chunks × 1 or 1 chunk × 12, // and only the second is a broken channel. var b strings.Builder states := []store.RetrievalState{ {Chapter: 1, ChunkIdx: 0, NBanknoteLines: 7}, {Chapter: 1, ChunkIdx: 1, NBanknoteLines: 0, BanknoteParseFail: 1}, {Chapter: 2, ChunkIdx: 0, NBanknoteLines: 5, BanknoteTruncated: 1}, } if err := renderReport(&b, func() ([]store.RequestLogView, error) { return nil, nil }, func() ([]store.ChunkStatus, error) { return nil, nil }, func() ([]store.RetrievalState, error) { return states, nil }, pricedByAnswerer, okLedger); err != nil { t.Fatal(err) } out := b.String() for _, want := range []string{"=== BANKNOTE", "lines=12", "over 2/3 chunk(s)", "parse-fail chunks=1", "truncated chunks=1"} { if !strings.Contains(out, want) { t.Fatalf("the banknote line must carry %q, got:\n%s", want, out) } } } func TestRenderReportStaysSilentWithoutTheBanknoteChannel(t *testing.T) { var b strings.Builder states := []store.RetrievalState{{Chapter: 1, ChunkIdx: 0, NExactHits: 2}} if err := renderReport(&b, func() ([]store.RequestLogView, error) { return nil, nil }, func() ([]store.ChunkStatus, error) { return nil, nil }, func() ([]store.RetrievalState, error) { return states, nil }, pricedByAnswerer, okLedger); err != nil { t.Fatal(err) } if strings.Contains(b.String(), "BANKNOTE") { t.Fatalf("a banknote-off book must not grow a banknote section:\n%s", b.String()) } } // TestRenderQualityPrintsTheConsistencySectionWithItsDenominators pins the shape of the book-consistency // section, and it pins the DENOMINATORS rather than the findings. The failure this guards against is the one // the polygon's own instrument committed three times on this book: printing a zero that meant "I did not // look" in the same words as a zero that meant "I looked and it is clean". Every count below therefore has to // appear beside the population it came out of. func TestRenderQualityPrintsTheConsistencySectionWithItsDenominators(t *testing.T) { var b strings.Builder q := &pipeline.QualityReport{Consistency: &pipeline.BookConsistency{ UnitsJudged: 4, BankRows: 69, Judged: 68, NoDst: 1, NeverFired: 2, BlockedOnly: 1, FiredTotal: 231, ShippedTotal: 188, Covered: 57, Absent: 5, Split: 3, UnsignedRows: 66, SuppressedSilent: 0, CoveredStrict: 47, AbsentStrict: 14, SplitStrict: 7, Terms: []pipeline.TermConsistency{{ Src: "家老", Dst: "старейшина рода", Status: "draft", Verdict: "absent", Fired: 11, Shipped: 0, ShippedStrict: 0, WithoutForm: []int{1, 3}, }}, }} if err := renderQuality(&b, q); err != nil { t.Fatalf("renderQuality: %v", err) } out := b.String() for _, want := range []string{ "=== BOOK CONSISTENCY", "bank rows=69", "judgeable (a dst to look for)=68", "no dst=1", "shipped units read=4", "key never fired in the source=2", "spoiler-window blocked=1", "bank keys fired and judged=231", "accepted renderings found in the text=188", "I1 ONE FORM PER TERM", "more than one rendering=3", "a single rendering=57", "I2 THE BANK'S FORM IS THE ONE SHIPPED", "never reached the reader=5", // The signing state is a fact about the BOOK, said once — there is no signed TERM to count. "THIS BANK IS NOT SIGNED", "66 of 68 judgeable rows", "NESTED ROWS EATEN BY A LONGER KEY", // printed at zero too: its silence is the finding it reports "家老", "старейшина рода", "without it: 1,3", } { if !strings.Contains(out, want) { t.Errorf("the consistency section is missing %q:\n%s", want, out) } } // The second column has to be visible whenever it disagrees, or the report states one matcher's answer // as the book's condition. if !strings.Contains(out, "absent=14") { t.Errorf("the strict-matcher column must be printed where it disagrees (absent 5 vs 14):\n%s", out) } } // TestRenderQualityStaysSilentWithoutABank is the negative twin: a book with no glossary must produce no // consistency section at all. A section reading "0 problems over 0 rows" is worse than none — it is the // reassuring answer to a question nobody asked. func TestRenderQualityStaysSilentWithoutABank(t *testing.T) { var b strings.Builder if err := renderQuality(&b, &pipeline.QualityReport{}); err != nil { t.Fatalf("renderQuality: %v", err) } if strings.Contains(b.String(), "BOOK CONSISTENCY") { t.Errorf("a bankless book must print no consistency section:\n%s", b.String()) } } // TestTheReportNamesADeviationOnAChunkWhoseConfirmedCountIsZero pins the condition that decides whether the // memory section prints a chunk at all, and the fixture is the shape that made it matter: a chunk whose // CONFIRMED miss count is zero and whose detail names four deviations anyway. // // That is not a corner case — it is what an unsigned bank looks like. A miss on an unsigned row is // deliberately not counted (counting it would invert the contract), so on the paid run of 11.09 every one of // the six chunks had `n_postcheck_miss = 0` while the detail beside them named eighteen terms. Listing only // chunks with a non-zero count therefore printed a zero and nothing else, and the names of the eighteen — // already written, already in the column — reached no reader. func TestTheReportNamesADeviationOnAChunkWhoseConfirmedCountIsZero(t *testing.T) { detail := `[{"src":"家老","dst":"старейшина рода","disp":"ambiguous"},{"src":"正道","dst":"праведный путь","disp":"ambiguous"}]` states := []store.RetrievalState{ {Chapter: 1, ChunkIdx: 0, NPostcheckMiss: 0, PostcheckDetail: detail}, {Chapter: 2, ChunkIdx: 1, NPostcheckMiss: 0, PostcheckDetail: ""}, // nothing recorded: must stay unlisted } var b strings.Builder err := renderReport(&b, func() ([]store.RequestLogView, error) { return nil, nil }, func() ([]store.ChunkStatus, error) { return nil, nil }, func() ([]store.RetrievalState, error) { return states, nil }, pricedByAnswerer, func() (float64, float64, error) { return 0, 0, nil }) if err != nil { t.Fatalf("renderReport: %v", err) } out := b.String() // PREMISE: the aggregate above still reports zero CONFIRMED misses. If it did not, the section would be // printing for the ordinary reason and this fixture would pin nothing. if !strings.Contains(out, "post-check-misses=0") { t.Fatalf("premise broken: the confirmed-miss total must be 0 here, so that the listing below cannot be explained by it:\n%s", out) } for _, want := range []string{"家老", "старейшина рода", "正道", "праведный путь"} { if !strings.Contains(out, want) { t.Errorf("a deviation recorded on a chunk with zero CONFIRMED misses was not named: %q missing from\n%s", want, out) } } // And a chunk with nothing recorded must still not be listed — otherwise the section grows a line per // chunk and the names are buried again, this time in noise rather than in a zero. if strings.Contains(out, "2 1") { t.Errorf("a chunk with no recorded deviation was listed anyway:\n%s", out) } } // TestRenderQualityPrintsOneWaveInjectionLinePerWave pins the surface that answers "did the bank reach this // wave's prompt". The two waves select over different banks — the draft over the BASE bank with mined rows // excluded, the editor over the enriched one — so the lines are not expected to agree, and a report that // printed one line for both would state the draft's numbers as the book's. // // A run made before the v17 table has no rows and must print NOTHING here: "this wave injected nothing" and // "this run predates the table" are different facts, and a row of zeroes would assert the first. func TestRenderQualityPrintsOneWaveInjectionLinePerWave(t *testing.T) { var b strings.Builder q := &pipeline.QualityReport{Waves: []pipeline.WaveInjection{ {Wave: "draft", Positions: 6, ExactHits: 11, Sticky: 2, Ambiguous: 9, Blocked: 1, Evicted: 0}, {Wave: "edit", Positions: 4, ExactHits: 25, Sticky: 0, Ambiguous: 24, Blocked: 0, Evicted: 3}, }} if err := renderQuality(&b, q); err != nil { t.Fatalf("renderQuality: %v", err) } out := b.String() for _, want := range []string{ "WAVE INJECTION [draft", "positions=6", "exact=11", "sticky=2", "WAVE INJECTION [edit", "positions=4", "exact=25", "evicted-by-budget=3", } { if !strings.Contains(out, want) { t.Errorf("the per-wave line is missing %q:\n%s", want, out) } } // The editor's evictions are the number that lived nowhere before v17 — chunkrun.go recorded the // draft's and said in as many words that the editor's went unrecorded. If it ever stops printing, the // table is being written and not read. if strings.Count(out, "WAVE INJECTION") != 2 { t.Errorf("want exactly one line per wave, got %d:\n%s", strings.Count(out, "WAVE INJECTION"), out) } var empty strings.Builder if err := renderQuality(&empty, &pipeline.QualityReport{}); err != nil { t.Fatalf("renderQuality: %v", err) } if strings.Contains(empty.String(), "WAVE INJECTION") { t.Errorf("a run with no wave rows must print no wave line at all:\n%s", empty.String()) } }