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 — because the BANK renders them", // 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", "the bank already renders every candidate this run found"}, mustNotSay: []string{partial, unmeasured, types, "every render batch was bought", "earlier purchase"}, }, { // ⚠ MAINTENANCE DECLARED, not a quiet edit: the settled basis added a SECOND reason for «the role // was asked nothing», and the single sentence this pin used to require asserted the FIRST one for // both. For a book answered from the memory of decisions it was false in both halves — the bank // holds no seed surface for those terms and the drafts were never asked to agree with anything. name: "the paid role was asked for nothing — because the BOOK had already decided them", c: &pipeline.BankConsolidation{Complete: true, SettledEarlier: 4}, want: []string{whole, "asked for nothing", "already decided every candidate", "4 term(s)", "has not moved"}, mustNotSay: []string{partial, unmeasured, types, "every render batch was bought", "the bank already renders"}, }, { // And the state that makes the two reasons a CHOICE rather than an either/or: both savings fired, // and the line must name both with their own numbers. Three different counts, so a line printing // one number in the other's slot cannot pass. name: "asked for nothing, and BOTH savings explain it", c: &pipeline.BankConsolidation{Complete: true, NeverAsked: 3, SettledEarlier: 7}, want: []string{whole, "asked for nothing", "3 term(s)", "7 because", "this book decided them in an earlier purchase"}, 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()) } }