textmachine/backend/cmd/tmctl/render_test.go

484 lines
22 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"unicode/utf8"
"textmachine/backend/internal/llm"
"textmachine/backend/internal/pipeline"
"textmachine/backend/internal/store"
)
// render_test.go pins the human/JSON output contracts of the extracted renders
// (package №4): sentinel returns (exit 2), «what to do» advice, sections only when
// data is non-empty, report err-tail, dry-run redrive.
func okLedger() (float64, float64, error) { return 0.123456, 0, nil }
func TestRenderTranslateOKAndLedger(t *testing.T) {
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1", TotalUSD: 0.01, Chunks: []pipeline.ChunkOutcome{{
Chapter: 1, ChunkIdx: 0, Disposition: pipeline.DispOK, FinalText: "ТЕКСТ",
Stages: []pipeline.StageResult{{Stage: "draft", Model: "m", Disposition: pipeline.DispOK,
Usage: llm.Usage{PromptTokens: 10, CompletionTokens: 5}, Attempts: 1, FinishReason: "stop"}},
}}}
if err := renderTranslate(&b, res, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
for _, want := range []string{
"=== CHAPTER 1 CHUNK 0 — ok ===", "ТЕКСТ",
"TOTAL (this run): $0.010000 — chunks 1, flags 0",
"Book ledger: committed=$0.123456 reserved=$0.000000",
} {
if !strings.Contains(out, want) {
t.Fatalf("translate output must contain %q, got:\n%s", want, out)
}
}
}
func TestRenderTranslateFlaggedSentinel(t *testing.T) {
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1", Flagged: 1, Chunks: []pipeline.ChunkOutcome{{
Chapter: 2, ChunkIdx: 1, Disposition: pipeline.DispFlagged, FlagReason: pipeline.FlagSoftRefusal,
}}}
err := renderTranslate(&b, res, okLedger)
var flagged *pipeline.CompletedWithFlags
if !errors.As(err, &flagged) || flagged.Flagged != 1 {
t.Fatalf("flagged run must return the exit-2 sentinel, got: %v", err)
}
if !strings.Contains(b.String(), "[FLAG soft_refusal]") {
t.Fatalf("flagged chunk banner missing:\n%s", b.String())
}
}
func TestRenderTranslateLedgerErrorAfterTotals(t *testing.T) {
// Frozen ordering of the partial output: the totals line is already printed,
// a ledger read error aborts AFTER it (hoisting the read ahead of the render would
// change the bytes of the partial output — extraction risk-map, item 4).
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1"}
err := renderTranslate(&b, res, func() (float64, float64, error) { return 0, 0, errors.New("boom") })
if err == nil || err.Error() != "boom" {
t.Fatalf("ledger error must propagate, got %v", err)
}
if !strings.Contains(b.String(), "TOTAL") || strings.Contains(b.String(), "Book ledger") {
t.Fatalf("totals must be printed, ledger line must not:\n%s", b.String())
}
}
func TestRenderReportColumnsAndErrTail(t *testing.T) {
var b strings.Builder
rows := []store.RequestLogView{
{TS: "2026-07-10 17:00:00", Chapter: 3, ChunkIdx: 2, Stage: "draft", Role: "translator",
ModelRequested: "fake-model", ModelActual: "", Err: "connect: refused", OK: 0},
{TS: "2026-07-10 17:00:05", Chapter: 3, ChunkIdx: 2, Stage: "draft", Role: "translator",
ModelRequested: "fake-model", ModelActual: "fake-model", Degraded: "cjk_artifact", OK: 0},
}
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
// Package №4: for a failed call model_actual is empty — the row must carry
// the requested model, its position, and the error text (post-mortem without sqlite3).
if !strings.Contains(out, "fake-model(req)") {
t.Fatalf("failed call must show the requested model, got:\n%s", out)
}
if !strings.Contains(out, "connect: refused") || !strings.Contains(out, "cjk_artifact") {
t.Fatalf("err/degraded tail missing:\n%s", out)
}
// Flag/memory sections are not printed without data.
if strings.Contains(out, "FLAGS") || strings.Contains(out, "MEMORY") {
t.Fatalf("empty sections must not print headers:\n%s", out)
}
}
// TestErrTailTruncatesOnRuneBoundary pins the D23.4 fix: errTail carries raw provider
// body snippets (CJK/Cyrillic), so a >120-byte tail must be cut on a RUNE boundary — a
// bare byte slice s[:120] would split a multibyte rune into invalid UTF-8. The existing
// column test does not exercise the boundary (its strings are short/ASCII), so a revert of
// the fix survived it — this closes that gap. Mutation: replace the RuneStart walk-back in
// errTail with `s = s[:120] + "…"` and this goes red.
func TestErrTailTruncatesOnRuneBoundary(t *testing.T) {
// One ASCII byte then 100 Cyrillic runes (2 bytes each): rune starts fall at offset 0
// then the ODD offsets, so byte 120 (even) lands on a continuation byte — a mid-rune cut.
long := "x" + strings.Repeat("я", 100) // 201 bytes, well over the 120 cap
got := errTail("", long)
if !utf8.ValidString(got) {
t.Fatalf("errTail produced invalid UTF-8 — a byte-boundary cut split a rune: %q", got)
}
if strings.ContainsRune(got, '<27>') {
t.Fatalf("errTail leaked a replacement char (broken rune): %q", got)
}
if !strings.HasSuffix(got, "…") {
t.Fatalf("a truncated tail must end with the ellipsis, got %q", got)
}
// A short tail is returned verbatim (degraded + err joined, no truncation, no ellipsis).
if s := errTail("cjk_artifact", "boom"); s != "cjk_artifact boom" {
t.Fatalf("short tail must be returned verbatim, got %q", s)
}
}
func TestRenderReportSections(t *testing.T) {
var b strings.Builder
flags := []store.ChunkStatus{
{Chapter: 1, ChunkIdx: 0, Stage: "draft", Disposition: "ok"},
{Chapter: 2, ChunkIdx: 1, Stage: "draft", Disposition: "flagged", FlagReason: "hard_refusal", Attempts: 1, Detail: "provider refused"},
}
states := []store.RetrievalState{{Chapter: 2, ChunkIdx: 1, NExactHits: 3, NPostcheckMiss: 1,
PostcheckDetail: `[{"src":"鈴木"}]`, NStyleFlags: 2, StyleDetail: `{"yo":2}`}}
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return nil, nil },
func() ([]store.ChunkStatus, error) { return flags, nil },
func() ([]store.RetrievalState, error) { return states, nil },
okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
for _, want := range []string{"=== FLAGS (disposition ≠ ok) ===", "hard_refusal",
"=== MEMORY (retrieval-state) ===", "post-check-misses=1", `[{"src":"鈴木"}]`,
"— total 2 ===", "Book ledger"} {
if !strings.Contains(out, want) {
t.Fatalf("report must contain %q, got:\n%s", want, out)
}
}
if strings.Contains(out, "1 0") && strings.Contains(out, "ok") && strings.Count(out, "draft") > 1 {
t.Fatalf("ok rows must not enter the flag section:\n%s", out)
}
}
func TestRenderReportPartialOutputOnMidAuditError(t *testing.T) {
// Frozen behavior (self-review №4): store reads are interleaved with printing,
// a failure AFTER the first section leaves it on stdout (the audit does not go silently empty).
var b strings.Builder
rows := []store.RequestLogView{{TS: "t", Chapter: 1, Stage: "draft", Role: "translator", ModelActual: "m", OK: 1}}
err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, errors.New("chunk_status corrupted") },
func() ([]store.RetrievalState, error) { t.Fatal("must not be reached"); return nil, nil },
okLedger)
if err == nil || err.Error() != "chunk_status corrupted" {
t.Fatalf("mid-audit error must propagate, got %v", err)
}
if !strings.Contains(b.String(), "draft") {
t.Fatalf("the request_log table printed before the failure must remain on the writer:\n%s", b.String())
}
}
func TestRenderStatusJSONSchemaAndSentinel(t *testing.T) {
var b strings.Builder
rep := &pipeline.StatusReport{BookID: "b1", TotalUnits: 4, Done: 3, Flagged: 1}
err := renderStatusJSON(&b, rep)
var flagged *pipeline.CompletedWithFlags
if !errors.As(err, &flagged) {
t.Fatalf("--json with flags must return the exit-2 sentinel (minor 1d), got %v", err)
}
var decoded map[string]any
if jerr := json.Unmarshal([]byte(b.String()), &decoded); jerr != nil {
t.Fatalf("output must be valid JSON: %v", jerr)
}
if decoded["book_id"] != "b1" {
t.Fatalf("stable schema field book_id missing: %v", decoded)
}
// Clean book → nil error → exit 0.
if err := renderStatusJSON(&strings.Builder{}, &pipeline.StatusReport{TotalUnits: 4, Done: 4}); err != nil {
t.Fatalf("clean book must exit 0, got %v", err)
}
}
func TestRenderStatusHumanDriftAndAdvice(t *testing.T) {
var b strings.Builder
rep := &pipeline.StatusReport{
BookID: "b1", Snapshot: "abcdef0123456789", TotalUnits: 10, Done: 7,
Flagged: 3, GlossaryMissFlagged: 1, SnapshotDrift: true, ConfigDrift: true,
}
err := renderStatusHuman(&b, rep, "book.yaml")
var flagged *pipeline.CompletedWithFlags
if !errors.As(err, &flagged) || flagged.Flagged != 3 {
t.Fatalf("flagged status must return the exit-2 sentinel, got %v", err)
}
out := b.String()
for _, want := range []string{
"snapshot abcdef012345", "⚠ SNAPSHOT-DRIFT", "⚠ CONFIG-DRIFT",
// The advice is split (minor 1d): re-drivable separately from glossary_miss (redrive is a no-op there).
"2 flagged chunk(s) need attention — re-attack: tmctl redrive --config book.yaml",
"1 chunk(s) flagged by the post-check gate (glossary_miss)",
} {
if !strings.Contains(out, want) {
t.Fatalf("status must contain %q, got:\n%s", want, out)
}
}
}
func TestRenderRedriveDryRunAndSentinel(t *testing.T) {
var b strings.Builder
sum := &pipeline.RedriveSummary{DryRun: true, Targets: []pipeline.RedriveTarget{
{Chapter: 1, ChunkIdx: 2, FlagReason: "cjk_artifact", Stages: []string{"draft", "edit"}}}}
if err := renderRedrive(&b, "b1", sum, nil, okLedger); err != nil {
t.Fatalf("dry-run must exit 0, got %v", err)
}
if !strings.Contains(b.String(), "[dry-run: nothing reset, no calls made]") {
t.Fatalf("dry-run banner missing:\n%s", b.String())
}
// No targets → informational exit 0.
b.Reset()
if err := renderRedrive(&b, "b1", &pipeline.RedriveSummary{}, nil, okLedger); err != nil {
t.Fatal(err)
}
if !strings.Contains(b.String(), "nothing to re-attack") {
t.Fatalf("no-targets banner missing:\n%s", b.String())
}
// Live re-run that still has flags → sentinel; a flaky ledger read is
// swallowed (no ledger line, exit code unchanged) — frozen behavior.
b.Reset()
sum = &pipeline.RedriveSummary{Targets: []pipeline.RedriveTarget{{Chapter: 1, ChunkIdx: 2, FlagReason: "x", Stages: []string{"draft"}}}}
res := &pipeline.BookResult{BookID: "b1", Flagged: 1, Chunks: make([]pipeline.ChunkOutcome, 3)}
err := renderRedrive(&b, "b1", sum, res, func() (float64, float64, error) { return 0, 0, errors.New("flaky") })
var flagged *pipeline.CompletedWithFlags
if !errors.As(err, &flagged) {
t.Fatalf("re-run with flags must return the exit-2 sentinel, got %v", err)
}
if strings.Contains(b.String(), "Book ledger") {
t.Fatalf("flaky ledger read must swallow the ledger line, not fail:\n%s", b.String())
}
}
// TestRenderSignatureStopShowsTheTable pins the operator surface D39.36 specified and the CLI did not
// have: the stop prints the term rows (src · proposed dst · origin · freq · spread), the draft variants
// that disagree, a source context, both artifact paths, and — when the list is long — how many rows it
// did NOT show. A silent truncation would read as "that was the whole bank".
func TestRenderSignatureStopShowsTheTable(t *testing.T) {
var rows []pipeline.BankStopRow
rows = append(rows, pipeline.BankStopRow{
Src: "方源", Dst: "Фан Юань", Origin: "both", Type: "name", Freq: 12, Spread: 2,
Variants: []pipeline.BankStopVariant{{Dst: "Фан Юань", Chunks: 3}, {Dst: "Фань Юань", Chunks: 1}}, Contexts: []string{"方源来到青茅山"},
})
for i := 0; i < 25; i++ {
rows = append(rows, pipeline.BankStopRow{Src: fmt.Sprintf("术%d", i), Origin: "mined", Freq: 5})
}
var b bytes.Buffer
renderSignatureStop(&b, &pipeline.WaveSignatureStop{
Terms: len(rows), SignaturePath: "/tmp/b.db.mined-signature.yaml", TablePath: "/tmp/b.db.bank-stop.txt", Rows: rows,
})
out := b.String()
for _, want := range []string{
"方源", "Фан Юань", "both", "Фань Юань ×1", "方源来到青茅山",
"/tmp/b.db.mined-signature.yaml", "/tmp/b.db.bank-stop.txt",
"6 more term(s)", "--verify-bank",
} {
if !strings.Contains(out, want) {
t.Fatalf("the stop banner must carry %q:\n%s", want, out)
}
}
// An unconsolidated term prints an explicit dash, never an empty column that reads as a rendering.
var b2 bytes.Buffer
renderSignatureStop(&b2, &pipeline.WaveSignatureStop{
Terms: 1, SignaturePath: "p", Rows: []pipeline.BankStopRow{{Src: "花海", Origin: "mined", Freq: 5}},
})
if !strings.Contains(b2.String(), "—") {
t.Fatalf("a term with no proposed dst must show a dash:\n%s", b2.String())
}
}
// TestRenderSignatureStopOrdersTheReviewLeastSureFirst pins the ONE use the role's stated confidence is
// allowed to have (D39.102): it orders the review list, an ordinal inside one model's reply, and nothing
// else. Without it the number is printed and never used — a column that claims to answer «what do I read
// first» and does not. The full sidecar keeps source-key order; only this capped view is the review surface.
func TestRenderSignatureStopOrdersTheReviewLeastSureFirst(t *testing.T) {
rows := []pipeline.BankStopRow{
{Src: "уверенный", Dst: "д", Origin: "mined", Freq: 9, Conf: 95},
{Src: "неконсолидированный", Origin: "mined", Freq: 9, Conf: -1},
{Src: "неуверенный", Dst: "д", Origin: "mined", Freq: 9, Conf: 20},
{Src: "безмолвный", Dst: "д", Origin: "mined", Freq: 9, Conf: -1},
}
var b bytes.Buffer
renderSignatureStop(&b, &pipeline.WaveSignatureStop{Terms: len(rows), SignaturePath: "p", Rows: rows})
out := b.String()
order := []string{"неуверенный", "уверенный", "безмолвный", "неконсолидированный"}
at := -1
for _, name := range order {
i := strings.Index(out, name)
if i < 0 {
t.Fatalf("row %q missing:\n%s", name, out)
}
if i < at {
t.Fatalf("review order broken at %q — want least-confident first, then silence, then unconsolidated:\n%s", name, out)
}
at = i
}
if !strings.Contains(out, "least-confident first") {
t.Fatalf("the banner must say what its order means, or a sorted table reads as arbitrary:\n%s", out)
}
}
// TestRenderTranslateNamesTheVolumeCeiling pins the operator-facing half of the "the stop must NAME its
// ceiling" rule. A volume-bounded run exits 0 like any completed run, so without this line an operator
// looking at "chunks 2" on a hundred-unit book cannot tell "the volume I bought is finished" from
// "something went quietly wrong" — the silent-limit complaint the design took from OpenHands. The line
// has to carry all three actionable numbers and to say it is a completion, not a pause.
func TestRenderTranslateNamesTheVolumeCeiling(t *testing.T) {
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1", TotalUSD: 0.01,
Volume: &pipeline.VolumeStop{MaxUnits: 2, Delivered: 1, Reworked: 1, Free: 3, LeftFresh: 5, LeftRework: 2}}
if err := renderTranslate(&b, res, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
for _, want := range []string{"VOLUME CEILING", "--max-units 2", "2 paying output unit(s)",
"1 NEW unit(s) delivered", "1 already-delivered unit(s) re-made",
"3 rode along at $0", "5 unit(s) NEVER delivered", "2 already delivered but not yet re-made",
"COMPLETION, not a pause",
// The invitation must be about the units that were never delivered, and only those.
"5 output unit(s) of this book have never been delivered"} {
if !strings.Contains(out, want) {
t.Errorf("the volume stop must say %q; got:\n%s", want, out)
}
}
// A run that was NOT bounded must say nothing at all — otherwise every ordinary run reads as cut short.
var b2 strings.Builder
if err := renderTranslate(&b2, &pipeline.BookResult{BookID: "b1"}, okLedger); err != nil {
t.Fatal(err)
}
if strings.Contains(b2.String(), "VOLUME CEILING") {
t.Errorf("an unbounded run must not mention a ceiling:\n%s", b2.String())
}
}
// TestRenderTranslateNeverSellsWhatIsAlreadyOwned pins the operator-facing half of the same defect. On a
// book every unit of which has been delivered, the stop must not invite a purchase: the units it held
// back are unrefreshed, not unbought, and the first version of this line offered them as "run again with
// --max-units to buy more".
func TestRenderTranslateNeverSellsWhatIsAlreadyOwned(t *testing.T) {
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1",
Volume: &pipeline.VolumeStop{MaxUnits: 1, Delivered: 0, Reworked: 1, LeftFresh: 0, LeftRework: 3}}
if err := renderTranslate(&b, res, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
if strings.Contains(out, "have never been delivered") {
t.Fatalf("the stop offered chapters the reader already owns:\n%s", out)
}
for _, want := range []string{
"every unit of this book HAS been delivered",
"That is a re-pass, not a purchase of new book",
"delivered NO new unit",
} {
if !strings.Contains(out, want) {
t.Errorf("want %q in:\n%s", want, out)
}
}
}
// TestRenderTranslateReportsBothRemaindersWhenBothExist pins the case an if/else would have swallowed:
// a book with BOTH never-delivered units and delivered-but-superseded ones. Printing only the first
// leaves an operator who has both believing the one number was the whole picture.
func TestRenderTranslateReportsBothRemaindersWhenBothExist(t *testing.T) {
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1",
Volume: &pipeline.VolumeStop{MaxUnits: 2, Delivered: 2, LeftFresh: 4, LeftRework: 6}}
if err := renderTranslate(&b, res, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
if !strings.Contains(out, "4 output unit(s) of this book have never been delivered") {
t.Errorf("the buyable remainder is missing:\n%s", out)
}
if !strings.Contains(out, "6 already-delivered unit(s) carry a superseded snapshot") {
t.Errorf("the re-make remainder was dropped — an if/else over the two would do exactly this:\n%s", out)
}
// A run that DID deliver must not carry the "delivered NO new unit" warning.
if strings.Contains(out, "delivered NO new unit") {
t.Errorf("this run delivered 2 units; it must not claim otherwise:\n%s", out)
}
}
// TestTheRePaymentHintDoesNotPromiseWhatTheGrantWillNotDo pins the acceptance hunter's finding 6. The
// hint offers rebill_output_units as "the granularity --max-units counts in", which is true and, on its
// own, misleading: the grant goes to NEVER-DELIVERED units first, so on a book that still has any, sizing
// --max-units from the re-payment figure delivers new units and re-makes nothing. The caveat lives in the
// code; it has to live where the operator reads.
func TestTheRePaymentHintDoesNotPromiseWhatTheGrantWillNotDo(t *testing.T) {
var b strings.Builder
rep := &pipeline.StatusReport{BookID: "b1", ConfigDrift: true,
RebillUnits: 9, RebillOutputUnits: 3, RebillUSD: 0.02, RebillBasis: pipeline.RebillBasisPending}
if err := renderStatusHuman(&b, rep, ""); err != nil {
t.Fatal(err)
}
out := b.String()
if !strings.Contains(out, "3 OUTPUT unit(s)") {
t.Fatalf("the sale-sized figure must be shown:\n%s", out)
}
if !strings.Contains(out, "does NOT buy those units back") {
t.Fatalf("the hint must say the grant goes to never-delivered units first, or it promises a re-pass it will not perform:\n%s", out)
}
}
// TestDriftDoesNotClaimARePaymentItNeverComputed is the trap the orchestrator named when he ratified A7:
// the config-drift flag used to end its own sentence with «= re-paying for the book», which is a claim
// about MONEY made by a boolean that computes none.
//
// The two come apart exactly when status learned to see a DROPPED stage (backlog row 239): projectRebill
// skips rows whose stage the current pipeline no longer runs, so that drift is real and its re-payment is
// genuinely zero. Under the old sentence the operator was told he would re-pay for a book that will not be
// re-paid for — one lie replaced by another, which is what «правка A7 идёт ВМЕСТЕ с базисом» was about.
//
// Mutation this catches: put the money clause back on the drift line and the first assertion fires.
func TestDriftDoesNotClaimARePaymentItNeverComputed(t *testing.T) {
rep := &pipeline.StatusReport{
BookID: "b", TotalUnits: 2, Done: 2,
ConfigDrift: true, ConfigDriftBasis: pipeline.DriftBasisDrift,
RebillUnits: 0, RebillUSD: 0, RebillBasis: pipeline.RebillBasisPending,
}
var b strings.Builder
_ = renderStatusHuman(&b, rep, "book.yaml")
out := b.String()
if !strings.Contains(out, "⚠ CONFIG-DRIFT") {
t.Fatalf("the drift itself must still be announced:\n%s", out)
}
if strings.Contains(out, "re-paying for the book") {
t.Fatalf("the drift flag must not assert a spend it never computed — the projection says zero "+
"here, and the money lines are what speak about money:\n%s", out)
}
if !strings.Contains(out, "NOTHING already billed is re-paid") {
t.Fatalf("drift with a zero re-payment is a real state and must be named, not left to be inferred "+
"from a missing line:\n%s", out)
}
}
// TestAnUnknownDriftBasisIsSaidOutLoud is the A12 half at the surface an operator actually reads: a
// `config_drift:false` that means «could not check» must not read as «checked, clean».
func TestAnUnknownDriftBasisIsSaidOutLoud(t *testing.T) {
rep := &pipeline.StatusReport{
BookID: "b", TotalUnits: 2, Done: 1,
ConfigDrift: false, ConfigDriftBasis: pipeline.DriftBasisUnknown,
RebillBasis: pipeline.RebillBasisPending,
}
var b strings.Builder
_ = renderStatusHuman(&b, rep, "book.yaml")
out := b.String()
if !strings.Contains(out, "CONFIG-DRIFT UNKNOWN") {
t.Fatalf("an unestablished drift verdict must say so; silence here reads as «no drift»:\n%s", out)
}
clean := &pipeline.StatusReport{
BookID: "b", TotalUnits: 2, Done: 1,
ConfigDrift: false, ConfigDriftBasis: pipeline.DriftBasisNone,
RebillBasis: pipeline.RebillBasisPending,
}
var cb strings.Builder
_ = renderStatusHuman(&cb, clean, "book.yaml")
if strings.Contains(cb.String(), "CONFIG-DRIFT UNKNOWN") {
t.Fatal("a book whose drift WAS checked and is clean must not carry the unknown caveat")
}
}