package pipeline import ( "strings" "testing" "textmachine/backend/internal/membank" "textmachine/backend/internal/store" ) // snapshot_wave_test.go: WS1 §1в per-wave snapshot mechanics — the folds the wave driver pins every job to. // ⚠ The book-global snapshotID() is TEST-ONLY (no production caller) and is NOT exercised here; it is // called from four other files, five call sites between them, and each pins one payload input. The golden pins the two per-wave ids and // their payloads, so nothing anywhere should be read as "the book-global fold is covered". func TestSnapshotIDForWave(t *testing.T) { r := newRunner(t, setupProject(t, "http://127.0.0.1:1")) defer r.Close() seed := []store.GlossaryEntry{ {Src: "方源", Dst: "Фан Юань", Status: "approved", Source: "seed"}, {Src: "古月", Dst: "Гу Юэ", Status: "approved", Source: "ruby"}, } r.memory = membank.Materialize(seed, false) w1id, w1p, err := r.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } w2id, w2p, err := r.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } // the draft wave and the edit wave differ (different stage subset AND different bank version). if w1id == w2id { t.Fatalf("draft-wave snapshot must differ from snapshot_W2 (stage subset + bank version)") } // The stage PARTITION: the draft wave folds the draft (translator) stage, the edit wave folds the edit (editor) stage. if !strings.Contains(w1p, `"name":"draft"`) || strings.Contains(w1p, `"name":"edit"`) { t.Fatalf("draft-wave snapshot must fold ONLY the draft stage, payload: %s", w1p) } if !strings.Contains(w2p, `"name":"edit"`) || strings.Contains(w2p, `"name":"draft"`) { t.Fatalf("snapshot_W2 must fold ONLY the edit stage, payload: %s", w2p) } // «Re-pay ONCE» at the snapshot level (§1в): a mined-approved addition at the bank-mining stop moves ONLY // the edit wave. enriched := append(append([]store.GlossaryEntry{}, seed...), store.GlossaryEntry{ Src: "蛊", Dst: "гу", Status: "approved", Source: "mined", }) r.memory = membank.Materialize(enriched, false) w1id2, _, _ := r.snapshotIDForWave(waveDraft) w2id2, _, _ := r.snapshotIDForWave(waveEdit) if w1id2 != w1id { t.Fatalf("draft-wave snapshot moved on a mined-approved addition — the draft wave checkpoints would re-bill (pay-once invariant broken)") } if w2id2 == w2id { t.Fatalf("snapshot_W2 did NOT move on a mined-approved addition — the edit wave would miss the mined canon") } } // TestWaveStagesPartition pins the role partition independently of the runner. func TestWaveStagesPartition(t *testing.T) { r := newRunner(t, setupProject(t, "http://127.0.0.1:1")) defer r.Close() draftStages := waveStages(r.Pipeline.Stages, waveDraft) editStages := waveStages(r.Pipeline.Stages, waveEdit) if len(draftStages) != 1 || draftStages[0].Role != roleTranslator { t.Fatalf("the draft wave must be the translator stage(s), got %+v", draftStages) } if len(editStages) != 1 || editStages[0].Role != roleEditor { t.Fatalf("the edit wave must be the editor stage(s), got %+v", editStages) } }