textmachine/backend/internal/pipeline/stopsheetfindings_test.go

181 lines
10 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 pipeline
import (
"strings"
"testing"
"textmachine/backend/internal/membank"
"textmachine/backend/internal/terminology"
"textmachine/backend/internal/text"
)
// stopsheetfindings_test.go pins the join between what the terminology phase FOUND and the row of the
// signature sheet the owner reads it on.
//
// ⚠ WHY THE FIXTURE IS SPELLED THE WAY IT IS, and it is the whole test. Every finding here used to cross
// the file boundary as a map: keyed on the writer's side in terminologist.go, read on the reader's side in
// mining.go. Two independent choices of key per finding, three findings, nothing comparing the six. They
// agreed — and no test in the module could have said whether they did, because a candidate whose KEY
// differs from its raw SRC appeared in no fixture, and on equal strings every wrong choice is right.
// Mutating any of the three keys survived the full pipeline, terminology and tmctl packages.
//
// So the candidates below are TRADITIONAL spellings, and the fixture asks the REAL normalizer whether
// they are: 長空 folds to 长空 and 長 to 长 (internal/text/data/trad2simp.txt lines 434, 193). ⚠ The first
// version of this file asserted the same premise about 雲海空竅 → 云海空窍 and was WRONG — neither 雲 nor 竅
// is in that table, so the two strings differed only because they were typed differently, and the guard
// below compared two hand-written literals and would have stayed green against a normalizer replaced by
// identity. A fixture whose premise is checked by a literal certifies the literal.
func TestEveryStopSheetFindingReachesTheRowItBelongsTo(t *testing.T) {
compound := terminology.Candidate{Key: "长空", Src: "長空", Type: "term", Freq: 4}
part := terminology.Candidate{Key: "长", Src: "長", Type: "term", Freq: 9}
// The premise is asked of the NORMALIZER, not of the literals: the key must be what production would
// derive from the surface, and it must differ from it. Either half checked against a typed constant
// would certify the constant — which is how the previous fixture stood on a fold that does not exist.
for _, c := range []terminology.Candidate{compound, part} {
if got := text.NormalizeSourceKey(c.Src); got != c.Key {
t.Fatalf("fixture premise: production keys %q as %q, not as %q — the fixture is not the population it claims", c.Src, got, c.Key)
}
if c.Key == c.Src {
t.Fatalf("degenerate fixture: %q keys as itself, so a wrong key choice cannot be seen here", c.Src)
}
}
cands := []terminology.Candidate{compound, part}
consolidated := map[string]string{"长空": "Пустота Небес", "长": "Море Облаков"}
// All three findings are produced the way production produces them — the self-conflict by the real
// check, so its Key is whatever ConsolidationConflicts decided to carry and not a value typed here.
self := terminology.ConsolidationConflicts(cands, consolidated)
if len(self) != 1 {
t.Fatalf("premise broken: the compound's rendering must drop its own part's, got %+v", self)
}
tres := terminologyResult{
Conf: map[string]int{"长空": 2},
SelfConflictRows: self,
BankHoldRows: []membank.BankKeyConflict{{
Key: "长空", Src: "长空", Dst: "Пустота Небес",
BankSrc: "長空", BankDst: "Полость Небес", BankStatus: "draft",
}},
}
rows := bankStopRows(cands, consolidated, tres)
if len(rows) != 2 || rows[0].Src != "長空" {
t.Fatalf("premise broken: the sheet must carry the raw surfaces, got %+v", rows)
}
marked, plain := rows[0], rows[1]
if marked.Conf != 2 {
t.Errorf("the role's confidence must reach the row it was stated for, want 2, got %d", marked.Conf)
}
if len(marked.Contradicts) != 1 || !strings.Contains(marked.Contradicts[0], "長") || !strings.Contains(marked.Contradicts[0], "Море Облаков") {
t.Errorf("the self-contradiction must reach the row and name the part it drops: %+v", marked.Contradicts)
}
if len(marked.BankHolds) != 1 || !strings.Contains(marked.BankHolds[0], "Полость Небес") {
t.Errorf("the bank's existing rendering must reach the row: %+v", marked.BankHolds)
}
// CONTROL, and it is what separates "the join works" from "the join attaches everything to everybody":
// the second candidate was named by none of the three findings and must carry none of them. Conf is
// -1 rather than 0 because silence and a stated zero are opposite facts for a reviewer.
if plain.Conf != -1 {
t.Errorf("a candidate the reply gave no confidence for must read as absent (-1), got %d", plain.Conf)
}
if len(plain.Contradicts) != 0 || len(plain.BankHolds) != 0 {
t.Errorf("a candidate no finding named must carry none: contradicts=%+v bankHolds=%+v", plain.Contradicts, plain.BankHolds)
}
}
// TestTheStopSheetJoinIsNotFooledByTheRawSurface is the same join asked the question the old map shape
// could not be asked: it puts the OTHER candidate's raw surface where a wrong key choice would look for
// one, so a lookup by Src finds a real string belonging to the wrong row instead of finding nothing.
// A join that merely returned empty on a wrong key would be caught by the test above; one that returned
// the NEIGHBOUR's finding would not, and that is the failure an operator cannot detect — the sheet is
// complete, plausible, and about a different term.
func TestTheStopSheetJoinIsNotFooledByTheRawSurface(t *testing.T) {
// The two candidates cross-reference each other's strings: each one's SURFACE is the other's KEY, so a
// lookup on the wrong field finds a real finding belonging to the wrong row. Deliberately mirrored, so
// unlike the fixture above these are not both what production would derive — that premise is checked
// there, and here what matters is only that the four strings collide the way a wrong key would.
first := terminology.Candidate{Key: "长空", Src: "長空"}
second := terminology.Candidate{Key: "長空", Src: "长空"}
if first.Key == first.Src || second.Key == second.Src || first.Key == second.Key {
t.Fatalf("degenerate fixture: the two candidates must have distinct keys and each differ from its own surface")
}
cands := []terminology.Candidate{first, second}
tres := terminologyResult{
Conf: map[string]int{"长空": 1, "長空": 9},
BankHoldRows: []membank.BankKeyConflict{
{Src: "长空", BankSrc: "长空", BankDst: "Море Облаков", BankStatus: "approved"},
{Src: "長空", BankSrc: "長空", BankDst: "Облачное Море", BankStatus: "draft"},
},
}
rows := bankStopRows(cands, map[string]string{"长空": "А", "長空": "Б"}, tres)
if len(rows) != 2 {
t.Fatalf("premise broken: %d row(s)", len(rows))
}
// Keyed by Src instead of Key, each row would take the other's confidence and the other's bank row —
// and every assertion about "a finding reached a row" would still be satisfied.
if rows[0].Conf != 1 {
t.Errorf("row %q took a confidence that is not its own: %d", rows[0].Src, rows[0].Conf)
}
if len(rows[0].BankHolds) != 1 || !strings.Contains(rows[0].BankHolds[0], "Море Облаков") {
t.Errorf("row %q took the wrong bank row: %+v", rows[0].Src, rows[0].BankHolds)
}
if rows[1].Conf != 9 {
t.Errorf("row %q took a confidence that is not its own: %d", rows[1].Src, rows[1].Conf)
}
if len(rows[1].BankHolds) != 1 || !strings.Contains(rows[1].BankHolds[0], "Облачное Море") {
t.Errorf("row %q took the wrong bank row: %+v", rows[1].Src, rows[1].BankHolds)
}
}
// TestTheCommonestSeedShapeStillReachesTheSigningSheet is the same disagreement as
// TestConsolidationAgainstTheBankIsReportedAtTheStop, on the seed row shape that actually occurs.
//
// ⚠ THE FIXTURE IS THE FINDING. That test's seed carries `until_ch: 20`, and its own comment says why:
// "until_ch keeps the seed row off the proposal's UNIQUE key". A hand-written seed row does not carry
// until_ch — three of the forty-nine rows in the owner's own seeds have a window at all — so it shares
// its UNIQUE tuple (src, sense, since_ch, until_ch) with a banknote proposal, which takes the default
// window likewise. The tuple skip fired first and the disagreement never reached the sheet: the shape
// that the sheet exists for was the one shape it could not show. The e2e that was supposed to cover this
// passed because its fixture had been given a window that stepped around the hole — the second-degeneracy
// class of D39.208 п.5, which is why this test's seed is spelled with NO window and no sense at all.
func TestTheCommonestSeedShapeStillReachesTheSigningSheet(t *testing.T) {
srv := newJSONProvider(&reqRec{}, func(body string) (string, string) {
if isTerminologyBody(body) {
return "方源\tФан Юань", "stop" // the seed calls the same surface «Странник»
}
return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop"
})
defer srv.Close()
// The commonest shape there is: a rendering, a status, and nothing else.
seed := "terms:\n - { src: 方源, dst: Странник, status: draft }\n"
r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed}))
defer r.Close()
_ = runToSignatureStop(t, r)
var marked *BankStopRow
for i, row := range r.lastBankStopRows {
if row.Src == "方源" {
marked = &r.lastBankStopRows[i]
}
}
if marked == nil {
t.Fatalf("premise broken: 方源 is not on the sheet at all: %+v", r.lastBankStopRows)
}
if len(marked.BankHolds) != 1 {
t.Fatalf("the row the owner wrote must be named on the sheet he signs by: %+v", *marked)
}
if !strings.Contains(marked.BankHolds[0], `unsigned draft "方源"→"Странник"`) {
t.Errorf("the mark must name the EXISTING row and its signature: %q", marked.BankHolds[0])
}
// PREMISE, asserted rather than assumed: the two really do share the whole UNIQUE tuple. If a later
// change gave a banknote proposal a window of its own, this test would keep passing for a reason that
// has nothing to do with what it guards — the way its predecessor did.
if marked.BankHolds[0] != `unsigned draft "方源"→"Странник" [ch 1..end]` {
t.Errorf("premise broken: the seed row must carry the DEFAULT window, or the tuple is not shared: %q", marked.BankHolds[0])
}
if !strings.Contains(renderBankStopTable(r.lastBankStopRows), "THE BANK ALREADY HOLDS: unsigned draft") {
t.Errorf("the rendered sheet must carry the mark:\n%s", renderBankStopTable(r.lastBankStopRows))
}
}