157 lines
6.1 KiB
Go
157 lines
6.1 KiB
Go
package pipeline
|
|
|
|
// waveselection_test.go: the record of what each wave was shown (backlog row 417, schema v17).
|
|
//
|
|
// The fixture is a chapter that splits into TWO draft chunks and groups into ONE edit unit, with a bank term
|
|
// whose key occurs ONLY in the second member. That shape is what makes the pin sharp: the leader's draft row
|
|
// cannot contain that term — the draft selected over the leader's own chunk, where the key is absent — so an
|
|
// editor record that carries it could not have been copied from the draft's.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"textmachine/backend/internal/obs"
|
|
"textmachine/backend/internal/store"
|
|
)
|
|
|
|
// TestTheEditorsSelectionIsRecordedAndIsNotTheDrafts pins the question the paid run of 11.09 could not put
|
|
// to the database: what did the EDITOR see. Before the v17 table the answer lived nowhere — the unit merge
|
|
// wrote the editor's post-check onto the leader's draft row and left the draft's injection counts in place,
|
|
// so `injected_ids` at a leader named the draft's rows and nothing named the editor's.
|
|
func TestTheEditorsSelectionIsRecordedAndIsNotTheDrafts(t *testing.T) {
|
|
rec := &reqRec{}
|
|
srv := newJSONProvider(rec, draftEdit)
|
|
defer srv.Close()
|
|
// Two paragraphs sized as in TestWaveMultiChunkEditUnit: two draft chunks, one edit unit. 鈴木 is put in
|
|
// the SECOND paragraph only, so it cannot be in the leader chunk's own draft selection.
|
|
first := strings.Repeat("文", 1400) + "。"
|
|
second := strings.Repeat("字", 1090) + "鈴木は来た。"
|
|
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: first + "\n\n" + second, regenerate: 0, glossarySeed: `
|
|
terms:
|
|
- src: 鈴木
|
|
dst: Судзуки
|
|
type: name
|
|
status: approved
|
|
decl: { invariant: true, forms: ["Судзуки"] }
|
|
`})
|
|
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
|
|
|
r0 := newRunner(t, bookPath)
|
|
manifest, err := r0.bookChunks()
|
|
r0.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
units := buildEditUnits(manifest)
|
|
// PREMISE: without the 2-chunks-in-1-unit shape the two records would cover the same text and the
|
|
// comparison below would pass on a record that simply copied the draft's.
|
|
if len(manifest) != 2 || len(units) != 1 || len(units[0].Members) != 2 {
|
|
t.Fatalf("fixture must be 2 draft chunks in 1 edit unit, got %d chunks / %d units", len(manifest), len(units))
|
|
}
|
|
if strings.Contains(manifest[0].Text, "鈴木") || !strings.Contains(manifest[1].Text, "鈴木") {
|
|
t.Fatalf("premise broken: 鈴木 must occur in the SECOND member only — that is what makes the editor's record distinguishable")
|
|
}
|
|
|
|
r1 := newRunner(t, bookPath)
|
|
if _, err := r1.TranslateBook(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
waves, err := r1.Store.WaveSelectionsForBook(r1.Book.BookID)
|
|
r1.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
byKey := map[string]store.WaveSelection{}
|
|
for _, w := range waves {
|
|
byKey[w.Wave+":"+itoa(w.ChunkIdx)] = w
|
|
}
|
|
leaderDraft, ok := byKey["draft:0"]
|
|
if !ok {
|
|
t.Fatalf("no draft record at the leader chunk; got %d wave rows: %+v", len(waves), waves)
|
|
}
|
|
edit, ok := byKey["edit:0"]
|
|
if !ok {
|
|
t.Fatalf("the EDITOR's selection was not recorded at the unit leader — that is the whole gap row 417 names; got %d wave rows: %+v", len(waves), waves)
|
|
}
|
|
|
|
// The editor is addressed at the leader and selects over the WHOLE unit, so its record must name the
|
|
// term that lives in the second member. The leader's own draft record must not.
|
|
if !hasSrc(t, edit.InjectedSrcs, "鈴木") {
|
|
t.Errorf("the editor's record at the leader does not name 鈴木, which occurs in the unit it edited: injected_srcs=%s", edit.InjectedSrcs)
|
|
}
|
|
if hasSrc(t, leaderDraft.InjectedSrcs, "鈴木") {
|
|
t.Errorf("premise broken: the LEADER's draft record names 鈴木, whose key is not in the leader chunk — the two records are not distinguishable: injected_srcs=%s", leaderDraft.InjectedSrcs)
|
|
}
|
|
if edit.NExactHits == 0 {
|
|
t.Errorf("the editor's record shows no exact hit at all, so nothing says the bank reached the editor's prompt: %+v", edit)
|
|
}
|
|
}
|
|
|
|
// TestAWaveRecordIsRewrittenNotAccumulatedOnResume pins the property that makes the table safe to recompute:
|
|
// a second run over the same book converges on the same rows instead of adding new ones. Without it the
|
|
// counts would grow with every resume and the column would measure how often a book was restarted.
|
|
func TestAWaveRecordIsRewrittenNotAccumulatedOnResume(t *testing.T) {
|
|
rec := &reqRec{}
|
|
srv := newJSONProvider(rec, draftEdit)
|
|
defer srv.Close()
|
|
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(1, 400), regenerate: 0, glossarySeed: `
|
|
terms:
|
|
- src: 文
|
|
dst: Вэнь
|
|
type: name
|
|
status: approved
|
|
allow_short: true
|
|
decl: { invariant: true, forms: ["Вэнь"] }
|
|
`})
|
|
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
|
|
|
r1 := newRunner(t, bookPath)
|
|
if _, err := r1.TranslateBook(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := r1.Store.WaveSelectionsForBook(r1.Book.BookID)
|
|
r1.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(first) == 0 {
|
|
t.Fatalf("premise broken: the first run recorded no wave selection at all, so a second run cannot show convergence")
|
|
}
|
|
|
|
r2 := newRunner(t, bookPath)
|
|
if _, err := r2.TranslateBook(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := r2.Store.WaveSelectionsForBook(r2.Book.BookID)
|
|
r2.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(second) != len(first) {
|
|
t.Errorf("a resume must REWRITE the wave records, not add rows: %d after the first run, %d after the second", len(first), len(second))
|
|
}
|
|
for i := range first {
|
|
if first[i].NExactHits != second[i].NExactHits || first[i].InjectedSrcs != second[i].InjectedSrcs {
|
|
t.Errorf("a resume changed what the record says was shown at ch%d/chunk%d/%s: %+v → %+v",
|
|
first[i].Chapter, first[i].ChunkIdx, first[i].Wave, first[i], second[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func hasSrc(t *testing.T, jsonArray, want string) bool {
|
|
t.Helper()
|
|
var got []string
|
|
if err := json.Unmarshal([]byte(jsonArray), &got); err != nil {
|
|
t.Fatalf("injected_srcs is not a JSON array (%q): %v", jsonArray, err)
|
|
}
|
|
for _, s := range got {
|
|
if s == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|