package pipeline import ( "strings" "testing" ) func TestMessagesInjectionLayout(t *testing.T) { tpl := writeTemplate(t, "Стабильный системный префикс.\n---USER---\nПереведи: {{text}}") v := RenderVars{Book: testBook(), Text: "исходный чанк"} injection := "ГЛОССАРИЙ:\n阿Q → А-кью" msgs, err := MessagesWithInjection(tpl, v, injection) if err != nil { t.Fatal(err) } if len(msgs) != 3 { t.Fatalf("want 3 messages (system, injection, user), got %d", len(msgs)) } // [0] stable system prefix carries the cache boundary. if msgs[0].Role != "system" || !msgs[0].CacheBoundary { t.Errorf("msg[0] must be the system prefix with CacheBoundary=true: %+v", msgs[0]) } // [1] injection sits AFTER the boundary (volatile → CacheBoundary=false). if msgs[1].Role != "system" || msgs[1].CacheBoundary || msgs[1].Content != injection { t.Errorf("msg[1] must be the injection with CacheBoundary=false: %+v", msgs[1]) } // [2] the volatile user chunk. It must contain ONLY the rendered chunk, never the // glossary (ch.Text stays clean — the 3a contract; the coverage gate reads ch.Text). if msgs[2].Role != "user" { t.Errorf("msg[2] must be user: %+v", msgs[2]) } if strings.Contains(msgs[2].Content, "ГЛОССАРИЙ") || strings.Contains(msgs[2].Content, "阿Q") { t.Errorf("glossary leaked into the user (ch.Text) message: %q", msgs[2].Content) } if !strings.Contains(msgs[2].Content, "исходный чанк") { t.Errorf("user message lost the chunk: %q", msgs[2].Content) } } func TestMessagesEmptyInjectionIdentical(t *testing.T) { tpl := writeTemplate(t, "Системный префикс.\n---USER---\nТекст: {{text}}") v := RenderVars{Book: testBook(), Text: "чанк"} plain, _ := Messages(tpl, v) withEmpty, _ := MessagesWithInjection(tpl, v, " ") // whitespace-only → no injection if len(plain) != 2 || len(withEmpty) != 2 { t.Fatalf("empty injection must yield 2 messages: %d vs %d", len(plain), len(withEmpty)) } for i := range plain { if plain[i] != withEmpty[i] { t.Errorf("empty-injection message %d differs from plain: %+v vs %+v", i, withEmpty[i], plain[i]) } } } // TestInjectionChangesRequestHash: the injection is folded into request_hash via the // messages, so a different glossary is a different request (a changed approved term // re-derives the chunk, never serves a stale checkpoint). func TestInjectionChangesRequestHash(t *testing.T) { tpl := writeTemplate(t, "Префикс.\n---USER---\n{{text}}") v := RenderVars{Book: testBook(), Text: "чанк"} none, _ := Messages(tpl, v) inj, _ := MessagesWithInjection(tpl, v, "ГЛОССАРИЙ:\n阿Q → А-кью") req := Request{BookID: "b", Chapter: 1, Stage: "draft", Role: "translator", Model: "m", Temperature: 0.3, Reasoning: "off", MaxTokens: 1024, SnapshotID: "snap"} reqNone, reqInj := req, req reqNone.Messages, reqInj.Messages = none, inj hNone, hInj := RequestHash(reqNone), RequestHash(reqInj) if hNone == hInj { t.Error("injection did not change request_hash — a glossary change would silently hit a stale checkpoint") } }