textmachine/backend/internal/pipeline/readerholewords_test.go

126 lines
6.8 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 (
"os"
"path/filepath"
"strings"
"testing"
"textmachine/backend/internal/lang"
)
// readerholewords_test.go: what the READER'S FILE says at a hole — the one surface the product exists for.
//
// ⛔ THE DEFECT THIS PINS WAS FOUND IN THE WORDS, NOT IN A TEST. The writer chose the sentence by the hole's
// KIND alone, and `withheld` says «этот фрагмент … требует проверки человеком». That is true of a refusal
// and of a contaminated output. It is FALSE of a stop MARK — a call a person cut, or a re-attack a ceiling
// refused — because nothing is wrong with that fragment: it was paid for, not finished, and the next run
// does it. A reader was being sent to wait for a human who cannot help, and the money mark of backlog row
// 291 was about to join the same sentence (before it, such a unit had no row at all and the file said the
// true thing: «ещё не переведён»).
// readerWordsFor writes a minimal target-language word set and loads it, returning distinguishable
// sentences so a test can say WHICH one the writer chose.
func readerWordsFor(t *testing.T) lang.ReaderWords {
t.Helper()
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "ru"), 0o755); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(root, "ru", "reader.txt"),
"hole.pending\tНЕ ПЕРЕВЕДЕНО.\nhole.withheld\tТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ.\nhole.incomplete\tНедостаёт {dropped}.\n"+
"hole.stale\tУстарело.\nhole.ghost\tВне нарезки {ghost}.\nnotice.holes\tПропусков {holes} из {total}.\nnotice.ghost\tВне нарезки всего {ghost}.\n")
words, present, err := lang.LoadReaderWords(root, "ru")
if err != nil || !present {
t.Fatalf("reader words: present=%v err=%v", present, err)
}
return words
}
// TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp is the pin, and it is a table over the two
// CLASSES rather than over one reason: a mark the next run erases, and a verdict a person must look at.
func TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp(t *testing.T) {
words := readerWordsFor(t)
for _, tc := range []struct {
name string
reason FlagReason
dropped int
want string
}{
{name: "a re-attack no money was left for", reason: FlagRetryUnaffordable, want: "НЕ ПЕРЕВЕДЕНО."},
{name: "a call a person cut", reason: FlagCancelled, want: "НЕ ПЕРЕВЕДЕНО."},
// A stop mark over a unit that ALSO lost a member for good: no purchase brings that text back, so
// the cheerier sentence would promise what the next run cannot deliver.
{name: "a stop mark over a unit that lost a member for good", reason: FlagRetryUnaffordable, dropped: 1, want: "ТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ."},
// The control, and without it the assertion above would pass on a writer that said «not translated»
// about EVERYTHING: a verdict a human really does have to look at keeps the withheld sentence.
{name: "a provider refusal", reason: FlagHardRefusal, want: "ТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ."},
{name: "a contaminated output that was dropped", reason: FlagSanitizerDefect, want: "ТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ."},
} {
t.Run(tc.name, func(t *testing.T) {
exp := &BookExport{
BookID: "syn", TotalUnits: 1,
Chunks: []ChunkExport{{Chapter: 1, ChunkIdx: 0, Disposition: string(DispFlagged), FlagReason: string(tc.reason),
FinalText: "", DroppedMembers: tc.dropped, DroppedReason: string(FlagHardRefusal)}},
}
// The premise: this record IS the hole kind whose sentence is under test. A projection that
// classified differently would make the assertion below about another branch entirely.
if got := UnitHole(exp.Chunks[0]); got != HoleWithheld {
t.Fatalf("the fixture's unit is hole %q, not %q — this test is about the withheld branch", got, HoleWithheld)
}
book, holes, err := assembleBook(exp, nil, "книга", "ru", words)
if err != nil {
t.Fatal(err)
}
if len(book.Chapters) != 1 || len(book.Chapters[0].Paragraphs) != 1 {
t.Fatalf("the file is not one chapter with one marked hole: %+v", book.Chapters)
}
got := book.Chapters[0].Paragraphs[0]
t.Logf("reason %q ⇒ the reader reads %q", tc.reason, got)
if !strings.Contains(got, tc.want) {
t.Fatalf("the reader reads %q for reason %q, want %q: a stop mark is work the next run buys, and telling a reader it needs a human is a sentence they can act on wrongly",
got, tc.reason, tc.want)
}
// ⛔ AND THE REASON SURVIVES ON THE HOLE. It is what makes a third sentence of its own («paid
// for, not finished: top up and resume») a row in reader.txt plus a branch, rather than a
// re-design: an operator's refusal text and any later phrase both read it from here.
if len(holes) != 1 || holes[0].Reason != string(tc.reason) {
t.Fatalf("the hole lost its reason: %+v", holes)
}
})
}
}
// TestTheOperatorsRefusalTellsHimBothHalves is the THIRD surface of the same distinction, and the one the
// first two repairs skipped: the build's refusal text is what the person who can ACT reads, and it was
// telling him only the cheerful half. A unit can be «paid for and not done» AND short a member for good;
// told only the first, he tops up, resumes, and the build refuses again with a different hole.
//
// ⛔ The reason the other two surfaces got the caveat and this one could not: the hole record it reads did
// not carry the drop count for a withheld unit at all. The field existed for `incomplete` and was simply
// not filled — «took the form, not the guarantee», one surface further along.
func TestTheOperatorsRefusalTellsHimBothHalves(t *testing.T) {
exp := &BookExport{BookID: "syn", TotalUnits: 3}
holes := []hole{
{Kind: HoleWithheld, Chapter: 1, Unit: 0, Reason: string(FlagRetryUnaffordable)},
{Kind: HoleWithheld, Chapter: 2, Unit: 0, Reason: string(FlagRetryUnaffordable), Dropped: 1},
// The control: a real verdict keeps the plain sentence, so «both halves» above is a statement about
// this shape and not what the text says about everything.
{Kind: HoleWithheld, Chapter: 3, Unit: 0, Reason: string(FlagHardRefusal)},
}
err := describeHoles("syn", exp, holes, false)
if err == nil {
t.Fatal("a book with holes and no --partial must refuse")
}
printed := err.Error()
t.Logf("what the operator reads:\n%s", printed)
for _, want := range []string{
"chapter 1 unit 0: withheld — paid for and NOT done; the next run re-does it (retry_unaffordable)",
"chapter 2 unit 0: withheld — paid for and NOT done, AND 1 member(s) of it are missing for good; the next run finishes the unit but cannot bring those back (retry_unaffordable)",
"chapter 3 unit 0: withheld (hard_refusal)",
} {
if !strings.Contains(printed, want) {
t.Fatalf("the refusal does not say %q", want)
}
}
}