package pipeline // checks_integration_test.go: the driver-side fixtures for the check subsystems — the ones that need a // live Runner (a cheap-gate count must reach the persisted retrieval-state and the status report) or the // memory bank (the DC3 gender constraint is rendered by the bank's injection renderer). The pure // per-rule fixtures live with their rules in internal/checks. import ( "context" "testing" "textmachine/backend/internal/checks" "textmachine/backend/internal/lang" ) // compileTestCheckers builds the checker spec from the REAL in-repo zh-ru pack + ru target data, so a // driver-side fixture exercises the pack's own patterns rather than a test-local copy of them. func compileTestCheckers(t *testing.T) *checks.Checkers { t.Helper() c := checks.CompileCheckers(testLangPack(t).DCCheckers, lang.TargetChecksFor("ru")) c.SetSourceScripts(lang.LangScripts("zh")) // the driver fixtures are zh→ru; the repair script-guard needs Han return c } // testLangPack loads the real in-repo zh→ru language pack (configs/langpacks/) for the driver-side // fixtures that need pair data. The book-scoped overlay (internal/miner/testdata) is deliberately NOT // loaded: it adds only the 蛊真人 clan surname, which no checker or seed fixture reads — the miner keeps // its own overlay-loading helper for the surname-anchored cases. func testLangPack(t *testing.T) *lang.Pack { t.Helper() p, err := lang.Load("../../configs/langpacks", "zh", "ru") if err != nil { t.Fatalf("load langpack: %v", err) } return p } // TestCheapGatesSurfaceInStatus is the end-to-end wiring test: a chunk whose FINAL text has a style // issue (a hyphen-led dialogue line) records a style flag in retrieval_state and surfaces it in the // status projection — WITHOUT flagging the chunk (observability, never a disposition). func TestCheapGatesSurfaceInStatus(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "- Привет, — сказал он.", "stop" // hyphen dialogue → dialogue-dash flag } return "ЧЕРНОВИК ПЕРЕВОДА", "stop" }) defer srv.Close() bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: "テキスト。", regenerate: 0}) ctx := context.Background() r := newRunner(t, bookPath) defer r.Close() res, err := r.TranslateBook(ctx) if err != nil { t.Fatal(err) } if res.Flagged != 0 { t.Fatalf("a style flag must not flag the chunk (observability only), got flagged=%d", res.Flagged) } rs, err := r.Store.GetRetrievalState("test-book", 1, 0) if err != nil || rs == nil { t.Fatalf("no retrieval state persisted: %v", err) } if rs.NStyleFlags == 0 { t.Fatalf("style flag not persisted in retrieval_state: %+v", rs) } rep, err := r.Status(ctx) if err != nil { t.Fatal(err) } if rep.StyleFlags == 0 { t.Fatal("status must surface the book-wide style-flag count") } if rep.Done != 1 || rep.Flagged != 0 { t.Fatalf("the chunk must be done, not flagged: done=%d flagged=%d", rep.Done, rep.Flagged) } } // TestRepairResidualScanSurfacesInQualityReport is the end-to-end wiring of the $0 residual scan (pack-16): // a unit whose SHIPPED text carries an addressable defect must surface as a repair CANDIDATE in the // read-only quality projection — no LLM call, no snapshot touch, no disposition change. This is the // measurement that gates whether the paid loop is worth enabling, so it has to work on stored runs. func TestRepairResidualScanSurfacesInQualityReport(t *testing.T) { srv := newJSONProvider(&reqRec{}, func(body string) (string, string) { if isEditBody(body) { // 半个时辰 ≈ 1 h rendered as half an hour — the defect class the pack-16 pair-data fix added. return "Он прождал полчаса и вошёл внутрь.", "stop" } return "ЧЕРНОВИК ПЕРЕВОДА.", "stop" }) defer srv.Close() bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: "他等了半个时辰。"}) r := newRunner(t, bookPath) defer r.Close() // The fixture book declares no langpack_root, so compile the REAL zh-ru checker spec onto the runner — // the scan is only meaningful for a pair that ships checker data, and this keeps the fixture honest // (the pack's own patterns, not a test-local copy). r.checkers = compileTestCheckers(t) res, err := r.TranslateBook(context.Background()) if err != nil { t.Fatal(err) } if res.Flagged != 0 { t.Fatalf("an addressable defect is OBSERVABILITY — it must not flag the unit, got flagged=%d", res.Flagged) } q, err := r.QualityReport() if err != nil { t.Fatalf("QualityReport: %v", err) } if q.RepairCandidates != 1 { t.Fatalf("residual scan = %d candidates, want 1 (%+v)", q.RepairCandidates, q.RepairCandidatesByClass) } if got := q.RepairCandidatesByClass["dc1_fractional"]; got != 1 { t.Fatalf("per-class residual = %+v, want dc1_fractional:1", q.RepairCandidatesByClass) } if len(q.Chunks) != 1 || q.Chunks[0].RepairCandidates != 1 { t.Fatalf("per-unit residual missing: %+v", q.Chunks) } } // A book whose pair ships NO checker data must report a zero residual rather than a spurious one — the // no-pack path every non-zh book runs, and the reason the field is omitempty. func TestRepairResidualScanZeroWithoutPackData(t *testing.T) { srv := newJSONProvider(&reqRec{}, func(body string) (string, string) { if isEditBody(body) { return "Он прождал полчаса и вошёл внутрь.", "stop" } return "ЧЕРНОВИК ПЕРЕВОДА.", "stop" }) defer srv.Close() bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: "他等了半个时辰。"}) r := newRunner(t, bookPath) defer r.Close() if _, err := r.TranslateBook(context.Background()); err != nil { t.Fatal(err) } q, err := r.QualityReport() if err != nil { t.Fatalf("QualityReport: %v", err) } if q.RepairCandidates != 0 || q.RepairCandidatesByClass != nil { t.Fatalf("a no-pack book must report a zero residual, got %d %+v", q.RepairCandidates, q.RepairCandidatesByClass) } }