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/delivery-group group feat/399-fixing-status delivered: every member is delivered
The note's by is who acted as the runtime proves it (ADR 0275 §5); the note's text names both workers. Tests for a resolved issue corrected, link and set keeping the worker current, and a take-over reason with a secret's shape. The ready-to-verify note says an issue resolves from fixing.
653 lines
24 KiB
Go
653 lines
24 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"reflect"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||
)
|
||
|
||
func TestTheFirstIssueGetsTheSettingsNumberAndEachNextOneMore(t *testing.T) {
|
||
h := newHarness(t)
|
||
for want := uint64(365); want < 368; want++ {
|
||
if got := h.open(t, "a symptom"); got != want {
|
||
t.Fatalf("opened %d, want %d", got, want)
|
||
}
|
||
}
|
||
is, err := h.Get(366)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if is.Status != StatusOpen || is.Title != "a symptom" || is.Report != "seen: a symptom" || is.OpenedBy != Unproven ||
|
||
is.Opened.IsZero() || is.Closed != nil {
|
||
t.Fatalf("kept %+v", is)
|
||
}
|
||
}
|
||
|
||
func TestTheCounterNeverStartsBelowAnIssueAlreadyKept(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.mem.put(IssuesState, "400", Issue{Number: 400, Title: "kept before", Status: StatusOpen, Opening: "x"})
|
||
if got := h.open(t, "after it"); got != 401 {
|
||
t.Fatalf("opened %d, want 401: the counter started below an issue kept", got)
|
||
}
|
||
}
|
||
|
||
func TestTheCounterIsStartedOnlyOnceAndTheSettingIsThenNotRead(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.open(t, "first")
|
||
h.settings.FirstNumber = 10 // a later setting cannot move a number
|
||
if got := h.open(t, "second"); got != 366 {
|
||
t.Fatalf("opened %d, want 366", got)
|
||
}
|
||
h2 := newHarness(t)
|
||
h2.settings.FirstNumber = 0
|
||
if _, err := h2.Open(OpenAsk{Title: "t", Report: "r"}); err == nil || !strings.Contains(err.Error(), "first-number") {
|
||
t.Fatalf("an unset first number: %v", err)
|
||
}
|
||
if h2.mem.counter(t) != 0 {
|
||
t.Fatal("a counter was started without its first number")
|
||
}
|
||
}
|
||
|
||
func TestARefusedOpenSpendsNoNumber(t *testing.T) {
|
||
h := newHarness(t)
|
||
refused := []OpenAsk{
|
||
{Title: "", Report: "r"},
|
||
{Title: "t", Report: " "},
|
||
{Title: "two\nlines", Report: "r"},
|
||
{Title: strings.Repeat("x", MostTitle+1), Report: "r"},
|
||
{Title: "t", Report: strings.Repeat("x", MostReport+1)},
|
||
{Title: "t", Report: "r", LocatedIn: []string{"somewhere-else"}},
|
||
{Title: "t", Report: "r", LocatedIn: []string{"mesh-controllerx"}},
|
||
{Title: "t", Report: "r", Record: "../escape"},
|
||
{Title: "t", Report: "r", Record: "04-ISSUES/365-x"},
|
||
{Title: "t", Report: "r", Record: "Upper"},
|
||
{Title: "t", Report: "the key is sk-ant-api03-abcdefghijklmnopqrstuvwxyz"},
|
||
{Title: "t\x1b[31m", Report: "r"},
|
||
{Title: "t", Report: "r"},
|
||
{Title: "t", Report: "r\x00"},
|
||
}
|
||
for i, a := range refused {
|
||
if _, err := h.Open(a); err == nil {
|
||
t.Errorf("case %d (%+v) was opened", i, a)
|
||
}
|
||
}
|
||
if h.mem.counter(t) != 0 || len(h.said()) != 0 {
|
||
t.Fatalf("a refused open moved the counter to %d or said %v", h.mem.counter(t), h.said())
|
||
}
|
||
if got := h.open(t, "valid"); got != 365 {
|
||
t.Fatalf("the first valid open got %d, want 365", got)
|
||
}
|
||
}
|
||
|
||
func TestAnIssuesKeyIsNeverOverwrittenAndTheFaultIsSaid(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.open(t, "first") // 365, the counter started
|
||
h.mem.put(IssuesState, "366", Issue{Number: 366, Title: "already here", Opening: "other"})
|
||
_, err := h.Open(OpenAsk{Title: "second", Report: "r"})
|
||
if err == nil || !strings.Contains(err.Error(), "already kept") || !strings.Contains(err.Error(), "366 stays unused") {
|
||
t.Fatalf("open over a kept key: %v", err)
|
||
}
|
||
is, _ := h.Get(366)
|
||
if is.Title != "already here" {
|
||
t.Fatal("the kept issue was overwritten")
|
||
}
|
||
if got := h.open(t, "third"); got != 367 {
|
||
t.Fatalf("the next open got %d, want 367", got)
|
||
}
|
||
}
|
||
|
||
func TestANumberTakenAndNotWrittenIsSaidAndNeverHandedOutAgain(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.open(t, "first")
|
||
h.mem.fail[IssuesState] = errors.New("the bus did not answer")
|
||
// One failure is tried again and succeeds: no gap.
|
||
if got := h.open(t, "second"); got != 366 {
|
||
t.Fatalf("got %d", got)
|
||
}
|
||
// Failing every time leaves a gap, said.
|
||
failing := &failingStore{memory: h.mem, failCreates: createAttempts}
|
||
h.Store = failing
|
||
_, err := h.Open(OpenAsk{Title: "third", Report: "r"})
|
||
if err == nil || !strings.Contains(err.Error(), "may have been written") || !strings.Contains(err.Error(), "get 367") {
|
||
t.Fatalf("a write that never landed: %v", err)
|
||
}
|
||
h.Store = h.mem
|
||
if got := h.open(t, "fourth"); got != 368 {
|
||
t.Fatalf("got %d, want 368: a number is never handed out twice", got)
|
||
}
|
||
}
|
||
|
||
// failingStore fails the next creates of issues, as a bus that takes no write.
|
||
type failingStore struct {
|
||
*memory
|
||
failCreates int
|
||
}
|
||
|
||
func (f *failingStore) Create(state, key string, value any) (uint64, error) {
|
||
if state == IssuesState && f.failCreates > 0 {
|
||
f.failCreates--
|
||
return 0, errors.New("the bus did not answer")
|
||
}
|
||
return f.memory.Create(state, key, value)
|
||
}
|
||
|
||
// lostAnswer writes the issue and then says it failed, as a bus whose answer was lost.
|
||
type lostAnswer struct {
|
||
*memory
|
||
once bool
|
||
}
|
||
|
||
func (l *lostAnswer) Create(state, key string, value any) (uint64, error) {
|
||
rev, err := l.memory.Create(state, key, value)
|
||
if state == IssuesState && !l.once && err == nil {
|
||
l.once = true
|
||
return 0, errors.New("no answer in 30 s")
|
||
}
|
||
return rev, err
|
||
}
|
||
|
||
func TestAWriteWhoseAnswerWasLostIsKnownAsItsOwn(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.Store = &lostAnswer{memory: h.mem}
|
||
o, err := h.Open(OpenAsk{Title: "t", Report: "r"})
|
||
if err != nil || o.Number != 365 {
|
||
t.Fatalf("%v %v", o, err)
|
||
}
|
||
}
|
||
|
||
// lostEvery writes the issue on its first create and then answers every create with a failure.
|
||
type lostEvery struct{ *memory }
|
||
|
||
func (l lostEvery) Create(state, key string, value any) (uint64, error) {
|
||
if state == IssuesState {
|
||
_, _ = l.memory.Create(state, key, value)
|
||
return 0, errors.New("no answer in 30 s")
|
||
}
|
||
return l.memory.Create(state, key, value)
|
||
}
|
||
|
||
// unreadable answers a taken key with a refusal and cannot read it back.
|
||
type unreadable struct{ *memory }
|
||
|
||
func (u unreadable) Create(state, key string, value any) (uint64, error) {
|
||
if state == IssuesState {
|
||
return 0, fmt.Errorf("%s: %w", key, stdio.ErrChanged)
|
||
}
|
||
return u.memory.Create(state, key, value)
|
||
}
|
||
func (u unreadable) Get(state, key string) (*Entry, error) {
|
||
if state == IssuesState {
|
||
return nil, errors.New("no answer in 30 s")
|
||
}
|
||
return u.memory.Get(state, key)
|
||
}
|
||
|
||
// An open whose write may have landed never says nothing was opened (the review of 2026-10-10).
|
||
func TestAnOpenWhoseWriteMayHaveLandedNeverSaysNothingWasOpened(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.Store = lostEvery{h.mem}
|
||
if o, err := h.Open(OpenAsk{Title: "t", Report: "r"}); err != nil || o.Number != 365 {
|
||
t.Fatalf("a write that landed, every answer lost: %v %v", o, err)
|
||
}
|
||
h.Store = unreadable{h.mem}
|
||
_, err := h.Open(OpenAsk{Title: "t", Report: "r"})
|
||
if err == nil || strings.Contains(err.Error(), "nothing was opened") || !strings.Contains(err.Error(), "get 366") {
|
||
t.Fatalf("a taken key that cannot be read back: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestOpenSaysAnEventWithWhatChangedAndWhoChangedIt(t *testing.T) {
|
||
h := newHarness(t)
|
||
o, err := h.Open(OpenAsk{Title: "a title", Report: "a report", LocatedIn: []string{"mesh-controller internal/broker"}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !o.Announced || o.By != Unproven || o.Status != StatusOpen || !strings.Contains(o.Said, "Opened issue 365") {
|
||
t.Fatalf("answer %+v", o)
|
||
}
|
||
ev := h.said()
|
||
if len(ev) != 1 || ev[0].Event != EventOpened || ev[0].Body["number"] != float64(365) || ev[0].Body["title"] != "a title" ||
|
||
ev[0].Body["by"] != Unproven || ev[0].Body["status"] != StatusOpen {
|
||
t.Fatalf("events %+v", ev)
|
||
}
|
||
if _, carried := ev[0].Body["report"]; carried {
|
||
t.Fatal("the opened event carries the whole report; it says what changed, and get has the rest")
|
||
}
|
||
}
|
||
|
||
func TestAChangeWhoseEventCannotBeSaidIsKeptAndSaysSo(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.emitErr = errors.New("the bus refused it")
|
||
o, err := h.Open(OpenAsk{Title: "t", Report: "r"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if o.Announced || !strings.Contains(o.Warning, "could not be said") {
|
||
t.Fatalf("a change not announced was reported as announced: %+v", o)
|
||
}
|
||
if _, err := h.Get(o.Number); err != nil {
|
||
t.Fatal("the change was not kept")
|
||
}
|
||
}
|
||
|
||
func TestAnIssueWithAFolderIsNumberedTitledAndPointsAtItsFolder(t *testing.T) {
|
||
h := newHarness(t)
|
||
o, err := h.Open(OpenAsk{Title: "t", Report: "r", Record: "the-counter-skips-a-number"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if o.Record != "04-ISSUES/365-the-counter-skips-a-number" || o.Status != StatusOpen {
|
||
t.Fatalf("answer %+v", o)
|
||
}
|
||
// Since the import of slice 3, an issue with a folder is whole in the tracker: its status moves here.
|
||
if _, err := h.Move(MoveAsk{Number: 365, Status: StatusDiagnosing, By: "fix-365@g14"}); err != nil {
|
||
t.Fatalf("a move of an issue with a folder: %v", err)
|
||
}
|
||
if _, err := h.Note(365, "notes are taken"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
l, _ := h.List(ListAsk{})
|
||
if l.Rows[0].Status != StatusDiagnosing {
|
||
t.Fatalf("listed %+v", l.Rows)
|
||
}
|
||
}
|
||
|
||
// The cycle's rules at the write (to-be 53 §5, ADR 0283): one case per row, each refused case changing nothing.
|
||
// A case starts from open, or, with from, from that status reached first (fixing by fix@g14).
|
||
func TestTheCyclesRulesHoldAtTheWrite(t *testing.T) {
|
||
const fx = StatusFixing
|
||
cases := []struct {
|
||
name string
|
||
located []string // at open
|
||
from string // the status it is moved to first; "" stays open
|
||
ask MoveAsk
|
||
refused string
|
||
}{
|
||
{"an unknown status", nil, "", MoveAsk{Status: "closed"}, "not one of"},
|
||
{"located without located-in", nil, "", MoveAsk{Status: StatusLocated}, "without located-in"},
|
||
{"located with an unregistered owner", nil, "", MoveAsk{Status: StatusLocated, LocatedIn: []string{"elsewhere"}}, "no registered owner"},
|
||
{"located with located-in", nil, "", MoveAsk{Status: StatusLocated, LocatedIn: []string{"mesh-catalog modules/records"}}, ""},
|
||
{"located, named at open", []string{"hq"}, "", MoveAsk{Status: StatusLocated}, ""},
|
||
{"resolved without located-in", nil, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"hq#1"}}, "without located-in"},
|
||
{"resolved without fixed-by", []string{"hq"}, fx, MoveAsk{Status: StatusResolved}, "without fixed-by"},
|
||
{"resolved", []string{"hq"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"hq#280"}}, ""},
|
||
{"resolved from open", []string{"hq"}, "", MoveAsk{Status: StatusResolved, FixedBy: []string{"hq#280"}}, "only from fixing"},
|
||
{"resolved from located", []string{"hq"}, StatusLocated, MoveAsk{Status: StatusResolved, FixedBy: []string{"hq#280"}}, "only from fixing"},
|
||
{"resolved from diagnosing", []string{"hq"}, StatusDiagnosing, MoveAsk{Status: StatusResolved, FixedBy: []string{"hq#280"}}, "only from fixing"},
|
||
{"a core issue resolved without a replay", []string{"mesh-controller"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"mesh-controller#9"}}, "core issue"},
|
||
{"a core issue in the bus resolved without a replay", []string{"mesh-catalog modules/nats"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"mesh-catalog#9"}}, "core issue"},
|
||
{"a core issue resolved with a replay", []string{"mesh-tools node-tools/internal/bus"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"mesh-tools#25"}, Replay: "R361"}, ""},
|
||
{"a core issue resolved with replay-none", []string{"mesh-sdk"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"mesh-sdk#16"}, ReplayNone: "it needs two machines"}, ""},
|
||
{"a replay that names none", []string{"mesh-sdk"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"mesh-sdk#16"}, Replay: "yes"}, "names no replay"},
|
||
{"replay and replay-none", []string{"mesh-sdk"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"x"}, Replay: "R1", ReplayNone: "y"}, "not both"},
|
||
{"a replay on another status", []string{"mesh-sdk"}, "", MoveAsk{Status: StatusLocated, Replay: "R1"}, "what resolving"},
|
||
{"wontfix without a reason", nil, "", MoveAsk{Status: StatusWontfix}, "without a reason"},
|
||
{"wontfix", nil, "", MoveAsk{Status: StatusWontfix, Reason: "the module is retired"}, ""},
|
||
{"wontfix from fixing", nil, fx, MoveAsk{Status: StatusWontfix, Reason: "the module is retired"}, ""},
|
||
{"a reason on another status", nil, "", MoveAsk{Status: StatusDiagnosing, By: "a@g14", Reason: "x"}, "what wontfix needs"},
|
||
{"to the status it has", nil, "", MoveAsk{Status: StatusOpen}, "already open"},
|
||
{"diagnosing", nil, "", MoveAsk{Status: StatusDiagnosing, By: "fix-365@g14"}, ""},
|
||
{"diagnosing without by", nil, "", MoveAsk{Status: StatusDiagnosing}, "needs by"},
|
||
{"fixing", nil, "", MoveAsk{Status: StatusFixing, By: "Jochen@shanks"}, ""},
|
||
{"fixing without by", nil, "", MoveAsk{Status: StatusFixing}, "needs by"},
|
||
{"fixing with a by that names no node", nil, "", MoveAsk{Status: StatusFixing, By: "an agent"}, "who@node"},
|
||
{"fixing with a by that carries a secret", nil, "", MoveAsk{Status: StatusFixing, By: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz@g14"}, "carries no secret"},
|
||
{"by on another status", []string{"hq"}, "", MoveAsk{Status: StatusLocated, By: "a@g14"}, "only diagnosing and fixing"},
|
||
{"take-over on another status", []string{"hq"}, "", MoveAsk{Status: StatusLocated, TakeOver: "x"}, "take-over is for"},
|
||
{"take-over with nobody to take from", nil, "", MoveAsk{Status: StatusFixing, By: "a@g14", TakeOver: "x"}, "nobody works on"},
|
||
{"an amended design that is not one", nil, "", MoveAsk{Status: StatusDiagnosing, By: "a@g14", AmendedDesign: []string{"../etc/passwd"}}, "not a design document"},
|
||
{"a take-over reason that carries a secret", nil, StatusDiagnosing, MoveAsk{Status: StatusFixing, By: "b@ace", TakeOver: "the key is sk-ant-api03-abcdefghijklmnopqrstuvwxyz"}, "carries no secret"},
|
||
{"a fixed-by of two lines", []string{"hq"}, fx, MoveAsk{Status: StatusResolved, FixedBy: []string{"a\nb"}}, "one line"},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
h := newHarness(t)
|
||
n := h.open(t, "an issue", c.located...)
|
||
from := StatusOpen
|
||
switch c.from {
|
||
case "":
|
||
case StatusFixing:
|
||
h.fixing(t, n)
|
||
from = StatusFixing
|
||
case StatusDiagnosing:
|
||
if _, err := h.Move(MoveAsk{Number: n, Status: c.from, By: "fix@g14"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
from = c.from
|
||
default:
|
||
if _, err := h.Move(MoveAsk{Number: n, Status: c.from}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
from = c.from
|
||
}
|
||
before, _ := h.mem.Get(IssuesState, keyOf(n))
|
||
events := len(h.said())
|
||
c.ask.Number = n
|
||
m, err := h.Move(c.ask)
|
||
if c.refused != "" {
|
||
if err == nil || !strings.Contains(err.Error(), c.refused) {
|
||
t.Fatalf("want refused with %q, got %v", c.refused, err)
|
||
}
|
||
after, _ := h.mem.Get(IssuesState, keyOf(n))
|
||
if after.Revision != before.Revision || string(after.Value) != string(before.Value) {
|
||
t.Fatal("a refused move changed the issue")
|
||
}
|
||
if len(h.said()) != events {
|
||
t.Fatalf("a refused move said %v", h.said())
|
||
}
|
||
return
|
||
}
|
||
if err != nil {
|
||
t.Fatalf("refused: %v", err)
|
||
}
|
||
is, _ := h.Get(n)
|
||
if is.Status != c.ask.Status || m.From != from || m.To != c.ask.Status {
|
||
t.Fatalf("moved %+v, kept %+v", m, is)
|
||
}
|
||
closed := c.ask.Status == StatusResolved || c.ask.Status == StatusWontfix
|
||
if (is.Closed != nil) != closed {
|
||
t.Fatalf("closed %v at %s", is.Closed, is.Status)
|
||
}
|
||
wantBy := Unproven
|
||
if worked(c.ask.Status) {
|
||
wantBy = strings.ToLower(c.ask.By)
|
||
if is.Worker == nil || is.Worker.By != wantBy || is.Worker.Since.IsZero() {
|
||
t.Fatalf("worker %+v", is.Worker)
|
||
}
|
||
} else if is.Worker != nil {
|
||
t.Fatalf("a worker kept at %s: %+v", is.Status, is.Worker)
|
||
}
|
||
ev := h.said()
|
||
if last := ev[len(ev)-1]; last.Event != EventMoved || last.Body["to"] != c.ask.Status || last.Body["from"] != from ||
|
||
last.Body["by"] != wantBy || last.Body["caller"] != Unproven {
|
||
t.Fatalf("event %+v", last)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestReopeningClearsWhenItClosed(t *testing.T) {
|
||
h := newHarness(t)
|
||
n := h.open(t, "x", "hq")
|
||
h.fixing(t, n)
|
||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusResolved, FixedBy: []string{"hq#1"}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusOpen}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
is, _ := h.Get(n)
|
||
if is.Closed != nil || is.Status != StatusOpen {
|
||
t.Fatalf("%+v", is)
|
||
}
|
||
}
|
||
|
||
func TestAMoveOfAnIssueThatDoesNotExistIsRefused(t *testing.T) {
|
||
h := newHarness(t)
|
||
if _, err := h.Move(MoveAsk{Number: 9, Status: StatusDiagnosing, By: "a@g14"}); !errors.Is(err, ErrNoIssue) {
|
||
t.Fatalf("%v", err)
|
||
}
|
||
if _, err := h.Get(9); err == nil || !strings.Contains(err.Error(), "there is no issue 9") {
|
||
t.Fatalf("%v", err)
|
||
}
|
||
}
|
||
|
||
func TestNotesAreAppendedNeverEditedAndEachIsSaid(t *testing.T) {
|
||
h := newHarness(t)
|
||
n := h.open(t, "x")
|
||
for _, text := range []string{"first finding", "second, over\ntwo lines"} {
|
||
if _, err := h.Note(n, text); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
is, _ := h.Get(n)
|
||
if len(is.Notes) != 2 || is.Notes[0].Text != "first finding" || is.Notes[1].Text != "second, over\ntwo lines" ||
|
||
is.Notes[0].By != Unproven {
|
||
t.Fatalf("%+v", is.Notes)
|
||
}
|
||
ev := h.said()
|
||
if ev[2].Event != EventNoted || ev[2].Body["text"] != "second, over\ntwo lines" || ev[2].Body["number"] != float64(n) {
|
||
t.Fatalf("%+v", ev)
|
||
}
|
||
for _, bad := range []string{"", "\x07", "password=hunter2hunter2", strings.Repeat("x", MostNote+1)} {
|
||
if _, err := h.Note(n, bad); err == nil {
|
||
t.Errorf("note %q taken", bad)
|
||
}
|
||
}
|
||
if _, err := h.Note(404, "x"); !errors.Is(err, ErrNoIssue) {
|
||
t.Fatalf("a note on no issue: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestANoteThatWouldMakeTheIssueTooLargeIsRefusedAndNothingChanges(t *testing.T) {
|
||
h := newHarness(t)
|
||
n := h.open(t, "x")
|
||
note := strings.Repeat("word ", MostNote/5-1)
|
||
var err error
|
||
for i := 0; i < 40 && err == nil; i++ {
|
||
_, err = h.Note(n, note)
|
||
}
|
||
if err == nil || !strings.Contains(err.Error(), "256 KiB") {
|
||
t.Fatalf("no refusal: %v", err)
|
||
}
|
||
e, _ := h.mem.Get(IssuesState, keyOf(n))
|
||
if len(e.Value) > MostIssueBytes {
|
||
t.Fatalf("kept %d bytes", len(e.Value))
|
||
}
|
||
}
|
||
|
||
func TestConcurrentNotesAreAllKept(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.Sleep = time.Sleep
|
||
n := h.open(t, "x")
|
||
var wg sync.WaitGroup
|
||
for i := 0; i < 20; i++ {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
if _, err := h.Note(n, "a note"); err != nil {
|
||
t.Error(err)
|
||
}
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
is, _ := h.Get(n)
|
||
if len(is.Notes) != 20 {
|
||
t.Fatalf("kept %d of 20 notes: a write over a stale read lost some", len(is.Notes))
|
||
}
|
||
}
|
||
|
||
func TestLinksAreTypedAddedOnceAndRemoved(t *testing.T) {
|
||
h := newHarness(t)
|
||
a, b := h.open(t, "a"), h.open(t, "b")
|
||
good := []LinkAsk{
|
||
{Kind: LinkDuplicates, Target: keyOf(b)},
|
||
{Kind: LinkRelatesTo, Target: keyOf(b)},
|
||
{Kind: LinkPullRequest, Target: "mesh-controller#246"},
|
||
{Kind: LinkDecision, Target: "0275"},
|
||
{Kind: LinkDesign, Target: "03-DESIGN/01-to-be/53-the-issue-tracker.md"},
|
||
{Kind: LinkCondition, Target: "healer-wanted.ace.backup"},
|
||
{Kind: LinkDelivery, Target: "d-2026-10-10.3"},
|
||
}
|
||
for _, l := range good {
|
||
l.Number = a
|
||
if _, err := h.Link(l); err != nil {
|
||
t.Fatalf("%+v: %v", l, err)
|
||
}
|
||
}
|
||
bad := []struct {
|
||
l LinkAsk
|
||
says string
|
||
}{
|
||
{LinkAsk{Kind: "blocks", Target: "1"}, "not a kind"},
|
||
{LinkAsk{Kind: LinkDuplicates, Target: keyOf(a)}, "itself"},
|
||
{LinkAsk{Kind: LinkDuplicates, Target: "999"}, "there is no issue 999"},
|
||
{LinkAsk{Kind: LinkPullRequest, Target: "mesh-controller#246"}, "already has"},
|
||
{LinkAsk{Kind: LinkPullRequest, Target: "../x#1"}, "target is"},
|
||
{LinkAsk{Kind: LinkDecision, Target: "275"}, "four-digit"},
|
||
{LinkAsk{Kind: LinkDesign, Target: "03-DESIGN/../../etc/passwd"}, "design document"},
|
||
{LinkAsk{Kind: LinkCondition, Target: "a key\nwith a line"}, "condition"},
|
||
{LinkAsk{Kind: LinkCondition, Target: "token=abcdefgh123"}, "secret"},
|
||
{LinkAsk{Kind: LinkRelatesTo, Target: keyOf(b), Remove: true}, ""},
|
||
{LinkAsk{Kind: LinkRelatesTo, Target: keyOf(b), Remove: true}, "no relates-to link"},
|
||
}
|
||
for _, c := range bad {
|
||
c.l.Number = a
|
||
_, err := h.Link(c.l)
|
||
if c.says == "" {
|
||
if err != nil {
|
||
t.Fatalf("%+v: %v", c.l, err)
|
||
}
|
||
continue
|
||
}
|
||
if err == nil || !strings.Contains(err.Error(), c.says) {
|
||
t.Fatalf("%+v: want %q, got %v", c.l, c.says, err)
|
||
}
|
||
}
|
||
is, _ := h.Get(a)
|
||
if len(is.Links) != len(good)-1 {
|
||
t.Fatalf("kept %d links", len(is.Links))
|
||
}
|
||
ev := h.said()
|
||
last := ev[len(ev)-1]
|
||
if last.Event != EventLinked || last.Body["removed"] != true || last.Body["kind"] != LinkRelatesTo || last.Body["by"] != Unproven {
|
||
t.Fatalf("%+v", last)
|
||
}
|
||
}
|
||
|
||
func TestListIsNewestFirstAndNarrows(t *testing.T) {
|
||
h := newHarness(t)
|
||
h.open(t, "the bus drops a message", "mesh-catalog modules/nats")
|
||
h.open(t, "a check passes and did nothing", "mesh-controller internal/catalogue")
|
||
n := h.open(t, "the console names a ghost")
|
||
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "a@g14"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
l, err := h.List(ListAsk{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var got []uint64
|
||
for _, r := range l.Rows {
|
||
got = append(got, r.Number)
|
||
}
|
||
if !reflect.DeepEqual(got, []uint64{367, 366, 365}) || l.Rows[1].Owner != "mesh-controller internal/catalogue" ||
|
||
l.Rows[0].Status != StatusDiagnosing {
|
||
t.Fatalf("%+v", l)
|
||
}
|
||
for _, c := range []struct {
|
||
ask ListAsk
|
||
want []uint64
|
||
}{
|
||
{ListAsk{Status: StatusOpen}, []uint64{366, 365}},
|
||
{ListAsk{Owner: "mesh-controller"}, []uint64{366}},
|
||
{ListAsk{Owner: "mesh-catalog"}, []uint64{365}},
|
||
{ListAsk{Owner: "mesh-catalog modules/nats"}, []uint64{365}},
|
||
{ListAsk{Phrase: "GHOST"}, []uint64{367}},
|
||
{ListAsk{Phrase: "seen: a check"}, []uint64{366}},
|
||
{ListAsk{Limit: 1}, []uint64{367}},
|
||
{ListAsk{Status: StatusResolved}, nil},
|
||
} {
|
||
l, err := h.List(c.ask)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var got []uint64
|
||
for _, r := range l.Rows {
|
||
got = append(got, r.Number)
|
||
}
|
||
if !reflect.DeepEqual(got, c.want) {
|
||
t.Errorf("%+v: %v, want %v (%s)", c.ask, got, c.want, l.Said)
|
||
}
|
||
}
|
||
if _, err := h.List(ListAsk{Status: "closed"}); err == nil {
|
||
t.Fatal("an unknown status was listed")
|
||
}
|
||
}
|
||
|
||
// The caller is the bus credential, never an argument (ADR 0275 §5): no verb takes one, so none can be
|
||
// named, and what is recorded is never what an argument said.
|
||
func TestNoArgumentNamesWhoActed(t *testing.T) {
|
||
h := newHarness(t)
|
||
for _, verb := range Verbs {
|
||
for _, field := range []string{"by", "actor", "caller", "opened-by", "user"} {
|
||
_, err := h.call(t, verb, map[string]any{field: "operator", "number": 1, "title": "t", "report": "r",
|
||
"status": "open", "text": "x", "kind": LinkDecision, "target": "0001"})
|
||
if err == nil || !strings.Contains(err.Error(), "does not take "+field) && !strings.Contains(err.Error(), field) {
|
||
t.Errorf("%s with %s: %v", verb, field, err)
|
||
}
|
||
}
|
||
}
|
||
out, err := h.call(t, "open", map[string]any{"title": "t", "report": "r"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if out.(*Opened).By != Unproven {
|
||
t.Fatal("open recorded a caller nothing proved")
|
||
}
|
||
is, _ := h.Get(out.(*Opened).Number)
|
||
raw, _ := json.Marshal(is)
|
||
if strings.Contains(string(raw), "operator") {
|
||
t.Fatal("an argument's name reached the record")
|
||
}
|
||
}
|
||
|
||
func TestTheVerbsReadTheirArgumentsStrictly(t *testing.T) {
|
||
h := newHarness(t)
|
||
n := h.open(t, "x")
|
||
refusals := []struct {
|
||
verb string
|
||
args map[string]any
|
||
}{
|
||
{"open", map[string]any{"title": 5, "report": "r"}},
|
||
{"open", map[string]any{"title": "t", "report": "r", "located-in": []any{1}}},
|
||
{"get", map[string]any{}},
|
||
{"get", map[string]any{"number": -1}},
|
||
{"get", map[string]any{"number": 1.5}},
|
||
{"get", map[string]any{"number": "../365"}},
|
||
{"get", map[string]any{"number": "0365"}},
|
||
{"move", map[string]any{"number": n, "status": true}},
|
||
{"note", map[string]any{"number": n}},
|
||
{"link", map[string]any{"number": n, "kind": LinkDecision, "target": "0275", "remove": "yes"}},
|
||
{"list", map[string]any{"limit": "all"}},
|
||
}
|
||
for _, r := range refusals {
|
||
if _, err := h.call(t, r.verb, r.args); err == nil {
|
||
t.Errorf("%s %v was taken", r.verb, r.args)
|
||
}
|
||
}
|
||
if out, err := h.call(t, "get", map[string]any{"number": keyOf(n)}); err != nil || out.(*View).Number != n {
|
||
t.Fatalf("a number as text: %v", err)
|
||
}
|
||
if out, err := h.call(t, "open", map[string]any{"title": "t", "report": "r", "located-in": "hq"}); err != nil ||
|
||
!reflect.DeepEqual(out.(*Opened).LocatedIn, []string{"hq"}) {
|
||
t.Fatalf("one owner as text: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAnOwnersEntryIsTheRegisteredOneOrStartsWithIt(t *testing.T) {
|
||
o := Owners{"mesh-catalog", "mesh-catalog modules/nats", "mesh-controller"}
|
||
for entry, want := range map[string]string{
|
||
"mesh-catalog": "mesh-catalog",
|
||
"mesh-catalog modules/nats": "mesh-catalog modules/nats",
|
||
"mesh-catalog modules/nats/module.go": "mesh-catalog modules/nats",
|
||
"mesh-catalog modules/does-not-exist": "",
|
||
"mesh-catalog checks/words": "mesh-catalog",
|
||
"mesh-catalog modules/nats (the gate)": "mesh-catalog modules/nats",
|
||
"mesh-controller cmd/mesh-controller": "mesh-controller",
|
||
"mesh-controllers": "",
|
||
"hal": "",
|
||
} {
|
||
if got := o.owns(entry); got != want {
|
||
t.Errorf("%q is owned by %q, want %q", entry, got, want)
|
||
}
|
||
}
|
||
}
|