153 lines
5.2 KiB
Go
153 lines
5.2 KiB
Go
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))
|
||
}
|
||
}
|