package pipeline import ( "bytes" "context" "errors" "log/slog" "os" "reflect" "strings" "testing" "textmachine/backend/internal/store" "textmachine/backend/internal/terminology" "textmachine/backend/internal/text" ) // askedAsCandidate reports whether a surface was sent to the paid role as a CANDIDATE of the block, which // is the only question about money. The candidate list is spelled as markdown headings, and every // candidate carries the source sentences it occurs in — so a surface can be all over a request without // anybody having been asked about it. func askedAsCandidate(rec *reqRec, src string) bool { for _, body := range rec.all() { if isTerminologyBody(body) && strings.Contains(body, "### "+src) { return true } } return false } // terminologyBodies is what the role was actually sent, for a failure that has to be readable. func terminologyBodies(rec *reqRec) string { var out []string for _, body := range rec.all() { if isTerminologyBody(body) { out = append(out, body) } } return strings.Join(out, "\n---\n") } // banksettled_test.go pins the filter that stops the paid terminology role being asked about surfaces the // bank has already settled. The two cases below are the same run with ONE byte of the draft's proposal // changed, so what they measure is the agreement and not two different fixtures. // TestABankSettledCandidateIsNotPaidFor is case (i): the seed holds the surface, the draft proposed the // rendering the seed holds, and the role is therefore never asked about it. func TestABankSettledCandidateIsNotPaidFor(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isTerminologyBody(body) { return "青茅山\tгора Цинмао", "stop" } // The draft proposes for 方源 exactly what the seed already holds. return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop" }) defer srv.Close() seed := "terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n" r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed})) defer r.Close() var runLog bytes.Buffer r.Log = slog.New(slog.NewTextHandler(&runLog, &slog.HandlerOptions{Level: slog.LevelInfo})) _ = runToSignatureStop(t, r) tres := r.lastTerminology // The number is stated WITH its denominator: "1 skipped" says nothing without "of how many". if tres.BankSettled != 1 { t.Fatalf("the settled candidate must be skipped before payment: skipped=%d of candidates=%d", tres.BankSettled, tres.Candidates) } if tres.Candidates < 2 { t.Fatalf("premise broken: with fewer than two candidates a skip is indistinguishable from an empty run: %d", tres.Candidates) } // What the money question actually asks: did 方源 reach a paid request as a CANDIDATE. Asked on the // block heading and not on the bare surface — every candidate's source contexts quote the sentences it // occurs in, so 方源 appears in the request whatever the filter does, and a substring assertion here // would have reported the filter broken while it was working. if askedAsCandidate(rec, "方源") { t.Errorf("the settled surface was sent to the paid role anyway:\n%s", terminologyBodies(rec)) } // CONTROL: the OTHER candidate did reach it, so the absence above is the filter and not a role that // was never called. if !askedAsCandidate(rec, "青茅山") { t.Fatalf("control: the unsettled candidate must still be paid for, or nothing was measured") } // ⚠ THE KEYS OF THE SAVING LINE ARE PINNED HERE and not by the operator-message catalogue, because that // catalogue guards message TEXTS and says so — the structured arguments are outside it by declaration. // This is the one line a later shift will grep to learn what the filter bought, and a number without // its denominator is unreadable: «12 skipped» says nothing without «of 300». // ⚠ Asserted on the ONE line that carries the saving, not on the whole buffer: three Contains over a // shared buffer are satisfiable by three DIFFERENT lines, so the denominator could go to zero and the // test stay green. Found by review after it was written that way. var saving string for _, line := range strings.Split(runLog.String(), "\n") { if strings.Contains(line, "already settled") { saving = line } } if saving == "" { t.Fatalf("the saving must be logged at all:\n%s", runLog.String()) } for _, want := range []string{"skipped=1", "of_candidates=3", "still_paid=2"} { if !strings.Contains(saving, want) { t.Errorf("the saving line must carry %q — a number without its denominator is unreadable:\n%s", want, saving) } } // The sheet still lists the settled surface — the skip is about what was BOUGHT, not about what the // owner is shown. Its dst column is empty, because no role answered for it and the sheet must not // print a rendering nobody produced this run. var settled *BankStopRow for i, row := range r.lastBankStopRows { if row.Src == "方源" { settled = &r.lastBankStopRows[i] } } if settled == nil { t.Fatalf("the skipped candidate must still be on the sheet: %+v", r.lastBankStopRows) } if settled.Dst != "" { t.Errorf("no role answered for this candidate, so the sheet must not carry a rendering as if one had: %q", settled.Dst) } // ⛔ AND THE SHEET MUST SAY WHY. An empty dst is the same glyph as «the role declined», «no reply line // covered it» and «the budget did not reach it» — three facts that mean UNDECIDED beside one that means // NOTHING TO DECIDE, and the review ranking files all four together. The report of this pack first // claimed the owner could read the difference off the row; he could not, and this is that claim made // true instead of withdrawn. if !settled.SettledByBank { t.Errorf("a row the role was never asked about must be marked as such: %+v", *settled) } if !strings.Contains(renderBankStopTable(r.lastBankStopRows), "NOT ASKED(the bank already renders this surface") { t.Errorf("the rendered sheet must carry the mark, not only the struct:\n%s", renderBankStopTable(r.lastBankStopRows)) } // CONTROL: the candidate that WAS paid for is not marked, so the mark is the filter answering. for _, row := range r.lastBankStopRows { if row.Src == "青茅山" && row.SettledByBank { t.Errorf("a candidate the role WAS asked about must not be marked as unasked: %+v", row) } } if len(settled.Variants) == 0 { t.Errorf("the drafts' own proposal must still be on the sheet — that is what the owner reads instead: %+v", *settled) } } // TestADisagreeingBankedCandidateIsStillPaidFor is case (ii), and it is the half that keeps the filter // from paying for itself with the owner's information: the seed holds the surface, the draft proposed // something ELSE, and that disagreement is the whole reason the role is asked about banked surfaces. // Measured on the corpus, this is the majority of the population — 366 of the 439 candidates the owner's // own seed holds — so a filter without this half would buy its saving by silencing them. func TestADisagreeingBankedCandidateIsStillPaidFor(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isTerminologyBody(body) { return "方源\tИсточник Фана\n青茅山\tгора Цинмао", "stop" } // One byte of difference from the case above: the draft calls 方源 something the seed does not. return "Странник пришёл." + "\n" + bankSeparator + "\n方源\tСтранник\tname\n青茅山\tгора Цинмао\tplace", "stop" }) defer srv.Close() seed := "terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n" r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed})) defer r.Close() _ = runToSignatureStop(t, r) if n := r.lastTerminology.BankSettled; n != 0 { t.Fatalf("a draft that disagrees with the bank must be PAID for, not skipped: skipped=%d", n) } if !askedAsCandidate(rec, "方源") { t.Fatalf("the disagreeing surface must reach the paid role as a candidate:\n%s", terminologyBodies(rec)) } // And the finding the payment bought has to be on the sheet, which is what the payment is FOR. var row *BankStopRow for i, cur := range r.lastBankStopRows { if cur.Src == "方源" { row = &r.lastBankStopRows[i] } } if row == nil { t.Fatalf("premise broken: 方源 is not on the sheet: %+v", r.lastBankStopRows) } if len(row.BankHolds) == 0 { t.Errorf("the payment bought the mark that the book already calls this term something else, and it must reach the sheet: %+v", *row) } } // TestAResumeFindsItsCheckpointsAfterTheFilter pins the resume cure, and it is the assertion that caught // the first cure being wrong. Batches are packed greedily, so dropping a candidate changes the composition // of a surviving batch, and a bank-role batch is addressed by the hash of its request: a second run whose // composition differs from the first finds no checkpoint and buys the whole pass again. The first cure // written here made the filter stand down for a book that had already paid — which produced exactly the // re-purchase it was meant to prevent, because run one had paid under the FILTERED composition. // // What is asserted is money, not a flag: the resumed run reaches the provider for the bank role zero // times and its pass costs $0. func TestAResumeFindsItsCheckpointsAfterTheFilter(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isTerminologyBody(body) { return "青茅山\tгора Цинмао", "stop" } return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop" }) defer srv.Close() seed := "terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n" bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed}) // ⚠ BOTH runs AUTO-CONTINUE (no --verify-bank), and that is load-bearing rather than convenience: a // run that STOPS at the bank boundary does not write the auto-bank (18-bank-ontology.md — the view is // not changed on a stop boundary), so the bank the second run reads would be byte-identical to the // first's whatever the filter looked at. The first version of this test stopped, and a planting that // made the filter read the WHOLE bank — auto-bank rows this very run rewrites, and therefore a bank // that MOVES between runs — survived it. Auto-continuing is what makes the second run see a bank that // the first one changed. r1 := newRunner(t, bookPath) if _, err := r1.TranslateBook(context.Background()); err != nil { t.Fatalf("the first run: %v", err) } if r1.lastTerminology.BankSettled != 1 { t.Fatalf("premise broken: the first run must filter, or the resume proves nothing: %+v", r1.lastTerminology) } if r1.lastTerminology.CostUSD == 0 { t.Fatalf("premise broken: the first run must actually BUY the pass, or a $0 resume is meaningless") } terminologyCallsAfterFirst := countTerminologyCalls(rec) r1.Close() // PREMISE for the whole test: the first run really did leave an auto-bank behind, so the second run's // bank is not the same object. Without this the assertions below hold for a reason unrelated to them. if _, err := os.Stat(r1.autoBankPath()); err != nil { t.Fatalf("premise broken: the first run wrote no auto-bank, so the bank cannot move between runs: %v", err) } r2 := newRunner(t, bookPath) defer r2.Close() var log bytes.Buffer r2.Log = slog.New(slog.NewTextHandler(&log, &slog.HandlerOptions{Level: slog.LevelWarn})) if _, err := r2.TranslateBook(context.Background()); err != nil { t.Fatalf("the resumed run: %v", err) } // ⛔ RUN 2 IS THE ONE-TIME RE-PACK, AND IT IS THE PACK'S ORDERED PRICE — NOT A REGRESSION. // // Until the settled basis existed, run 2 was $0: the filter is unconditional, so the composition was a // pure function of the candidates and the bank, identical on every run, and every checkpoint replayed. // The basis adds a THIRD input, and that input goes from empty to full at run 1's output boundary. So // run 2 sees a composition run 1 never had, finds no checkpoint for it, and buys the pass ONCE. From // run 3 on the basis is stable and every run replays for $0 — which is the half this test now pins, and // the half the old two-run shape could not express at all. // // ⚠ THE CURE THAT LOOKS OBVIOUS IS THE BUG THIS FILE ALREADY DOCUMENTS: «stand down for a book that has // already paid» makes the rule decide differently on the second run, and a rule that decides differently // on the second run is not a resume rule (see dropBankSettled). The one-time cost is bounded by // gates.terminology.budget_usd and ANNOUNCED — the reconsolidation warning below is that announcement, // and run 2 is exactly the case its sentence describes. if !strings.Contains(log.String(), logKeyReconsolidated+"=true") { t.Errorf("run 2 re-bought the pass and said nothing: the one-time cost must announce itself, or an operator meets it as an unexplained charge\n%s", log.String()) } if r2.lastTerminology.BankSettled != 1 { t.Errorf("the filter must decide the same way on every run, or the composition moves: skipped=%d", r2.lastTerminology.BankSettled) } if r2.lastTerminology.BasisServed == 0 { t.Errorf("premise broken: run 2 must be served by the basis run 1 wrote, or the re-pack below has another cause entirely") } callsAfterSecond := countTerminologyCalls(rec) // The re-pack is a PURCHASE, and it is asserted rather than left to the cost field: «run 2 paid» and // «run 2 called the provider» are different facts, and the second is what makes run 3's zero below mean // «replayed» instead of «never ran». if callsAfterSecond <= terminologyCallsAfterFirst { t.Errorf("run 2 announced a re-purchase but reached the provider %d → %d times: either the announcement is false or the pass never ran", terminologyCallsAfterFirst, callsAfterSecond) } r2.Close() // --- run 3: the basis is stable now, so the composition is, so every checkpoint replays --- r3 := newRunner(t, bookPath) defer r3.Close() var log3 bytes.Buffer r3.Log = slog.New(slog.NewTextHandler(&log3, &slog.HandlerOptions{Level: slog.LevelWarn})) if _, err := r3.TranslateBook(context.Background()); err != nil { t.Fatalf("the third run: %v", err) } if r3.lastTerminology.CostUSD != 0 { t.Errorf("run 3 must replay from its checkpoints for $0, got $%f — the basis is not stable between runs and EVERY purchase would re-buy the pass", r3.lastTerminology.CostUSD) } // Counted on the BANK ROLE's own calls: the run still walks the edit wave, so a count of all provider // calls would move for a reason that has nothing to do with this contour. if n := countTerminologyCalls(rec); n != callsAfterSecond { t.Errorf("run 3 must reach the provider zero times for the bank role: role calls went %d → %d", callsAfterSecond, n) } if strings.Contains(log3.String(), logKeyReconsolidated+"=true") { t.Errorf("the one-time-cost warning fired on a run that re-bought nothing — it would then cry money on every ordinary resume, which is the defect this message was rewritten to remove:\n%s", log3.String()) } } // countTerminologyCalls is how many provider calls carried the terminology role's block. func countTerminologyCalls(rec *reqRec) int { n := 0 for _, body := range rec.all() { if isTerminologyBody(body) { n++ } } return n } // TestTheFilterChangesNothingOnTheCANDIDATESITKEEPS is the output-equivalence half of the order: the run // must buy less and produce the same artifact for everything it still buys. // // The comparison is against a run of the SAME corpus and the SAME replies whose seed names an unrelated // surface, so nothing is settled and the role is paid for every candidate. What the two runs must agree on, // byte for byte, is the sheet row of the candidate both of them paid for; what they may differ in is the // skipped surface itself, which is exactly "equal to the original minus the skipped rows". func TestTheFilterChangesNothingOnTheCANDIDATESITKEEPS(t *testing.T) { reply := func(body string) (string, string) { if isTerminologyBody(body) { return "方源\tФан Юань\n青茅山\tгора Цинмао", "stop" } return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop" } run := func(seed string) (*Runner, []BankStopRow) { rec := &reqRec{} srv := newJSONProvider(rec, reply) t.Cleanup(srv.Close) r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed})) t.Cleanup(func() { _ = r.Close() }) _ = runToSignatureStop(t, r) return r, r.lastBankStopRows } // A: the seed settles 方源. B: the seed names a surface this book does not contain, so nothing is // settled and every candidate is paid for. withFilter, rowsA := run("terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n") _, rowsB := run("terms:\n - { src: 張三, dst: Чжан Сань, status: approved }\n") if withFilter.lastTerminology.BankSettled != 1 { t.Fatalf("premise broken: run A must have filtered exactly one candidate: %+v", withFilter.lastTerminology) } find := func(rows []BankStopRow, src string) *BankStopRow { for i := range rows { if rows[i].Src == src { return &rows[i] } } return nil } kept, kept2 := find(rowsA, "青茅山"), find(rowsB, "青茅山") if kept == nil || kept2 == nil { t.Fatalf("premise broken: the paid-for candidate must be on both sheets: A=%+v B=%+v", rowsA, rowsB) } if !reflect.DeepEqual(*kept, *kept2) { t.Errorf("the filter changed the sheet row of a candidate it did not skip:\n with filter: %+v\n without: %+v", *kept, *kept2) } // CONTROL for that equality: the SKIPPED surface is where the two runs are allowed to differ, and they // must — otherwise the DeepEqual above is comparing two runs that took the same path. skipped, paidFor := find(rowsA, "方源"), find(rowsB, "方源") if skipped == nil || paidFor == nil { t.Fatalf("premise broken: the skipped surface must be on both sheets: A=%+v B=%+v", rowsA, rowsB) } if skipped.Dst != "" { t.Errorf("the skipped candidate was never asked about, so its sheet row carries no rendering: %q", skipped.Dst) } if paidFor.Dst == "" { t.Fatalf("control: without the filter the same candidate IS answered for, or the two runs did not differ") } } // TestTheClassifiersTypeStillReachesTheSheetThroughTheFilter is the regression the filter's own first // implementation carried, and it is here because the tests written for that filter could not have seen it. // // The classifier and the scorer MUTATE candidates in place, and the caller renders the sheet and the delta // from the slice it passed in. Filtering the caller's slice copied the survivors into a new array, so those // mutations stopped reaching the caller: the sheet would have carried the heuristic type and the unscored // ranking, with every counter reading clean. Every other fixture in this package runs with the classifier // OFF, where neither mutation happens — so this test turns it ON, which is the only state in which the // defect is observable at all. func TestTheClassifiersTypeStillReachesTheSheetThroughTheFilter(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isClassifierBody(body) { // The heuristic drafted 青茅山 as a place; the classifier says otherwise, and THAT is the byte // that has to survive the filter and reach the sheet. return "青茅山\tterm\tnone", "stop" } if isTerminologyBody(body) { return "青茅山\tгора Цинмао", "stop" } return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop" }) defer srv.Close() seed := "terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n" r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, classify: true, glossarySeed: seed})) defer r.Close() _ = runToSignatureStop(t, r) if r.lastTerminology.BankSettled != 1 { t.Fatalf("premise broken: the filter must have skipped a candidate, or nothing is being crossed: %+v", r.lastTerminology) } if r.lastTerminology.Reclassified != 1 { t.Fatalf("premise broken: the classifier must have CHANGED a type, or there is no mutation to lose: %+v", r.lastTerminology) } var row *BankStopRow for i, cur := range r.lastBankStopRows { if cur.Src == "青茅山" { row = &r.lastBankStopRows[i] } } if row == nil { t.Fatalf("premise broken: 青茅山 is not on the sheet: %+v", r.lastBankStopRows) } if row.Type != "term" { t.Errorf("the classifier's type must reach the sheet through the filter, got %q — the filter copied the candidates and the mutation was lost", row.Type) } // ⛔ AND THE WIRE, which neither of the two assertions above reaches. The classifier is bought FOR the // type field, and that field travels to the ROLE for the whole batch — so the money is wasted the // moment the request carries the heuristic's guess while the run prints `reclassified=N`. Asserted on // the request bytes: this is the third form of one defect (the subset held as its own array), and the // first two were both found after their own tests went green. var asked string for _, body := range rec.all() { if isTerminologyBody(body) && strings.Contains(body, "### 青茅山") { asked = body } } if asked == "" { t.Fatalf("premise broken: the surface must reach the paid role at all:\n%s", terminologyBodies(rec)) } if !strings.Contains(asked, "type: term") || strings.Contains(asked, "type: place") { t.Errorf("the classifier's corrected type must reach the ROLE's request, not only the sheet — the run paid for that field:\n%s", asked) } // THE SECOND HALF OF THE SAME GUARANTEE, and it needs its own assertion: applyTypes and ScoreVariants // are two in-place mutations, and pinning one leaves the other free. Re-scoring is what makes the // heuristic's `conform` signal go away here — transliteration conformance speaks only about name/place // (scoreOpts), so once the classifier calls this surface a `term` the signal is no longer true of it. // Applied to the filtered copy instead, the sheet keeps a ranking factor the classification retired, // and the operator reads a reason that no longer holds. Measured both ways: correct code gives no // signals, the planting gives [conform]. for _, sig := range row.Signals { if sig == "conform" { t.Errorf("the re-scoring must reach the sheet too: %q is ranked by a conformance that only speaks about name/place, and the classifier made it a term — signals=%v", row.Src, row.Signals) } } } // TestTheOneTimeCostIsAnnouncedExactlyWhenItHappens pins the migration warning in BOTH directions, and it // exists because that warning has been wrong twice, each time in a way its own test could not see. // // - first it was emitted before the pass, on «this book has paid» — true of every ordinary resume of a // filtered book, so a live resume that re-bought nothing was told it had paid again; // - then the probe moved after the pass, where the pass's OWN checkpoints make «has paid» trivially true // — so a fresh book's FIRST run, having paid once, was told it had paid twice. // // Both halves are therefore asserted: the warning must be SILENT on a fresh first run and must FIRE on the // one case it describes. A test that only asserts silence is vacuous here — the firing branch is // unreachable in it — and that is exactly how the second version shipped. func TestTheOneTimeCostIsAnnouncedExactlyWhenItHappens(t *testing.T) { reply := func(body string) (string, string) { if isTerminologyBody(body) { return "青茅山\tгора Цинмао", "stop" } return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop" } t.Run("silent on a fresh book that paid once", func(t *testing.T) { srv := newJSONProvider(&reqRec{}, reply) defer srv.Close() seed := "terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n" r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed})) defer r.Close() var log bytes.Buffer r.Log = slog.New(slog.NewTextHandler(&log, &slog.HandlerOptions{Level: slog.LevelWarn})) _ = runToSignatureStop(t, r) if r.lastTerminology.BankSettled != 1 || r.lastTerminology.CostUSD == 0 { t.Fatalf("premise broken: the run must both FILTER and PAY, or the branch is not reached: %+v", r.lastTerminology) } if strings.Contains(log.String(), logKeyReconsolidated+"=true") { t.Errorf("a book paying for the FIRST time was told it had paid before — the probe is reading this run's own checkpoints:\n%s", log.String()) } }) t.Run("fires when an earlier composition really was paid for", func(t *testing.T) { srv := newJSONProvider(&reqRec{}, reply) defer srv.Close() // Run one has NO seed surface for 方源, so nothing is filtered and the pass is paid for over the // FULL candidate set. bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true}) r1 := newVerifyRunner(t, bookPath) _ = runToSignatureStop(t, r1) if r1.lastTerminology.BankSettled != 0 || r1.lastTerminology.CostUSD == 0 { t.Fatalf("premise broken: run one must filter NOTHING and pay: %+v", r1.lastTerminology) } minedDelta := r1.Book.MinedDelta r1.Close() // The owner signs 方源 with the rendering the drafts proposed. Its surface is now a seed surface, // so run two filters it — and the composition it pays under is not the one run one paid under. writeFile(t, minedDelta, "terms:\n - { src: 方源, dst: Фан Юань, status: approved }\n") r2 := newVerifyRunner(t, bookPath) defer r2.Close() var log bytes.Buffer r2.Log = slog.New(slog.NewTextHandler(&log, &slog.HandlerOptions{Level: slog.LevelWarn})) if _, err := r2.TranslateBook(context.Background()); err != nil { var stop *WaveSignatureStop if !errors.As(err, &stop) { t.Fatalf("run two: %v", err) } } if r2.lastTerminology.BankSettled != 1 { t.Fatalf("premise broken: run two must filter the newly-signed surface: %+v", r2.lastTerminology) } if r2.lastTerminology.CostUSD == 0 { t.Fatalf("premise broken: the changed composition must make run two PAY again, or there is nothing to announce") } if !strings.Contains(log.String(), logKeyReconsolidated+"=true") { t.Errorf("the one-time re-consolidation happened and was not announced:\n%s", log.String()) } }) } // TestTheSettledTestAnswersOnEachOfItsConditions pins the three conditions bankSettles rests on, one at a // time. Two of them had no witness at all: every fixture that reached the filter carried a banknote-origin // candidate whose surface matched a seed row's own src, so «banknote-only» and «aliases count» were both // true of the fixture by accident and could have been dropped without a test noticing. func TestTheSettledTestAnswersOnEachOfItsConditions(t *testing.T) { seed := []store.GlossaryEntry{ {Src: "方源", Dst: "Фан Юань", Status: "approved", Source: "seed", Aliases: []store.GlossaryAlias{{Alias: "小方"}}}, {Src: "青茅山", Dst: "", Status: "approved", Source: "seed"}, // no rendering: nothing to agree with } settled := bankSettledSurfaces(seed) // A row with no rendering is not in the index at all — there is nothing for a draft to agree with. if _, ok := settled[text.NormalizeSourceKey("青茅山")]; ok { t.Errorf("a seed row with no dst cannot settle anything: %+v", settled) } banknote := func(key, draft string) terminology.Candidate { return terminology.Candidate{Key: key, Src: key, Origin: terminology.OriginBanknote, Variants: []terminology.Variant{{Dst: draft}}} } // BASE: the shape the filter is for. if !bankSettles(settled, banknote("方源", "Фан Юань")) { t.Fatalf("premise broken: a banknote candidate on a seeded surface whose draft agrees must be settled") } // (1) ORIGIN. A MINER-origin candidate is not dropped by reverseSectionTerms, so it DOES reach the // delta — filtering it would lose a term from the artifact, not just a paid question. mined := banknote("方源", "Фан Юань") mined.Origin = terminology.OriginMined if bankSettles(settled, mined) { t.Errorf("only the population the EMISSION drops may be filtered; a mined candidate reaches the delta") } // (2) ALIASES. The emission's own guard reads a seed row's aliases, so this one must too, or a // proposal named by an alias is paid for and then thrown away. if !bankSettles(settled, banknote("小方", "Фан Юань")) { t.Errorf("a surface the seed holds as an ALIAS is settled just as its src is") } // (3) AGREEMENT, per variant and not per candidate: one disagreeing draft among several is enough. two := banknote("方源", "Фан Юань") two.Variants = append(two.Variants, terminology.Variant{Dst: "Источник Фана"}) if bankSettles(settled, two) { t.Errorf("one disagreeing draft is the finding; the candidate must be paid for") } // (4) NO EVIDENCE is not evidence of agreement. Unreachable from a banknote candidate by construction // — they always carry a proposal — and asserted so the rule cannot be quietly inverted. none := banknote("方源", "") none.Variants = nil if bankSettles(settled, none) { t.Errorf("a candidate with no observed rendering has not agreed with anything") } } // TestTheFilterFoldsBothSidesTheWayProductionDoes pins the two normalizations inside the money filter, // each of which decides whether a paid call happens and neither of which had a witness. Breaking either // left the whole battery green: the source fold silently stops the filter firing for every traditional or // katakana spelling — and BankSettled then reads 0, indistinguishable from «nothing to skip» — while the // target fold is the money decision itself. func TestTheFilterFoldsBothSidesTheWayProductionDoes(t *testing.T) { // A seed row spelled TRADITIONALLY, and a candidate keyed the way production keys it. seed := []store.GlossaryEntry{{Src: "長空", Dst: "Пустота Небес", Status: "approved", Source: "seed"}} settled := bankSettledSurfaces(seed) if _, ok := settled["长空"]; !ok { t.Fatalf("the seed surface must be indexed by its NORMALIZED key, or the filter never fires for the traditional population: %+v", settled) } cand := func(draft string) terminology.Candidate { return terminology.Candidate{Key: "长空", Src: "長空", Origin: terminology.OriginBanknote, Variants: []terminology.Variant{{Dst: draft}}} } if !bankSettles(settled, cand("Пустота Небес")) { t.Fatalf("premise broken: an agreeing draft on a seeded surface must settle") } // THE TARGET FOLD. A draft differing only by case and ё is the SAME rendering under the fold the vote // is counted with, so it agrees — and paying for it would be paying to be told what the bank says. if !bankSettles(settled, cand("пустота небес")) { t.Errorf("the agreement must be judged under the same target-form fold the vote uses, not byte-wise") } // CONTROL: a genuinely different rendering still disagrees, so the fold above is a fold and not a // comparison that has stopped comparing. if bankSettles(settled, cand("Небесная пустота")) { t.Errorf("a different rendering is the finding and must be paid for") } }