153 lines
8 KiB
Go
153 lines
8 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"unicode"
|
|
|
|
"textmachine/backend/internal/checks"
|
|
"textmachine/backend/internal/config"
|
|
"textmachine/backend/internal/lang"
|
|
"textmachine/backend/internal/llm"
|
|
)
|
|
|
|
// classifyinput_test.go pins the ONE assembler of a live classifyInput.
|
|
//
|
|
// ⚠ THE DEFECT THIS REPLACES WAS A SECOND COPY, NOT A WRONG LINE. classifyOutput and repairReGate each
|
|
// built the input by hand from the same fields. When the target-language screen landed, `TargetScripts` was
|
|
// added to the first and forgotten in the second, so the screen abstained inside the repair re-gate while
|
|
// judging everywhere else — a repair that came back in another language passed a gate whose entire job is
|
|
// «is this still an acceptable completion». It was then fixed at the second site by hand, and a planting
|
|
// proved the fix was guarded by nothing: dropping the field there again left the whole battery green.
|
|
//
|
|
// So the fix is not a test on two call sites, it is one builder with one pin. The assertion below is
|
|
// deliberately written over REFLECTION rather than as a list of fields: a list would have to be updated by
|
|
// the same person who forgets to update the builder, which is the failure it is supposed to prevent. A new
|
|
// axis added to classifyInput and not wired in fails here on the day it is declared.
|
|
func TestClassifyInputForLeavesNoAxisUnset(t *testing.T) {
|
|
// ⚠ A NON-ru TARGET AND A NON-zh SOURCE, AND THAT IS THE WHOLE DESIGN OF THIS FIXTURE. The first
|
|
// version used ru/zh — the shipping pair — so a builder that ignored the book entirely and hard-coded
|
|
// `lang.LangScripts("ru")` produced exactly the values asserted, and three separate plantings that did
|
|
// precisely that survived the whole package. Nothing else in the battery could catch them either:
|
|
// every runner fixture translates INTO Russian. A pair the fixture does not share with the hard-code
|
|
// is the only shape of this test that can fail.
|
|
const target, source = "ja", "ko"
|
|
if len(lang.LangScripts(target)) == len(lang.LangScripts("ru")) || len(lang.LangScripts(source)) == len(lang.LangScripts("zh")) {
|
|
t.Fatalf("premise broken: this test can only catch a hard-coded ru/zh builder while the script COUNTS "+
|
|
"differ — %s has %d, ru has %d; %s has %d, zh has %d. Pick another pair or assert by identity.",
|
|
target, len(lang.LangScripts(target)), len(lang.LangScripts("ru")),
|
|
source, len(lang.LangScripts(source)), len(lang.LangScripts("zh")))
|
|
}
|
|
ck := checks.CompileCheckersFor(nil, lang.TargetChecksFor(target))
|
|
ck.SetSourceScripts(lang.LangScripts(source))
|
|
r := &Runner{Book: &config.Book{TargetLang: target}, checkers: ck}
|
|
|
|
// Every argument is chosen NON-ZERO, so that any zero field in the result is an unwired axis and not an
|
|
// artefact of the fixture.
|
|
in := r.classifyInputFor("源文本", "перевод", llm.FinishStop, true)
|
|
|
|
v := reflect.ValueOf(in)
|
|
for i := 0; i < v.NumField(); i++ {
|
|
if v.Field(i).IsZero() {
|
|
t.Errorf("classifyInput.%s came back zero: the builder does not populate it, so EVERY live "+
|
|
"classify() call — the stage gate and the repair re-gate alike — judges without it",
|
|
v.Type().Field(i).Name)
|
|
}
|
|
}
|
|
// The two axes the incident was actually about, asserted by value and not merely as non-zero.
|
|
if in.TargetLang != target {
|
|
t.Errorf("TargetLang = %q, want the book's %q", in.TargetLang, target)
|
|
}
|
|
if len(in.TargetScripts) != len(lang.LangScripts(target)) {
|
|
t.Errorf("TargetScripts must come from the BOOK's target, not a constant: got %d tables, want %d for %s",
|
|
len(in.TargetScripts), len(lang.LangScripts(target)), target)
|
|
}
|
|
if len(in.SourceScripts) != len(lang.LangScripts(source)) {
|
|
t.Errorf("SourceScripts must come from the RUN's compiled checkers, not a constant: got %d tables, want %d for %s",
|
|
len(in.SourceScripts), len(lang.LangScripts(source)), source)
|
|
}
|
|
if !in.NonProseReply {
|
|
t.Error("NonProseReply must pass through: a bank role's reply is not prose and must not be echo-screened")
|
|
}
|
|
// And the caller's own flag is not hard-wired: the stage path passes false.
|
|
if r.classifyInputFor("源文本", "перевод", llm.FinishStop, false).NonProseReply {
|
|
t.Error("NonProseReply is pinned to true inside the builder — the prose path would be exempted from the echo rule")
|
|
}
|
|
|
|
// ⛔ TWO BOOKS, AND THIS IS THE ASSERTION A CONSTANT CANNOT SATISFY. Everything above compares the
|
|
// builder's output against values chosen for THIS book, so it catches a hard-code to the shipping pair
|
|
// and nothing else — a hard-code to ja/ko would sail through the very fixture written to catch
|
|
// hard-codes. Two books whose answers must DIFFER is the only shape that cannot be met by any constant,
|
|
// whichever one is picked.
|
|
other := checks.CompileCheckersFor(nil, lang.TargetChecksFor("ru"))
|
|
other.SetSourceScripts(lang.LangScripts("zh"))
|
|
r2 := &Runner{Book: &config.Book{TargetLang: "ru"}, checkers: other}
|
|
a := r.classifyInputFor("源文本", "перевод", llm.FinishStop, false)
|
|
b := r2.classifyInputFor("源文本", "перевод", llm.FinishStop, false)
|
|
if a.TargetLang == b.TargetLang {
|
|
t.Errorf("two books with different targets produced the same TargetLang %q — the builder is not "+
|
|
"reading the book at all", a.TargetLang)
|
|
}
|
|
if len(a.TargetScripts) == len(b.TargetScripts) {
|
|
t.Errorf("two books with different targets produced the same TargetScripts (%d tables) — the builder "+
|
|
"is not reading the book's target", len(a.TargetScripts))
|
|
}
|
|
if len(a.SourceScripts) == len(b.SourceScripts) {
|
|
t.Errorf("two runs with different compiled checkers produced the same SourceScripts (%d tables) — the "+
|
|
"builder is not reading the run's checkers", len(a.SourceScripts))
|
|
}
|
|
}
|
|
|
|
// TestTheRunCompilesTheBooksOwnSourceScripts pins the OTHER end of the same axis, one call site away from
|
|
// the builder above — and it was found by an adversarial pass over that very fix.
|
|
//
|
|
// ⚠ The builder reads r.checkers.SourceScripts(); loadLangPack is what FILLS them, from the book's
|
|
// SourceLang. Pinning the reader and not the writer leaves the whole axis decided by an unguarded line: a
|
|
// hard-code there gives a ko→ru or ja→ru book the wrong declared source, the echo detector then measures a
|
|
// script the book does not use, and nothing in the battery says a word. Same two-book shape as above,
|
|
// because the same reason applies — any constant makes two books agree.
|
|
func TestTheRunCompilesTheBooksOwnSourceScripts(t *testing.T) {
|
|
srv := newJSONProvider(&reqRec{}, draftEdit)
|
|
defer srv.Close()
|
|
|
|
sourceScriptsFor := func(t *testing.T, sourceLang string) []*unicode.RangeTable {
|
|
t.Helper()
|
|
bookPath := setupProjectOpts(t, srv.URL, projectOpts{})
|
|
if sourceLang != "ja" {
|
|
raw, err := os.ReadFile(bookPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := strings.Replace(string(raw), "source_lang: ja", "source_lang: "+sourceLang, 1)
|
|
if body == string(raw) {
|
|
t.Fatalf("premise broken: the fixture's book.yaml no longer carries `source_lang: ja`:\n%s", raw)
|
|
}
|
|
writeFile(t, bookPath, body)
|
|
}
|
|
r := newRunner(t, bookPath)
|
|
defer r.Close()
|
|
return r.checkers.SourceScripts()
|
|
}
|
|
|
|
// ja declares three scripts, ko two — so a constant of either kind is visible as a disagreement.
|
|
if len(lang.LangScripts("ja")) == len(lang.LangScripts("ko")) {
|
|
t.Fatalf("premise broken: this test needs two sources whose script COUNTS differ, got ja=%d ko=%d",
|
|
len(lang.LangScripts("ja")), len(lang.LangScripts("ko")))
|
|
}
|
|
ja := sourceScriptsFor(t, "ja")
|
|
ko := sourceScriptsFor(t, "ko")
|
|
|
|
if len(ja) != len(lang.LangScripts("ja")) {
|
|
t.Errorf("a ja-source book compiled %d source scripts, want %d — the run is not reading the book's "+
|
|
"source language, so the echo detector measures the wrong script", len(ja), len(lang.LangScripts("ja")))
|
|
}
|
|
if len(ko) != len(lang.LangScripts("ko")) {
|
|
t.Errorf("a ko-source book compiled %d source scripts, want %d", len(ko), len(lang.LangScripts("ko")))
|
|
}
|
|
if len(ja) == len(ko) {
|
|
t.Errorf("two books with different source languages compiled the SAME source scripts (%d tables) — "+
|
|
"the run is using a constant, and no assertion about one book alone can see that", len(ja))
|
|
}
|
|
}
|