textmachine/docs/research/31-harness-audit/context_probe_test.go.txt

107 lines
4.1 KiB
Text

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))
}
}