textmachine/backend/internal/pipeline/driftbasis_test.go

235 lines
8.1 KiB
Go

package pipeline
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"textmachine/backend/internal/obs"
)
// driftbasis_test.go: backlog row 239 (status blind to a dropped stage) and its basis half (A12).
//
// The cold run of 31.08 reproduced 239 word for word: ONE config, ONE database, `status --json` answering
// config_drift=false with percent_done=85 while `export --json` on the SAME config answered true. These
// pin BOTH halves, because the orchestrator's ratification made them one edit: a `config_drift` that flips
// to true without a basis produces a NEW lie one field over (the human render turns the boolean into a
// re-payment claim), so the rule and the basis land together or not at all.
// dropEditStage removes the editor stage from a fixture's pipeline, leaving its stored rows behind — the
// exact state row 239 describes.
func dropEditStage(t *testing.T, bookPath string) {
t.Helper()
p := filepath.Join(filepath.Dir(bookPath), "pipeline.yaml")
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
var kept []string
for _, line := range strings.Split(string(b), "\n") {
if strings.Contains(line, "name: edit,") || strings.Contains(line, "name: edit ") {
continue
}
kept = append(kept, line)
}
out := strings.Join(kept, "\n")
if out == string(b) {
t.Fatalf("fixture drifted: no edit stage line to drop:\n%s", string(b))
}
if err := os.WriteFile(p, []byte(out), 0o644); err != nil {
t.Fatal(err)
}
}
// TestStatusSeesADroppedStageJustLikeExport is row 239. Two $0 read-models over one config and one
// database must not answer opposite things about whether the stored rows are the rows the run shipped.
//
// Mutation this catches: delete the orphanStageRows call from status.go and status answers
// config_drift=false / basis=none over a book whose editor stage no longer exists — the reproduced defect.
func TestStatusSeesADroppedStageJustLikeExport(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := volumeBook(t, srv.URL, 2)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r1 := newRunner(t, bookPath)
if _, err := r1.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
r1.Close()
dropEditStage(t, bookPath)
r2 := newRunner(t, bookPath)
defer r2.Close()
rep, err := r2.Status(ctx)
if err != nil {
t.Fatal(err)
}
exp, err := r2.Export(false)
if err != nil {
t.Fatal(err)
}
if !exp.ConfigDrift {
t.Fatal("fixture drifted: export has always caught a dropped stage; if it no longer does, the " +
"premise of row 239 is gone and this test is testing nothing")
}
if !rep.ConfigDrift {
t.Fatalf("status must see the dropped stage that export sees — one config, one database, two "+
"$0 surfaces, opposite answers is the defect (row 239). status=%t export=%t basis=%q",
rep.ConfigDrift, exp.ConfigDrift, rep.ConfigDriftBasis)
}
if rep.ConfigDriftBasis != DriftBasisDrift || exp.ConfigDriftBasis != DriftBasisDrift {
t.Fatalf("a drift that WAS established must say so in the basis; status=%q export=%q",
rep.ConfigDriftBasis, exp.ConfigDriftBasis)
}
}
// TestACleanBookSaysItsDriftWasACTUALLYChecked is the other side of the basis, and without it the field
// would be free to answer `unknown` always and still pass the test above.
func TestACleanBookSaysItsDriftWasACTUALLYChecked(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := volumeBook(t, srv.URL, 2)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r1 := newRunner(t, bookPath)
if _, err := r1.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
r1.Close()
r2 := newRunner(t, bookPath)
defer r2.Close()
rep, err := r2.Status(ctx)
if err != nil {
t.Fatal(err)
}
if rep.ConfigDrift {
t.Fatalf("nothing was edited, so there is no drift: %+v", rep.ConfigDriftBasis)
}
if rep.ConfigDriftBasis != DriftBasisNone {
t.Fatalf("`false` on an unedited book is an ANSWER and must say so, not hide behind unknown; got %q",
rep.ConfigDriftBasis)
}
exp, err := r2.Export(false)
if err != nil {
t.Fatal(err)
}
if exp.ConfigDriftBasis != DriftBasisNone {
t.Fatalf("export must reach the same answer on the same book; got %q", exp.ConfigDriftBasis)
}
}
// TestABookWithNoRowsCannotHaveItsDriftChecked pins the third value, which is the whole reason the field
// exists: `config_drift:false` on a book nothing has run is NOT «the rows match».
func TestABookWithNoRowsCannotHaveItsDriftChecked(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := volumeBook(t, srv.URL, 2)
r := newRunner(t, bookPath)
defer r.Close()
rep, err := r.Status(context.Background())
if err != nil {
t.Fatal(err)
}
if rep.ConfigDrift {
t.Fatal("a book with no rows cannot have drifted")
}
if rep.ConfigDriftBasis != DriftBasisUnknown {
t.Fatalf("with no stored row there is nothing to compare, so the verdict is UNKNOWN and must not "+
"read as «checked, clean»; got %q", rep.ConfigDriftBasis)
}
}
// TestTheDriftBasisVocabularyIsClosed keeps the three words honest against each other in one place: the
// mapping is the only thing both surfaces share, so a fourth state entering by accident shows up here.
func TestTheDriftBasisVocabularyIsClosed(t *testing.T) {
cases := []struct {
ran, drifted bool
want string
}{
{false, false, DriftBasisUnknown},
{false, true, DriftBasisUnknown}, // never established ⇒ never «drift», whatever the boolean says
{true, false, DriftBasisNone},
{true, true, DriftBasisDrift},
}
for _, c := range cases {
if got := driftBasisFor(c.ran, c.drifted); got != c.want {
t.Errorf("driftBasisFor(ran=%t, drifted=%t) = %q, want %q", c.ran, c.drifted, got, c.want)
}
}
}
// TestBothSurfacesReachTheSameBasisOnTheSameBook is the acceptance's Д2: the CURE reproduced the DISEASE.
//
// `config_drift_basis` exists because `false` meant two things. The first version then let each surface
// decide for itself when the check was possible — and on one book with one config `status` answered
// `unknown` while `export` answered `none`. That is backlog row 239 word for word, inside the field built
// to stop it.
//
// Mutation this catches: give either surface its own precondition again (e.g. `ran := true` in export) and
// the two answers separate on the never-run book → RED.
func TestBothSurfacesReachTheSameBasisOnTheSameBook(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
// Three books in three states: never run · run clean · run then a stage dropped. On EVERY one the two
// surfaces must reach the same word, because they are answering the same question about the same rows.
t.Run("never run", func(t *testing.T) {
r := newRunner(t, volumeBook(t, srv.URL, 2))
defer r.Close()
assertSameBasis(t, r, ctx)
})
t.Run("run clean", func(t *testing.T) {
bookPath := volumeBook(t, srv.URL, 2)
w := newRunner(t, bookPath)
if _, err := w.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
w.Close()
r := newRunner(t, bookPath)
defer r.Close()
assertSameBasis(t, r, ctx)
})
t.Run("stage dropped", func(t *testing.T) {
bookPath := volumeBook(t, srv.URL, 2)
w := newRunner(t, bookPath)
if _, err := w.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
w.Close()
dropEditStage(t, bookPath)
r := newRunner(t, bookPath)
defer r.Close()
assertSameBasis(t, r, ctx)
})
}
func assertSameBasis(t *testing.T, r *Runner, ctx context.Context) {
t.Helper()
rep, err := r.Status(ctx)
if err != nil {
t.Fatal(err)
}
exp, err := r.Export(false)
if err != nil {
t.Fatal(err)
}
if rep.ConfigDriftBasis != exp.ConfigDriftBasis {
t.Fatalf("one book, one config, two $0 surfaces, two different answers about the SAME fact — that "+
"is row 239 reproduced inside the field built to stop it: status=%q export=%q",
rep.ConfigDriftBasis, exp.ConfigDriftBasis)
}
if rep.ConfigDrift != exp.ConfigDrift {
t.Fatalf("and the booleans must agree too: status=%t export=%t", rep.ConfigDrift, exp.ConfigDrift)
}
}