textmachine/backend/cmd/tmctl/render_observability_test.go

95 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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