package pipeline import ( "context" "crypto/sha256" "encoding/json" "os" "path/filepath" "strings" "testing" "textmachine/backend/internal/llm" ) // This is a request-capture probe, not a model-quality test. It executes the // actual book runner and current production prompt files with an in-memory // provider. Every response is synthetic and no network client is called. type contextAuditClient struct{ requests []llm.LLMRequest } func (c *contextAuditClient) Complete(_ context.Context, r llm.LLMRequest) (*llm.LLMResponse, error) { c.requests = append(c.requests, r) body, _ := json.Marshal(r.Messages) output := "Наконец согласился." if strings.Contains(string(body), "女孩") { output = "Девушка одна сидела в комнате и обдумывала просьбу." } if strings.Contains(string(body), "男孩") { output = "Юноша один сидел в комнате и обдумывал просьбу." } return &llm.LLMResponse{Text: output, Model: r.Model, FinishReason: llm.FinishStop, Usage: llm.Usage{PromptTokens: 100, CompletionTokens: 20}}, nil } func TestAuditContextAcrossEditUnits(t *testing.T) { current := "终于答应了。" prefixes := []string{"女孩独自坐在房里,反复考虑那个请求。", "男孩独自坐在房里,反复考虑那个请求。"} var captures [2][]llm.LLMRequest for variant, prefix := range prefixes { bookPath := setupProjectOpts(t, "http://127.0.0.1:1", projectOpts{ source: prefix + "\f" + current, banknote: true, waveWorkers: 1}) projectDir := filepath.Dir(bookPath) bookBytes, err := os.ReadFile(bookPath) if err != nil { t.Fatal(err) } writeFile(t, bookPath, strings.Replace(string(bookBytes), "source_lang: ja", "source_lang: zh", 1)) for _, pair := range [][2]string{{"translator-banknote.md", "translator.md"}, {"editor.md", "editor.md"}} { data, err := os.ReadFile(filepath.Join("..", "..", "prompts", "zh-ru", pair[0])) if err != nil { t.Fatal(err) } writeFile(t, filepath.Join(projectDir, "prompts", pair[1]), string(data)) } r := newRunner(t, bookPath) chunks, err := r.bookChunks() if err != nil { r.Close() t.Fatal(err) } units := buildEditUnits(chunks) if len(units) != 2 || len(units[0].Members) != 1 || len(units[1].Members) != 1 || units[0].Chapter == units[1].Chapter || units[1].sourceText() != current { r.Close() t.Fatalf("fixture did not cross an edit-unit boundary: chunks=%+v units=%+v", chunks, units) } fake := &contextAuditClient{} r.clients["fake-model"] = fake result, err := r.TranslateBook(context.Background()) r.Close() if err != nil { t.Fatal(err) } if len(fake.requests) != 4 || result.Flagged != 0 { t.Fatalf("want 2 draft+2 edit calls, clean result; calls=%d result=%+v", len(fake.requests), result) } captures[variant] = fake.requests t.Logf("variant=%d: 2 chapters / 2 draft chunks / 2 edit units / %d in-memory provider calls; previous=%s", variant, len(fake.requests), prefix) } var currentByVariant [2][]llm.LLMRequest var priorByVariant [2][]llm.LLMRequest for variant, reqs := range captures { for _, req := range reqs { b, _ := json.Marshal(req.Messages) if strings.Contains(string(b), current) { currentByVariant[variant] = append(currentByVariant[variant], req) } else { priorByVariant[variant] = append(priorByVariant[variant], req) } } if len(currentByVariant[variant]) != 2 || len(priorByVariant[variant]) != 2 { t.Fatal("missing draft/editor captures") } } for i, role := range []string{"draft", "editor"} { a, _ := json.Marshal(currentByVariant[0][i]) b, _ := json.Marshal(currentByVariant[1][i]) if string(a) != string(b) { t.Fatalf("current %s request DID change with preceding context: A=%s B=%s", role, a, b) } pa, _ := json.Marshal(priorByVariant[0][i]) pb, _ := json.Marshal(priorByVariant[1][i]) if string(pa) == string(pb) { t.Fatalf("prior %s requests should differ", role) } t.Logf("current %s request is byte-identical: bytes=%d sha256=%x; prior request differs", role, len(a), sha256.Sum256(a)) } }