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
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.
291 lines
7.4 KiB
Go
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
|
|
}
|