Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/memory_test.go
T
jochen d413da203c
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mesh-issues: resume an interrupted carry by revision and fault one issue instead of the whole start, so no kept value is lost and the tracker always starts (review of PR 228)
An open's lost answer is judged by the issue's first entry; an entry that does not
fold faults its issue on start; a value written beside the log is never folded over;
one issue is kept in at most 252 KiB so its whole operation fits a log entry.
2026-10-12 10:35:57 +02:00

291 lines
7.4 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// memory is Store kept in this process with the bus's compare-and-set: a create is refused when the key
// has a value, an update when its revision is not the one given — each refusal stdio.ErrChanged, as the
// runtime answers it.
type memory struct {
mu sync.Mutex
rev uint64
data map[string]map[string]Entry
// fail, when set, is the error the next write of that state answers.
fail map[string]error
// hist is every value each key was given, the last eight kept, as the bucket kept them before the tracker
// switched to its log (novox/hq ADR 0300 §6).
hist map[string]map[string][]Entry
}
func newMemory() *memory {
return &memory{data: map[string]map[string]Entry{IssuesState: {}, CountersState: {}, GroomingState: {}, ReservationsState: {}}, fail: map[string]error{},
hist: map[string]map[string][]Entry{}}
}
func (m *memory) bucket(state string) (map[string]Entry, error) {
b, ok := m.data[state]
if !ok {
return nil, fmt.Errorf("%s is not declared", state)
}
return b, nil
}
func (m *memory) Get(state, key string) (*Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
b, err := m.bucket(state)
if err != nil {
return nil, err
}
e, ok := b[key]
if !ok {
return nil, nil
}
return &Entry{Value: append(json.RawMessage(nil), e.Value...), Revision: e.Revision}, nil
}
func (m *memory) write(state, key string, value any, check func(Entry, bool) bool) (uint64, error) {
raw, err := json.Marshal(value)
if err != nil {
return 0, err
}
m.mu.Lock()
defer m.mu.Unlock()
if err := m.fail[state]; err != nil {
delete(m.fail, state)
return 0, err
}
b, err := m.bucket(state)
if err != nil {
return 0, err
}
e, ok := b[key]
if !check(e, ok) {
return 0, fmt.Errorf("%s.%s: %w", state, key, stdio.ErrChanged)
}
m.rev++
b[key] = Entry{Value: raw, Revision: m.rev}
if m.hist[state] == nil {
m.hist[state] = map[string][]Entry{}
}
kept := append(m.hist[state][key], b[key])
if len(kept) > 8 {
kept = kept[len(kept)-8:]
}
m.hist[state][key] = kept
return m.rev, nil
}
// History is every value the key still keeps, oldest first, as the SDK's State(...).History answers it: puts and
// deletes, empty for a key that keeps nothing.
func (m *memory) History(state, key string) ([]stdio.StateRevision, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := []stdio.StateRevision{}
for _, e := range m.hist[state][key] {
op, value := "put", e.Value
if e.Value == nil {
op = "delete"
}
out = append(out, stdio.StateRevision{Key: key, Value: value, Revision: e.Revision, Op: op})
}
return out, nil
}
// deleteKey deletes a key, keeping the deletion among its values as the bucket does.
func (m *memory) deleteKey(state, key string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data[state], key)
m.rev++
m.hist[state][key] = append(m.hist[state][key], Entry{Revision: m.rev})
}
// empty takes every key out of a state, as an emptied bucket.
func (m *memory) empty(state string) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[state] = map[string]Entry{}
delete(m.hist, state)
}
func (m *memory) Create(state, key string, value any) (uint64, error) {
return m.write(state, key, value, func(_ Entry, there bool) bool { return !there })
}
func (m *memory) Update(state, key string, value any, revision uint64) (uint64, error) {
if revision == 0 {
return 0, errors.New("an update names the revision it read")
}
return m.write(state, key, value, func(e Entry, there bool) bool { return there && e.Revision == revision })
}
func (m *memory) put(state, key string, value any) {
_, _ = m.write(state, key, value, func(Entry, bool) bool { return true })
}
func (m *memory) Keys(state string) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
b, err := m.bucket(state)
if err != nil {
return nil, err
}
var keys []string
for k := range b {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, nil
}
// Increment is the SDK's (mesh-sdk go/state.go), over this store.
func (m *memory) Increment(state, key string, attempts int) (uint64, error) {
for i := 0; i < attempts; i++ {
e, err := m.Get(state, key)
if err != nil {
return 0, err
}
var next uint64 = 1
if e == nil {
_, err = m.Create(state, key, next)
} else {
var n uint64
if err := json.Unmarshal(e.Value, &n); err != nil {
return 0, err
}
next = n + 1
_, err = m.Update(state, key, next, e.Revision)
}
if err == nil {
return next, nil
}
if !errors.Is(err, stdio.ErrChanged) {
return 0, err
}
}
return 0, fmt.Errorf("%s: %w", key, stdio.ErrChanged)
}
func (m *memory) counter(t *testing.T) uint64 {
t.Helper()
e, _ := m.Get(CountersState, IssueCounter)
if e == nil {
return 0
}
n, err := strconv.ParseUint(string(e.Value), 10, 64)
if err != nil {
t.Fatalf("the counter holds %s", e.Value)
}
return n
}
// said is one event the tracker said.
type said struct {
Event string
Body map[string]any
}
type harness struct {
*Tracker
mem *memory
log *fakeLog
mu sync.Mutex
events []said
settings Settings
emitErr error
}
func newHarness(t *testing.T) *harness {
t.Helper()
h := &harness{mem: newMemory(), log: newFakeLog(), settings: Settings{FirstNumber: 365, Owners: []string{
"hq", "mesh-catalog", "mesh-controller", "mesh-host", "mesh-sdk", "mesh-tools",
"mesh-catalog modules/nats", "mesh-catalog modules/records", "mesh-catalog modules/mesh-issues"}}}
clock := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)
h.Tracker = &Tracker{
Store: h.mem,
Log: h.log,
Emit: func(event string, body any) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.emitErr != nil {
return h.emitErr
}
raw, _ := json.Marshal(body)
var m map[string]any
_ = json.Unmarshal(raw, &m)
h.events = append(h.events, said{event, m})
return nil
},
Settings: func() (Settings, error) { return h.settings, nil },
Now: func() time.Time { return clock },
Logf: t.Logf,
Sleep: func(time.Duration) {},
}
t.Cleanup(func() {
if h.Tracker.Log == Log(h.log) {
h.log.foldCheck(t, h.mem)
}
})
return h
}
func (h *harness) said() []said {
h.mu.Lock()
defer h.mu.Unlock()
return append([]said(nil), h.events...)
}
func (h *harness) open(t *testing.T, title string, located ...string) uint64 {
t.Helper()
o, err := h.Open(OpenAsk{Title: title, Report: "seen: " + title, LocatedIn: located})
if err != nil {
t.Fatalf("open %q: %v", title, err)
}
return o.Number
}
// fixing moves an issue to fixing, as the agent that starts its fix does (ADR 0284).
func (h *harness) fixing(t *testing.T, n uint64) {
t.Helper()
if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix@g14"}); err != nil {
t.Fatalf("move %d to fixing: %v", n, err)
}
}
// call runs one verb as the tool runner does: by its tool, with arguments as JSON gives them.
func (h *harness) call(t *testing.T, verb string, args map[string]any) (any, error) {
t.Helper()
for _, tool := range Tools(h.Tracker) {
if tool.Name == Seat+"."+verb {
raw, _ := json.Marshal(args)
var m map[string]any
_ = json.Unmarshal(raw, &m)
return tool.Run(m)
}
}
t.Fatalf("no verb %s", verb)
return nil, nil
}
// snapshot is every value of a state, by key, as kept now.
func (m *memory) snapshot(state string) map[string]string {
m.mu.Lock()
defer m.mu.Unlock()
out := map[string]string{}
for k, e := range m.data[state] {
out[k] = string(e.Value)
}
return out
}