166 lines
7.3 KiB
Go
166 lines
7.3 KiB
Go
package pipeline
|
||
|
||
// consistencyseam_test.go: the SEAM between the book-consistency measure and a real run.
|
||
//
|
||
// ⛔ WHY THIS FILE EXISTS. Every other test of this measure drives `consistencyScan` directly, handing it a
|
||
// source and a shipped text by hand. That leaves exactly one wire untested — the line in QualityReport that
|
||
// decides WHICH text is the source and which is the shipped one — and the acceptance proved the cost:
|
||
// swapping the unit's source for its shipped text there passed the ENTIRE battery green. With the source
|
||
// side wrong no bank key can fire, every row falls into «key never fired in the source», and the report
|
||
// prints I1=0 · I2=0 — a clean bill of health from an instrument that looked at nothing. That is the exact
|
||
// failure the pack was written to make impossible, committed by the pack.
|
||
//
|
||
// So these tests go through `QualityReport()` on a real translated book, and they are built so that the
|
||
// wrong side of the seam is LOUD rather than quiet.
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/obs"
|
||
"textmachine/backend/internal/store"
|
||
)
|
||
|
||
// seamSeed is a bank whose source key is Han and whose rendering is Russian — the asymmetry is the whole
|
||
// detector. A Han key cannot fire in Russian output, so if the measure is ever fed the shipped text as its
|
||
// source, `fired` collapses to zero and every assertion below fails at once.
|
||
const seamSeed = `
|
||
terms:
|
||
- src: 蛊师
|
||
dst: гу-мастер
|
||
type: term
|
||
status: approved
|
||
decl: { invariant: true, forms: ["гу-мастер", "гу-мастера"] }
|
||
`
|
||
|
||
func seamRunner(t *testing.T, source, edited string) *Runner {
|
||
t.Helper()
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, func(body string) (string, string) {
|
||
if isEditBody(body) {
|
||
return edited, "stop"
|
||
}
|
||
return "ЧЕРНОВИК ПЕРЕВОДА", "stop"
|
||
})
|
||
t.Cleanup(srv.Close)
|
||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: source, glossarySeed: seamSeed})
|
||
r := newRunner(t, bookPath)
|
||
if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return r
|
||
}
|
||
|
||
// TestTheMeasureReadsTheSourceOnTheSourceSideAndTheShippedTextOnTheOther is the seam pin. It asserts the
|
||
// two sides SEPARATELY — that a key fired at all (the source side is really the source) and that a
|
||
// rendering was found (the target side is really what shipped) — because a single "it says covered" check
|
||
// passes on a measure that read the same text twice.
|
||
func TestTheMeasureReadsTheSourceOnTheSourceSideAndTheShippedTextOnTheOther(t *testing.T) {
|
||
r := seamRunner(t, "蛊师来了。", "Гу-мастер вошёл в зал.")
|
||
defer r.Close()
|
||
q, err := r.QualityReport()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
c := q.Consistency
|
||
if c == nil {
|
||
t.Fatalf("a book WITH a bank must carry a consistency section; got nil")
|
||
}
|
||
if c.UnitsJudged == 0 {
|
||
t.Fatalf("premise broken: no shipped unit was read, so neither side of the seam is being tested")
|
||
}
|
||
if c.FiredTotal == 0 {
|
||
t.Errorf("the SOURCE side is not the source: the bank's Han key 蛊师 fired %d times in a book whose source is 蛊师来了。 — if the shipped text is being passed as the source, no Han key can ever fire and every row falls into «never fired»", c.FiredTotal)
|
||
}
|
||
if c.NeverFired != 0 {
|
||
t.Errorf("«key never fired in the source»=%d on a one-term book whose key IS in the source — the control line of this whole measure is reporting the opposite of the truth", c.NeverFired)
|
||
}
|
||
if c.ShippedTotal == 0 {
|
||
t.Errorf("the TARGET side is not the shipped text: «гу-мастер» is in what this run shipped, and the measure found %d occurrences", c.ShippedTotal)
|
||
}
|
||
if c.Covered != 1 || c.Absent != 0 || c.Split != 0 {
|
||
t.Errorf("a term whose key fired and whose rendering shipped must read as covered: covered=%d absent=%d split=%d", c.Covered, c.Absent, c.Split)
|
||
}
|
||
}
|
||
|
||
// TestTheMeasureSeesADriftedTermThroughTheWholeReport is the other half: the seam must carry a REAL finding
|
||
// out, not just a clean sheet. Same wiring, shipped text that renders the term some other way.
|
||
func TestTheMeasureSeesADriftedTermThroughTheWholeReport(t *testing.T) {
|
||
r := seamRunner(t, "蛊师来了。", "Заклинатель вошёл в зал.")
|
||
defer r.Close()
|
||
q, err := r.QualityReport()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
c := q.Consistency
|
||
if c == nil {
|
||
t.Fatalf("a book WITH a bank must carry a consistency section; got nil")
|
||
}
|
||
if c.FiredTotal == 0 {
|
||
t.Fatalf("premise broken: the key must fire, or the absence below is the source side failing rather than a finding")
|
||
}
|
||
if c.Absent != 1 {
|
||
t.Errorf("the bank says «гу-мастер», the book shipped «Заклинатель»: want absent=1, got absent=%d covered=%d split=%d", c.Absent, c.Covered, c.Split)
|
||
}
|
||
if len(c.Terms) != 1 || c.Terms[0].Src != "蛊师" {
|
||
t.Fatalf("the finding must be NAMED, not only counted: %+v", c.Terms)
|
||
}
|
||
if len(c.Terms[0].WithoutForm) == 0 {
|
||
t.Errorf("the chapter that did not get the bank's rendering must be named: %+v", c.Terms[0])
|
||
}
|
||
}
|
||
|
||
// TestTheJudgeablePartitionClosesOnARealRun pins the printed identity end to end. Every judgeable row lands
|
||
// in exactly one of four buckets, and the report prints that sum; a fifth overlapping bucket printed as if
|
||
// it belonged to the partition is how these numbers stop meaning what they say.
|
||
func TestTheJudgeablePartitionClosesOnARealRun(t *testing.T) {
|
||
r := seamRunner(t, "蛊师来了。", "Гу-мастер вошёл в зал.")
|
||
defer r.Close()
|
||
q, err := r.QualityReport()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
c := q.Consistency
|
||
if c.Judged == 0 {
|
||
t.Fatalf("premise broken: nothing judgeable, so the partition below is 0=0")
|
||
}
|
||
if sum := c.NeverFired + c.BlockedOnly + c.Covered + c.Absent + c.Split; sum != c.Judged {
|
||
t.Errorf("the judgeable partition does not close: never-fired=%d + blocked=%d + covered=%d + absent=%d + split=%d = %d, judgeable=%d",
|
||
c.NeverFired, c.BlockedOnly, c.Covered, c.Absent, c.Split, sum, c.Judged)
|
||
}
|
||
if c.BankRows != c.Judged+c.NoDst {
|
||
t.Errorf("bank rows=%d but judgeable=%d + no-dst=%d — every row must be in exactly one of the two", c.BankRows, c.Judged, c.NoDst)
|
||
}
|
||
}
|
||
|
||
// TestTheReportCarriesWhatEachWaveWasShown pins the other field no test reached: the per-wave injection
|
||
// summary. Its absence is not a clean book — it is a run whose bank reached nobody, and the two must not
|
||
// print the same.
|
||
func TestTheReportCarriesWhatEachWaveWasShown(t *testing.T) {
|
||
r := seamRunner(t, "蛊师来了。", "Гу-мастер вошёл в зал.")
|
||
defer r.Close()
|
||
q, err := r.QualityReport()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(q.Waves) == 0 {
|
||
t.Fatalf("a run with a bank must report what each wave was shown; got none")
|
||
}
|
||
seen := map[string]WaveInjection{}
|
||
for _, w := range q.Waves {
|
||
seen[w.Wave] = w
|
||
}
|
||
for _, want := range []string{store.WaveDraft, store.WaveEdit} {
|
||
w, ok := seen[want]
|
||
if !ok {
|
||
t.Errorf("no line for the %s wave — the two waves select over different banks and both must be reportable", want)
|
||
continue
|
||
}
|
||
if w.Positions == 0 {
|
||
t.Errorf("the %s wave reports %d positions; nothing was recorded for it", want, w.Positions)
|
||
}
|
||
}
|
||
if seen[store.WaveEdit].ExactHits == 0 {
|
||
t.Errorf("the EDITOR wave reports no exact hit, so nothing says the bank reached the editor's prompt — the question row 417 exists for")
|
||
}
|
||
}
|