Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/move_test.go
T
jschoubben 2bf608fc5f CAT-K9: the move to the ledger: a dry run, the copy as operations, the check, the switch key, the database written beside it
A movableStore serves the database until the switch key says the ledger. The move copies every version of every record as an
import operation (the old author kept apart, marked imported; the ledger's author the module's account), refuses writes for its
length, checks record by record, and sets the switch only on a clean check. From then on the ledger is served and the database is
written beside it, behind or not (database.sequence, database.behind); the check runs at every start before the drop, and the
search index is built then.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:15:52 +01:00

123 lines
5.1 KiB
Go

package main
import (
"context"
"errors"
"strings"
"testing"
)
// The move (to-be 55 §8.1, AC53): a dry run writes nothing; the copy writes every version as an operation with the old
// author kept apart; writes wait while it runs; the check names every difference; the switch is set only on a clean check;
// from then on the ledger is served and the database written beside it, behind or not; run again nothing is copied twice.
func TestTheMoveCopiesChecksAndSwitchesOnlyOnACleanCheck(t *testing.T) {
ctx := context.Background()
db := newMemStore()
seed := imported(t, db) // the database holds the fixture's records, one version each
n := seed.reserved(t, "author@g14")
seed.accepted(t, "author@g14", valid(n))
f := newFakeLedger(t)
states := newFakeStates()
led := newLedgerStore(f, states)
m := newMovableStore(db, led, states)
w := newWorld(t, m, fixture())
w.k.States = states
w.tracker.next[KindDecision] = n + 1 // the numbers seed took are spent
if _, err := m.Move(ctx, MoveAsk{}, nil); err == nil {
t.Fatal("a move that is neither a dry run nor a commit")
}
dry, err := m.Move(ctx, MoveAsk{DryRun: true}, nil)
if err != nil || dry.Records < 10 || dry.Versions < dry.Records || len(f.applied) != 0 || m.Source() != SourceDatabase {
t.Fatalf("%v %+v, %d operations", err, dry, len(f.applied))
}
// Writes wait while the copy runs, and the copy says how far it is.
refused := false
m2 := newMovableStore(db, newLedgerStore(newFakeLedger(t), newFakeStates()), states)
m2.moving = true
if err := m2.Create(ctx, Version{Record: ledgerRecord(900, StatusReserved)}); errors.Is(err, ErrMoving) {
refused = true
}
if !refused {
t.Fatal("a write was taken while the records move")
}
// A difference stops the switch: a record changed in the database after the copy is named by the check.
moved, err := m.Move(ctx, MoveAsk{Commit: true}, nil)
if err != nil || !moved.Switched || len(moved.Problems) != 0 || m.Source() != SourceLedger || moved.Records != dry.Records {
t.Fatalf("%v %+v", err, moved)
}
// The old author is kept apart, the module's account is the ledger's.
var imports int
for _, a := range f.applied {
if a.Operation == OpImport {
imports++
}
}
if imports < moved.Versions {
t.Fatalf("%d import operations for %d versions", imports, moved.Versions)
}
last := f.hist[InstanceOf(Key{KindDecision, n})]
if op := last[len(last)-1]; op.Operation != OpImport || op.Author.Account != "mesh-knowledge" || op.Author.Caller != "" || !strings.Contains(string(op.Body), `"imported-by":"the agent on test`) {
t.Fatalf("the old author is not kept apart from the ledger's: %+v", op.Author)
}
// Run again: nothing is copied twice, and it is refused as the switch is set.
if _, err := m.Move(ctx, MoveAsk{Commit: true}, nil); err == nil {
t.Fatal("a second move was taken")
}
// Served from the ledger, and a write goes to both: the database is written beside it.
before := len(f.applied)
m2n := w.reserved(t, "author@g14")
if len(f.applied) == before {
t.Fatal("a reservation after the switch did not reach the ledger")
}
if e, _ := db.Get(ctx, Key{KindDecision, m2n}); e == nil {
t.Fatal("the database is not written beside the ledger")
}
if diffs, err := m.Check(ctx); err != nil || len(diffs) != 0 {
t.Fatalf("a check after the writes: %v %v", err, diffs)
}
// The database changed behind the ledger's back, and a write it refuses: the check names both.
e, _ := db.Get(ctx, Key{KindDecision, n})
r := e.Current.Clone()
r.Title = "Changed in the database only"
if err := db.Save(ctx, Version{By: "x", At: w.clock, Why: "x", Record: r}, true, false, e.Revision); err != nil {
t.Fatal(err)
}
diffs, _ := m.Check(ctx)
if len(diffs) == 0 || !strings.Contains(strings.Join(diffs, "\n"), "ADR 0300") {
t.Fatalf("the check missed a difference: %v", diffs)
}
}
// A copy that is not clean leaves the database the source.
func TestACopyThatIsNotCleanLeavesTheDatabaseTheSource(t *testing.T) {
ctx := context.Background()
db := newMemStore()
imported(t, db)
f := newFakeLedger(t)
states := newFakeStates()
m := newMovableStore(db, newLedgerStore(f, states), states)
f.failApply = OpImport
if _, err := m.Move(ctx, MoveAsk{Commit: true}, nil); err == nil || m.Source() != SourceDatabase {
t.Fatalf("a copy the ledger refused switched: %v", err)
}
f.failApply = ""
// A mirror the database refuses is written down: the ledger took the write, and the database is behind.
if moved, err := m.Move(ctx, MoveAsk{Commit: true}, nil); err != nil || !moved.Switched {
t.Fatalf("%v %+v", err, moved)
}
m.db = &failingSaves{memStore: db, key: Key{KindDecision, 11}, fail: true}
e, _ := m.Get(ctx, Key{KindDecision, 11})
r := e.Current.Clone()
r.Title = "Changed"
if err := m.Save(ctx, Version{By: "x", Record: r, At: e.Current.AcceptedAt}, true, false, e.Revision); err != nil {
t.Fatalf("the ledger's write is refused for the database's: %v", err)
}
if kv, _ := states.State(findingsState).Get(behindKey); kv == nil {
t.Fatal("a database behind is not written down")
}
if diffs, _ := m.Check(ctx); !strings.Contains(strings.Join(diffs, "\n"), "database is behind") {
t.Fatalf("%v", diffs)
}
}