textmachine/backend/internal/pipeline/memory_f1_test.go

99 lines
4.5 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 (
"testing"
"textmachine/backend/internal/store"
)
// memory_f1_test.go covers the F1 content-hash (D8): memoryVersion is a pure function of
// the frozen APPROVED rows + the algorithm versions. A change to the approved glossary
// re-pins loudly; auto/draft churn does not; the row id never leaks in.
func TestMemoryVersionStableAndSensitive(t *testing.T) {
base := []store.GlossaryEntry{
gl("阿Q", "А-кью", "name", "approved"),
gl("鲁镇", "Лучжэнь", "", "approved"),
}
v0 := computeMemoryVersion(base, false)
// Identical rows → identical version (idempotent; drift-proof content hash).
if computeMemoryVersion(base, false) != v0 {
t.Fatal("memoryVersion is not stable for identical rows")
}
// Changing an APPROVED dst → different version (a loud --resnapshot).
changed := []store.GlossaryEntry{
gl("阿Q", "А-Кью (изменено)", "name", "approved"),
gl("鲁镇", "Лучжэнь", "", "approved"),
}
if computeMemoryVersion(changed, false) == v0 {
t.Error("changing an approved dst did not change memoryVersion — F1 would silently regress")
}
// Adding an alias to an approved row → different version (aliases affect matching).
withAlias := []store.GlossaryEntry{
func() store.GlossaryEntry { e := gl("阿Q", "А-кью", "name", "approved"); e.Aliases = []store.GlossaryAlias{alias("阿桂")}; return e }(),
gl("鲁镇", "Лучжэнь", "", "approved"),
}
if computeMemoryVersion(withAlias, false) == v0 {
t.Error("adding an alias to an approved row did not change memoryVersion")
}
// Changing an AUTO row → SAME version (approved-only, D8; auto changes are caught by
// the per-chunk content_hash instead).
withAuto := append(append([]store.GlossaryEntry{}, base...), gl("小D", "Малыш Дэ", "", "auto"))
if computeMemoryVersion(withAuto, false) != v0 {
t.Error("an auto row changed memoryVersion — it must hash only approved rows (D8)")
}
withAuto2 := append(append([]store.GlossaryEntry{}, base...), gl("小D", "Другой Дэ", "", "auto"))
if computeMemoryVersion(withAuto, false) != computeMemoryVersion(withAuto2, false) {
t.Error("editing an auto row changed memoryVersion — auto must not enter the F1 hash")
}
}
// TestMemoryVersionIgnoresID: the autoincrement glossary.id is fresh on every replace;
// it must NOT feed the hash (else a re-seed of identical content would false-resnapshot).
func TestMemoryVersionIgnoresID(t *testing.T) {
a := []store.GlossaryEntry{{BookID: "b", ID: 1, Src: "甲", Dst: "А", Status: "approved"}}
b := []store.GlossaryEntry{{BookID: "b", ID: 999, Src: "甲", Dst: "А", Status: "approved"}}
if computeMemoryVersion(a, false) != computeMemoryVersion(b, false) {
t.Error("memoryVersion depends on glossary.id — a re-seed would false-resnapshot")
}
}
// TestMaterializeMemoryVersionMatches: the bank's Version() equals computeMemoryVersion
// of its input rows (the runner hashes the same thing it matches on).
func TestMaterializeMemoryVersionMatches(t *testing.T) {
rows := specGlossary()
b := materializeMemory(rows, false)
if b.Version() != computeMemoryVersion(rows, false) {
t.Error("bank Version() diverges from computeMemoryVersion of its rows")
}
}
// TestMemoryVersionGateFoldsUnapprovedDecl covers self-review #4: when the post-check
// hard gate is ON, editing a DRAFT/auto row's decl (which changes the gate's resolved
// disposition but is in NEITHER content_hash NOR the approved-only hash) MUST change
// memoryVersion — else a $0 resume silently flips a chunk. Gate OFF: it must NOT change
// it (D8 approved-only holds; decl affects only the recomputed retrieval-state).
func TestMemoryVersionGateFoldsUnapprovedDecl(t *testing.T) {
mk := func(declForm string) []store.GlossaryEntry {
e := gl("鈴木", "Судзуки", "", "draft")
e.Decl = declJSON(false, declForm)
return []store.GlossaryEntry{gl("阿Q", "А-кью", "", "approved"), e}
}
a := mk("Судзуки")
b := mk("Судзуку") // only the DRAFT decl form changed (dst unchanged, status draft)
if computeMemoryVersion(a, false) != computeMemoryVersion(b, false) {
t.Error("gate OFF: a draft decl change must NOT move memoryVersion (D8 approved-only)")
}
if computeMemoryVersion(a, true) == computeMemoryVersion(b, true) {
t.Error("gate ON: a draft decl change MUST move memoryVersion (else a resume silently flips the chunk)")
}
// Toggling the gate itself also changes the version (a mode change is a loud re-pin).
if computeMemoryVersion(a, false) == computeMemoryVersion(a, true) {
t.Error("toggling the post-check gate must change memoryVersion")
}
}