textmachine/platform/internal/runner/translate_resnapshot_live_test.go

175 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package runner
import (
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"textmachine/platform/internal/ingest"
)
// The P10 mine, reproduced against the REAL engine (D39.165 §3; workflow errata 28.08-и): a bank
// correction moves the snapshot, and the engine's guard stops a CONTINUING run loudly unless
// `--resnapshot` travels — which this platform did not pass until P10. The state must be the
// errata's: the edit jobs EXIST before the correction (a finished book is the flagship case) —
// against fresh jobs the guard is silent and the reproduction is green without the fix, proving
// nothing. That precondition is therefore ASSERTED, not assumed: the no-flag run must die, and die
// naming the flag.
//
// Free of provider keys and of paid calls: the pipeline is the deployment's local $0 pair, served
// by an in-test OpenAI-compatible stub (the P9 live-probe's provider, ported); the guard itself
// fires before any model call.
//
// Mutation caught: TranslateArgs dropping either consent flag; the guard notice changing shape.
func TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem(t *testing.T) {
bin := os.Getenv("TM_PLATFORM_TEST_ENGINE_BIN")
tpl := os.Getenv("TM_PLATFORM_TEST_BOOK_TEMPLATE")
if bin == "" || tpl == "" {
t.Skip("TM_PLATFORM_TEST_ENGINE_BIN and TM_PLATFORM_TEST_BOOK_TEMPLATE not set: " +
"the snapshot-guard mine is not reproduced against a real engine")
}
// The deployment's models.yaml points the $0 pair at this fixed address; a busy port means
// another stand is live on this host — skip loudly rather than fight it.
ln, err := new(net.ListenConfig).Listen(t.Context(), "tcp", "127.0.0.1:11434")
if err != nil {
t.Skipf("the local provider address is busy (another stand?): %v", err)
}
srv := &http.Server{Handler: http.HandlerFunc(fakeProvider)}
go func() { _ = srv.Serve(ln) }()
defer srv.Close()
dir := t.TempDir()
writeProbeBook(t, tpl, dir, "bk_MINEPROBE")
// Chapter 1 carries the term (its units re-render after the correction and re-translate at $0
// stub prices); chapter 2 does not (its units are the $0 re-pin the flag permits).
source := "第一章 山路\n\n方源走在山路上方源看着远方的云。\n\n第二章 夜色\n\n老人在夜色中叹息风吹过山谷。\n"
if err := os.WriteFile(filepath.Join(dir, "source.txt"), []byte(source), 0o644); err != nil {
t.Fatal(err)
}
ceiling := []string{"--ceiling-usd", "1.000000"}
run := func(args []string) (int, string) {
cmd := exec.CommandContext(t.Context(), bin, args...)
cmd.Dir = dir
out, _ := cmd.CombinedOutput()
return cmd.ProcessState.ExitCode(), string(out)
}
// Prime: a full run through BOTH waves. After it the edit jobs exist — the errata's state.
if code, out := run(TranslateArgs(dir, false, "", false, 0, 0, ceiling)); code != 0 {
t.Fatalf("the priming run failed (%d): %s", code, lastLines(out, 12))
}
// The bank moves through the live verb — the exact door P9 landed.
doc, err := ingest.EncodeDecisions("bk_MINEPROBE", []ingest.BankDecision{
{Action: "approve", Src: "方源", Sense: "", Dst: "Фан Юань-P10", Kind: "name", Note: "P10 mine probe"},
})
if err != nil {
t.Fatal(err)
}
decisions := filepath.Join(t.TempDir(), "decisions.json")
if err := os.WriteFile(decisions, doc, 0o600); err != nil {
t.Fatal(err)
}
if out, err := New(nil).BankApply(t.Context(), bin, dir, decisions, false); err != nil ||
out.ExitCode != 0 || !out.Report.Changed {
t.Fatalf("the live correction did not land: %+v (%v; stderr %q)", out, err, out.Stderr)
}
// Post-fix invariant, half one — and the errata's precondition in the same breath: WITHOUT the
// flag the continuing run dies loudly, naming the flag. A green run here means the edit jobs
// did not exist and this fixture proves nothing about the mine.
code, out := run(TranslateArgs(dir, false, "", false, 0, 0, ceiling))
if code == 0 {
t.Fatal("no snapshot guard fired on a moved bank: the edit-job precondition is not met " +
"(errata 28.08-и) and this reproduction is degenerate")
}
if !strings.Contains(out, "--resnapshot") {
t.Fatalf("the guard's stop does not name --resnapshot (%d): %s", code, lastLines(out, 6))
}
// Half two: the flags the platform now stores on the run row and renders through TranslateArgs
// carry the same run through — re-pins at $0, re-translates the term's units at stub prices.
// The engine names its pre-run restore point by the SECOND; on this $0 stub two translates fit
// in one, and the second refuses to overwrite the first's backup — wait the second out.
time.Sleep(1100 * time.Millisecond)
// The consent travels live too — the funded cap the platform renders (P10): the projection here
// is $0 (stub prices), well under the cap, so the engine accepts and the flag's whole live path
// is exercised rather than left to the argv pin alone.
if code, out := run(TranslateArgs(dir, false, "", true, 30_000, 0, ceiling)); code != 0 {
t.Fatalf("with the consents the run still dies (%d): %s", code, lastLines(out, 6))
}
}
func lastLines(s string, n int) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
if len(lines) > n {
lines = lines[len(lines)-n:]
}
return strings.Join(lines, "\n")
}
// fakeProvider is the P9 live-probe's $0 stand-in (fake_provider.py), ported: prose for the
// translator and editor, TSV for the terminology roles. The probe proves the CHAIN, not quality.
func fakeProvider(w http.ResponseWriter, r *http.Request) {
var req struct {
Model string `json:"model"`
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
var system, user strings.Builder
for _, m := range req.Messages {
switch m.Role {
case "system":
system.WriteString(m.Content)
case "user":
user.WriteString(m.Content)
}
}
content := proseFor(system.String(), user.String())
resp := map[string]any{
"id": "fake-1", "object": "chat.completion", "model": req.Model,
"choices": []map[string]any{{
"index": 0,
"message": map[string]any{"role": "assistant", "content": content},
"finish_reason": "stop",
}},
"usage": map[string]any{"prompt_tokens": 100, "completion_tokens": max(1, len(content)/3),
"total_tokens": 100 + max(1, len(content)/3)},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}
var termKeys = regexp.MustCompile(`(?m)^key: (.+)$`)
func proseFor(system, user string) string {
if strings.Contains(system, "терминолог") {
var b strings.Builder
for i, m := range termKeys.FindAllStringSubmatch(user, -1) {
fmt.Fprintf(&b, "%s\tЗаглушка-%d\t55\n", m[1], i+1)
}
return b.String()
}
if strings.Contains(system, "классиф") || strings.Contains(system, "класс (name | place | title | term)") {
var b strings.Builder
for _, m := range termKeys.FindAllStringSubmatch(user, -1) {
fmt.Fprintf(&b, "%s\tname\n", m[1])
}
return b.String()
}
const filler = "Фан Юань неторопливо шёл по горной тропе, и ветер приносил запах трав. " +
"Старейшина посмотрел на него и тяжело вздохнул, вспоминая давние годы. " +
"В долине клубился туман, и где-то вдалеке кричала ночная птица. "
out := filler
for len(out) < len(user) {
out += filler
}
return out[:max(len(user), len(filler))]
}