textmachine/backend/internal/store/glossary_test.go

153 lines
5.2 KiB
Go
Raw Permalink 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 store
import "testing"
func gEntry(src, dst, status string, aliases ...string) GlossaryEntry {
e := GlossaryEntry{BookID: "book", Src: src, Dst: dst, Status: status, Source: "seed"}
for _, a := range aliases {
e.Aliases = append(e.Aliases, GlossaryAlias{Alias: a, AliasType: "прозвище"})
}
return e
}
func TestReplaceGlossaryIdempotentAndScoped(t *testing.T) {
s, _ := openTemp(t)
entries := []GlossaryEntry{
gEntry("阿Q", "А-кью", "approved"),
gEntry("四叔", "Четвёртый дядюшка", "approved", "鲁四老爷"),
}
if err := s.ReplaceGlossary("book", entries); err != nil {
t.Fatal(err)
}
// Another book is untouched.
if err := s.ReplaceGlossary("other", []GlossaryEntry{{BookID: "other", Src: "x", Dst: "икс", Status: "approved"}}); err != nil {
t.Fatal(err)
}
got1, err := s.GlossaryForBook("book")
if err != nil {
t.Fatal(err)
}
if len(got1) != 2 {
t.Fatalf("want 2 entries, got %d", len(got1))
}
// Re-replacing the identical set yields the identical content (idempotent).
if err := s.ReplaceGlossary("book", entries); err != nil {
t.Fatal(err)
}
got2, err := s.GlossaryForBook("book")
if err != nil {
t.Fatal(err)
}
if len(got2) != 2 {
t.Fatalf("re-replace changed count: %d", len(got2))
}
// The alias survived the replace and attached to the right term.
var sishu *GlossaryEntry
for i := range got2 {
if got2[i].Src == "四叔" {
sishu = &got2[i]
}
}
if sishu == nil || len(sishu.Aliases) != 1 || sishu.Aliases[0].Alias != "鲁四老爷" {
t.Fatalf("alias not preserved: %+v", sishu)
}
// other book still has its one row.
other, _ := s.GlossaryForBook("other")
if len(other) != 1 {
t.Fatalf("scope leak: other book has %d rows", len(other))
}
}
func TestReplaceGlossaryRemovesDroppedRows(t *testing.T) {
s, _ := openTemp(t)
if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("阿Q", "А-кью", "approved"), gEntry("四叔", "дядюшка", "approved")}); err != nil {
t.Fatal(err)
}
// Drop one term: full replace must make it disappear (no phantom).
if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("阿Q", "А-кью", "approved")}); err != nil {
t.Fatal(err)
}
got, _ := s.GlossaryForBook("book")
if len(got) != 1 || got[0].Src != "阿Q" {
t.Fatalf("dropped term still present: %+v", got)
}
}
func TestGlossaryRevisionLogged(t *testing.T) {
s, _ := openTemp(t)
if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("王胡", "Бородатый Ван", "approved")}); err != nil {
t.Fatal(err)
}
// Change the approved rendering: a B1 editorial revision must be journaled.
if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("王胡", "Ван Ху", "approved")}); err != nil {
t.Fatal(err)
}
revs, err := s.GlossaryRevisionsForBook("book")
if err != nil {
t.Fatal(err)
}
if len(revs) != 1 || revs[0].OldDst != "Бородатый Ван" || revs[0].NewDst != "Ван Ху" {
t.Fatalf("revision not logged correctly: %+v", revs)
}
// Re-replacing identical does NOT add a second revision.
if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("王胡", "Ван Ху", "approved")}); err != nil {
t.Fatal(err)
}
revs, _ = s.GlossaryRevisionsForBook("book")
if len(revs) != 1 {
t.Fatalf("idempotent replace added a spurious revision: %d", len(revs))
}
}
func TestUniqueSrcSenseWindow(t *testing.T) {
s, _ := openTemp(t)
// Same src+sense+window twice → UNIQUE violation (B2).
dup := []GlossaryEntry{gEntry("道", "путь", "approved"), gEntry("道", "дао", "approved")}
if err := s.ReplaceGlossary("book", dup); err == nil {
t.Fatal("expected UNIQUE(src,sense,window) violation for duplicate src+sense+window")
}
// Same src, different SENSE → allowed (polysemy, A3).
ok := []GlossaryEntry{
{BookID: "book", Src: "道", Dst: "путь", Sense: "way", Status: "approved"},
{BookID: "book", Src: "道", Dst: "дао", Sense: "dao", Status: "approved"},
}
if err := s.ReplaceGlossary("book", ok); err != nil {
t.Fatalf("distinct senses should be allowed: %v", err)
}
got, _ := s.GlossaryForBook("book")
if len(got) != 2 {
t.Fatalf("want 2 polysemy rows, got %d", len(got))
}
}
func TestRetrievalStateUpsert(t *testing.T) {
s, _ := openTemp(t)
rs := RetrievalState{BookID: "book", Chapter: 1, ChunkIdx: 0, SnapshotID: "snap",
NExactHits: 3, NAmbiguousFlagged: 1, NSpoilerBlocked: 1, NPostcheckMiss: 1,
PostcheckDetail: `[{"src":"阿Q","dst":"А-кью"}]`, InjectedIDs: `["ah_q"]`}
if err := s.UpsertRetrievalState(rs); err != nil {
t.Fatal(err)
}
got, err := s.GetRetrievalState("book", 1, 0)
if err != nil || got == nil {
t.Fatalf("get: %v %v", got, err)
}
if got.NExactHits != 3 || got.NSpoilerBlocked != 1 || got.NPostcheckMiss != 1 {
t.Fatalf("roundtrip mismatch: %+v", got)
}
// Overwrite (resolve semantics on resume): new counts win.
rs.NExactHits = 5
rs.NPostcheckMiss = 0
if err := s.UpsertRetrievalState(rs); err != nil {
t.Fatal(err)
}
got, _ = s.GetRetrievalState("book", 1, 0)
if got.NExactHits != 5 || got.NPostcheckMiss != 0 {
t.Fatalf("overwrite failed: %+v", got)
}
all, _ := s.RetrievalStatesForBook("book")
if len(all) != 1 {
t.Fatalf("want 1 retrieval-state row, got %d", len(all))
}
}