235 lines
8.1 KiB
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)
|
|
}
|
|
}
|