149 lines
6 KiB
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)
|
|
}
|
|
}
|