textmachine/backend/internal/pipeline/checks_integration_test.go

149 lines
6 KiB
Go

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