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 superseded: a newer head of the same pull request
A new verb, issue-tracker.reserve {kind, for} -> {kind, number}: one counter
per sequence beside the issue counter, advanced the same way, started above
what hq's record holds, a floor setting for open branches and the
reservations kept, and raised when hq holds a higher number. A reserved
number is spent forever, kept with who, when and for what, and said as the
event reserved (hq ADR 0275 rule 8, ADR 0290 rule 5).
239 lines
5.9 KiB
Go
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 0283).
|
|
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
|
|
}
|