package pipeline import ( "context" "strings" "testing" ) // systemmessages_wave_test.go proves the SystemMessages fix on the two paths that actually carry // the memory bank to a model — the DRAFT wave (the translator's src→dst glossary) and the EDIT // wave (the editor's CONFIRMED-dst constraint block). The edit wave matters on its own account: // it is the wave that delivers the OWNER'S SIGNED decisions, so a hop that ate its system message // would silently eat the owner's edits, not merely a machine's proposals. // // ⚠ The assertion is the POST-FIX invariant, deliberately: "the injection is in the request" is // true of unfixed code too (the assembler always built it; the endpoint dropped it), so it would // be green on a broken engine. What separates fixed from unfixed is ONE system message with the // injection INSIDE it. // systemMessagesOf returns the wire body's system messages, in order. func systemMessagesOf(t *testing.T, body string) []wireMsg { t.Helper() var out []wireMsg for _, m := range bodyMessages(t, body) { if m.Role == "system" { out = append(out, m) } } return out } // TestSingleSystemEndpointCarriesTheBankOnBothWaves runs a real two-wave book against a provider // declared single-system and asserts, for the draft AND the edit request, that the endpoint got // exactly one system message and that the bank block is inside it. func TestSingleSystemEndpointCarriesTheBankOnBothWaves(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД про Судзуки.", "stop" } return "Судзуки пошёл в тихую библиотеку.", "stop" }) defer srv.Close() bookPath := setupProjectOpts(t, srv.URL, projectOpts{ regenerate: 1, source: suzukiSource, glossarySeed: suzukiSeed, systemMessagesSingle: true, }) r := newRunner(t, bookPath) res, err := r.TranslateBook(context.Background()) if err != nil { t.Fatal(err) } if res.Flagged != 0 { t.Fatalf("the join must not disturb the verdict: flagged=%d", res.Flagged) } cases := []struct { wave string isEdit bool wantBlock string // the bank text this wave's role receives }{ // waverun.go runDraftChunk: the translator's src→dst glossary. {wave: "draft", isEdit: false, wantBlock: "ГЛОССАРИЙ"}, // waverun.go runEditUnit: the editor's CONFIRMED-dst constraint block — the carrier of the // owner's signed decisions. {wave: "edit", isEdit: true, wantBlock: "КАНОНИЧЕСКИЕ ПЕРЕВОДЫ"}, } for _, c := range cases { t.Run(c.wave, func(t *testing.T) { seen := 0 for _, body := range rec.all() { if isEditBody(body) != c.isEdit { continue } seen++ sys := systemMessagesOf(t, body) if len(sys) != 1 { t.Fatalf("%s wave: a single-system endpoint received %d system messages — the bank is being dropped by the provider", c.wave, len(sys)) } if !strings.Contains(sys[0].Content, c.wantBlock) { t.Fatalf("%s wave: the bank block %q is not inside the single system message: %q", c.wave, c.wantBlock, sys[0].Content) } if !strings.Contains(sys[0].Content, "Судзуки") { t.Fatalf("%s wave: the approved rendering did not reach the model: %q", c.wave, sys[0].Content) } // The stable prompt prefix must stay FIRST inside the join, or the provider's prefix // cache stops hitting on every call. iPrefix := strings.Index(sys[0].Content, "Редактируй перевод.") if !c.isEdit { iPrefix = strings.Index(sys[0].Content, "Переводи с") } if iPrefix != 0 { t.Fatalf("%s wave: the stable prefix is not first in the join (index %d): %q", c.wave, iPrefix, sys[0].Content) } // The user turn survives the join untouched, and the bank never leaks into it. msgs := bodyMessages(t, body) if len(msgs) != 2 || msgs[1].Role != "user" { t.Fatalf("%s wave: wire shape = %+v, want [system user]", c.wave, msgs) } if strings.Contains(msgs[1].Content, c.wantBlock) { t.Fatalf("%s wave: the bank leaked into the user message: %q", c.wave, msgs[1].Content) } } if seen == 0 { t.Fatalf("%s wave recorded no request", c.wave) } }) } } // TestMultiSystemEndpointIsUnchangedOnBothWaves is the control on the SAME book: a provider that // declares nothing still receives the two-message wire, so the fix cannot have changed what every // other provider in the stack sees. func TestMultiSystemEndpointIsUnchangedOnBothWaves(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД про Судзуки.", "stop" } return "Судзуки пошёл в тихую библиотеку.", "stop" }) defer srv.Close() bookPath := setupProjectOpts(t, srv.URL, projectOpts{ regenerate: 1, source: suzukiSource, glossarySeed: suzukiSeed, }) r := newRunner(t, bookPath) if _, err := r.TranslateBook(context.Background()); err != nil { t.Fatal(err) } for _, body := range rec.all() { if sys := systemMessagesOf(t, body); len(sys) != 2 { t.Fatalf("an undeclared endpoint must keep the two-message wire, got %d: %+v", len(sys), sys) } } }