184 lines
9.6 KiB
Go
184 lines
9.6 KiB
Go
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())
|
||
}
|
||
}
|