82 lines
4.4 KiB
Go
82 lines
4.4 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// regressionguard_test.go — the post-reflow regression guard (D38). Precision over recall: each
|
||
// flagger asserts BOTH firing on a real regression AND non-firing on a legitimate reflow.
|
||
|
||
func TestRegressionGuardLengthCollapse(t *testing.T) {
|
||
// A draft well over the min-chars floor, halved by the "reflow" → collapse fires.
|
||
draft := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 12) // ~560 non-space chars
|
||
shortFinal := "Судзуки шёл."
|
||
if r := runRegressionGuard(draft, shortFinal); r.LengthCollapse == 0 {
|
||
t.Errorf("expected a length-collapse flag (draft≫final)")
|
||
}
|
||
// A legitimate reflow that merges lines keeps ~all the content → must NOT fire.
|
||
sameFinal := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 11)
|
||
if r := runRegressionGuard(draft, sameFinal); r.LengthCollapse != 0 {
|
||
t.Errorf("length-collapse FALSE POSITIVE on a near-equal reflow: %v", r.Detail)
|
||
}
|
||
// A short draft below the floor must never fire (a % ratio is meaningless there).
|
||
if r := runRegressionGuard("Он ушёл.", "Он."); r.LengthCollapse != 0 {
|
||
t.Errorf("length-collapse fired below the min-chars floor: %v", r.Detail)
|
||
}
|
||
}
|
||
|
||
func TestRegressionGuardNumberDrift(t *testing.T) {
|
||
// 四成四=44% rendered as «44» in the draft, drifted to a word form in the final → 44 disappears.
|
||
if r := runRegressionGuard("У него было 44 процента запаса.", "У него было четыре десятых запаса."); r.NumberDrift == 0 {
|
||
t.Errorf("expected a number-drift flag (44 vanished)")
|
||
}
|
||
// A number APPEARING in the final that was not in the draft (hallucinated figure).
|
||
if r := runRegressionGuard("Он собрал войско.", "Он собрал войско из 3000 солдат."); r.NumberDrift == 0 {
|
||
t.Errorf("expected a number-drift flag (3000 appeared)")
|
||
}
|
||
clean := []struct{ draft, final string }{
|
||
// The multi-digit number is preserved across the reflow (44 on both sides).
|
||
{"У него было 44 процента.", "У него имелось 44 процента запаса."},
|
||
// Grouped-triple vs joined form of the SAME number must not drift (10 000 ≡ 10000).
|
||
{"Армия в 10 000 воинов.", "Армия насчитывала 10000 воинов."},
|
||
// SINGLE digits are excluded (routinely spelled out), so «5»→«пять» is not a drift.
|
||
{"У него было 5 мечей.", "У него было пять мечей."},
|
||
// No numbers at all — never fires.
|
||
{"Судзуки шёл по коридору академии магии.", "Судзуки медленно шёл по длинному коридору."},
|
||
}
|
||
for _, c := range clean {
|
||
if r := runRegressionGuard(c.draft, c.final); r.NumberDrift != 0 {
|
||
t.Errorf("number-drift FALSE POSITIVE on %q→%q: %v", c.draft, c.final, r.Detail)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestRunCheapGatesRegressionOptIn pins that the guard is OFF by default and folds into the
|
||
// observability total only when enabled (a book that does not opt in serialises identically).
|
||
func TestRunCheapGatesRegressionOptIn(t *testing.T) {
|
||
draft := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 12)
|
||
final := "Судзуки шёл."
|
||
off := runCheapGates("", draft, final, cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}})
|
||
if off.LengthCollapse != 0 || off.NumberDrift != 0 {
|
||
t.Errorf("regression guard fired while disabled: %+v", off)
|
||
}
|
||
on := runCheapGates("", draft, final, cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}, regressionEnabled: true})
|
||
if on.LengthCollapse == 0 {
|
||
t.Errorf("regression guard did not fire while enabled: %+v", on)
|
||
}
|
||
if on.total() <= off.total() {
|
||
t.Errorf("enabled guard must raise the observability total: off=%d on=%d", off.total(), on.total())
|
||
}
|
||
}
|
||
|
||
// TestRegressionGuardDeterministic — a pure function of (draft, final), resume-safe.
|
||
func TestRegressionGuardDeterministic(t *testing.T) {
|
||
d := strings.Repeat("текст с числом 44 и 128. ", 20)
|
||
f := "короткий финал без чисел"
|
||
a, b := runRegressionGuard(d, f), runRegressionGuard(d, f)
|
||
if a.LengthCollapse != b.LengthCollapse || a.NumberDrift != b.NumberDrift ||
|
||
strings.Join(a.Detail, "|") != strings.Join(b.Detail, "|") {
|
||
t.Fatalf("runRegressionGuard not deterministic: %+v vs %+v", a, b)
|
||
}
|
||
}
|