textmachine/platform/internal/runner/priceprojection_live_test.go

215 lines
10 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 (
"os"
"path/filepath"
"strings"
"testing"
"gopkg.in/yaml.v3"
"textmachine/platform/internal/ingest"
)
// ⛔ THE SEAM AGAINST THE REAL ENGINE, and it exists because every other test of the price
// projection reads a document THIS SIDE WROTE.
//
// The allowlist in `ingest` is a transcription of the engine's own JSON, and a transcription is
// exactly the artefact that goes stale in silence: a key the engine renames leaves a zero behind,
// `json.Unmarshal` says nothing, and the platform then sells a book with no floor under its hold —
// which is the PD-440 wall coming back invisibly. A hand-written fixture cannot catch that, because
// it agrees with the reader by construction.
//
// So this one asks the ENGINE. It runs `tmctl manifest --json` against a probe book, decodes the
// bytes with the platform's own reader, and asserts that the projection ARRIVED — not merely that
// the document parsed.
//
// Gated like every live-engine test: a bare clone stays green and says why.
func TestTheRealEnginesPriceProjectionIsReadByThisBuild(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 price projection is not checked against a real engine")
}
// Two chapters the engine's own heading rule will find, so the cut is `detected` and the tree has
// something to price per chapter.
const src = "第1章 测试\n这是一个测试文本。天空是蓝色的云朵飘过。\n\n第2章 又一章\n他走进了那座古老的图书馆书架高耸入云。\n"
dir := writeReadProbeBook(t, tpl, "bk_PRICEPROBE", src)
m, err := New(nil).Manifest(t.Context(), bin, dir)
if err != nil {
t.Fatalf("the live manifest did not run: %v", err)
}
if err := m.Whole(); err != nil {
t.Fatalf("this build did not read the live manifest whole: %v", err)
}
// THE PROJECTION, read by the platform's own reader out of the engine's own bytes.
p, ok := m.Priced()
if !ok {
t.Fatalf("this build read no projection out of a live manifest of %d chapters: the allowlist "+
"and the engine have drifted apart, and a book priced by nobody cannot be sold "+
"(ingest.Priced refuses it, which is the honest half — but the SEAM is what broke)",
m.ChaptersTotal)
}
// ⚠ EVERY LOAD-BEARING FIGURE, each named for what depends on it rather than checked for being
// non-zero in a loop: what a reader needs from a failure here is WHICH number stopped arriving.
if p.StepMaxUSD <= 0 {
t.Error("step_max_usd did not arrive: it is the floor of every hold, and without it a run is " +
"admitted with a ceiling no single call can clear (PD-440)")
}
if p.ExpectedUSD <= 0 {
t.Error("expected_usd did not arrive: the order has nothing to be priced from")
}
if p.SourceChars <= 0 {
t.Error("source_chars did not arrive: the screen falls back to the intake's approximation")
}
// The per-chapter roll-up is what an ORDER is priced from, and its absence is the failure the
// book-level figure alone cannot show.
for _, c := range m.Chapters {
if c.Price == nil || c.Price.ExpectedUSD <= 0 {
t.Errorf("chapter %d arrived with no price, so no partial order over this book can be quoted", c.Number)
}
}
// The per-UNIT figure, which is what a CHARACTER order resolves against — the only partial order
// a book with no chapter structure can carry.
for _, c := range m.Chapters {
for _, u := range c.Units {
if u.Price == nil || u.Price.SourceChars <= 0 {
t.Errorf("unit %q arrived with no size, so a character order cannot be resolved", u.ID)
}
}
}
// The cut's PROVENANCE, and the vocabulary it comes in. This build reads three words and treats
// anything else as untrusted; a live engine emitting a fourth is not a failure — it is the day
// the platform stops offering chapter orders for this book, which is what should happen.
switch m.Structure {
case ingest.StructureDetected:
if !m.ChapterOrdersOffered() {
t.Error("a detected cut is not offered in chapters")
}
case ingest.StructureDeclared, ingest.StructureNone:
t.Logf("the live engine cut this probe as %q rather than `detected`", m.Structure)
case "":
t.Error("the live engine published no `structure` at all: this build then refuses to sell " +
"chapter orders for every book, which is safe and wrong")
default:
t.Logf("the live engine published a structure word this build does not know (%q); "+
"chapter orders are withheld, which is the intended degradation", m.Structure)
}
// ⛔ AND THE ONE ASSERTION A FIXTURE COULD NEVER MAKE: the book-level `expected_usd` INCLUDES a
// flat `book_once_usd`, and on a short book it DOMINATES. Measured on this very probe: two
// chapters whose own bills come to fractions of a cent against a book-level bound of whole
// dollars. It is pinned because the platform's whole order arithmetic rests on NOT using the
// book-level figure as the base of a hold — doing so would put a flat two-dollar threshold under
// every purchase and make a short book unbuyable again.
if p.BookOnceUSD > 0 {
units := p.ExpectedUSD - p.BookOnceUSD
if units <= 0 {
t.Fatalf("book_once_usd (%s) is not less than expected_usd (%s): the book-level bound is "+
"not a PART of the expected bill, and every order priced from the difference is wrong",
p.BookOnceUSD.USD(), p.ExpectedUSD.USD())
}
t.Logf("live projection: expected %s of which book-level %s; the units' own share is %s",
p.ExpectedUSD.USD(), p.BookOnceUSD.USD(), units.USD())
}
// The chapters' own bills and the book figure describe ONE book. Two numbers that must agree are
// where a silent drift lives, and here they come from two different keys of one document.
sum := int64(0)
for _, c := range m.Chapters {
sum += int64(c.Price.ExpectedUSD)
}
if want := int64(p.ExpectedUSD - p.BookOnceUSD); sum < want-int64(len(m.Chapters))-1 ||
sum > want+int64(len(m.Chapters))+1 {
t.Errorf("the live chapters sum to %d micro-USD and the book figure leaves %d for them", sum, want)
}
}
// The engine's manifest is the ONE document the order form is built on, so this build's idea of its
// shape has to be checkable without a database and without a run. It is the same probe as above with
// the assertions inverted: what the engine publishes that this build does NOT read is listed, so a
// field arriving for the first time is a decision somebody takes rather than a value dropped.
func TestWhatTheLiveManifestCarriesAndThisBuildDeclinesToRead(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")
}
dir := writeReadProbeBook(t, tpl, "bk_SHAPEPROBE", "第1章 测试\n这是一个测试文本。\n")
doc, _, err := readEngine(t.Context(), bin, dir, ManifestArgs(dir), maxManifest, "manifest")
if err != nil {
t.Fatalf("the live manifest did not run: %v", err)
}
// The version this build was written against. It is a SHAPE gate and not a value pin — see
// ingest.KnownManifestVersion — and the point of asserting it against a live engine is that the
// constant stops being a copy of a copy.
m, err := ingest.DecodeManifest(doc)
if err != nil {
t.Fatalf("the live manifest did not decode: %v", err)
}
if m.Version != ingest.KnownManifestVersion {
t.Errorf("the live engine publishes manifest %q and this build reads %s: the allowlist is a "+
"transcription of a document that has moved", m.Version, ingest.KnownManifestVersion)
}
// ⚠ ASKED OF THE DECODED BOOK FIGURE, not grepped out of the bytes. A raw `strings.Contains` for
// `"expected_usd"` is satisfied by any CHAPTER's price object, so a rename of the BOOK-level key —
// the one the whole order form is built on — would leave this green. The keys that have no twin
// elsewhere in the document are still worth a grep, and they are the ones grepped.
if m.Price == nil {
t.Fatal("the live manifest carries no book-level `price` object at all")
}
if m.Price.ExpectedUSD <= 0 || m.Price.StepMaxUSD <= 0 || m.Price.SourceChars <= 0 {
t.Errorf("a book-level price key did not arrive: %+v", *m.Price)
}
for _, key := range []string{`"book_once_usd"`, `"step_max_usd"`, `"structure"`} {
if !strings.Contains(string(doc), key) {
t.Errorf("the live manifest carries no %s: the platform's order form is built on it", key)
}
}
}
// writeReadProbeBook is writeProbeBook's READ-PATH twin: the deployment's own template, with this
// book's identity on it and nothing else changed.
//
// ⛔ IT DELIBERATELY DOES NOT REWRITE THE PIPELINE, and both halves of that matter.
//
// It does not swap the stage models for a local one, because `manifest` reaches no provider: it is a
// $0, key-less verb (D20.4) that ingests, cuts and prints. The swap exists so a WRITE-path probe does
// not buy its calls, and buying is not on this path.
//
// And it does not inherit the contrast-artefact SKIP. That condition is the write path's — its own
// message says «a live write-path probe cannot run» — because the bank contour mines during a
// translate. A manifest mines nothing. Inheriting it cost this probe its whole existence: the seam
// test that is supposed to be the one independent witness of the allowlist SKIPPED on every host
// without a multi-megabyte word list nobody needs to read a manifest, and skipped silently, which is
// the failure shape this project keeps paying for.
//
// ⚠ AND IT WANTS THE SHIPPING PIPELINE'S OWN NUMBERS, which is the positive reason rather than the
// absence of a negative one: the projection this test reads is made of that pipeline's stages, its
// prices and its gate budgets. Rewritten, the probe would measure a book nobody sells.
func writeReadProbeBook(t *testing.T, tpl, bookID, source string) string {
t.Helper()
dir := t.TempDir()
raw, err := os.ReadFile(tpl)
if err != nil {
t.Fatal(err)
}
var cfg map[string]any
if err := yaml.Unmarshal(raw, &cfg); err != nil {
t.Fatal(err)
}
cfg["book_id"], cfg["source_lang"], cfg["target_lang"] = bookID, "zh", "ru"
cfg["source_file"] = "source.txt"
out, err := yaml.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, ConfigFile), out, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "source.txt"), []byte(source), 0o600); err != nil {
t.Fatal(err)
}
return dir
}