Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/memory_test.go
T
jschoubben 3aaccba4e2
mesh/delivery superseded: a newer head of the same pull request
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-issues: read merged from the pull request, not the state, so a closed one is withdrawn (hq issue 483)
mesh-delivery says stopped for a pull request closed unmerged too; read as
merged, it was said stuck to the operator for good. Cite ADR 0284 for the
worker rules, which the comments gave as 0283.
2026-10-11 12:24:45 +02:00

239 lines
5.9 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
}
func newMemory() *memory {
return &memory{data: map[string]map[string]Entry{IssuesState: {}, CountersState: {}, GroomingState: {}, ReservationsState: {}}, fail: map[string]error{}}
}
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}
return m.rev, nil
}
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
mu sync.Mutex
events []said
settings Settings
emitErr error
}
func newHarness(t *testing.T) *harness {
t.Helper()
h := &harness{mem: newMemory(), 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,
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) {},
}
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
}