181 lines
10 KiB
Go
181 lines
10 KiB
Go
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))
|
||
}
|
||
}
|