package store import "testing" // waveselection_test.go: the per-wave record (schema v17). These are store-level because the property being // pinned is the UPSERT's, and a pipeline fixture cannot reach it: a re-run of the same book re-derives an // IDENTICAL selection, so every column could be dropped from the update clause and the round trip would // still agree with itself. The acceptance found exactly that hole. func waveRow(wave string, hits int, srcs string) WaveSelection { return WaveSelection{BookID: "book", Chapter: 1, ChunkIdx: 0, Wave: wave, SnapshotID: "snap", NExactHits: hits, InjectedSrcs: srcs} } // TestEveryColumnOfAWaveRecordIsRewrittenOnConflict pins that a re-derived selection REPLACES the stored // one, column by column. The record is recomputed on every run, so a column left out of the update clause // keeps the first run's value forever — and `injected_srcs` in particular is the attribution this table was // built for: which rows the editor was actually shown. A stale list answers the question wrongly and looks // exactly like a fresh one. func TestEveryColumnOfAWaveRecordIsRewrittenOnConflict(t *testing.T) { s, _ := openTemp(t) first := waveRow(WaveEdit, 3, `["方源"]`) first.NSticky, first.NAmbiguousFlagged, first.NSpoilerBlocked, first.NEvicted = 1, 2, 3, 4 first.EvictedSrcs = `["古月"]` if err := s.UpsertWaveSelection(first); err != nil { t.Fatal(err) } // A LATER run over the same position that saw something different — the case a same-book re-run can // never produce, and therefore the case no pipeline fixture can pin. second := waveRow(WaveEdit, 9, `["方源","蛊师"]`) second.SnapshotID = "snap2" second.NSticky, second.NAmbiguousFlagged, second.NSpoilerBlocked, second.NEvicted = 8, 7, 6, 5 second.EvictedSrcs = `["白家寨"]` if err := s.UpsertWaveSelection(second); err != nil { t.Fatal(err) } got, err := s.WaveSelectionsForBook("book") if err != nil { t.Fatal(err) } if len(got) != 1 { t.Fatalf("the same position and wave must hold ONE row, not accumulate: got %d", len(got)) } g := got[0] for _, c := range []struct { col string got, want any }{ {"snapshot_id", g.SnapshotID, second.SnapshotID}, {"n_exact_hits", g.NExactHits, second.NExactHits}, {"n_sticky", g.NSticky, second.NSticky}, {"n_ambiguous_flagged", g.NAmbiguousFlagged, second.NAmbiguousFlagged}, {"n_spoiler_blocked", g.NSpoilerBlocked, second.NSpoilerBlocked}, {"n_evicted", g.NEvicted, second.NEvicted}, {"injected_srcs", g.InjectedSrcs, second.InjectedSrcs}, {"evicted_srcs", g.EvictedSrcs, second.EvictedSrcs}, } { if c.got != c.want { t.Errorf("%s was not rewritten by the later run: got %v, want %v — a column missing from the update clause keeps the first run's value forever", c.col, c.got, c.want) } } } // TestTheTwoWavesAreSeparateRowsAtOnePosition pins the axis the table exists for. The draft and the editor // select over different banks at the SAME chapter and chunk; if the wave were not in the key they would // overwrite each other and the schema would be no better than the one-row-per-chunk table it was added // beside. func TestTheTwoWavesAreSeparateRowsAtOnePosition(t *testing.T) { s, _ := openTemp(t) if err := s.UpsertWaveSelection(waveRow(WaveDraft, 2, `["方源"]`)); err != nil { t.Fatal(err) } if err := s.UpsertWaveSelection(waveRow(WaveEdit, 5, `["方源","蛊师"]`)); err != nil { t.Fatal(err) } got, err := s.WaveSelectionsForBook("book") if err != nil { t.Fatal(err) } if len(got) != 2 { t.Fatalf("one position, two waves, two rows: got %d — %+v", len(got), got) } by := map[string]WaveSelection{} for _, w := range got { by[w.Wave] = w } if by[WaveDraft].NExactHits != 2 || by[WaveEdit].NExactHits != 5 { t.Errorf("the waves overwrote each other: draft=%d edit=%d, want 2 and 5", by[WaveDraft].NExactHits, by[WaveEdit].NExactHits) } if by[WaveDraft].InjectedSrcs == by[WaveEdit].InjectedSrcs { t.Errorf("both waves report the same injected rows (%s) — the two select over different banks and the record must be able to say so", by[WaveDraft].InjectedSrcs) } }