textmachine/platform/internal/runner/translate_resnapshot_live_test.go

207 lines
9.8 KiB
Go
Raw Permalink 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. ⚠ It is a question about
// the ADDRESS and never about money: the guard above is what answers that one.
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")
// ⛔ CONSENT, ASKED OF WHAT WILL ACTUALLY RUN — and asked HERE rather than of the template, which is
// the correction the acceptance of 11.09 forced. `writeProbeBook` does not hand the engine the
// deployment's pipeline: it derives a zero-cost one from it (`zeroCostPipeline`) and points the
// book at that. A guard on the TEMPLATE therefore judged a document nothing loads — it refused
// stands whose real configuration was free, and it could never see a vendor that SURVIVED the
// derivation, which is the only way one reaches a model here. The derivation is a rewriter by key
// (`stage["model"]`, `delete escalate_to`, `gates[*].model`, `classify_model`) — the same shape
// this guard exists to be independent of — so what it produces is exactly what has to be checked.
if paid, decided, why := paidModelIn(filepath.Join(dir, ConfigFile)); os.Getenv(liveConsentEnv) != "1" {
switch {
case !decided:
t.Skipf("what this probe would hand the engine cannot be resolved to the models it would "+
"call (%s), so it is not started. Fix the deployment template, or set %s=1 to say you "+
"mean it", why, liveConsentEnv)
case paid != "":
// ⛔ FAIL, NOT SKIP, AND THE DIFFERENCE IS WHOSE ARTEFACT THIS IS. What the guard just judged
// was written by `writeProbeBook` two lines up, from this deployment's template: the
// derivation's whole promise is that what it produces is free. A paid render is therefore a
// DEFECT of ours and not a condition of the host — and answering it with a skip would file
// the one failure this guard exists to catch under «nothing to see».
//
// It is also what makes the guard's AIM measurable: point it back at the template and a
// stand whose template is paid turns red here, which is the mutation that survived a skip
// (acceptance of 11.09, finding 1).
t.Fatalf("the configuration this probe would hand the engine names %q, whose provider is not "+
"on this host: running the engine's translate under it would spend real credit. This file "+
"is produced by zeroCostPipeline from the deployment template, so a vendor in it is a hole "+
"in that derivation — fix it rather than the guard. (%s=1 says you mean it.)",
paid, liveConsentEnv)
}
}
// 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))]
}