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 → А-кью") hNone := RequestHash("b", 1, 0, 0, "draft", "translator", "m", 0.3, "off", false, 1024, "snap", none) hInj := RequestHash("b", 1, 0, 0, "draft", "translator", "m", 0.3, "off", false, 1024, "snap", inj) if hNone == hInj { t.Error("injection did not change request_hash — a glossary change would silently hit a stale checkpoint") } } func TestRenderGlossaryBlock(t *testing.T) { confirmed := pickedEntry{entry: &memoryEntry{src: "阿Q", dst: "А-кью", status: "approved"}, disp: memConfirmed} ambiguous := pickedEntry{entry: &memoryEntry{src: "小D", dst: "Малыш Дэ", status: "auto"}, disp: memAmbiguous} emptyDst := pickedEntry{entry: &memoryEntry{src: "鈴木", dst: "", status: "auto"}, disp: memAmbiguous} block := renderGlossaryBlock([]pickedEntry{confirmed, ambiguous, emptyDst}) if !strings.Contains(block, "阿Q → А-кью") { t.Errorf("confirmed line missing: %q", block) } if !strings.Contains(block, "小D → Малыш Дэ ⟨проверить⟩") { t.Errorf("ambiguous line missing its unverified tag: %q", block) } if strings.Contains(block, "鈴木") { t.Errorf("empty-dst candidate should be skipped (nothing to inject): %q", block) } // Nothing to inject → empty block (лучше ничего). if renderGlossaryBlock(nil) != "" { t.Error("empty selection must render an empty block") } if renderGlossaryBlock([]pickedEntry{emptyDst}) != "" { t.Error("only-empty-dst selection must render an empty block") } }