textmachine/backend/internal/store/ruby_test.go

87 lines
3 KiB
Go

package store
import (
"reflect"
"testing"
)
func TestRubyReadingsUpsertAndRead(t *testing.T) {
s, _ := openTemp(t)
rows := []RubyReading{
{BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 2, Occurrences: 5},
{BookID: "b", Base: "東京", Reading: "とうきょう", FirstChapter: 1, Occurrences: 3},
{BookID: "other", Base: "京", Reading: "きょう", FirstChapter: 1, Occurrences: 1},
}
for _, rr := range rows {
if err := s.UpsertRubyReading(rr); err != nil {
t.Fatal(err)
}
}
got, err := s.RubyReadingsForBook("b")
if err != nil {
t.Fatal(err)
}
// Ordered by (first_chapter, base, reading): 東京(ch1) before 漢字(ch2).
want := []RubyReading{
{BookID: "b", Base: "東京", Reading: "とうきょう", FirstChapter: 1, Occurrences: 3},
{BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 2, Occurrences: 5},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("read = %#v, want %#v (other book must not leak)", got, want)
}
}
func TestRubyReadingsUpsertIsIdempotentAndConverges(t *testing.T) {
s, _ := openTemp(t)
rr := RubyReading{BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 3, Occurrences: 4}
if err := s.UpsertRubyReading(rr); err != nil {
t.Fatal(err)
}
// Re-ingesting the IDENTICAL full-book aggregate is a no-op (idempotent): one row,
// same values, occurrences never doubled.
if err := s.UpsertRubyReading(rr); err != nil {
t.Fatal(err)
}
got, err := s.RubyReadingsForBook("b")
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].FirstChapter != 3 || got[0].Occurrences != 4 {
t.Fatalf("identical re-upsert must be a no-op: %#v", got)
}
// A re-ingest after a source edit (e.g. a foreword inserted) moves the pair's
// first appearance to ch 4 and changes the count. persistRuby recomputes the
// authoritative full-book aggregate, so the store must REPLACE BOTH columns —
// it must NOT MIN-pin first_chapter to the stale-early 3 where the pair no
// longer occurs (self-review finding).
if err := s.UpsertRubyReading(RubyReading{BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 4, Occurrences: 2}); err != nil {
t.Fatal(err)
}
got, err = s.RubyReadingsForBook("b")
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].FirstChapter != 4 || got[0].Occurrences != 2 {
t.Fatalf("re-ingest must REPLACE (converge) both columns, not MIN-pin: %#v", got)
}
}
func TestRubyReadingsVariantReadingsAreDistinctRows(t *testing.T) {
// The same base with two readings (variant yomi) are two rows, not a collision.
s, _ := openTemp(t)
for _, rr := range []RubyReading{
{BookID: "b", Base: "海", Reading: "うみ", FirstChapter: 1, Occurrences: 2},
{BookID: "b", Base: "海", Reading: "かい", FirstChapter: 4, Occurrences: 1},
} {
if err := s.UpsertRubyReading(rr); err != nil {
t.Fatal(err)
}
}
got, err := s.RubyReadingsForBook("b")
if err != nil {
t.Fatal(err)
}
if len(got) != 2 {
t.Fatalf("variant readings must be distinct rows, got %#v", got)
}
}