package pipeline import ( "strings" "testing" "textmachine/backend/internal/checks" "textmachine/backend/internal/config" "textmachine/backend/internal/lang" "textmachine/backend/internal/langscreen" "textmachine/backend/internal/llm" ) // offtargetflag_test.go: the LIVE delivery of backlog row 46. The screen module is pure and pinned by its // own package; the corpus test pins the verdict OFFLINE, over rune accounting. Neither makes `classify()` // return the flag, and neither makes the runner hand it the data — so the whole path from «a completion // came back in the wrong language» to «the run flags it and escalates it» existed only as code. // // ⚠ THIS IS THE D39.187 SHAPE AND IT WAS FOUND BY PLANTING, not by reading: three separate mutations — // screening against nil scripts, dropping TargetScripts at the call site, and removing the flag from // escalatable() — each left the whole battery green. // TestAnOffTargetCompletionIsFlaggedAndEscalated is the verdict itself, through the real classifier. func TestAnOffTargetCompletionIsFlaggedAndEscalated(t *testing.T) { // A fluent English completion, past the evidence floor — the live class this flag exists for (two such // drafts shipped as `ok` on the stand, 4690 and 4811 letters). english := strings.Repeat("The librarian walked the long corridor and remembered the lecture. ", 8) if n := countLetters(english); n < langscreen.MinLetters { t.Fatalf("premise broken: the fixture must clear the evidence floor, got %d letters", n) } in := classifyInput{ Output: english, Finish: llm.FinishStop, TargetLang: "ru", SourceScripts: lang.LangScripts("zh"), TargetScripts: lang.LangScripts("ru"), } got := classify(in) if got.Reason != FlagOffTargetLang { t.Fatalf("an English completion for a →ru book must flag as off-target, got %q (%s)", got.Reason, got.Detail) } // The DETAIL is what an operator acts on: a bare flag with no score is the class of report this project // keeps replacing. if !strings.Contains(got.Detail, "target's script") { t.Errorf("the flag must say what it measured: %q", got.Detail) } // ⛔ AND IT MUST BE ESCALATABLE — a direct requirement of D39.93 п.2, and the whole reason the design // was NOT allowed to reuse FlagCJKArtifact: a named flag outside the whitelist is a chunk silently LOST // instead of re-attacked on another model. if !got.Reason.escalatable() { t.Fatal("FlagOffTargetLang must be in escalatable(): a deterministic content failure another model might fix (D39.93 п.2)") } if got.Reason.retryable() { t.Fatal("re-asking the SAME model buys the same answer; the flag must not be retryable") } // The echo rule still owns its own class: a source echo is named an echo, not «off target». The ORDER // is the diagnosis, and a screen that swallowed the echo class would make every echo unattributable. echoed := strings.Repeat("方源看着蛊,那是一只月光蛊。", 30) if r := classify(classifyInput{Output: echoed, Finish: llm.FinishStop, TargetLang: "ru", SourceScripts: lang.LangScripts("zh"), TargetScripts: lang.LangScripts("ru")}); r.Reason != FlagCJKArtifact { t.Fatalf("a source echo must still be named an echo, got %q", r.Reason) } } // TestTheScreenIsInertWithoutTargetData is the layer-7 discipline ON THE LIVE PATH: a pair whose target // nobody has authored a script row for must be INERT, not condemned. It is asserted here as well as in the // module because this is where the data is handed over, and handing over nothing is a live possibility. func TestTheScreenIsInertWithoutTargetData(t *testing.T) { english := strings.Repeat("The librarian walked the long corridor and remembered the lecture. ", 8) got := classify(classifyInput{Output: english, Finish: llm.FinishStop, TargetLang: "xx", SourceScripts: lang.LangScripts("zh"), TargetScripts: lang.LangScripts("xx")}) if got.Reason != reasonOK { t.Fatalf("a target that declares no script must not be judged, got %q", got.Reason) } } // TestTheRunnerHandsTheScreenItsData closes the seam between the two tests above: the classifier can be // perfectly correct and never receive the target's scripts. classifyOutput is the ONE call site the run // uses (live and replay both), so the assertion is made through it rather than through classify. func TestTheRunnerHandsTheScreenItsData(t *testing.T) { r := &Runner{Book: &config.Book{TargetLang: "ru", SourceLang: "zh"}, Pipeline: &config.Pipeline{}} r.checkers = checks.CompileCheckers(nil, lang.TargetChecksFor("ru")) r.checkers.SetSourceScripts(lang.LangScripts("zh")) english := strings.Repeat("The librarian walked the long corridor and remembered the lecture. ", 8) cls, _ := r.classifyOutput(roleTranslator, "方源看着蛊。", english, llm.FinishStop, true) if cls.Reason != FlagOffTargetLang { t.Fatalf("the runner must hand the screen the target's scripts; got %q (%s)", cls.Reason, cls.Detail) } // ⚠ AND THE BANK ROLES MUST STILL BE EXEMPT, through the same call. A term table's letters are engine // identifiers and source terms, so BOTH rules would fire on every healthy batch — the echo rule already // did, on the classifier, for as long as no shipping config ran it (§Б-105). table := strings.Repeat("元石\tterm\tnone\n方源\tname\tmale\n", 40) for _, role := range []string{roleTerminologist, roleClassifier} { if cls, _ := r.classifyOutput(role, "方源看着蛊。", table, llm.FinishStop, false); cls.Reason != reasonOK { t.Errorf("%s answers a term table and must be exempt from both rules, got %q (%s)", role, cls.Reason, cls.Detail) } } // The exemption is the role SET, not one role: the regression that leaves it at the terminologist alone // is exactly the state this pack found and fixed. if cls, _ := r.classifyOutput(roleClassifier, "方源看着蛊。", table, llm.FinishStop, false); cls.Reason != reasonOK { t.Errorf("the classifier's reply has the same shape as the terminologist's: %q", cls.Reason) } } // TestTheOffTargetThresholdIsUSED, not merely declared: a completion BETWEEN the two bounds decides which // side of the floor the rule reads, and without such a case the constant could be anything. func TestTheOffTargetThresholdIsUsed(t *testing.T) { for _, tc := range []struct { name string cyr, lat int want FlagReason }{ // 60% of the letters in the target's script — above the 0.50 floor, so the run keeps it. {"mostly target", 600, 400, reasonOK}, // 40% — below the floor. {"mostly foreign", 400, 600, FlagOffTargetLang}, } { t.Run(tc.name, func(t *testing.T) { out := strings.Repeat("а", tc.cyr) + strings.Repeat("a", tc.lat) got := classify(classifyInput{Output: out, Finish: llm.FinishStop, TargetLang: "ru", SourceScripts: lang.LangScripts("zh"), TargetScripts: lang.LangScripts("ru")}) if got.Reason != tc.want { t.Fatalf("share %.2f → %q, want %q (%s)", float64(tc.cyr)/float64(tc.cyr+tc.lat), got.Reason, tc.want, got.Detail) } }) } } func countLetters(s string) int { n := 0 for _, r := range s { if r >= 'A' { n++ } } return n }