textmachine/backend/cmd/tmctl/bankcompleteness_render_test.go

184 lines
9.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 (
"bytes"
"fmt"
"strings"
"testing"
"textmachine/backend/internal/pipeline"
)
// bankcompleteness_render_test.go: the SCREEN half of backlog row 253(б) and row 357(а). The bank's
// completeness and the rows the paid role was never asked about reach the surface an owner reads FIRST —
// stdout — and not only the machine sidecar and the full text table beside the database.
// TestTheSigningScreenSaysHowCompleteTheBankIs pins all four states of one conditional line. Three of them
// are the states the engine can be in; the fourth is the false alarm the naive version raises.
//
// ⛔ EACH CASE ASSERTS A SENTENCE AND FORBIDS THE OTHER TWO. A pin that only checks the alarm appears is
// green on a screen that shows it always, and a pin that only checks silence is green on a screen that
// says nothing at all — this project has bought both mistakes.
func TestTheSigningScreenSaysHowCompleteTheBankIs(t *testing.T) {
const (
whole = "Bank completeness: WHOLE"
partial = "Bank completeness: PARTIAL"
unmeasured = "Bank completeness: NOT MEASURED"
types = "Term TYPES are unrefined"
)
for _, tc := range []struct {
name string
c *pipeline.BankConsolidation
want []string
mustNotSay []string
}{{
name: "the pass never ran",
c: nil,
want: []string{unmeasured, "did not run"},
mustNotSay: []string{whole, partial, types},
}, {
name: "every render batch was bought",
// Three DIFFERENT numbers, and all three asserted: equal ones would let the line print any of
// them in any slot and still pass, which is the same degeneracy the money fixtures guard against.
c: &pipeline.BankConsolidation{Complete: true, Consolidated: 41, Declined: 2, Unanswered: 3},
want: []string{whole, "41 consolidated", "2 declined", "3 unanswered"},
mustNotSay: []string{partial, unmeasured, types},
}, {
name: "the paid role was asked for nothing",
// Reachable state, not a hypothetical: the already-banked filter can empty the paid set, and then
// zero batches were planned. «Every render batch was bought» is true of zero batches and reads as a
// report on work that happened — the instrument answering its own question (D39.202).
c: &pipeline.BankConsolidation{Complete: true, NeverAsked: 3},
want: []string{whole, "asked for nothing", "No batch was planned"},
mustNotSay: []string{partial, unmeasured, types, "every render batch was bought"},
}, {
name: "a budget cut the render pass",
c: &pipeline.BankConsolidation{
Complete: false, RenderBatchesDropped: 5, Consolidated: 38, Unanswered: 47,
},
// The count of unbought batches AND the warning that `unanswered` is not the role's silence here:
// on the live run of 04.09 those 47 read as terms the model skipped.
// ⚠ THE BUDGET IS NAMED IN FULL, because `budget_usd` is a SUBSTRING of `classify_budget_usd`:
// asserted loosely, this case stays green while the screen sends an operator to raise the budget
// that did not cut his pass. The forbidden list says the other budget must not appear at all here.
want: []string{partial, "5 batch(es)", "47 unanswered", "38 consolidated", "gates.terminology.budget_usd"},
mustNotSay: []string{whole, unmeasured, types, "classify_budget_usd"},
}, {
name: "only the classifier was cut",
c: &pipeline.BankConsolidation{
Complete: true, ClassifyBatchesDropped: 1, Consolidated: 29,
},
// The run of 08.09: the render pass was intact and the engine's own log still said «PARTIALLY
// consolidated». The screen must not repeat that — the renderings are all there.
want: []string{whole, types, "gates.terminology.classify_budget_usd", "1 batch(es)"},
mustNotSay: []string{partial, unmeasured},
}} {
t.Run(tc.name, func(t *testing.T) {
var b bytes.Buffer
renderBankConsolidation(&b, tc.c)
out := b.String()
for _, want := range tc.want {
if !strings.Contains(out, want) {
t.Errorf("the screen must say %q:\n%s", want, out)
}
}
for _, never := range tc.mustNotSay {
if strings.Contains(out, never) {
t.Errorf("the screen must NOT say %q here:\n%s", never, out)
}
}
})
}
}
// TestTheNeverAskedCountSurvivesTheStdoutCap is the trap this surface was built around, and the fixture is
// deliberately WIDER than the cap because a narrower one is green on a blind screen.
//
// A row the role was never asked about carries no rendering, so reviewRank sends it to 1000 — dead LAST —
// and the stdout table shows twenty rows. On any book with twenty candidates every such row is below the
// fold, so a per-row mark alone would tell the owner nothing at the moment he decides. The count sits
// above the table, outside the cap.
//
// The test asserts BOTH halves, and the second is what makes the first mean something: the marked row
// really is cut from the table here.
//
// Mutation this catches: move the summary inside renderBankStopRows (or drop the NeverAsked clause) and
// the count disappears from a screen whose table cannot show the rows it counts → RED.
func TestTheNeverAskedCountSurvivesTheStdoutCap(t *testing.T) {
var rows []pipeline.BankStopRow
for i := 0; i < bankStopStdoutCap+5; i++ {
// Every one of these has a rendering and a confidence, so all of them outrank the settled row below.
rows = append(rows, pipeline.BankStopRow{
Src: fmt.Sprintf("术%d", i), Dst: fmt.Sprintf("приём%d", i), Origin: "mined", Freq: 5, Conf: 10,
})
}
rows = append(rows, pipeline.BankStopRow{Src: "方源", Origin: "banknote", Freq: 30, SettledByBank: true})
var b bytes.Buffer
renderSignatureStop(&b, &pipeline.WaveSignatureStop{
Terms: len(rows), SignaturePath: "p", Rows: rows,
Consolidation: &pipeline.BankConsolidation{Complete: true, Consolidated: 25, NeverAsked: 1},
})
out := b.String()
if !strings.Contains(out, "1 term(s) were NOT ASKED about") {
t.Errorf("the count of never-asked rows must reach the screen above the cap:\n%s", out)
}
// ABOVE the table, which the code claims in a comment and nothing else checked — an invariant living
// only in prose is held by nothing. The owner meets the state of the bank before the rows, not after
// scrolling past twenty of them.
// ⚠ BOTH INDICES ARE CHECKED FOR PRESENCE FIRST. `strings.Index` returns -1 for an absent needle, and
// -1 < N is true for every N — so the bare comparison passes loudest exactly when the line has vanished.
iSummary, iTable := strings.Index(out, "Bank completeness"), strings.Index(out, "least-confident first")
if iSummary < 0 || iTable < 0 {
t.Fatalf("both the completeness line and the table must be on the screen (summary=%d table=%d):\n%s", iSummary, iTable, out)
}
if iSummary > iTable {
t.Errorf("the completeness line must come BEFORE the table it qualifies:\n%s", out)
}
// F7: the SENTENCE, not only the number. A count with no explanation puts good news in the same shape
// as a gap, which is what the whole marker exists to prevent.
if !strings.Contains(out, "the bank already renders those surfaces and every draft agreed") {
t.Errorf("the count must carry WHY those rows were skipped, not just how many:\n%s", out)
}
// The half that proves the fixture: the row itself is NOT on the screen, so the count is the only thing
// carrying it. If this ever starts failing because the cap or the ranking moved, the assertion above
// stopped being a test of the cap and has to be rewritten, not relaxed.
if strings.Contains(out, "方源") {
t.Fatalf("premise broken: the settled row is supposed to fall under the stdout cap in this fixture, "+
"so a per-row mark alone would be invisible — rewrite the fixture, do not relax the assertion:\n%s", out)
}
// CONTROL: shown a settled row that FITS, the table does mark it — so the count above is a second
// carrier for the capped case and not a replacement that left the row unexplained.
var small bytes.Buffer
renderSignatureStop(&small, &pipeline.WaveSignatureStop{
Terms: 1, SignaturePath: "p",
Rows: []pipeline.BankStopRow{{Src: "方源", Origin: "banknote", Freq: 30, SettledByBank: true}},
Consolidation: &pipeline.BankConsolidation{Complete: true, Consolidated: 1, NeverAsked: 1},
})
if !strings.Contains(small.String(), "NOT ASKED: the bank already renders this surface") {
t.Errorf("a settled row that fits on the screen must say why it has no rendering:\n%s", small.String())
}
// ⛔ AND THE LINE DOES NOT LIVE AND DIE WITH THE TABLE. renderBankStopRows returns early on zero rows,
// so a summary printed from inside it — or guarded by the rows being non-empty — disappears exactly
// where the state still matters: a stop whose table this build could not fill still put a bank in front
// of the owner. Without this case, «outside the cap» is asserted only by ordering.
var noRows bytes.Buffer
renderSignatureStop(&noRows, &pipeline.WaveSignatureStop{
Terms: 0, SignaturePath: "p",
Consolidation: &pipeline.BankConsolidation{RenderBatchesDropped: 2, Consolidated: 7, Unanswered: 9},
})
if !strings.Contains(noRows.String(), "Bank completeness: PARTIAL") {
t.Errorf("the completeness line is not a part of the table and must survive an empty one:\n%s", noRows.String())
}
// And a row that was ASKED about prints no such line, or the marker means nothing.
var quiet bytes.Buffer
renderSignatureStop(&quiet, &pipeline.WaveSignatureStop{
Terms: 1, SignaturePath: "p",
Rows: []pipeline.BankStopRow{{Src: "花家", Dst: "семья Хуа", Origin: "mined", Freq: 9, Conf: 50}},
Consolidation: &pipeline.BankConsolidation{Complete: true, Consolidated: 1},
})
if strings.Contains(quiet.String(), "NOT ASKED") {
t.Errorf("control: a row the role WAS asked about must carry no such mark:\n%s", quiet.String())
}
}