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-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.
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 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
|
|
}
|