VW5: the view reads neither the gate's asks nor the teller's answers
The Approve per merge goes with G, so the gate's merge asks are no asks of the view's; the teller's answers are followed by their views family (teller_read_answers, then views.answers), never read from its state. Builds: design 47 G §512, §431; lead answers A-172, A-175 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -22,7 +22,6 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
)
|
||||
|
||||
@@ -132,19 +131,18 @@ func mustHash(t *testing.T, h meshdeliveryclient.Head) string {
|
||||
// the gate's, as mesh-sdk go/delivery EncodeAsk writes it under the key MergeAskKey gives (as internal/view asks_test.go).
|
||||
func TestPrintsTheOpenAsksInTheTableAndTheJSON(t *testing.T) { // 1345/AC2
|
||||
m := view.New()
|
||||
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
|
||||
key, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
|
||||
key, err := asks.HoldAskKey("plex", "src2:2222222222222222222222222222222222222222222222222222222222222222")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
|
||||
Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the merge stays held",
|
||||
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
|
||||
value, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, ask)})
|
||||
ask := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check on the new build.", Who: occ.Operator,
|
||||
Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the fingerprint stays held",
|
||||
Options: []oc.Option{{ID: "release", Label: "Release", Does: "goes on", Level: occ.Approve}, {ID: "keep", Label: "Keep", Does: "stays", Level: occ.Approve}}}
|
||||
value, err := asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, ask)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Apply(view.Entry{Bucket: view.AsksBucket, Key: key, Value: value})
|
||||
m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value})
|
||||
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -162,7 +160,7 @@ func TestPrintsTheOpenAsksInTheTableAndTheJSON(t *testing.T) { // 1345/AC2
|
||||
if code := run(nil, socket, &out, &errOut); code != 0 {
|
||||
t.Fatalf("exit %d: %s", code, errOut.String())
|
||||
}
|
||||
for _, want := range []string{"ASKS", "Merge into main: mesh-delivery#12", "Merges one pull request.", "answer on the phone"} {
|
||||
for _, want := range []string{"ASKS", "plex is held after a failed health check", "ace failed the check on the new build.", view.AnswerInTheView} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Errorf("the table lacks %q:\n%s", want, out.String())
|
||||
}
|
||||
@@ -179,7 +177,7 @@ func TestPrintsTheOpenAsksInTheTableAndTheJSON(t *testing.T) { // 1345/AC2
|
||||
} `json:"asks"`
|
||||
}
|
||||
if err := json.Unmarshal(out.Bytes(), &got); err != nil || len(got.Asks) != 1 || got.Asks[0].Key != key ||
|
||||
got.Asks[0].Headline != "Merge into main: mesh-delivery#12" || got.Asks[0].How != "answer on the phone" {
|
||||
got.Asks[0].Headline != "plex is held after a failed health check" || got.Asks[0].How != view.AnswerInTheView {
|
||||
t.Errorf("json = %s (%v)", out.String(), err)
|
||||
}
|
||||
}
|
||||
@@ -221,16 +219,9 @@ func TestTheAnswerCommandAnswersAnAskNeedingNoProof(t *testing.T) { // 1297/B9b
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
merge := oc.Ask{ID: meshdeliveryclient.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held",
|
||||
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
|
||||
mv, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, merge)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, w := view.New(), &putter{puts: map[string][]byte{}}
|
||||
m.SetWriter(w)
|
||||
m.Apply(view.Entry{Bucket: view.AssignerAsksBucket, Key: key, Value: value})
|
||||
m.Apply(view.Entry{Bucket: view.AsksBucket, Key: mkey, Value: mv})
|
||||
socket := serveWith(t, m)
|
||||
|
||||
var out, errOut bytes.Buffer
|
||||
@@ -309,12 +300,3 @@ func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// askHeads is the heads of a merge ask as the seat carries them.
|
||||
func askHeads(heads []meshdeliveryclient.Head) []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem {
|
||||
out := make([]meshdelivery.AsksByMergeGroupHeadsValueHeadsItem, len(heads))
|
||||
for i, h := range heads {
|
||||
out[i] = meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -396,98 +396,6 @@ func TestProcessWholeShowsTheIndexersBucketsOnceTheyExist(t *testing.T) { // 124
|
||||
t.Fatalf("the snapshot never held the indexer's keys; last: %+v", last)
|
||||
}
|
||||
|
||||
// TestProcessWholeShowsTheOpenAsksAndDropsTheAnsweredOnes starts the program whole before the gate's asks bucket and the
|
||||
// teller's answers bucket exist, then creates them: an ask appears in the snapshot, and leaves it once the teller has an
|
||||
// answer under its key (1345/AC1 against a real bus; the retry is the view's own, every 5 s).
|
||||
func TestProcessWholeShowsTheOpenAsksAndDropsTheAnsweredOnes(t *testing.T) { // 1345/AC1
|
||||
srv, err := server.NewServer(&server.Options{Host: "127.0.0.1", Port: -1, JetStream: true, StoreDir: t.TempDir(), NoLog: true, NoSigs: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go srv.Start()
|
||||
if !srv.ReadyForConnections(10 * time.Second) {
|
||||
t.Fatal("the bus did not come up")
|
||||
}
|
||||
t.Cleanup(srv.Shutdown)
|
||||
nc, err := nats.Connect(srv.ClientURL())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
js, err := jetstream.New(nc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "i")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
broker := filepath.Join(dir, "broker")
|
||||
cred, _ := json.Marshal(process.Account{URL: srv.ClientURL(), Node: "n", Module: "mesh-delivery-view", User: "mesh-delivery-view", Password: "x"})
|
||||
if err := os.WriteFile(broker, cred, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
socket := filepath.Join(dir, "run", "cli.sock")
|
||||
env := map[string]string{"MESH_BROKER_FILE": broker, "MESH_DELIVERY_SOCKET": socket, "MESH_DELIVERY_WEB": "127.0.0.1:0"}
|
||||
runCtx, stop := context.WithCancel(ctx)
|
||||
done := make(chan error, 1)
|
||||
serveOwners(t, ctx, js, nc)
|
||||
serveCatalog(t, ctx, js, nc)
|
||||
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
|
||||
t.Cleanup(func() { stop(); <-done })
|
||||
|
||||
gate, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: view.AsksBucket, History: view.AsksHistory})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
teller, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: view.AnswersBucket, History: view.AnswersHistory})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
|
||||
key, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
|
||||
Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the merge stays held",
|
||||
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
|
||||
value, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, ask)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := gate.Put(ctx, key, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wait := func(check func(view.Snapshot) bool, what string) {
|
||||
t.Helper()
|
||||
var last view.Snapshot
|
||||
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(100 * time.Millisecond) {
|
||||
if s, err := view.Ask(socket); err == nil {
|
||||
last = s
|
||||
if check(s) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s; last: %+v", what, last)
|
||||
}
|
||||
wait(func(s view.Snapshot) bool {
|
||||
return len(s.Asks) == 1 && s.Asks[0].Key == key && s.Asks[0].How == "answer on the phone"
|
||||
}, "the open ask never showed")
|
||||
answer, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "approve", Warrant: ask.ID + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := teller.Put(ctx, key, answer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wait(func(s view.Snapshot) bool { return len(s.Asks) == 0 }, "the answered ask never left the list")
|
||||
}
|
||||
|
||||
// TestProcessWholeListsEveryDifference starts the program whole before the assigner's differences bucket exists, then
|
||||
// creates it and writes a difference explained by a rule and one unexplained: the socket's snapshot lists both with their
|
||||
// rule, and a difference taken away leaves the list (1297/B8: the view lists every difference with its rule).
|
||||
@@ -609,7 +517,6 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
|
||||
askBucket := bucket(view.AssignerAsksBucket, view.AssignerAsksHistory)
|
||||
viewBucket := bucket(viewstate.ViewAnswersBucket, viewstate.ViewAnswersHistory)
|
||||
teller := bucket(view.AnswersBucket, view.AnswersHistory)
|
||||
gate := bucket(view.AsksBucket, view.AsksHistory)
|
||||
|
||||
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "i")
|
||||
if err != nil {
|
||||
@@ -669,22 +576,11 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
|
||||
}
|
||||
wait(func(s view.Snapshot) bool { return len(s.Asks) == 1 && s.Asks[0].Chosen == "release" }, "the ask never showed the view's choice")
|
||||
|
||||
// The merge's Approve is answered on the phone alone.
|
||||
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}}
|
||||
mkey, err := meshdeliveryclient.MergeAskKey("feat/x", heads)
|
||||
// The merge's Approve is no ask of the view's (design 47 G §431): refused by its key, never written.
|
||||
mkey, err := meshdeliveryclient.MergeAskKey("feat/x", []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
merge := oc.Ask{ID: meshdeliveryclient.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held",
|
||||
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
|
||||
mv, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Ask: rawAsk(t, merge)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := gate.Put(ctx, mkey, mv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wait(func(s view.Snapshot) bool { return len(s.Asks) == 2 }, "the merge ask never showed")
|
||||
if err := view.AnswerAsk(socket, mkey, "approve", ""); err == nil || !strings.Contains(err.Error(), "answer it on the phone (issue 953)") {
|
||||
t.Errorf("merge Approve = %v, want the refusal naming the phone", err)
|
||||
}
|
||||
@@ -700,7 +596,7 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129
|
||||
if _, err := teller.Put(ctx, key, answer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wait(func(s view.Snapshot) bool { return len(s.Asks) == 1 && s.Asks[0].Key == mkey }, "the answered ask never left the list")
|
||||
wait(func(s view.Snapshot) bool { return len(s.Asks) == 0 }, "the answered ask never left the list")
|
||||
}
|
||||
|
||||
// TestProcessWholeShowsTheBacklogOfBuildAsks starts the program whole against a bus holding the go seat's work-queue stream
|
||||
@@ -1040,11 +936,3 @@ func TestProcessWholeSaysModulesInMotion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// askHeads is the heads of a merge ask as the seat carries them.
|
||||
func askHeads(heads []meshdeliveryclient.Head) []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem {
|
||||
out := make([]meshdelivery.AsksByMergeGroupHeadsValueHeadsItem, len(heads))
|
||||
for i, h := range heads {
|
||||
out[i] = meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
}
|
||||
open := buckets{js: js, reader: reader}
|
||||
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AsksBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} {
|
||||
for _, bucket := range []string{view.HaveBucket, view.AssignmentsBucket, view.PresenceBucket, view.StuckBucket, view.CandidatesBucket, view.AnswersBucket, builds.BuilderBuildsBucket, view.AssignerWantsBucket, view.SourceBucket, view.AssignerAsksBucket, viewstate.ViewAnswersBucket, view.TakenBucket} {
|
||||
go follow(ctx, open, bucket, false, model, logger)
|
||||
}
|
||||
// The source's history gives each head's source revision (ADR 0556); the modules' history is each revision of the
|
||||
@@ -202,7 +202,7 @@ type ReachedEvent struct {
|
||||
// The view's own state of its first views (views.StatusState).
|
||||
var firstViewsBucket = viewstate.ViewModule + "_" + views.StatusState
|
||||
|
||||
// ownersFollowed are the indexer's and the assigner's states the view's process follows by their first views, each by its
|
||||
// ownersFollowed are the indexer's, the assigner's and the teller's states the view's process follows by their first views, each by its
|
||||
// owner's read tool (as the view's invokes names it) and views family (its events on the shared events stream); the
|
||||
// view's tools ask the other six through the owners' read tools on each call.
|
||||
var ownersFollowed = []struct{ owner, state, tool string }{
|
||||
@@ -211,6 +211,7 @@ var ownersFollowed = []struct{ owner, state, tool string }{
|
||||
{view.AssignerModule, "asks", view.AssignerModule + ".assigner_read_asks"},
|
||||
{view.IndexerModule, "candidates", view.IndexerModule + ".indexer_read_candidates"},
|
||||
{view.IndexerModule, "taken", view.IndexerModule + ".indexer_read_taken"},
|
||||
{view.TellerModule, view.AnswersState, view.TellerModule + ".teller_read_answers"},
|
||||
}
|
||||
|
||||
// buckets opens what the view watches: an owner's state as its picture (firstview), any other bucket on the bus.
|
||||
|
||||
@@ -97,12 +97,15 @@ func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a A
|
||||
|
||||
func TestTheViewRefusesWhatItMayNotAnswer(t *testing.T) { // 1297/B9a AC
|
||||
e, a := holdAsk(t)
|
||||
merge := askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges.")
|
||||
mergeKey, err := meshdeliveryclient.MergeAskKey("feat/x", []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases := []struct {
|
||||
name, key, option, want string
|
||||
prep []Entry
|
||||
}{
|
||||
{"the merge's Approve", merge.Key, "approve", "answer it on the phone (issue 953)", []Entry{merge}},
|
||||
{"the merge's Approve", mergeKey, "approve", "answer it on the phone (issue 953)", nil},
|
||||
{"an option the ask does not offer", e.Key, "delete", `offers no option "delete"`, []Entry{e}},
|
||||
{"an ask the view does not hold", e.Key, "release", "no open ask", nil},
|
||||
{"an ask that is no ask key", "hold.x", "release", "not an ask key", nil},
|
||||
@@ -145,9 +148,11 @@ func TestTheSocketAnswersAnAsk(t *testing.T) { // 1297/B9b
|
||||
if _, ok := w.puts[e.Key+".view"]; !ok {
|
||||
t.Fatalf("puts = %v", w.puts)
|
||||
}
|
||||
merge := askEntry(t, "feat/x", "Merge", "Merges.")
|
||||
fill(m, merge)
|
||||
err := AnswerAsk(path, merge.Key, "approve", "")
|
||||
mergeKey, err := meshdeliveryclient.MergeAskKey("feat/x", []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = AnswerAsk(path, mergeKey, "approve", "")
|
||||
if err == nil || !strings.Contains(err.Error(), "answer it on the phone (issue 953)") {
|
||||
t.Errorf("merge over the socket: %v", err)
|
||||
}
|
||||
@@ -204,9 +209,11 @@ func TestThePageAnswersAnAskAndRefusesAForeignPost(t *testing.T) { // 1297/B9a
|
||||
t.Fatalf("answer = %d %q, puts %v", rec.Code, rec.Body.String(), w.puts)
|
||||
}
|
||||
// The merge's Approve is refused with its reason, and the rest of the page stays read-only.
|
||||
merge := askEntry(t, "feat/x", "Merge", "Merges.")
|
||||
fill(m, merge)
|
||||
rec = postAnswer(h, url.Values{"key": {merge.Key}, "option": {"approve"}}, nil)
|
||||
mergeKey, err := meshdeliveryclient.MergeAskKey("feat/x", []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec = postAnswer(h, url.Values{"key": {mergeKey}, "option": {"approve"}}, nil)
|
||||
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "answer it on the phone (issue 953)") || len(w.puts) != 1 {
|
||||
t.Errorf("merge = %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
@@ -7,35 +7,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
)
|
||||
|
||||
const askHeadA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
// askEntry is a key of the gate's asks as the gate writes it (mesh-sdk go/delivery EncodeAsk, MergeAskKey): the router's ask
|
||||
// whole, under the id its key gives.
|
||||
func askEntry(t *testing.T, group, headline, words string) Entry {
|
||||
t.Helper()
|
||||
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
|
||||
key, err := meshdeliveryclient.MergeAskKey(group, heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: headline, Explanation: words, Who: occ.Operator, Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
|
||||
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
|
||||
v, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: group, Heads: askHeads(heads), Ask: rawAsk(t, a)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Entry{Bucket: AsksBucket, Key: key, Value: v, At: t0}
|
||||
}
|
||||
|
||||
// answerEntry is the teller's answer to the first asking of the key; answerToAsking to the asking with this router id. The
|
||||
// answer's warrant is `<router id>@<time>` (teller.go warrantID).
|
||||
func answerEntry(t *testing.T, key string) Entry {
|
||||
@@ -44,7 +21,7 @@ func answerEntry(t *testing.T, key string) Entry {
|
||||
|
||||
func answerToAsking(t *testing.T, key, id string) Entry {
|
||||
t.Helper()
|
||||
v, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "approve", Warrant: id + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
|
||||
v, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "release", Warrant: id + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Release"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -53,17 +30,12 @@ func answerToAsking(t *testing.T, key, id string) Entry {
|
||||
|
||||
func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/AC1
|
||||
m := New()
|
||||
e := askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request.\nReview: approved.")
|
||||
e, a := holdAsk(t)
|
||||
fill(m, e)
|
||||
s := m.Snapshot()
|
||||
if len(s.Asks) != 1 {
|
||||
if len(s.Asks) != 1 || s.Asks[0].Key != e.Key || s.Asks[0].Headline != a.Headline || s.Asks[0].How != AnswerInTheView {
|
||||
t.Fatalf("asks = %+v, want the one open ask", s.Asks)
|
||||
}
|
||||
a := s.Asks[0]
|
||||
if a.Key != e.Key || a.Group != "feat/x" || a.Headline != "Merge into main: mesh-delivery#12" || !strings.Contains(a.Words, "Merges one pull request.") ||
|
||||
a.How != "answer on the phone" || len(a.Pulls) != 1 || !strings.Contains(a.Pulls[0], "novox/mesh-delivery#12") || !strings.Contains(a.Pulls[0], askHeadA[:12]) {
|
||||
t.Errorf("ask = %+v", a)
|
||||
}
|
||||
fill(m, answerEntry(t, e.Key))
|
||||
if got := m.Snapshot().Asks; len(got) != 0 {
|
||||
t.Errorf("an answered ask is listed: %+v", got)
|
||||
@@ -73,15 +45,26 @@ func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/
|
||||
if got := m.Snapshot().Asks; len(got) != 1 {
|
||||
t.Errorf("asks after the answer was taken away = %+v", got)
|
||||
}
|
||||
fill(m, Entry{Bucket: AsksBucket, Key: e.Key, Deleted: true})
|
||||
fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Deleted: true})
|
||||
if got := m.Snapshot().Asks; len(got) != 0 {
|
||||
t.Errorf("a withdrawn ask is listed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The gate's asks go with the Approve per merge (design 47 G §431, §512): a value under the gate's former state is no ask
|
||||
// of the view's, and nothing names it unreadable.
|
||||
func TestTheGatesMergeAsksAreNoAsksOfTheView(t *testing.T) {
|
||||
m := New()
|
||||
fill(m, Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.g.0123456789ab", Value: []byte(`{"v":1,"group":"g"}`), At: t0},
|
||||
Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.h.0123456789ab", Value: []byte("{"), At: t0})
|
||||
if s := m.Snapshot(); len(s.Asks) != 0 || len(s.Unreadable) != 0 {
|
||||
t.Errorf("snapshot = %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
|
||||
m := New()
|
||||
e := askEntry(t, "feat/x", "Merge", "Merges.")
|
||||
e, _ := holdAsk(t)
|
||||
fill(m, answerEntry(t, e.Key), e)
|
||||
if got := m.Snapshot().Asks; len(got) != 0 {
|
||||
t.Errorf("asks = %+v", got)
|
||||
@@ -90,26 +73,28 @@ func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
|
||||
|
||||
func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped silently
|
||||
m := New()
|
||||
fill(m, Entry{Bucket: AsksBucket, Key: "merge.g.0123456789ab", Value: []byte("{"), At: t0})
|
||||
e, _ := holdAsk(t)
|
||||
fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Value: []byte("{"), At: t0})
|
||||
s := m.Snapshot()
|
||||
if len(s.Asks) != 0 || len(s.Unreadable) != 1 || s.Unreadable[0].Bucket != AsksBucket {
|
||||
if len(s.Asks) != 0 || len(s.Unreadable) != 1 || s.Unreadable[0].Bucket != AssignerAsksBucket {
|
||||
t.Errorf("snapshot = %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
|
||||
m := New()
|
||||
fill(m, askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request."))
|
||||
e, a := holdAsk(t)
|
||||
fill(m, e)
|
||||
var out bytes.Buffer
|
||||
Table(&out, m.Snapshot())
|
||||
for _, want := range []string{"ASKS", "Merge into main: mesh-delivery#12", "Merges one pull request.", "answer on the phone", "novox/mesh-delivery#12"} {
|
||||
for _, want := range []string{"ASKS", a.Headline, a.Explanation, AnswerInTheView} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Errorf("the table lacks %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
Web(m).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
for _, want := range []string{"ASKS", "Merge into main: mesh-delivery#12", "Merges one pull request.", "answer on the phone"} {
|
||||
for _, want := range []string{"ASKS", a.Headline, AnswerInTheView} {
|
||||
if !strings.Contains(rec.Body.String(), want) {
|
||||
t.Errorf("the page lacks %q:\n%s", want, rec.Body.String())
|
||||
}
|
||||
@@ -120,11 +105,6 @@ func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
|
||||
if strings.Contains(out.String(), "ASKS") {
|
||||
t.Errorf("the table shows an ASKS section with no ask:\n%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheSnapshotsJSONCarriesTheAsks(t *testing.T) { // 1345/AC2
|
||||
m := New()
|
||||
fill(m, askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request."))
|
||||
raw, err := json.Marshal(m.Snapshot())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -135,7 +115,7 @@ func TestTheSnapshotsJSONCarriesTheAsks(t *testing.T) { // 1345/AC2
|
||||
How string `json:"how"`
|
||||
} `json:"asks"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &back); err != nil || len(back.Asks) != 1 || back.Asks[0].How != "answer on the phone" {
|
||||
if err := json.Unmarshal(raw, &back); err != nil || len(back.Asks) != 1 || back.Asks[0].How != AnswerInTheView {
|
||||
t.Errorf("json %s: %+v %v", raw, back, err)
|
||||
}
|
||||
}
|
||||
@@ -164,33 +144,7 @@ func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 137
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAnswerToAnEarlierAskingDoesNotHideTheNextAsking(t *testing.T) { // R2-1 of review round 2
|
||||
m := New()
|
||||
e := askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request.")
|
||||
fill(m, e, answerEntry(t, e.Key))
|
||||
if got := m.Snapshot().Asks; len(got) != 0 {
|
||||
t.Fatalf("asking 1 is answered and listed: %+v", got)
|
||||
}
|
||||
// The gate rewrites the key at asking 2; only asking 1's answer exists.
|
||||
heads := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
|
||||
a := oc.Ask{ID: meshdeliveryclient.AskingID(e.Key, 2), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
|
||||
Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
|
||||
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
|
||||
v, err := meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: "feat/x", Heads: askHeads(heads), Asking: 2, Ask: rawAsk(t, a)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fill(m, Entry{Bucket: AsksBucket, Key: e.Key, Value: v, At: t0})
|
||||
if got := m.Snapshot().Asks; len(got) != 1 {
|
||||
t.Fatalf("asking 2 is not listed while only asking 1's answer exists: %+v", got)
|
||||
}
|
||||
fill(m, answerToAsking(t, e.Key, a.ID))
|
||||
if got := m.Snapshot().Asks; len(got) != 0 {
|
||||
t.Errorf("asking 2 is answered and listed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// rawAsk is the router's ask as the JSON a gate's or an assigner's value carries (mesh-sdk keeps it raw, ADR 0627 section 4).
|
||||
// rawAsk is the router's ask as the JSON an assigner's value carries (mesh-sdk keeps it raw, ADR 0627 section 4).
|
||||
func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(a)
|
||||
@@ -199,12 +153,3 @@ func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// askHeads is the heads of a merge ask as the seat carries them.
|
||||
func askHeads(heads []meshdeliveryclient.Head) []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem {
|
||||
out := make([]meshdelivery.AsksByMergeGroupHeadsValueHeadsItem, len(heads))
|
||||
for i, h := range heads {
|
||||
out[i] = meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -40,10 +40,11 @@ const (
|
||||
AssignerWantsBucket = AssignerModule + "_wants"
|
||||
AssignerAsksBucket = AssignerModule + "_asks"
|
||||
|
||||
// The gate's and the teller's states.
|
||||
// The gate's source, and the teller's state answers as the view follows it by its views family (design 47 G §431, A-175):
|
||||
// its picture `<owner>_<state>`.
|
||||
SourceBucket = GateModule + "_source"
|
||||
AsksBucket = GateModule + "_asks"
|
||||
AnswersBucket = TellerModule + "_answers"
|
||||
AnswersState = "answers"
|
||||
AnswersBucket = TellerModule + "_" + AnswersState
|
||||
|
||||
// The watchman's and the node-engine's states.
|
||||
PresenceBucket = WatchmanModule + "_presence"
|
||||
@@ -54,7 +55,6 @@ const (
|
||||
StepsState = "steps"
|
||||
|
||||
// The revisions each of those states keeps per key.
|
||||
AsksHistory = 5
|
||||
AnswersHistory = 5
|
||||
AssignerAsksHistory = 5
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
@@ -23,7 +22,6 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildrecord"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/trunk"
|
||||
@@ -160,10 +158,6 @@ type AskRow struct {
|
||||
Chosen string `json:"chosen,omitempty"`
|
||||
}
|
||||
|
||||
// AnswerOnThePhone is what an open ask says about answering it: the view only shows, and the Approve is answered on the
|
||||
// operator's phone (ADR 0437 §10: answering in the view is release 5's).
|
||||
const AnswerOnThePhone = "answer on the phone"
|
||||
|
||||
// AnswerInTheView is what an open ask that needs no proof of who answered says (ADR 0528 clause 6): the operator answers it here.
|
||||
const AnswerInTheView = "answer in the view"
|
||||
|
||||
@@ -240,7 +234,6 @@ type Model struct {
|
||||
groups map[string]groupEntry // the indexer's candidates, by group
|
||||
modules map[string]moduleEntry // the indexer's modules, by module
|
||||
keys map[string]keyEntry // the builder's build keys, by <module>.<fingerprint>
|
||||
asks map[string]askRecord // the gate's asks, by key
|
||||
wants map[string]wantEntry // the assigner's wants, by <node>.<module>
|
||||
source *sourceEntry // the gate's source key
|
||||
deliveredEvent func(DeliveredEvent) error
|
||||
@@ -349,12 +342,6 @@ type moduleEntry struct {
|
||||
err string
|
||||
}
|
||||
|
||||
type askRecord struct {
|
||||
value meshdelivery.AsksByMergeGroupHeadsValue
|
||||
ask oc.Ask // the router's ask the gate's value carries
|
||||
err string
|
||||
}
|
||||
|
||||
type haveEntry struct {
|
||||
have nodeengine.HaveByNodeModuleValue
|
||||
err string
|
||||
@@ -369,7 +356,7 @@ type nodeEntry struct {
|
||||
|
||||
// New is an empty model.
|
||||
func New() *Model {
|
||||
return &Model{placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, asks: map[string]askRecord{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]viewstate.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]meshdeliveryclient.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{},
|
||||
return &Model{placed: meshmoduleassignerclient.NewPlaced(), builds: map[string]buildEntry{}, groups: map[string]groupEntry{}, modules: map[string]moduleEntry{}, keys: map[string]keyEntry{}, wants: map[string]wantEntry{}, told: map[string]bool{}, initial: map[string]bool{}, aasks: map[string]assignerAskRecord{}, chosen: map[string]viewstate.ViewAnswer{}, answers: map[string]string{}, answerOf: map[string]meshdeliveryclient.Answer{}, holds: map[string]heldEntry{}, newest: map[string]string{},
|
||||
haves: map[string]haveEntry{}, nodes: map[string]nodeEntry{},
|
||||
presence: map[string]presenceEntry{}, stuck: map[string]stuckEntry{},
|
||||
modHist: map[string][]meshcatalog.ModulesByModuleValue{}, srcHist: map[uint64]map[string]string{}, taken: map[string]takein.Taken{},
|
||||
@@ -559,20 +546,6 @@ func (m *Model) apply(e Entry) {
|
||||
}
|
||||
mod, err := meshcatalogclient.DecodeModule(e.Value)
|
||||
m.modules[e.Key] = moduleEntry{value: mod, err: errText(err)}
|
||||
case AsksBucket:
|
||||
if e.Deleted {
|
||||
delete(m.asks, e.Key)
|
||||
return
|
||||
}
|
||||
a, err := meshdeliveryclient.DecodeAsk(e.Value)
|
||||
var ask oc.Ask
|
||||
if err == nil {
|
||||
var raw []byte
|
||||
if raw, err = json.Marshal(a.Ask); err == nil {
|
||||
ask, err = occ.Carried(raw, meshdeliveryclient.AskingID(e.Key, int(a.Asking)))
|
||||
}
|
||||
}
|
||||
m.asks[e.Key] = askRecord{value: a, ask: ask, err: errText(err)}
|
||||
case AssignerAsksBucket:
|
||||
if e.Deleted {
|
||||
delete(m.aasks, e.Key)
|
||||
@@ -797,11 +770,6 @@ func (m *Model) snapshot() Snapshot {
|
||||
unreadable = append(unreadable, UnreadableRow{Bucket: ModulesBucket, Key: key, Error: mod.err})
|
||||
}
|
||||
}
|
||||
for key, a := range m.asks {
|
||||
if a.err != "" {
|
||||
unreadable = append(unreadable, UnreadableRow{Bucket: AsksBucket, Key: key, Error: a.err})
|
||||
}
|
||||
}
|
||||
for key, a := range m.aasks {
|
||||
if a.err != "" {
|
||||
unreadable = append(unreadable, UnreadableRow{Bucket: AssignerAsksBucket, Key: key, Error: a.err})
|
||||
@@ -927,21 +895,6 @@ const (
|
||||
// pull requests it covers (repository, number, head shortened to 12 digits) and how to answer.
|
||||
func (m *Model) askRows() []AskRow {
|
||||
out := []AskRow{}
|
||||
for key, a := range m.asks {
|
||||
if a.err != "" || m.answers[key] == a.ask.ID {
|
||||
continue
|
||||
}
|
||||
row := AskRow{Key: key, Group: a.value.Group, Headline: a.ask.Headline, Words: a.ask.Explanation,
|
||||
Expires: expiry(a.ask), How: AnswerOnThePhone, Pulls: []string{}}
|
||||
for _, h := range a.value.Heads {
|
||||
head := h.Head
|
||||
if len(head) > 12 {
|
||||
head = head[:12]
|
||||
}
|
||||
row.Pulls = append(row.Pulls, fmt.Sprintf("%s#%d @%s", h.Repository, h.Number, head))
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
for key, a := range m.aasks {
|
||||
if a.err != "" || m.answers[key] == a.ask.ID {
|
||||
continue
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
if m.Module != "mesh-delivery-view" {
|
||||
t.Errorf("module = %q", m.Module)
|
||||
}
|
||||
want := []string{"mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-build-go.pauses", "node-build-go.steps", "node-build-image.pauses", "node-build-image.steps", "node-build-node.pauses", "node-build-node.steps", "node-engine.have"} // the build seats' states by seat, never a build module (design 47 G A-82); // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
want := []string{"mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.source", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-build-go.pauses", "node-build-go.steps", "node-build-image.pauses", "node-build-image.steps", "node-build-node.pauses", "node-build-node.steps", "node-engine.have"} // the build seats' states by seat, never a build module (design 47 G A-82); // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -123,6 +123,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_config_candidates",
|
||||
"mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_pending",
|
||||
"mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken",
|
||||
"mesh-delivery-teller.teller_read_answers", // A-175: the teller's answers by their views family
|
||||
"seat:mesh-catalog.revision", // ADR 0663: the modules are the catalog half's, read through the seat's verb revision, never the indexer's
|
||||
"seat:mesh-catalog.seat-definitions", // design 47 G A-82, §204: the build seats are read from the seat list
|
||||
"seat:mesh-hand-acts.hand-act", // the join: its first view asked again by hand is a hand act (views.StatusTools)
|
||||
@@ -141,7 +142,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
t.Errorf("version = %q, want 0.1", m.Version)
|
||||
}
|
||||
if want := []string{"mesh-conditions.findings-asked", "mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.wants",
|
||||
"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken", "mesh-delivery-teller.views.answers"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
// The one event the view says: a source revision is delivered (1906/D9, ADR 0539 §17), derived by it from the wants and the haves.
|
||||
@@ -172,7 +173,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
// D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a
|
||||
// state it reads directly is in `reads`, one it follows is a family it consumes, and the owners' tools it asks are invoked.
|
||||
t.Run("the names are the manifest's", func(t *testing.T) {
|
||||
direct := map[string]string{view.SourceBucket: view.GateModule + ".source", view.AsksBucket: view.GateModule + ".asks", view.AnswersBucket: view.TellerModule + ".answers",
|
||||
direct := map[string]string{view.SourceBucket: view.GateModule + ".source",
|
||||
view.PresenceBucket: view.WatchmanModule + ".presence", view.StuckBucket: view.WatchmanModule + ".stuck", view.HaveBucket: view.EngineModule + ".have"}
|
||||
for bucket, read := range direct {
|
||||
if strings.ReplaceAll(read, ".", "_") != bucket {
|
||||
@@ -190,7 +191,8 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
}
|
||||
followed := map[string]string{view.CandidatesBucket: view.IndexerModule + ".views.candidates",
|
||||
view.TakenBucket: view.IndexerModule + ".views.taken", view.AssignmentsBucket: view.AssignerModule + ".views.assignments",
|
||||
view.AssignerWantsBucket: view.AssignerModule + ".views.wants", view.AssignerAsksBucket: view.AssignerModule + ".views.asks"}
|
||||
view.AssignerWantsBucket: view.AssignerModule + ".views.wants", view.AssignerAsksBucket: view.AssignerModule + ".views.asks",
|
||||
view.AnswersBucket: view.TellerModule + ".views.answers"}
|
||||
for bucket, family := range followed {
|
||||
if module, state, _ := strings.Cut(family, ".views."); bucket != module+"_"+state {
|
||||
t.Errorf("the bucket %s is not the state %s of %s", bucket, state, module)
|
||||
@@ -199,11 +201,18 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
|
||||
t.Errorf("the view follows %s, its manifest consumes %v", family, m.Consumes)
|
||||
}
|
||||
}
|
||||
for _, tool := range []string{meshcatalogclient.RevisionVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants"} {
|
||||
for _, tool := range []string{meshcatalogclient.RevisionVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants", view.TellerModule + ".teller_read_answers"} {
|
||||
if !slices.Contains(m.Invokes, tool) {
|
||||
t.Errorf("the view asks %s, its manifest invokes %v", tool, m.Invokes)
|
||||
}
|
||||
}
|
||||
// The gate's asks go with the Approve per merge, and the teller's answers are followed by their views family, never read
|
||||
// (design 47 G §431, §512, A-175).
|
||||
for _, read := range []string{view.GateModule + ".asks", view.TellerModule + ".answers"} {
|
||||
if slices.Contains(m.Reads, read) {
|
||||
t.Errorf("the manifest reads %s (design 47 G §512)", read)
|
||||
}
|
||||
}
|
||||
// The modules are the catalog half's: the seat's verb is invoked, its event consumed, and the indexer's state modules is neither.
|
||||
if !slices.Contains(m.Consumes, view.CatalogSeat+"."+meshcatalog.EventCatalogRevision) {
|
||||
t.Errorf("the view hears the seat's %s, its manifest consumes %v", meshcatalog.EventCatalogRevision, m.Consumes)
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-delivery-indexer.views.candidates",
|
||||
"mesh-delivery-indexer.views.taken"
|
||||
"mesh-delivery-indexer.views.taken",
|
||||
"mesh-delivery-teller.views.answers"
|
||||
],
|
||||
"invokes": [
|
||||
"mesh-delivery-assigner.assigner_read_asks",
|
||||
@@ -76,6 +77,7 @@
|
||||
"mesh-delivery-indexer.indexer_read_packages",
|
||||
"mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_taken",
|
||||
"mesh-delivery-teller.teller_read_answers",
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
@@ -93,9 +95,7 @@
|
||||
"mesh-delivery-builder.build-log",
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-checker.tries",
|
||||
"mesh-delivery-gate.asks",
|
||||
"mesh-delivery-gate.source",
|
||||
"mesh-delivery-teller.answers",
|
||||
"mesh-conditions.conditions",
|
||||
"mesh-healers.attempts",
|
||||
"mesh-provisioner.provisions",
|
||||
|
||||
Reference in New Issue
Block a user