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 }, 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 }, 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()) } }