125 lines
5.2 KiB
Go
125 lines
5.2 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
)
|
|
|
|
// voice_test.go: the pack-19 schema, on the two axes that can only be checked by execution — that an
|
|
// EXISTING database upgrades onto it, and that the bank's three record types are replaced as one unit.
|
|
|
|
// The migration that matters is not the fresh one (every test creates that) but the UPGRADE: a stand
|
|
// database already at an older version must gain the pack-19 tables and columns without losing a row.
|
|
func TestMigrationUpgradesAnOlderDatabase(t *testing.T) {
|
|
const olderVersion = 12 // the schema head before pack-19 (v13 tables + v14 retrieval-state columns)
|
|
if len(migrations) <= olderVersion {
|
|
t.Fatalf("this test assumes pack-19 appended to a %d-step schema, got %d", olderVersion, len(migrations))
|
|
}
|
|
path := t.TempDir() + "/old.db"
|
|
// Build a database at the OLD head by applying only the old steps through a raw connection.
|
|
raw, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := raw.Exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < olderVersion; i++ {
|
|
if _, err := raw.Exec(migrations[i]); err != nil {
|
|
t.Fatalf("apply old migration %d: %v", i+1, err)
|
|
}
|
|
if _, err := raw.Exec(`INSERT INTO schema_version (version) VALUES (?)`, i+1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// A row written under the OLD schema must survive the upgrade untouched.
|
|
if _, err := raw.Exec(`INSERT INTO glossary (book_id, src, dst, status) VALUES ('b','旧','старое','approved')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Premise check: at the OLD head the pack-19 schema must genuinely be absent, or this test would
|
|
// prove nothing about upgrading. A successful query means it is already there.
|
|
mustNotQuery := func(query, why string) {
|
|
t.Helper()
|
|
rows, qerr := raw.Query(query)
|
|
if qerr != nil {
|
|
return // the column/table is absent — exactly the premise this test needs
|
|
}
|
|
defer rows.Close()
|
|
if rows.Err() == nil {
|
|
t.Fatal(why)
|
|
}
|
|
}
|
|
mustNotQuery(`SELECT n_voice_flags FROM retrieval_state`,
|
|
"the old schema already has the pack-19 columns — the upgrade is not being exercised")
|
|
mustNotQuery(`SELECT 1 FROM voice_profiles`,
|
|
"the old schema already has voice_profiles — the upgrade is not being exercised")
|
|
if err := raw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
s, err := Open(path) // runs the pending migrations
|
|
if err != nil {
|
|
t.Fatalf("an existing database must upgrade onto the pack-19 schema: %v", err)
|
|
}
|
|
defer s.Close()
|
|
|
|
rows, err := s.GlossaryForBook("b")
|
|
if err != nil || len(rows) != 1 || rows[0].Dst != "старое" {
|
|
t.Fatalf("the pre-existing row must survive the upgrade: %v %+v", err, rows)
|
|
}
|
|
// The new tables and columns are usable, not merely present.
|
|
if err := s.ReplaceBank("b", rows,
|
|
[]VoiceProfile{{Src: "旧", SelfRef: "ваша служанка"}},
|
|
[]AddressPair{{SpeakerSrc: "旧", AddresseeSrc: "новое", Register: "formal"}}); err != nil {
|
|
t.Fatalf("write to the upgraded schema: %v", err)
|
|
}
|
|
if v, err := s.VoiceProfilesForBook("b"); err != nil || len(v) != 1 {
|
|
t.Fatalf("voice_profiles unusable after the upgrade: %v %+v", err, v)
|
|
}
|
|
if err := s.UpsertRetrievalState(RetrievalState{
|
|
BookID: "b", Chapter: 1, ChunkIdx: 0, SnapshotID: "s", NVoiceFlags: 3, VoiceDetail: `{"replies":2}`,
|
|
NSpoilerLeaks: 1, SpoilerLeakDetail: `[{"src":"旧"}]`,
|
|
}); err != nil {
|
|
t.Fatalf("write the new retrieval-state columns after the upgrade: %v", err)
|
|
}
|
|
got, err := s.GetRetrievalState("b", 1, 0)
|
|
if err != nil || got == nil || got.NVoiceFlags != 3 || got.NSpoilerLeaks != 1 || got.VoiceDetail == "" {
|
|
t.Fatalf("the new columns did not round-trip after the upgrade: %v %+v", err, got)
|
|
}
|
|
}
|
|
|
|
// The bank is replaced as ONE unit: a re-seed that drops a profile must drop it from the store too, or a
|
|
// stale row would keep feeding the flagger a character the seed no longer signs.
|
|
func TestReplaceBankIsAFullReplaceOfAllThreeRecordTypes(t *testing.T) {
|
|
s, _ := openTemp(t)
|
|
terms := []GlossaryEntry{{BookID: "b", Src: "方源", Dst: "Фан Юань", Status: "approved"}}
|
|
if err := s.ReplaceBank("b", terms,
|
|
[]VoiceProfile{{Src: "方源", SelfRef: "первый"}, {Src: "白凝冰", SelfRef: "второй"}},
|
|
[]AddressPair{{SpeakerSrc: "方源", AddresseeSrc: "白凝冰", Register: "formal"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, _ := s.VoiceProfilesForBook("b"); len(v) != 2 {
|
|
t.Fatalf("setup: want 2 profiles, got %d", len(v))
|
|
}
|
|
// Re-seed with ONE profile and no pairs at all.
|
|
if err := s.ReplaceBank("b", terms, []VoiceProfile{{Src: "方源", SelfRef: "первый"}}, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v, err := s.VoiceProfilesForBook("b")
|
|
if err != nil || len(v) != 1 || v[0].Src != "方源" {
|
|
t.Fatalf("the dropped profile lingered: %v %+v", err, v)
|
|
}
|
|
if p, _ := s.AddressPairsForBook("b"); len(p) != 0 {
|
|
t.Fatalf("the dropped address pairs lingered: %+v", p)
|
|
}
|
|
// Another book's content is untouched by a replace scoped to this one.
|
|
if err := s.ReplaceBank("other", nil, []VoiceProfile{{Src: "X", SelfRef: "чужой"}}, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReplaceBank("b", terms, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o, _ := s.VoiceProfilesForBook("other"); len(o) != 1 {
|
|
t.Fatalf("a replace must be scoped to its book, got %+v", o)
|
|
}
|
|
}
|