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") } }