textmachine/backend/internal/pipeline/consistencyseam_test.go

166 lines
7.3 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
// 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")
}
}