textmachine/backend/internal/langscreen/langscreen_test.go

172 lines
8.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 langscreen
import (
"fmt"
"strconv"
"strings"
"testing"
"unicode"
)
func cyr() []*unicode.RangeTable { return []*unicode.RangeTable{unicode.Cyrillic} }
// pad repeats s until it clears the evidence floor, so a case can be about the SHARE without also being
// about the length.
func pad(s string, letters int) string {
var b strings.Builder
for n := 0; n < letters; n += len([]rune(s)) {
b.WriteString(s)
}
return b.String()
}
func TestTheScreenAnswersTheClosedSet(t *testing.T) {
for _, tc := range []struct {
name string
text string
want Verdict
}{
{"target prose", pad("текстанаязыке", 400), OnTarget},
{"a foreign completion", pad("englishtextinstead", 400), OffTarget},
{"the source echoed back", pad("源文本汉字", 400), OffTarget},
{"too short to judge", "коротко", Abstain},
} {
t.Run(tc.name, func(t *testing.T) {
if got := Screen(tc.text, cyr()).Verdict; got != tc.want {
t.Fatalf("verdict %q, want %q", got, tc.want)
}
})
}
}
// TestATargetWithNoDataIsNeverJudged is the layer-7 discipline and the review question of the whole
// project in one assertion: a pair the repository does not carry must be INERT, not condemned. The screen
// abstains on an empty script set however foreign the text looks, so adding a pair is a data row and never
// an edit here.
func TestATargetWithNoDataIsNeverJudged(t *testing.T) {
r := Screen(pad("englishtextinstead", 400), nil)
if r.Verdict != Abstain {
t.Fatalf("a target that declares no script must not be judged: %+v", r)
}
if r.Share != 0 {
t.Fatalf("an abstention must carry no score to be misread as one: %+v", r)
}
}
// TestTheEvidenceFloorIsAFloorAndNotAVerdict pins the abstain boundary on both sides. Below it the screen
// says nothing — never «off target», which on a chapter heading would be a flag on a healthy unit — and one
// letter above it the same text is judged normally.
func TestTheEvidenceFloorIsAFloorAndNotAVerdict(t *testing.T) {
foreign := strings.Repeat("a", MinLetters-1)
if got := Screen(foreign, cyr()); got.Verdict != Abstain || got.Letters != MinLetters-1 {
t.Fatalf("under the floor the screen must abstain and say how little it saw: %+v", got)
}
if got := Screen(strings.Repeat("a", MinLetters), cyr()); got.Verdict != OffTarget {
t.Fatalf("at the floor the screen must judge: %+v", got)
}
// Non-letters carry no evidence and must not lift a text over the floor: a table of numbers is not
// 200 letters of anything.
padded := strings.Repeat("a", MinLetters-1) + strings.Repeat("1234567890 ", 100)
if got := Screen(padded, cyr()); got.Verdict != Abstain {
t.Fatalf("digits and spacing must not count as evidence: %+v", got)
}
}
// TestTheThresholdSitsInTheMeasuredGap holds the constant against the corpus it was placed from (backlog
// row 113): every off-target completion on the stand scores 0.0000 and the worst healthy one 0.9767. Both
// sides are asserted, because a threshold is only a threshold if BOTH are on the right side of it.
func TestTheThresholdSitsInTheMeasuredGap(t *testing.T) {
const worstHealthyMeasured, bestOffTargetMeasured = 0.9767, 0.0000
if OnTargetFloor >= worstHealthyMeasured {
t.Errorf("the floor %.4f is at or above the worst HEALTHY completion measured (%.4f): it would flag clean prose", OnTargetFloor, worstHealthyMeasured)
}
if OnTargetFloor <= bestOffTargetMeasured {
t.Errorf("the floor %.4f is at or below the best OFF-TARGET completion measured (%.4f): it would catch nothing", OnTargetFloor, bestOffTargetMeasured)
}
}
// TestTheScoreIsOverLettersOnly is the measurement rule made explicit. Sharing punctuation, digits and
// whitespace across languages is exactly why they are excluded: counted, a heavily formatted reply and a
// dense one would be judged on different scales.
func TestTheScoreIsOverLettersOnly(t *testing.T) {
letters := pad("текст", 400)
plain := Screen(letters, cyr())
noisy := Screen(strings.Join(strings.Split(letters, ""), " — 42, "), cyr())
if plain.Share != noisy.Share || plain.Letters != noisy.Letters {
t.Fatalf("formatting changed the score: %+v vs %+v", plain, noisy)
}
}
// TestTheSupersetLimitIsRealAndNamed asserts the limitation the package doccomment declares, because a
// limit that is only written down is a limit somebody will later be surprised by. A target whose declared
// scripts CONTAIN another language's — ja declares han — cannot tell that language's output apart here.
func TestTheSupersetLimitIsRealAndNamed(t *testing.T) {
ja := []*unicode.RangeTable{unicode.Han, unicode.Hiragana, unicode.Katakana}
if got := Screen(pad("源文本汉字", 400), ja).Verdict; got != OnTarget {
t.Fatalf("the doccomment says a purely Chinese output scores 1.000 for a ja target; it now says %q — if the rule gained a datum, say so there", got)
}
}
// TestTheVersionActuallyTracksTheCalibration closes three plantings that the interval pins could not see,
// and it is the difference between a mechanism and a claim about one.
//
// ⚠ Version's own doc comment says it is derived «by construction» so that a slid threshold takes a loud
// --resnapshot with it. Nothing checked that. Planted: the var rewritten as the LITERAL it currently
// prints, then the floor slid 0.50 → 0.45 — the string stayed byte-identical, the golden stayed
// byte-identical, both interval pins passed (0.45 is inside the empty band), and the whole pipeline package
// was green. A resumed chunk would be re-verdicted under a threshold nobody re-billed for. The property was
// documented, believed, and held by nothing.
func TestTheVersionActuallyTracksTheCalibration(t *testing.T) {
want := fmt.Sprintf("langscreen-v1-target-script-share+floor%.2f+min%d", OnTargetFloor, MinLetters)
if Version != want {
t.Fatalf("Version has stopped tracking the constants it names:\n is: %s\n want: %s\n"+
"A literal here means a calibration change moves no snapshot, and a resumed chunk is re-judged "+
"by a rule nobody paid to change.", Version, want)
}
// ⚠ AND THE PRECISION WINDOW, which is the same hole one decimal down. `%.2f` cannot express a floor of
// 0.504: planted, that slide rendered the identical `floor0.50` and survived everything above. The fix
// is NOT to widen the format — that would change the shipped string and cost a --resnapshot of every
// book for a rule that did not change. It is to refuse a floor the version cannot express. A future
// calibration to three decimals is then a deliberate act: widen the format AND take the re-snapshot.
// The test is a ROUND TRIP, not a string comparison: what matters is whether the printed floor still
// denotes the same number, not whether it is spelled the same way (`%.2f` of 0.5 is "0.50", and that is
// fine — it reads back as 0.5). A floor of 0.504 also prints "0.50" and reads back as 0.5, which is not.
printed := fmt.Sprintf("%.2f", OnTargetFloor)
back, err := strconv.ParseFloat(printed, 64)
if err != nil {
t.Fatalf("the version's floor field %q does not parse: %v", printed, err)
}
if back != OnTargetFloor {
t.Fatalf("OnTargetFloor is %v but the version can only say %s, which reads back as %v — a change "+
"inside the last decimal would move no snapshot at all. Either keep the floor at two decimals, "+
"or widen Version's format and take the --resnapshot deliberately.", OnTargetFloor, printed, back)
}
}
// TestTheFloorIsAFloorAndNotACeiling pins the BOUNDARY comparison, which no constant expresses and which
// therefore moves nothing when it changes: planted, `share < OnTargetFloor` → `share <= OnTargetFloor`
// survived the whole pipeline package, because no fixture sat exactly on the line. The rule is «below the
// floor is off target», so a completion sitting EXACTLY on it is on target.
func TestTheFloorIsAFloorAndNotACeiling(t *testing.T) {
// Exactly 200 letters — the evidence floor — of which exactly 100 are Cyrillic: share is 0.50 exactly,
// with no floating-point slack (100/200 is representable).
text := strings.Repeat("а", 100) + strings.Repeat("z", 100)
got := Screen(text, cyr())
if got.Letters != 200 {
t.Fatalf("premise broken: the fixture must weigh exactly 200 letters, got %d", got.Letters)
}
if got.Share != OnTargetFloor {
t.Fatalf("premise broken: the fixture must sit exactly ON the floor, got %v", got.Share)
}
if got.Verdict != OnTarget {
t.Fatalf("a share exactly AT the floor must be on-target — the rule is «below the floor», and a "+
"comparison that quietly became «at or below» moves neither a constant nor the version: %+v", got)
}
// One letter less on target and it falls: the assertion above must not be passing because the screen
// says OnTarget to everything.
below := strings.Repeat("а", 99) + strings.Repeat("z", 101)
if b := Screen(below, cyr()); b.Verdict != OffTarget {
t.Fatalf("one letter below the floor must be off-target, got %+v", b)
}
}