FB3, FB4, FB5: the bridge performs what the gate owes through the seat git and says each outcome, reads groups from the seat, and its owed tools

Taken over from the join (6ada2089, f1417ab0, 6e162a60, 3a4e62b0, 73fb42d7, ee15a280, a5476f62, 94a4e62a, 33907a81, e519cc87, e8f268c1) and fitted to the final
contracts: the seat's events are pull-updated, pull-merged, pull-closed, forge-delete; OwedEnded keeps only the fields §164 names; merge-pull
takes no ask. A merge act is performed only as a switched act (§169); from G the gate owes none (§167).

Builds: design 47 G §43, §45, §157, §161, §162, §163, §164, §167, §168, §169, §171, §250

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 11:54:56 +01:00
parent 3a4768ab43
commit 0b597eb417
26 changed files with 1940 additions and 1492 deletions
@@ -9,17 +9,13 @@ import (
"context"
"encoding/json"
"fmt"
"maps"
"os"
"slices"
"strconv"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
@@ -28,9 +24,6 @@ import (
"mesh-delivery-forge-bridge/internal/groups"
)
// gateMergesState is the gate's state merges, which the tools read (reads: mesh-delivery-gate.merges in the manifest).
const gateMergesState = "mesh-delivery-gate.merges"
// Tool names.
const (
GroupsTool = "bridge_groups"
@@ -42,8 +35,8 @@ const (
// asker writes an end-asks value and waits for the bridge's answer to it.
type asker func(key string, value []byte) (groups.EndAsk, error)
// Tools are the bridge's tools: groupsRead reads the groups the gate holds with what the bridge kept of them.
func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []string, error), merged func(repository, merge string, number int) (json.RawMessage, bool, error), forge bridge.Caller) []stdio.Tool {
// Tools are the bridge's tools: groupsRead reads the seat mesh-delivery's groups with what the bridge kept of them.
func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []string, error), merged func(repository, merge string, number int) (json.RawMessage, error), forge bridge.Caller) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: bridge.TagsTool,
@@ -59,37 +52,32 @@ func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []s
{Name: MergedTool,
Description: "The merged message of one merge, asked again (issue 1585): the message the bridge wrote on its stream FORGE when the " +
"pull request was merged into the trunk, built the same way from the forge (the merge commit's trees and the manifests " +
"and lock files of its module directories) and from the gate's merges state (the pull request's number, head and " +
"group). For the indexer, which asks it when it takes in a held merge it does not hold in memory (a restart, a new " +
"and lock files of its module directories), the pull request's number, branch and head from the seat git's verb pull, and " +
"for a delivery group's member its group and members from the seat mesh-delivery's verb groups. For the indexer, which asks it when it takes in a held merge it does not hold in memory (a restart, a new " +
"build), and the ask-again a lost message lacked. About 0.5 MB for a repository of many modules. Answers {message} " +
"with the forge message whole, or why it cannot: neither the gate nor the forge gives a merged pull request with that commit, or the forge did not answer. " +
"A merge made on the forge (a recovery merge, the operator's own) has no merge in the gate: the bridge reads that pull request from the forge, by number when given, and says made_on_forge.",
"with the forge message whole, or why it cannot: the forge gives no merged pull request with that commit, or a seat did not answer. " +
"A merge made on the forge (a recovery merge, the operator's own) is read the same way.",
Input: map[string]any{"type": "object", "required": []string{"repository", "merge"}, "properties": map[string]any{
"repository": map[string]any{"type": "string", "description": "the repository, owner/name, as the forge names it"},
"merge": map[string]any{"type": "string", "description": "the merge commit, 40 hex digits"},
"number": map[string]any{"type": "integer", "description": "the pull request's number, when known: for a merge made on the forge the bridge reads that pull request and checks its merge commit"}}},
"number": map[string]any{"type": "integer", "description": "the pull request's number, when known: the bridge reads that pull request and checks its merge commit"}}},
Run: func(args map[string]any) (any, error) {
number, _ := args["number"].(float64)
raw, onForge, err := merged(str(args, "repository"), str(args, "merge"), int(number))
raw, err := merged(str(args, "repository"), str(args, "merge"), int(number))
if err != nil {
return nil, err
}
out := map[string]any{"message": raw}
if onForge {
out["made_on_forge"] = true
out["note"] = "the merge was made on the forge, not through the gate: its pull request was read from the forge"
fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] merged message of %s at %s answered from the forge: the merge was made on the forge, not through the gate\n", str(args, "repository"), str(args, "merge"))
}
return out, nil
return map[string]any{"message": raw}, nil
}},
{Name: GroupsTool,
Description: "The delivery groups the gate holds (more than one member), each with its members' merge state and whether each " +
"merged message is written on the forge stream, and whether the group's end (forge.group.merged) is written: ended, " +
"or open with why (gathered, or merged and its end not written yet). An open group with every member merged is " +
Description: "The delivery groups the seat mesh-delivery's verb groups answers (more than one member), each with its members' delivery " +
"state and whether each merged message is written on the forge stream (with the merge commit it named), and whether the " +
"group's end (forge.group.merged) is written: ended, or open with why (gathered, or every message written and its end " +
"not written yet). An open group with every member's message written is " +
"what bridge_group_end_again says the end of. Each group names its last end ask (who, why, when) and the bridge's " +
"answer to it, from the state end-asks. A group's name narrows it to that group; open lists only open ones.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "only this group, by the name the gate keys it"},
"group": map[string]any{"type": "string", "description": "only this group, by its id (its branch)"},
"open": map[string]any{"type": "boolean", "description": "only the groups whose end is not written"}}},
Run: func(args map[string]any) (any, error) {
rows, unread, err := groupsRead()
@@ -110,11 +98,11 @@ func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []s
{Name: EndAgainTool,
Description: "Say a delivery group's end (forge.group.merged) again, by hand, for a group whose end the indexer did not take " +
"in (a group bridge_groups names as merged with its end not written, or one the indexer waits for). The bridge " +
"writes it again from the gate's merges, with the time appended to the message id so the stream does not drop it as " +
"writes it again from the seat mesh-delivery's groups, with the time appended to the message id so the stream does not drop it as " +
"a duplicate; the indexer takes an end for a group it already took in without changing anything. Refused while " +
"a member is not merged or its merged message is not written. Answers whether it was written.",
"a member's merged message is not written. Answers whether it was written.",
Input: map[string]any{"type": "object", "required": []string{"group"}, "properties": map[string]any{
"group": map[string]any{"type": "string", "description": "the group, by the name the gate keys it"},
"group": map[string]any{"type": "string", "description": "the group, by its id (its branch)"},
"why": map[string]any{"type": "string", "description": "why its end is said again, in plain words"}}},
Run: func(args map[string]any) (any, error) {
key, value, err := groups.NewEndAsk(str(args, "group"), str(args, "why"), stdio.CallerOf(args), now())
@@ -131,87 +119,50 @@ func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []s
}
// MergedOf is the merged message of the merge of repository at commit merge, as the bridge writes it: the pull request's
// number, head and group from the gate's merges (raw, by key), the rest from the forge through trees and files
// (bridge.MergedMessage), and, for a member of a delivery group, the group and its members as the process sets them.
//
// When the gate holds no merge with that commit, the merge was made on the forge (a recovery merge, the operator's own):
// the pull request is read through the seat git's verb pull (by number when given, else by the merge commit), the message is
// built the same way, and madeOnForge is true, for the answer and its log line to say. It is a lone pull request: no
// group is set.
func MergedOf(raws map[string][]byte, repository, merge string, number int, trees bridge.Trees, files bridge.Files, forge bridge.Caller) (out json.RawMessage, madeOnForge bool, err error) {
// number, branch and head through the seat git's verb pull (by number when given, else by the merge commit; a merge made
// on the forge is read the same way), the rest from the forge through trees and files (bridge.MergedMessage), and, for a
// member of a delivery group the seat mesh-delivery names (of), the group and its members as the process sets them.
func MergedOf(of []seat.Group, repository, merge string, number int, trees bridge.Trees, files bridge.Files, forge bridge.Caller) (json.RawMessage, error) {
owner, repo, ok := strings.Cut(repository, "/")
if !ok || owner == "" || repo == "" {
return nil, false, fmt.Errorf("%q is not owner/name", repository)
}
for _, key := range slices.Sorted(maps.Keys(raws)) {
m, err := meshdeliveryclient.DecodeMerge(raws[key])
if err != nil {
continue
}
at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool {
return trunk.SameRepository(p.Repository, repository) && p.Commit == merge
})
if at < 0 {
continue
}
p := m.Pulls[at]
msg, err := bridge.MergedMessage(owner, repo, int(p.Number), m.Group, p.Head, merge, trees, files)
if err != nil {
return nil, false, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err)
}
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
return nil, false, err
}
if _, approved := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; approved && len(m.Approved) >= 2 {
f.Group, f.Members = m.Group, groups.MembersOf(m)
}
out, err = forgemsg.EncodeForge(f)
return out, false, err
}
if forge == nil {
return nil, false, fmt.Errorf("the gate's merges hold no merge of %s with the commit %s, so its pull request is not known", repository, merge)
return nil, fmt.Errorf("%q is not owner/name", repository)
}
fp, err := bridge.ForgeMade(context.Background(), forge, owner, repo, number, merge)
if err != nil {
return nil, false, fmt.Errorf("the gate's merges hold no merge of %s with the commit %s, and the forge does not give it as a merged pull request either: %w", repository, merge, err)
return nil, fmt.Errorf("the forge gives no merged pull request of %s with the commit %s: %w", repository, merge, err)
}
msg, err := bridge.MergedMessage(owner, repo, fp.Number, fp.Branch, fp.Head, merge, trees, files)
if err != nil {
return nil, false, fmt.Errorf("the merged message of %s at %s (a merge made on the forge) cannot be built: %w", repository, merge, err)
return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err)
}
return msg.Data, true, nil
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
return nil, err
}
if g, ok := groups.Named(of, f.Branch, f.Repository, f.Number); ok {
f.Group, f.Members = g.ID, groups.MembersOf(g)
}
return forgemsg.EncodeForge(f)
}
// read is the groups the gate holds with what the bridge kept of them: their ends in performed, and the last ask of
// each in end-asks with the bridge's answer.
func read(merges, performed, asks stdio.KeptState) ([]groups.Row, []string, error) {
keys, err := merges.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the gate's merges cannot be listed: %w", err)
}
raws := map[string][]byte{}
for _, k := range keys {
e, err := merges.Get(k)
if err != nil || e == nil {
continue
}
raws[k] = e.Value
}
// read is the groups the seat mesh-delivery answers with what the bridge kept of them: the merged messages written and
// their ends in performed, and the last ask of each in end-asks with the bridge's answer.
func read(of []seat.Group, performed, asks stdio.KeptState) ([]groups.Row, []string, error) {
pkeys, err := performed.Keys()
if err != nil {
return nil, nil, fmt.Errorf("the bridge's performed state cannot be listed: %w", err)
}
published, ended := map[string]bool{}, map[string]uint64{}
published, ended := map[string]string{}, map[string]string{}
for _, k := range pkeys {
into := ended
switch {
case strings.HasPrefix(k, "published."):
published[k] = true
case strings.HasPrefix(k, "groups."):
if e, err := performed.Get(k); err == nil && e != nil {
rev, _ := strconv.ParseUint(strings.TrimSpace(string(e.Value)), 10, 64)
ended[strings.TrimPrefix(k, "groups.")] = rev
}
into = published
case !strings.HasPrefix(k, "groups."):
continue
}
if e, err := performed.Get(k); err == nil && e != nil {
into[k] = strings.TrimSpace(string(e.Value))
}
}
akeys, err := asks.Keys()
@@ -224,28 +175,39 @@ func read(merges, performed, asks stdio.KeptState) ([]groups.Row, []string, erro
askRaws[k] = e.Value
}
}
rows, unread := groups.Rows(raws, published, ended, askRaws)
rows, unread := groups.Rows(of, published, ended, askRaws)
return rows, unread, nil
}
// askEnd writes the end-asks value at key and waits, on a watch of the key, at most bound for the bridge's answer.
// askEnd writes the end-asks value at key and waits at most bound for the bridge's answer.
func askEnd(s stdio.KeptState, bound time.Duration) asker {
wait := askState(s, groups.EndAsksBucket, bound, func(v []byte) bool { a, err := groups.DecodeEndAsk(v); return err == nil && a.Done() })
return func(key string, value []byte) (groups.EndAsk, error) {
answered := make(chan groups.EndAsk, 1)
v, err := wait(key, value)
if err != nil {
return groups.EndAsk{}, err
}
return groups.DecodeEndAsk(v)
}
}
// askState writes an ask's value at key of the state s (its bucket named bucket) and waits, on a watch of the key, at most
// bound for the bridge's answer, a later value that done says is answered.
func askState(s stdio.KeptState, bucket string, bound time.Duration, done func([]byte) bool) func(key string, value []byte) ([]byte, error) {
return func(key string, value []byte) ([]byte, error) {
answered := make(chan []byte, 1)
var rev uint64
var mu sync.Mutex
take := func(revision uint64, v []byte) {
mu.Lock()
mine := rev
mu.Unlock()
if mine == 0 || revision <= mine {
if mine == 0 || revision <= mine || !done(v) {
return
}
if a, err := groups.DecodeEndAsk(v); err == nil && a.Done() {
select {
case answered <- a:
default:
}
select {
case answered <- v:
default:
}
}
if err := s.Watch(key, func(c stdio.StateChange) error {
@@ -254,11 +216,11 @@ func askEnd(s stdio.KeptState, bound time.Duration) asker {
}
return nil
}); err != nil {
return groups.EndAsk{}, fmt.Errorf("watching %s.%s: %w", groups.EndAsksState, key, err)
return nil, fmt.Errorf("watching %s.%s: %w", bucket, key, err)
}
r, err := s.Put(key, json.RawMessage(value))
if err != nil {
return groups.EndAsk{}, fmt.Errorf("the ask could not be written to %s: %w", groups.EndAsksBucket, err)
return nil, fmt.Errorf("the ask could not be written to %s: %w", bucket, err)
}
mu.Lock()
rev = r
@@ -269,10 +231,10 @@ func askEnd(s stdio.KeptState, bound time.Duration) asker {
wait, cancel := context.WithTimeout(context.Background(), bound) // a context's bound, no timer (ADR 0437 §8)
defer cancel()
select {
case a := <-answered:
return a, nil
case v := <-answered:
return v, nil
case <-wait.Done():
return groups.EndAsk{}, fmt.Errorf("the bridge did not answer within %s: its answer will be in %s.%s", bound, groups.EndAsksBucket, key)
return nil, fmt.Errorf("the bridge did not answer within %s: its answer will be in %s.%s", bound, bucket, key)
}
}
}
@@ -283,23 +245,32 @@ func askCaller(_ context.Context, key string, body any) (json.RawMessage, error)
}
func main() {
merges, performed, asks := stdio.State(gateMergesState), stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState)
performed, asks := stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState)
tools := Tools(askEnd(asks, 25*time.Second), time.Now,
func() ([]groups.Row, []string, error) { return read(merges, performed, asks) },
func(repository, merge string, number int) (json.RawMessage, bool, error) {
keys, err := merges.Keys()
func() ([]groups.Row, []string, error) {
of, err := groups.Read(context.Background(), askCaller)
if err != nil {
return nil, false, fmt.Errorf("the gate's merges cannot be listed: %w", err)
}
raws := map[string][]byte{}
for _, k := range keys {
if e, err := merges.Get(k); err == nil && e != nil {
raws[k] = e.Value
}
return nil, nil, err
}
return read(of, performed, asks)
},
func(repository, merge string, number int) (json.RawMessage, error) {
ctx := context.Background()
return MergedOf(raws, repository, merge, number, bridge.TreesVia(ctx, askCaller), bridge.FilesVia(ctx, askCaller), askCaller)
of, err := groups.Read(ctx, askCaller)
if err != nil {
return nil, err
}
return MergedOf(of, repository, merge, number, bridge.TreesVia(ctx, askCaller), bridge.FilesVia(ctx, askCaller), askCaller)
}, askCaller)
owedAsks := stdio.State(groups.OwedAsksState)
recorder := meshhandactsclient.Recorder{Ask: stdio.Ask}
tools = append(tools, OwedTools(askState(owedAsks, groups.OwedAsksBucket, 25*time.Second, func(v []byte) bool {
a, err := groups.DecodeOwedAsk(v)
return err == nil && a.Done()
}), func() ([]OwedRow, error) { return performedRows(performed) }, func(what, why string) error {
_, err := recorder.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "owed-dropped"})
return err
}, time.Now)...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] %v\n", err)
os.Exit(1)
@@ -20,28 +20,24 @@ import (
"mesh-delivery-forge-bridge/internal/groups"
)
// 1585/T18, T19 (ADR 0633 §4): bridge_groups names each group the gate holds with its members' state and whether its end is
// written (open or ended, with why), narrowed by group and by open; bridge_group_end_again writes an ask naming the group,
// who asked and why, and answers what the bridge answered.
// 1585/T18, T19 (ADR 0633 §4), acceptance 49: bridge_groups names each group the seat mesh-delivery's verb groups answers
// with its members' state and whether its end is written (open or ended, with why), narrowed by group and by open;
// bridge_group_end_again writes an ask naming the group, who asked and why, and answers what the bridge answered.
func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
enc := func(group string, state2 meshdeliveryclient.MergeState, c string) []byte {
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: group, Ask: "ask", Approved: map[string]string{"novox/a@1": h1, "novox/b@2": h2},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 1, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1},
{Repository: "novox/b", Number: 2, Head: h2, State: string(state2), Commit: c}}})
if err != nil {
t.Fatal(err)
}
return v
grp := func(id string, a, b int, stateB string) seat.Group {
return seat.Group{ID: id, State: "merging", Members: []seat.Member{{Repository: "novox/a", Number: int64(a), Head: h1, State: "merged"},
{Repository: "novox/b", Number: int64(b), Head: h2, State: stateB}}}
}
lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "ask", Approved: map[string]string{"novox/a@5": h1},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}})
merges := map[string][]byte{"ended": enc("ended", meshdeliveryclient.MergeMerged, c2), "ready": enc("ready", meshdeliveryclient.MergeMerged, c2),
"gathered": enc("gathered", meshdeliveryclient.MergeMerging, ""), "solo": lone, "bad": []byte("{")}
performed := map[string]bool{}
for _, c := range []string{c1, c2} {
performed[groups.PublishedKey("novox/a", 1, c)], performed[groups.PublishedKey("novox/b", 2, c)] = true, true
of := []seat.Group{grp("ended", 1, 2, "merged"), grp("ready", 3, 4, "merged"), grp("gathered", 5, 6, "merging"),
{ID: "solo", Members: []seat.Member{{Repository: "novox/a", Number: 7, Head: h1}}}, grp("", 8, 9, "merged")}
published := map[string]string{}
for _, n := range []int{1, 3, 5} {
published[groups.PublishedKey("novox/a", n)] = c1
}
for _, n := range []int{2, 4} {
published[groups.PublishedKey("novox/b", n)] = c2
}
// The state end-asks keeps the last ask of the group ready, answered, and one that does not read.
answeredAsk, _ := json.Marshal(groups.EndAsk{V: 1, Group: "ready", Why: "lost", By: "lead@shanks", At: time.Date(2026, 11, 4, 22, 0, 0, 0, time.UTC),
@@ -58,7 +54,7 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
}
tools := Tools(ask, func() time.Time { return time.Date(2026, 11, 4, 23, 0, 0, 0, time.UTC) },
func() ([]groups.Row, []string, error) {
rows, unread := groups.Rows(merges, performed, map[string]uint64{"ended": 7}, asks)
rows, unread := groups.Rows(of, published, map[string]string{"groups.ended": c2}, asks)
return rows, unread, nil
}, nil, nil)
call := func(name string, args map[string]any) any {
@@ -87,22 +83,22 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
for _, r := range out.Groups {
m[r.Group] = r
}
if len(args) == 0 && strings.Join(out.Unread, " ") != "bad end-asks.gathered" {
t.Errorf("unread = %v, want bad and end-asks.gathered", out.Unread)
if len(args) == 0 && strings.Join(out.Unread, ", ") != "a group with no id, end-asks.gathered" {
t.Errorf("unread = %v, want the group with no id and end-asks.gathered", out.Unread)
}
return m
}
all := rowsOf(map[string]any{})
if len(all) != 3 || !all["ended"].Ended || all["ended"].EndedAt != 7 {
if len(all) != 3 || !all["ended"].Ended || all["ended"].EndedWith != c2 {
t.Fatalf("groups = %+v", all)
}
if r := all["ready"]; !r.Open || !strings.Contains(r.Words, "bridge_group_end_again") || !r.Members[1].Published {
t.Errorf("ready = %+v, want open, merged and published, its end the thing to say", r)
if r := all["ready"]; !r.Open || !strings.Contains(r.Words, "bridge_group_end_again") || !r.Members[1].Published || r.Members[1].Commit != c2 {
t.Errorf("ready = %+v, want open and published with its merge commit, its end the thing to say", r)
}
if a := all["ready"].LastEndAsk; a == nil || a.By != "lead@shanks" || a.Why != "lost" || !a.Written || a.Answer == "" || all["ended"].LastEndAsk != nil {
t.Errorf("ready's last end ask = %+v, ended's %+v: want ready's ask and its answer, none for ended", a, all["ended"].LastEndAsk)
}
if r := all["gathered"]; !r.Open || !strings.Contains(r.Words, "gathered") || r.Members[1].State != string(meshdeliveryclient.MergeMerging) {
if r := all["gathered"]; !r.Open || !strings.Contains(r.Words, "gathered") || r.Members[1].State != "merging" || r.Members[1].Published {
t.Errorf("gathered = %+v", r)
}
if open := rowsOf(map[string]any{"open": true}); len(open) != 2 || open["ended"].Group != "" {
@@ -150,54 +146,47 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
}
return f
}
lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}})
group, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 6, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1},
{Repository: "novox/b", Number: 7, Head: h2, State: string(meshdeliveryclient.MergeMerging)}}})
// The seat mesh-delivery's groups: feat/g of novox/a#9 and novox/b#7; a lone pull request is in none.
of := []seat.Group{{ID: "fix/x", Members: []seat.Member{{Repository: "novox/a", Number: 9, Head: h2}, {Repository: "novox/b", Number: 7, Head: h1}}}}
for _, c := range []struct {
name string
raws map[string][]byte
of []seat.Group
repository string
merge string
number int
api bridge.Caller
onForge bool
want forgemsg.Forge
fail string
}{
{name: "a lone pull request", raws: map[string][]byte{"solo": lone}, repository: "novox/a", merge: c1, want: published("a", 5, h1, "solo", c1)},
{name: "a member of a group", raws: map[string][]byte{"feat~g": group}, repository: "novox/a", merge: c1, want: func() forgemsg.Forge {
f := published("a", 6, h1, "feat/g", c1)
f.Group, f.Members = "feat/g", []meshdeliveryclient.Head{{Repository: "novox/a", Number: 6, Head: h1}, {Repository: "novox/b", Number: 7, Head: h2}}
return f
}()},
{name: "a merge the gate does not hold", raws: map[string][]byte{"solo": lone, "bad": []byte("{")}, repository: "novox/a", merge: c2, fail: "hold no merge"},
// Issue 1966: a merge made on the forge (a recovery merge) has no merge in the gate; the bridge reads the pull request
// from the forge, by number or by the merge commit, and checks its merge commit.
{name: "a merge made on the forge, by number", raws: map[string][]byte{"solo": lone}, repository: "novox/a", merge: c2, number: 9, onForge: true,
{name: "a lone pull request, by number", repository: "novox/a", merge: c2, number: 9,
api: forgeAnswers(t, 9, "", "merged", "fix/x", h2, c2, "main"), want: published("a", 9, h2, "fix/x", c2)},
{name: "a merge made on the forge, by the merge commit", raws: map[string][]byte{}, repository: "novox/a", merge: c2, onForge: true,
{name: "a lone pull request, by the merge commit", repository: "novox/a", merge: c2,
api: forgeAnswers(t, 0, c2, "merged", "fix/x", h2, c2, "main"), want: published("a", 9, h2, "fix/x", c2)},
{name: "a forge pull request merged at another commit", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9,
{name: "a member of a group", of: of, repository: "novox/a", merge: c2,
api: forgeAnswers(t, 0, c2, "merged", "fix/x", h2, c2, "main"), want: func() forgemsg.Forge {
f := published("a", 9, h2, "fix/x", c2)
f.Group, f.Members = "fix/x", []meshdeliveryclient.Head{{Repository: "novox/a", Number: 9, Head: h2}, {Repository: "novox/b", Number: 7, Head: h1}}
return f
}()},
{name: "a forge pull request merged at another commit", repository: "novox/a", merge: c2, number: 9,
api: forgeAnswers(t, 9, "", "merged", "fix/x", h2, c1, "main"), fail: "not at " + c2},
{name: "a forge pull request not merged", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9,
{name: "a forge pull request not merged", repository: "novox/a", merge: c2, number: 9,
api: forgeAnswers(t, 9, "", "open", "fix/x", h2, "", "main"), fail: "not merged"},
{name: "a forge pull request merged into another branch", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9,
{name: "a forge pull request merged into another branch", repository: "novox/a", merge: c2, number: 9,
api: forgeAnswers(t, 9, "", "merged", "fix/x", h2, c2, "dev"), fail: "not the trunk"},
{name: "a forge pull request with no head branch", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9,
{name: "a forge pull request with no head branch", repository: "novox/a", merge: c2, number: 9,
api: forgeAnswers(t, 9, "", "merged", "", h2, c2, "main"), fail: "no head branch"},
{name: "a seat answer that says an error", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9,
{name: "a seat answer that says an error", repository: "novox/a", merge: c2, number: 9,
api: answering(gitfixture.Given(t, "pull answers an error, never none", "verb:pull.answer")), fail: "holds no pull request"},
{name: "a seat answer with no pull request", raws: map[string][]byte{}, repository: "novox/a", merge: c2, number: 9,
{name: "a seat answer with no pull request", repository: "novox/a", merge: c2, number: 9,
api: answering(json.RawMessage(`{}`)), fail: "carries nothing"},
{name: "a forge that does not answer", raws: map[string][]byte{}, repository: "novox/a", merge: c2,
{name: "a forge that does not answer", repository: "novox/a", merge: c2,
api: func(context.Context, string, any) (json.RawMessage, error) {
return nil, errors.New("the forge is down")
}, fail: "the forge is down"},
} {
t.Run(c.name, func(t *testing.T) {
raw, onForge, err := MergedOf(c.raws, c.repository, c.merge, c.number, trees, files, c.api)
raw, err := MergedOf(c.of, c.repository, c.merge, c.number, trees, files, c.api)
if c.fail != "" {
if err == nil || !strings.Contains(err.Error(), c.fail) {
t.Fatalf("err %v, want %q", err, c.fail)
@@ -205,20 +194,67 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
return
}
want, _ := forgemsg.EncodeForge(c.want)
if err == nil && onForge != c.onForge {
t.Fatalf("made on the forge %v, want %v", onForge, c.onForge)
}
if err != nil || !bytes.Equal(raw, want) {
t.Fatalf("err %v, answer\n%s\nwant the published message\n%s", err, raw, want)
}
})
}
if _, _, err := MergedOf(map[string][]byte{"solo": lone}, "novox/a", c1, 0, func(string, string, string) (string, map[string]string, error) {
if _, err := MergedOf(nil, "novox/a", c2, 9, func(string, string, string) (string, map[string]string, error) {
return "", nil, errors.New("the forge does not answer")
}, files, nil); err == nil || !strings.Contains(err.Error(), "does not answer") {
}, files, forgeAnswers(t, 9, "", "merged", "fix/x", h2, c2, "main")); err == nil || !strings.Contains(err.Error(), "does not answer") {
t.Errorf("a forge that does not answer: %v", err)
}
})
// Design 47 *What the gate owes the forge*: bridge_owed reads performed and asks the backlog; bridge_owed_again asks the
// act again; bridge_owed_drop writes the hand-act log first and drops nothing it could not record.
t.Run("the owed tools", func(t *testing.T) {
at := time.Date(2026, 11, 9, 21, 0, 0, 0, time.UTC)
var asked []groups.OwedAsk
ask := func(key string, value []byte) ([]byte, error) {
a, err := groups.DecodeOwedAsk(value)
if err != nil {
return nil, err
}
asked = append(asked, a)
a.Answered, a.Outcome, a.Answer = at, forgemsg.OwedPerformed, "said as owed.ended: performed"
if a.Kind == groups.OwedBacklog {
a.Backlog = []groups.Backlog{{Stream: seat.StreamOwed, Waiting: 1}}
}
return json.Marshal(a)
}
rows := []OwedRow{{Of: seat.Seat, ID: "a", Outcome: forgemsg.OwedPerformed, At: at}, {Of: "mesh-delivery-checker", ID: "b", Outcome: forgemsg.OwedRefused, At: at}}
var recorded []string
recordErr := error(nil)
record := func(what, why string) error { recorded = append(recorded, what+": "+why); return recordErr }
tools := map[string]func(map[string]any) (any, error){}
for _, tl := range OwedTools(ask, func() ([]OwedRow, error) { return rows, nil }, record, func() time.Time { return at }) {
tools[tl.Name] = tl.Run
}
out, err := tools[OwedTool](map[string]any{"of": seat.Seat})
if m, _ := out.(map[string]any); err != nil || len(m["performed"].([]OwedRow)) != 1 || len(m["backlog"].([]groups.Backlog)) != 1 {
t.Errorf("bridge_owed: %v %v", out, err)
}
if out, err := tools[OwedAgainTool](map[string]any{"of": seat.Seat, "id": "a"}); err != nil || out.(map[string]any)["outcome"] != forgemsg.OwedPerformed ||
asked[len(asked)-1].Kind != groups.OwedAgain {
t.Errorf("bridge_owed_again: %v %v", out, err)
}
if _, err := tools[OwedDropTool](map[string]any{"of": seat.Seat, "id": "a"}); err == nil || len(recorded) != 0 {
t.Errorf("a drop with no why: %v, recorded %v", err, recorded)
}
n := len(asked)
recordErr = errors.New("mesh-hand-acts has no holder that answers")
if _, err := tools[OwedDropTool](map[string]any{"of": seat.Seat, "id": "a", "why": "merged by hand"}); err == nil || len(asked) != n {
t.Errorf("a drop whose hand act was not recorded: %v, asked %d", err, len(asked)-n)
}
recordErr = nil
if _, err := tools[OwedDropTool](map[string]any{"of": seat.Seat, "id": "a", "why": "merged by hand"}); err != nil || asked[len(asked)-1].Why != "merged by hand" {
t.Errorf("bridge_owed_drop: %v %+v", err, asked)
}
if _, _, err := groups.NewOwedAsk(groups.OwedAgain, "", "a", "", "", at); err == nil {
t.Error("an ask naming no seat was taken")
}
})
}
// forgeAnswers is the seat git's verb pull for one pull request, asked by number (when number is given) or by the merge
@@ -0,0 +1,132 @@
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-forge-bridge/internal/groups"
)
// The tools of what the bridge performs of what is owed to the forge (novox/hq design 47 *What the gate owes the forge*).
const (
OwedTool = "bridge_owed"
OwedAgainTool = "bridge_owed_again"
OwedDropTool = "bridge_owed_drop"
)
// OwedRow is one act the bridge performed, as the state performed keeps it: the outcome said as owed.ended.
type OwedRow = forgemsg.OwedEnded
// performedRows reads every act the state performed holds; a key that is not an act (a group end's bookkeeping) is left.
func performedRows(s stdio.KeptState) ([]OwedRow, error) {
keys, err := s.Keys()
if err != nil {
return nil, fmt.Errorf("the bridge's performed state cannot be listed: %w", err)
}
var rows []OwedRow
for _, k := range keys {
e, err := s.Get(k)
if err != nil || e == nil {
continue
}
var p struct {
Ended OwedRow `json:"ended"`
}
if json.Unmarshal(e.Value, &p) == nil && p.Ended.Of != "" {
rows = append(rows, p.Ended)
}
}
sort.Slice(rows, func(i, j int) bool { return rows[i].At.After(rows[j].At) })
return rows, nil
}
// OwedTools are bridge_owed, bridge_owed_again and bridge_owed_drop: ask writes an owed-asks value and waits for the
// bridge's answer; performed reads the state performed; record writes a hand act in the seat mesh-hand-acts' log before
// a drop is asked, so a drop is never made without its record.
func OwedTools(ask func(key string, value []byte) ([]byte, error), performed func() ([]OwedRow, error), record func(what, why string) error, now func() time.Time) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
act := map[string]any{
"of": map[string]any{"type": "string", "description": "the seat that said the act: mesh-delivery (the gate) or mesh-delivery-checker"},
"id": map[string]any{"type": "string", "description": "the act's id, as owed and owed.ended say it"}}
asked := func(kind string, args map[string]any) (groups.OwedAsk, error) {
key, value, err := groups.NewOwedAsk(kind, str(args, "of"), str(args, "id"), str(args, "why"), stdio.CallerOf(args), now())
if err != nil {
return groups.OwedAsk{}, err
}
v, err := ask(key, value)
if err != nil {
return groups.OwedAsk{}, err
}
return groups.DecodeOwedAsk(v)
}
withWhy := map[string]any{"why": map[string]any{"type": "string", "description": "why it is dropped, in plain words"}}
for k, v := range act {
withWhy[k] = v
}
return []stdio.Tool{
{Name: OwedTool,
Description: "What the forge bridge performed of what the gate and the checker owe the forge (design 47): each act it said as " +
"owed.ended, newest first, with its seat, id, pull request, outcome (performed, refused, could not be performed, " +
"undecodable, dropped), the forge's words and when, from its state performed (kept 7 days); and the backlog of its " +
"durable on each seat's stream owed: the last event applied, the events waiting and held, the age of the oldest. " +
"of or id narrow it. Read-only.",
Input: map[string]any{"type": "object", "properties": act},
Run: func(args map[string]any) (any, error) {
rows, err := performed()
if err != nil {
return nil, err
}
kept := []OwedRow{}
for _, r := range rows {
if (str(args, "of") == "" || r.Of == str(args, "of")) && (str(args, "id") == "" || r.ID == str(args, "id")) {
kept = append(kept, r)
}
}
out := map[string]any{"performed": kept}
if a, err := asked(groups.OwedBacklog, args); err != nil {
out["backlog-error"] = err.Error()
} else {
out["backlog"] = a.Backlog
}
return out, nil
}},
{Name: OwedAgainTool,
Description: "Perform one act owed to the forge again now, by hand, whatever its last outcome (a refusal the forge no longer " +
"makes, an act that could not be performed): the bridge calls the seat git's verb of the act under its id (safe to " +
"repeat) and says its outcome as owed.ended. The act is the one the bridge kept, or, for the gate's, the seat " +
"mesh-delivery's groups. Answers the outcome.",
Input: map[string]any{"type": "object", "required": []string{"of", "id"}, "properties": act},
Run: func(args map[string]any) (any, error) {
a, err := asked(groups.OwedAgain, args)
if err != nil {
return nil, err
}
return map[string]any{"of": a.Of, "id": a.ID, "outcome": a.Outcome, "answer": a.Answer}, nil
}},
{Name: OwedDropTool,
Description: "Drop one act owed to the forge, by hand, with why: written first in the hand-act log of the seat mesh-hand-acts, " +
"then said by the bridge as owed.ended with the outcome dropped, so its owner records it answered and stops saying " +
"it again. Nothing is done at the forge. Answers the outcome said.",
Input: map[string]any{"type": "object", "required": []string{"of", "id", "why"}, "properties": withWhy},
Run: func(args map[string]any) (any, error) {
if str(args, "why") == "" {
return nil, fmt.Errorf("a drop says why")
}
if err := record(fmt.Sprintf("dropped the act %s that %s owes the forge", str(args, "id"), str(args, "of")), str(args, "why")); err != nil {
return nil, err
}
a, err := asked(groups.OwedDrop, args)
if err != nil {
return nil, err
}
return map[string]any{"of": a.Of, "id": a.ID, "outcome": a.Outcome, "answer": a.Answer}, nil
}},
}
}
@@ -3,142 +3,10 @@ package main
import (
"context"
"errors"
"fmt"
"log"
"strconv"
"github.com/nats-io/nats.go/jetstream"
"mesh-delivery-forge-bridge/internal/bridge"
)
// performedBucket is the module's state bucket performed, `<module>_<name>` as heads is: for each key of the gate's
// buckets the bridge has acted on, the revision it acted at (manifest state entry performed, novox/hq ADR 0437 §8).
// Its keys are `owed.<owed key>` and `merges.<merges key>`; its value is the revision in decimal.
const performedBucket = forgeBridgeModule + "_performed"
// kvRevisions is the bucket performed as the performer's revisions.
type kvRevisions struct{ kv jetstream.KeyValue }
func (s kvRevisions) Get(ctx context.Context, key string) (uint64, bool, error) {
e, err := s.kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return 0, false, nil
}
if err != nil {
return 0, false, err
}
rev, err := strconv.ParseUint(string(e.Value()), 10, 64)
if err != nil {
return 0, false, fmt.Errorf("%s holds %q, which is no revision: %w", key, e.Value(), err)
}
return rev, true, nil
}
func (s kvRevisions) Put(ctx context.Context, key string, rev uint64) error {
_, err := s.kv.PutString(ctx, key, strconv.FormatUint(rev, 10))
return err
}
// revisions is what the performer keeps of the revisions it acted at.
type revisions interface {
Get(ctx context.Context, key string) (rev uint64, found bool, err error)
Put(ctx context.Context, key string, rev uint64) error
}
// performer does on the forge what the gate's keys ask (novox/hq ADR 0437 §8): one act for each revision of an owed
// key, one merge for each revision of a merges key. A refusal is written on FORGE and the act is not tried again until
// the key has a new revision; the performer holds no timer and serves no request.
type performer struct {
call caller
pub publisher
done revisions
logger *log.Logger
}
// seen says whether the revision of the key was acted on already. A revision that cannot be read is not seen, so the
// act is done: a status set twice is the same status.
func (p *performer) seen(ctx context.Context, doneKey string, rev uint64) bool {
kept, found, err := p.done.Get(ctx, doneKey)
if err != nil {
p.logger.Printf("%s: the revision acted at could not be read, so the act is done again: %v", doneKey, err)
return false
}
return found && kept >= rev
}
func (p *performer) keep(ctx context.Context, doneKey string, rev uint64) {
if err := p.done.Put(ctx, doneKey, rev); err != nil {
p.logger.Printf("%s: revision %d was acted on but could not be kept, so a restart acts on it again: %v", doneKey, rev, err)
}
}
// refuse writes the refused message of an act on FORGE; it reports whether it was written. The message id makes a
// second write of one revision's refusal the same message to the stream.
func (p *performer) refuse(ctx context.Context, doneKey string, rev uint64, a bridge.Act, words string) bool {
msg, err := bridge.RefusedMessage(a, words)
if err != nil {
p.logger.Printf("%s: the forge refused %s (%s) and the refusal cannot be said on FORGE: %v", doneKey, a.Name, words, err)
return false
}
id := fmt.Sprintf("refused.%s.%d", doneKey, rev)
if _, err := p.pub.Publish(ctx, forgeSubject(msg.Event), msg.Data, jetstream.WithMsgID(id)); err != nil {
p.logger.Printf("%s: the forge refused %s (%s) and FORGE could not be written: %v", doneKey, a.Name, words, err)
return false
}
return true
}
// owed performs the act an owed key of the gate asks at revision rev, once.
func (p *performer) owed(ctx context.Context, key string, rev uint64, value []byte) {
p.perform(ctx, "owed."+key, key, rev, value)
}
// perform does the act of an owed key once for the revision, under the bookkeeping key doneKey.
func (p *performer) perform(ctx context.Context, doneKey, key string, rev uint64, value []byte) {
if p.seen(ctx, doneKey, rev) {
return
}
a, err := bridge.OwedAct(key, value)
if err != nil {
p.logger.Printf("%s at revision %d is not an act the bridge can perform, and is left: %v", key, rev, err)
p.keep(ctx, doneKey, rev)
return
}
if _, err := p.call(ctx, a.Call.Tool, a.Call.Args); err != nil {
p.logger.Printf("%s at revision %d: %s refused: %v", key, rev, a.Call.Tool, err)
if !p.refuse(ctx, doneKey, rev, a, err.Error()) {
return
}
}
p.keep(ctx, doneKey, rev)
}
// merges performs the merges a merges key hands the bridge at revision rev, once: each pull request in the state
// merging, in the order the value names them, stopping at the first refusal (novox/hq ADR 0441 §3).
func (p *performer) merges(ctx context.Context, key string, rev uint64, value []byte) {
doneKey := "merges." + key
if p.seen(ctx, doneKey, rev) {
return
}
acts, err := bridge.MergeActs(value)
if err != nil {
p.logger.Printf("%s at revision %d cannot be merged, and is left: %v", key, rev, err)
p.keep(ctx, doneKey, rev)
return
}
for _, a := range acts {
if _, err := p.call(ctx, a.Call.Tool, a.Call.Args); err != nil {
p.logger.Printf("%s at revision %d: the merge of %s#%d refused: %v", key, rev, a.Repository, a.Number, err)
if !p.refuse(ctx, doneKey, rev, a, err.Error()) {
return
}
break
}
}
p.keep(ctx, doneKey, rev)
}
// watch hands each key of a bucket, with the revision it has now and then each new one, to react until ctx ends or the
// watch fails. A deleted key is not an act.
func watch(ctx context.Context, kv jetstream.KeyValue, react func(key string, rev uint64, value []byte)) error {
@@ -8,10 +8,15 @@ import (
"log"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-forge-bridge/internal/gitfixture"
"mesh-delivery-forge-bridge/internal/groups"
)
const (
@@ -19,195 +24,231 @@ const (
headB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
)
type memRevisions map[string]uint64
type memStrs map[string]string
func (m memRevisions) Get(_ context.Context, key string) (uint64, bool, error) {
func (m memStrs) Get(_ context.Context, key string) (string, bool, error) {
r, ok := m[key]
return r, ok, nil
}
func (m memRevisions) Put(_ context.Context, key string, rev uint64) error { m[key] = rev; return nil }
func (m memStrs) Put(_ context.Context, key, v string) error { m[key] = v; return nil }
type tcall struct {
key string
body string
type memPerformed map[string][]byte
func (m memPerformed) Get(_ context.Context, key string) ([]byte, bool, error) {
v, ok := m[key]
return v, ok, nil
}
func (m memPerformed) Put(_ context.Context, key string, v []byte) error { m[key] = v; return nil }
// seatFake answers each seat verb from answers, by its key; one absent is not answered (an error, as a bound passed).
type seatFake struct {
calls []string
answers map[string]json.RawMessage
}
// forgeFake records the calls and answers each from refuse, a map from a tool to its refusal.
type forgeFake struct {
calls []tcall
refuse map[string]string
}
func (f *forgeFake) call(_ context.Context, key string, body any) (json.RawMessage, error) {
b, _ := json.Marshal(body)
f.calls = append(f.calls, tcall{key, string(b)})
if w, ok := f.refuse[key]; ok {
return nil, errors.New(w)
func (f *seatFake) call(_ context.Context, key string, _ any) (json.RawMessage, error) {
f.calls = append(f.calls, key)
if a, ok := f.answers[key]; ok {
return a, nil
}
return json.RawMessage(`{}`), nil
return nil, errors.New("nats: timeout")
}
func newPerformer(f *forgeFake) (*performer, *fakePub, memRevisions, *bytes.Buffer) {
pub, done, logs := &fakePub{}, memRevisions{}, &bytes.Buffer{}
return &performer{call: f.call, pub: pub, done: done, logger: log.New(logs, "", 0)}, pub, done, logs
func newOwing(f *seatFake) (*owing, *fakePub, memPerformed) {
pub, kept := &fakePub{}, memPerformed{}
at := time.Date(2026, 11, 9, 21, 0, 0, 0, time.UTC)
return &owing{call: f.call, pub: pub, performed: kept, now: func() time.Time { return at }, logger: log.New(&bytes.Buffer{}, "", 0)}, pub, kept
}
// The values are the SDK's own encoding (mesh-sdk go/delivery EncodeOwed, EncodeMerge), the keys its OwedKey.
func owedKV(t *testing.T, repository string, number int, act string, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
t.Helper()
key, err := meshdeliveryclient.OwedKey(repository, number, headA, act)
if err != nil {
t.Fatal(err)
}
v, err := meshdeliveryclient.EncodeOwed(o)
if err != nil {
t.Fatal(err)
}
return key, v
}
func TestAnOwedStatusIsSetOnceForARevision(t *testing.T) { // 1341/AC1
// Design 47 *What the gate owes the forge*: an owed act is the seat git's verb under its id, its outcome said as
// owed.ended on the stream forge before it is kept in performed, an act it cannot read said undecodable; and at its start
// the bridge performs from the seat's groups every act said and not answered.
func TestWhatIsOwedIsPerformedSaidAndKept(t *testing.T) {
t.Run("the names are the manifest's", theNamesAreTheManifests)
f := &forgeFake{}
p, _, _, _ := newPerformer(f)
key, v := owedKV(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate",
seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: "mesh/merge-gate", State: "success", Text: "all green", Target: "https://forge.example/p/12"})
p.owed(context.Background(), key, 7, v)
if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_commit_status" ||
f.calls[0].body != `{"context":"mesh/merge-gate","description":"all green","owner":"novox","repo":"mesh-delivery","sha":"`+headA+`","state":"success","target_url":"https://forge.example/p/12"}` {
t.Fatalf("calls = %+v", f.calls)
}
p.owed(context.Background(), key, 7, v) // the same revision seen again (a restart's replay)
if len(f.calls) != 1 {
t.Errorf("the same revision called the tool again: %+v", f.calls)
}
p.owed(context.Background(), key, 8, v) // a new revision of the key is a new act
if len(f.calls) != 2 {
t.Errorf("a new revision was not performed: %+v", f.calls)
}
}
func TestViewNoteAndCommentEachCallTheirToolOnce(t *testing.T) { // 1341/AC2
f := &forgeFake{}
p, _, _, _ := newPerformer(f)
gate := func(name string) []byte { return gitfixture.Seat(t, seat.Seat, "events", name, "event:owed")[0] }
answer := func(verb, name string) json.RawMessage { return gitfixture.Given(t, name, "verb:"+verb+".answer") }
for _, c := range []struct {
act string
o seat.OwedByRepositoryNumberHeadActValue
name, of string
data []byte
answers map[string]json.RawMessage
pubErr error
call string
outcome string
kept bool
err string
}{
{"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"}},
{"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"}},
{"comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"}},
{"a merge performed", seat.Seat, gate("owed is said, a merge"),
map[string]json.RawMessage{"seat:git.merge-pull": answer("merge-pull", "merge-pull answers")}, nil,
"seat:git.merge-pull", forgemsg.OwedPerformed, true, ""},
{"a status refused", seat.Seat, gate("owed is said, a status"),
map[string]json.RawMessage{"seat:git.status": answer("status", "status answers an error, never none")}, nil,
"seat:git.status", forgemsg.OwedRefused, true, ""},
{"the checker's status not answered", checker.Seat, gitfixture.Seat(t, checker.Seat, "events", "owed is said", "event:owed")[0],
nil, nil, "seat:git.status", forgemsg.OwedCouldNotBePerformed, true, ""},
{"an act it cannot read", seat.Seat, []byte(`{`), nil, nil, "", forgemsg.OwedUndecodable, false, ""},
{"a seat that says no owed act", "mesh-wants", gate("owed is said, a merge"), nil, nil, "", forgemsg.OwedUndecodable, false, ""},
{"the stream forge refusing the outcome", seat.Seat, gate("owed is said, a merge"),
map[string]json.RawMessage{"seat:git.merge-pull": answer("merge-pull", "merge-pull answers")}, errors.New("no responders"),
"seat:git.merge-pull", "", false, "could not be said"},
} {
key, v := owedKV(t, "novox/mesh-delivery", 12, c.act, c.o)
p.owed(context.Background(), key, 1, v)
p.owed(context.Background(), key, 1, v)
f := &seatFake{answers: c.answers}
o, pub, kept := newOwing(f)
pub.err = c.pubErr
_, err := o.heard(context.Background(), c.of, c.data)
if c.err != "" {
if err == nil || !strings.Contains(err.Error(), c.err) || len(kept) != 0 {
t.Errorf("%s: %v, kept %d; want an error naming %q and nothing kept", c.name, err, len(kept), c.err)
}
continue
}
if err != nil || len(pub.data) != 1 || pub.subjects[0] != forgeSubject(forgemsg.OwedEndedEvent) {
t.Fatalf("%s: %v, said %v; want one owed.ended", c.name, err, pub.subjects)
}
e, err := forgemsg.DecodeOwedEnded(pub.data[0])
if err != nil || e.Outcome != c.outcome || e.Of != c.of {
t.Errorf("%s: said %+v %v, want %s of %s", c.name, e, err, c.outcome, c.of)
}
if (c.call == "") != (len(f.calls) == 0) || (c.call != "" && f.calls[0] != c.call) {
t.Errorf("%s: called %v, want %q", c.name, f.calls, c.call)
}
if v, ok := kept[performedKey(c.of, e.ID)]; ok != c.kept {
t.Errorf("%s: kept %v (%s), want %v", c.name, ok, v, c.kept)
} else if ok {
var p Performed
if err := json.Unmarshal(v, &p); err != nil || p.Ended.Outcome != c.outcome || !json.Valid(p.Owed) {
t.Errorf("%s: kept %s, want the outcome and the owed event", c.name, v)
}
}
}
var tools []string
for _, c := range f.calls {
tools = append(tools, c.key)
}
if got := strings.Join(tools, " "); got != "gitea.gitea_delivery_view gitea.gitea_note_append gitea.gitea_add_comment" {
t.Errorf("calls = %s", got)
}
}
func TestARefusedActIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1341/AC3
f := &forgeFake{refuse: map[string]string{"gitea.gitea_add_comment": "the pull request is locked"}}
p, pub, _, _ := newPerformer(f)
key, v := owedKV(t, "novox/mesh-delivery", 12, "comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"})
p.owed(context.Background(), key, 3, v)
p.owed(context.Background(), key, 3, v)
if len(f.calls) != 1 {
t.Errorf("the refused act was tried %d times at one revision", len(f.calls))
}
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.refused" {
t.Fatalf("published on %v", pub.subjects)
}
m, err := forgemsg.DecodeForge(pub.data[0])
if err != nil || m.Kind != forgemsg.ForgeRefused || m.Act != "comment.1" || m.Number != 12 || m.Head != headA || m.Words != "the pull request is locked" {
t.Errorf("message %+v err %v", m, err)
}
f.refuse = nil
p.owed(context.Background(), key, 4, v) // only a new revision tries again
if len(f.calls) != 2 {
t.Errorf("a new revision was not tried: %d calls", len(f.calls))
}
}
// Design 47 *The switch carries what is owed*: an act the gate switched from its old state is performed only when the old
// bridge's own bookkeeping does not name its key at its revision or later; one it names is said performed, not done.
t.Run("a switched act the old bridge performed", func(t *testing.T) {
var owed map[string]any
if err := json.Unmarshal(gate("owed is said, a merge"), &owed); err != nil {
t.Fatal(err)
}
id, err := meshdeliveryclient.SwitchedActID(meshdeliveryclient.SwitchedMerges, "feat~x", 7)
if err != nil {
t.Fatal(err)
}
owed["id"] = id
data, _ := json.Marshal(owed)
for _, c := range []struct {
name string
kept string // the old bridge's merges.feat~x, "" for none
called bool
}{
{"kept at its revision", "7", false},
{"kept at a later revision", "9", false},
{"kept at an earlier revision", "6", true},
{"not kept", "", true},
} {
f := &seatFake{answers: map[string]json.RawMessage{"seat:git.merge-pull": answer("merge-pull", "merge-pull answers")}}
o, pub, kept := newOwing(f)
if c.kept != "" {
kept["merges.feat~x"] = []byte(c.kept)
}
if _, err := o.heard(context.Background(), seat.Seat, data); err != nil || len(pub.data) != 1 {
t.Fatalf("%s: %v, said %d", c.name, err, len(pub.data))
}
e, _ := forgemsg.DecodeOwedEnded(pub.data[0])
if (len(f.calls) == 1) != c.called || e.Outcome != forgemsg.OwedPerformed || e.ID != id {
t.Errorf("%s: called %v, said %+v; want called %v and performed", c.name, f.calls, e, c.called)
}
if _, ok := kept[performedKey(seat.Seat, id)]; !ok {
t.Errorf("%s: the outcome is not kept under the act's id", c.name)
}
}
})
func TestAnOwedValueThatCannotBeReadIsLoggedAndNotRetried(t *testing.T) {
f := &forgeFake{}
p, pub, done, logs := newPerformer(f)
p.owed(context.Background(), "not-a-key", 2, []byte(`{}`))
if len(f.calls) != 0 || len(pub.subjects) != 0 {
t.Errorf("calls %v published %v", f.calls, pub.subjects)
}
if !strings.Contains(logs.String(), "not-a-key") {
t.Errorf("log %q does not name the key", logs.String())
}
if rev, ok := done["owed.not-a-key"]; !ok || rev != 2 {
t.Errorf("revision kept = %v %v", rev, ok)
}
}
t.Run("heard from a seat's stream owed", func(t *testing.T) {
f := &seatFake{answers: map[string]json.RawMessage{"seat:git.merge-pull": answer("merge-pull", "merge-pull answers")}}
o, pub, _ := newOwing(f)
m := &fakeMsg{subject: "mesh.seat.mesh-delivery.event.owed.novox/mesh-sdk.178", data: gate("owed is said, a merge")}
o.hear(context.Background(), seat.Seat, m)
again := &fakeMsg{subject: m.subject, data: m.data}
o.hear(context.Background(), seat.Seat, again)
if m.acked != 1 || again.acked != 1 || len(f.calls) != 1 || len(pub.data) != 1 {
t.Errorf("an act said twice: acked %d and %d, called %v, said %d; want performed and said once, both acknowledged",
m.acked, again.acked, f.calls, len(pub.data))
}
pub.err = errors.New("no responders")
lost := &fakeMsg{subject: m.subject, data: gate("owed is said, a status")}
o.hear(context.Background(), seat.Seat, lost)
if lost.acked != 0 || len(lost.nak) != 1 {
t.Errorf("an outcome not said: acked %d, nak %v; want it heard again", lost.acked, lost.nak)
}
})
func mergeKV(t *testing.T, m seat.MergesByGroupValue) []byte {
t.Helper()
v, err := meshdeliveryclient.EncodeMerge(m)
if err != nil {
t.Fatal(err)
}
return v
}
t.Run("asked again, dropped, and its backlog read by hand", func(t *testing.T) {
groupsAnswer := gitfixture.Seat(t, seat.Seat, "verbs", "groups answers a group with its members and acts", "verb:groups.answer")[0]
f := &seatFake{answers: map[string]json.RawMessage{"seat:git.merge-pull": answer("merge-pull", "merge-pull answers"),
"seat:mesh-delivery.groups": groupsAnswer, "seat:git.status": answer("status", "status answers")}}
o, pub, kept := newOwing(f)
merge := "merge.feat=47-G.0123456789ab.novox/mesh-sdk#178"
if _, err := o.heard(context.Background(), seat.Seat, gate("owed is said, a merge")); err != nil {
t.Fatal(err)
}
backlog := func(context.Context) []groups.Backlog { return []groups.Backlog{{Stream: seat.StreamOwed, Waiting: 2}} }
for _, c := range []struct {
name string
ask groups.OwedAsk
calls, said int
outcome, answers string
}{
{"again, from performed", groups.OwedAsk{Kind: groups.OwedAgain, Of: seat.Seat, ID: merge}, 2, 2, forgemsg.OwedPerformed, "performed"},
// The fixture's status is in the groups only: the bridge never heard it.
{"again, from the groups", groups.OwedAsk{Kind: groups.OwedAgain, Of: seat.Seat, ID: "status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate"}, 4, 3, forgemsg.OwedPerformed, "performed"},
{"dropped", groups.OwedAsk{Kind: groups.OwedDrop, Of: seat.Seat, ID: merge, Why: "merged by hand", By: "lead@shanks"}, 4, 4, forgemsg.OwedDropped, "merged by hand"},
{"again, an act nobody holds", groups.OwedAsk{Kind: groups.OwedAgain, Of: checker.Seat, ID: "x"}, 4, 4, "", "checker says"},
{"the backlog", groups.OwedAsk{Kind: groups.OwedBacklog}, 4, 4, "", "backlog"},
} {
a := o.answer(context.Background(), c.ask, backlog)
if !a.Done() || a.Outcome != c.outcome || !strings.Contains(a.Answer, c.answers) || len(f.calls) != c.calls || len(pub.data) != c.said {
t.Errorf("%s: %+v, called %v, said %d; want %q naming %q, %d calls, %d said", c.name, a, f.calls, len(pub.data), c.outcome, c.answers, c.calls, c.said)
}
if c.ask.Kind == groups.OwedBacklog && (len(a.Backlog) != 1 || a.Backlog[0].Waiting != 2) {
t.Errorf("%s: %+v", c.name, a.Backlog)
}
}
var p Performed
if err := json.Unmarshal(kept[performedKey(seat.Seat, merge)], &p); err != nil || p.Ended.Outcome != forgemsg.OwedDropped {
t.Errorf("performed after the drop: %+v %v", p, err)
}
})
func twoMembers(a, b meshdeliveryclient.MergeState) seat.MergesByGroupValue {
return seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab",
Approved: map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA},
Pulls: []seat.MergesByGroupValuePullsItem{
{Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(a), Commit: map[bool]string{true: "c1"}[a == meshdeliveryclient.MergeMerged]},
{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(b), Commit: map[bool]string{true: "c2"}[b == meshdeliveryclient.MergeMerged]},
}}
}
func TestAMergeRevisionMergesOnlyTheMemberHanded(t *testing.T) { // 1342/AC1
f := &forgeFake{}
p, _, _, _ := newPerformer(f)
m := twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged)
p.merges(context.Background(), "g", 5, mergeKV(t, m))
want := `{"approved_heads":["novox/mesh-delivery@` + headA + `","novox/mesh-sdk@` + headB + `"],"head":"` + headA + `","number":12,"owner":"novox","repo":"mesh-delivery"}`
if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_merge_pull_request" || f.calls[0].body != want {
t.Fatalf("calls = %+v", f.calls)
}
p.merges(context.Background(), "g", 5, mergeKV(t, m))
if len(f.calls) != 1 {
t.Errorf("a revision merged twice: %+v", f.calls)
}
}
func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/AC2
f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "novox/mesh-delivery#12 is not merged: its head moved"}}
p, pub, _, _ := newPerformer(f)
v := mergeKV(t, twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged))
p.merges(context.Background(), "g", 5, v)
p.merges(context.Background(), "g", 5, v)
if len(f.calls) != 1 || len(pub.data) != 1 {
t.Fatalf("calls %d messages %d, want one each", len(f.calls), len(pub.data))
}
m, err := forgemsg.DecodeForge(pub.data[0])
if err != nil || m.Kind != forgemsg.ForgeRefused || m.Repository != "novox/mesh-delivery" || m.Number != 12 || m.Head != headA ||
m.Act != "merge" || m.Words != "novox/mesh-delivery#12 is not merged: its head moved" {
t.Errorf("message %+v err %v", m, err)
}
f.refuse = nil
p.merges(context.Background(), "g", 6, v)
if len(f.calls) != 2 {
t.Errorf("a new revision was not tried: %d calls", len(f.calls))
}
}
func TestAMergeWithTwoHandedStopsAtTheFirstRefusal(t *testing.T) { // ADR 0441 §3
f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "no"}}
p, pub, _, _ := newPerformer(f)
p.merges(context.Background(), "g", 1, mergeKV(t, twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerging)))
if len(f.calls) != 1 || len(pub.data) != 1 {
t.Errorf("calls %d messages %d, want the first refusal to stop the pass", len(f.calls), len(pub.data))
}
t.Run("caught up from the groups", func(t *testing.T) {
groups := gitfixture.Seat(t, seat.Seat, "verbs", "groups answers a group with its members and acts", "verb:groups.answer")[0]
f := &seatFake{answers: map[string]json.RawMessage{"seat:mesh-delivery.groups": groups,
"seat:git.status": answer("status", "status answers")}}
o, pub, kept := newOwing(f)
// The fixture's merge has its outcome recorded; its status is said and unanswered.
if n, err := o.catchUp(context.Background()); err != nil || n != 1 || len(pub.data) != 1 || f.calls[1] != "seat:git.status" {
t.Fatalf("catch-up: %d %v, called %v; want the one unanswered status performed", n, err, f.calls)
}
if n, err := o.catchUp(context.Background()); err != nil || n != 0 || len(f.calls) != 3 {
t.Errorf("catch-up again: %d %v, called %v; want nothing performed that performed holds", n, err, f.calls)
}
if len(kept) != 1 {
t.Errorf("kept %v, want the status", kept)
}
// The seat answers a group it does not know with no groups, never an error (W11.10): nothing is performed and
// nothing fails; a groups the seat leaves unanswered is the catch-up's error.
none := gitfixture.Seat(t, seat.Seat, "verbs", "groups answers a group it does not know with no groups", "verb:groups.answer")[0]
for _, row := range []struct {
answer json.RawMessage
fails bool
}{{none, false}, {nil, true}} {
f := &seatFake{answers: map[string]json.RawMessage{}}
if row.answer != nil {
f.answers["seat:mesh-delivery.groups"] = row.answer
}
o, pub, _ := newOwing(f)
n, err := o.catchUp(context.Background())
if (err != nil) != row.fails || n != 0 || len(pub.data) != 0 {
t.Errorf("catch-up from %s: %d performed, %v; want none performed, failing %v", row.answer, n, err, row.fails)
}
}
})
}
@@ -8,11 +8,13 @@ import (
"github.com/nats-io/nats.go/jetstream"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"mesh-delivery-forge-bridge/internal/groups"
)
// endAsked answers one value of the state end-asks (the tool bridge_group_end_again, ADR 0633 §4): a value not yet answered
// says the group's end again from the gate's merges, with the ask's time appended to the message id so the stream's duplicate
// says the group's end again from the seat's groups, with the ask's time appended to the message id so the stream's duplicate
// window does not drop it, and is written back by compare-and-set at its revision with whether it was written. A value
// answered already (this bridge's own write) is left.
func (g *grouper) endAsked(ctx context.Context, kv jetstream.KeyValue, key string, rev uint64, value []byte) {
@@ -24,7 +26,7 @@ func (g *grouper) endAsked(ctx context.Context, kv jetstream.KeyValue, key strin
if a.Done() {
return
}
a = g.answerEnd(ctx, key, a, time.Now())
a = g.answerEnd(ctx, a, time.Now())
g.logger.Printf("the end of the group %q said again by %s (%s): %s", a.Group, a.By, a.Why, a.Answer)
data, err := json.Marshal(a)
if err != nil {
@@ -35,27 +37,31 @@ func (g *grouper) endAsked(ctx context.Context, kv jetstream.KeyValue, key strin
}
}
// readMerges reads one key of the gate's merges state.
func (g *grouper) readMerges(ctx context.Context, key string) ([]byte, uint64, bool, error) {
if g.merges == nil {
return nil, 0, false, errors.New("the bridge does not read the gate's merges (its bucket was not there at the start)")
}
value, rev, found, err := g.merges(ctx, key)
return value, rev, found, err
}
// answerEnd says the group's end again for an ask and returns the ask with its answer.
func (g *grouper) answerEnd(ctx context.Context, key string, a groups.EndAsk, at time.Time) groups.EndAsk {
func (g *grouper) answerEnd(ctx context.Context, a groups.EndAsk, at time.Time) groups.EndAsk {
a.Answered = at.UTC()
switch merges, mrev, found, err := g.readMerges(ctx, key); {
case err != nil:
a.Answer = "the gate's merges " + key + " cannot be read: " + err.Error()
case !found:
a.Answer = "the gate holds no merge of the group " + a.Group
case g.end(ctx, key, mrev, merges, groups.AgainStamp(a.At)):
if g.groups == nil {
a.Answer = "the bridge reads no groups"
return a
}
of, err := g.groups(ctx)
if err != nil {
a.Answer = "the groups cannot be read: " + err.Error()
return a
}
m, ok := seat.Group{}, false
for _, c := range of {
if c.ID == a.Group {
m, ok = c, true
}
}
switch {
case !ok || len(m.Members) < 2:
a.Answer = "the seat " + seat.Seat + " holds no group " + a.Group + " of more than one member"
case g.end(ctx, m, groups.AgainStamp(a.At)):
a.Written, a.Answer = true, "the end of the group is written again"
default:
a.Answer = "the end is not written: a member is not merged or its merged message is not written, or FORGE refused it (the bridge's journal says which)"
a.Answer = "the end is not written: a member's merged message is not written, or FORGE refused it (the bridge's journal says which)"
}
return a
}
@@ -2,37 +2,40 @@ package main
import (
"context"
"errors"
"log"
"slices"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
groupkeys "mesh-delivery-forge-bridge/internal/groups"
)
// A delivery group's merges reach the indexer together (novox/hq ADR 0633 §4): the bridge sets the group on each
// member's merged message, from the gate's merges state it reads (ADR 0437 §8), and once every approved member is
// merged and its merged message written on FORGE it says the group's end, forge.group.merged, once.
// member's merged message, from the seat mesh-delivery's verb groups (design 47 *A group's merges are taken in together*;
// it reads no state of the gate's), and once every member's merged message is written on FORGE it says the group's end,
// forge.group.merged, once, after the last of them.
//
// What it keeps of it is in its state performed, beside the gate's keys it acted on: `published.<hash>` for each
// member's merged message written (the hash is the first 16 hex digits of the sha256 of `<repository>#<number>@<merge>`),
// and `groups.<merges key>` once the group's end is written, its value the revision of the gate's merges key it was
// worked out from. A group whose end is not written yet is open (bridge_groups reads them).
// What it keeps of it is in its state performed: `published.<hash>` for each member's merged message written, its value
// the merge commit it named (the hash is the first 16 hex digits of the sha256 of `<repository>#<number>`), and
// `groups.<group>` once the group's end is written, its value the last member's merge commit. A group whose end is not
// written yet is open (bridge_groups reads them).
// merges reads one key of the gate's merges state.
type mergesRead func(ctx context.Context, key string) (value []byte, rev uint64, found bool, err error)
// groupsRead is every delivery group the seat answers.
type groupsRead func(ctx context.Context) ([]seat.Group, error)
// strs is performed as string values by key: the grouper's `published.` and `groups.` keys.
type strs interface {
Get(ctx context.Context, key string) (value string, found bool, err error)
Put(ctx context.Context, key, value string) error
}
// grouper sets a member's group and says a group's end.
type grouper struct {
merges mergesRead
groups groupsRead
send func(ctx context.Context, subject string, data []byte, id string) error // writes on FORGE with the message id
done revisions
done strs
logger *log.Logger
}
@@ -44,63 +47,40 @@ func sendVia(pub publisher) func(context.Context, string, []byte, string) error
}
}
func kvMerges(kv jetstream.KeyValue) mergesRead {
return func(ctx context.Context, key string) ([]byte, uint64, bool, error) {
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, 0, false, nil
}
if err != nil {
return nil, 0, false, err
}
return e.Value(), e.Revision(), true, nil
}
// groupsVia reads the groups through the seat mesh-delivery's verb groups.
func groupsVia(call caller) groupsRead {
return func(ctx context.Context) ([]seat.Group, error) { return groupkeys.Read(ctx, call) }
}
// groupOf is the gate's merge of the group a merged message's branch names, when the message's pull request is one of
// its approved members and the group has more than one: a lone pull request is no group.
func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (seat.MergesByGroupValue, uint64, bool) {
if g == nil || g.merges == nil || f.Branch == "" {
return seat.MergesByGroupValue{}, 0, false
// groupOf is the group a merged message's branch names, when the seat names the message's pull request among its
// members and the group has more than one: a lone pull request is no group.
func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (seat.Group, bool) {
if g == nil || g.groups == nil || f.Branch == "" {
return seat.Group{}, false
}
key, err := meshdeliveryclient.MergesKey(f.Branch)
of, err := g.groups(ctx)
if err != nil {
return seat.MergesByGroupValue{}, 0, false
g.logger.Printf("%s#%d: the groups cannot be read, so its merged message names no group: %v", f.Repository, f.Number, err)
return seat.Group{}, false
}
value, rev, found, err := g.merges(ctx, key)
if err != nil {
g.logger.Printf("%s#%d: the gate's merges.%s cannot be read, so its merged message names no group: %v", f.Repository, f.Number, key, err)
return seat.MergesByGroupValue{}, 0, false
}
if !found {
return seat.MergesByGroupValue{}, 0, false
}
m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil {
g.logger.Printf("%s#%d: the gate's merges.%s is not a merge, so its merged message names no group: %v", f.Repository, f.Number, key, err)
return seat.MergesByGroupValue{}, 0, false
}
if _, ok := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; !ok || len(m.Approved) < 2 {
return seat.MergesByGroupValue{}, 0, false
}
return m, rev, true
return groupkeys.Named(of, f.Branch, f.Repository, f.Number)
}
// member sets the group and its approved members on a merged message's body (ADR 0633 §4); any other body, and a
// merge of no group, is left as it is.
// member sets the group and its members on a merged message's body (ADR 0633 §4); any other body, and a merge of no
// group, is left as it is.
func (g *grouper) member(ctx context.Context, data []byte) ([]byte, forgemsg.Forge, bool) {
f, err := forgemsg.DecodeForge(data)
if err != nil || f.Kind != forgemsg.ForgeMerged {
return data, f, false
}
m, _, ok := g.groupOf(ctx, f)
m, ok := g.groupOf(ctx, f)
if !ok {
return data, f, false
}
f.Group, f.Members = m.Group, groupkeys.MembersOf(m)
f.Group, f.Members = m.ID, groupkeys.MembersOf(m)
out, err := forgemsg.EncodeForge(f)
if err != nil {
g.logger.Printf("%s#%d: its merged message cannot name the group %q, written without: %v", f.Repository, f.Number, m.Group, err)
g.logger.Printf("%s#%d: its merged message cannot name the group %q, written without: %v", f.Repository, f.Number, m.ID, err)
return data, f, false
}
return out, f, true
@@ -108,73 +88,55 @@ func (g *grouper) member(ctx context.Context, data []byte) ([]byte, forgemsg.For
// published keeps that a member's merged message is written, and says the group's end when it was the last.
func (g *grouper) published(ctx context.Context, f forgemsg.Forge) {
if err := g.done.Put(ctx, groupkeys.PublishedKey(f.Repository, f.Number, f.Merge), 1); err != nil {
g.logger.Printf("%s#%d: that its merged message is written is not kept, so the group's end waits for the next act: %v", f.Repository, f.Number, err)
if err := g.done.Put(ctx, groupkeys.PublishedKey(f.Repository, f.Number), f.Merge); err != nil {
g.logger.Printf("%s#%d: that its merged message is written is not kept, so the group's end waits for bridge_group_end_again: %v", f.Repository, f.Number, err)
return
}
key, err := meshdeliveryclient.MergesKey(f.Branch)
if err != nil {
return
if m, ok := g.groupOf(ctx, f); ok {
g.end(ctx, m, "")
}
value, rev, found, err := g.merges(ctx, key)
if err != nil || !found {
return
}
g.ended(ctx, key, rev, value)
}
// ended says the group's end (forge.group.merged) once every approved member of the gate's merge is merged and its
// merged message written, after the last of them; never on a refusal (ADR 0441 §3: a held group merges on from it).
// It is written once per group: `groups.<key>` in performed says it was. again says it is written again by hand
// end says the group's end (forge.group.merged) once every member's merged message is written, after the last of them;
// never on a refusal (ADR 0441 §3: a held group merges on from it), since a refused member's message is never written. It
// is written once per group: `groups.<group>` in performed says it was. again says it is written again by hand
// (bridge_group_end_again), with the time appended to the message id, so the duplicate window does not drop it.
func (g *grouper) ended(ctx context.Context, key string, rev uint64, value []byte) {
g.end(ctx, key, rev, value, "")
}
func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, again string) bool {
m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil || len(m.Approved) < 2 {
func (g *grouper) end(ctx context.Context, m seat.Group, again string) bool {
if len(m.Members) < 2 {
return false
}
key, err := groupkeys.Key(m.ID)
if err != nil {
g.logger.Printf("the group %q cannot be kept, so its end is not said: %v", m.ID, err)
return false
}
if again == "" {
if _, seen, err := g.done.Get(ctx, groupkeys.Key(key)); err == nil && seen {
if _, seen, err := g.done.Get(ctx, key); err == nil && seen {
return false
}
}
end := forgemsg.ForgeGroupEnd{Group: m.Group}
for _, member := range groupkeys.MembersOf(m) {
at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool {
return trunk.SameRepository(p.Repository, member.Repository) && int(p.Number) == member.Number
})
if at < 0 || m.Pulls[at].Commit == "" {
return false // not every member is merged (a merge commit is written only for a merged pull): the group stays gathered
}
if _, ok, err := g.done.Get(ctx, groupkeys.PublishedKey(member.Repository, member.Number, m.Pulls[at].Commit)); err != nil || !ok {
// The members in the group's order, which the gate merges them in (ADR 0441 §5), so the last commit names the end.
end := forgemsg.ForgeGroupEnd{Group: m.ID}
for _, member := range m.Members {
merge, ok, err := g.done.Get(ctx, groupkeys.PublishedKey(member.Repository, int(member.Number)))
if err != nil || !ok || merge == "" {
return false // its merged message is not written yet: said when it is (published)
}
end.Members = append(end.Members, forgemsg.GroupMerge{Repository: member.Repository, Number: member.Number, Merge: m.Pulls[at].Commit})
end.Members = append(end.Members, forgemsg.GroupMerge{Repository: member.Repository, Number: int(member.Number), Merge: merge})
}
// The members in the order the gate merged them, so the last commit names the end.
slices.SortStableFunc(end.Members, func(a, b forgemsg.GroupMerge) int {
return slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == a.Merge }) -
slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == b.Merge })
})
data, err := forgemsg.EncodeForgeGroupEnd(end)
if err != nil {
g.logger.Printf("the end of the group %q cannot be written: %v", m.Group, err)
g.logger.Printf("the end of the group %q cannot be written: %v", m.ID, err)
return false
}
id := groupkeys.EndID(m.Group, end.Members[len(end.Members)-1].Merge, "")
if again != "" {
id += "." + again
}
if err := g.send(ctx, forgeSubject(forgemsg.ForgeGroupEndEvent), data, id); err != nil {
g.logger.Printf("the end of the group %q is not written on FORGE, written at the gate's next write or by bridge_group_end_again: %v", m.Group, err)
last := end.Members[len(end.Members)-1].Merge
if err := g.send(ctx, forgeSubject(forgemsg.ForgeGroupEndEvent), data, groupkeys.EndID(m.ID, last, again)); err != nil {
g.logger.Printf("the end of the group %q is not written on FORGE, written by bridge_group_end_again: %v", m.ID, err)
return false
}
if err := g.done.Put(ctx, groupkeys.Key(key), rev); err != nil {
g.logger.Printf("the end of the group %q is written and not kept: the stream's duplicate window drops a second one: %v", m.Group, err)
if err := g.done.Put(ctx, key, last); err != nil {
g.logger.Printf("the end of the group %q is written and not kept: the stream's duplicate window drops a second one: %v", m.ID, err)
}
g.logger.Printf("the end of the group %q is written: %d members merged, the last %s", m.Group, len(end.Members), end.Members[len(end.Members)-1].Merge)
g.logger.Printf("the end of the group %q is written: %d members merged, the last %s", m.ID, len(end.Members), last)
return true
}
@@ -3,12 +3,12 @@ package main
import (
"bytes"
"context"
"errors"
"log"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
@@ -26,32 +26,20 @@ func (p *idPub) send(_ context.Context, subject string, data []byte, id string)
return nil
}
func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T17, T19 (ADR 0633 §4)
// 1585/T16, T17, T19 (ADR 0633 §4), acceptance 49: a member's merged message names its group from the seat
// mesh-delivery's verb groups, the group's end is said once after the last member's merged message, and said again by hand.
func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) {
ctx := context.Background()
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
merge := func(states ...seat.MergesByGroupValuePullsItem) []byte {
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Pulls: states,
Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}})
if err != nil {
t.Fatal(err)
}
return v
}
sdk := func(state meshdeliveryclient.MergeState, commit, words string) seat.MergesByGroupValuePullsItem {
return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 37, Head: h1, State: string(state), Commit: commit, Words: words}
}
dlv := func(state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(state), Commit: commit}
}
gate := map[string][]byte{}
group := seat.Group{ID: "feat/take-in", State: "merging", Members: []seat.Member{
{Repository: "novox/mesh-sdk", Number: 37, Head: h1, State: "merged"}, {Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: "merging"}}}
var of []seat.Group
var read error
var logs bytes.Buffer
pub, done := &idPub{}, memRevisions{}
pub, done := &idPub{}, memStrs{}
g := &grouper{send: pub.send, done: done, logger: log.New(&logs, "", 0),
merges: func(_ context.Context, key string) ([]byte, uint64, bool, error) {
v, ok := gate[key]
return v, 7, ok, nil
}}
groups: func(context.Context) ([]seat.Group, error) { return of, read }}
mergedOf := func(repo string, number int, head, commit string) []byte {
data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/take-in", Number: number, Head: head, Merge: commit, Root: "r"})
if err != nil {
@@ -59,79 +47,64 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
}
return data
}
// A lone pull request's merged message carries no group.
if _, f, ok := g.member(ctx, mergedOf("novox/hq", 4, h1, c1)); ok || f.Group != "" {
// A lone pull request's merged message carries no group, and neither does one whose groups cannot be read.
of = []seat.Group{group, {ID: "solo", Members: []seat.Member{{Repository: "novox/hq", Number: 4, Head: h1}}}}
lone, _ := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/hq", Branch: "solo", Number: 4, Head: h1, Merge: c1, Root: "r"})
if _, f, ok := g.member(ctx, lone); ok || f.Group != "" {
t.Errorf("a lone pull request names the group %q", f.Group)
}
// The first member: its message names the group and both members; the group is not ended.
gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerging, ""))
read = errors.New("the seat did not answer")
if _, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1)); ok || f.Group != "" || !strings.Contains(logs.String(), "the seat did not answer") {
t.Errorf("a group read that failed names the group %q: %s", f.Group, logs.String())
}
read = nil
// The first member: its message names the group and both members in the group's order; the group is not ended.
data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1))
if back, err := forgemsg.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 {
if back, err := forgemsg.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 || back.Members[0].Number != 37 {
t.Fatalf("a member's message: %+v %v", back, err)
}
g.published(ctx, f)
// A refusal of the other member is no end (ADR 0441 §3: the group merges on from it later).
gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(meshdeliveryclient.MergeRefused), Words: "405"})
g.ended(ctx, "feat/take-in", 8, gate["feat/take-in"])
// Merged on at the gate before its merged message is written: still no end; then its message ends the group.
gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerged, c2))
g.ended(ctx, "feat/take-in", 9, gate["feat/take-in"])
if len(pub.subjects) != 0 {
t.Fatalf("an end before every member's merged message is written: %v", pub.subjects)
if len(pub.subjects) != 0 || done[groupkeys.PublishedKey("novox/mesh-sdk", 37)] != c1 {
t.Fatalf("after the first member: ends %v, performed %v", pub.subjects, done)
}
// The last member's merged message ends the group; a second merged message of it says no second end.
_, f, _ = g.member(ctx, mergedOf("novox/mesh-delivery", 9, h2, c2))
g.published(ctx, f)
g.ended(ctx, "feat/take-in", 10, gate["feat/take-in"]) // a later write of the gate says no second end
g.published(ctx, f)
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.group.merged" || pub.ids[0] != "group-end.feat/take-in."+c2 {
t.Fatalf("ends %v ids %v, want one forge.group.merged with id group-end.feat/take-in.%s", pub.subjects, pub.ids, c2)
}
end, err := forgemsg.DecodeForgeGroupEnd(pub.data[0])
if err != nil || len(end.Members) != 2 || end.Members[0].Merge != c1 || end.Members[1].Merge != c2 {
t.Errorf("the end = %+v %v, want both members in their merge order", end, err)
t.Errorf("the end = %+v %v, want both members in the group's order", end, err)
}
if rev, ok := done[groupkeys.Key("feat/take-in")]; !ok || rev != 7 {
t.Errorf("performed %v: want groups.feat/take-in kept", done)
}
// Again by hand: written once more, its id not the first's.
if !g.end(ctx, "feat/take-in", 10, gate["feat/take-in"], "20261104T2300") || pub.ids[1] == pub.ids[0] {
t.Errorf("the end again by hand: ids %v", pub.ids)
}
// 1585/T19: an ask to say a group's end again is answered by the bridge from the gate's merges, with the ask's time on
// the message id; not written while a member is not merged, with the reason; not for a group the gate does not hold.
pull := func(repo string, n int, head string, state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
return seat.MergesByGroupValuePullsItem{Repository: repo, Number: int64(n), Head: head, State: string(state), Commit: commit}
}
askMerge := func(second meshdeliveryclient.MergeState, commit2 string) []byte {
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2},
Pulls: []seat.MergesByGroupValuePullsItem{pull("novox/mesh-sdk", 37, h1, meshdeliveryclient.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}})
if err != nil {
t.Fatal(err)
}
return v
if key, _ := groupkeys.Key("feat/take-in"); done[key] != c2 {
t.Errorf("performed %v: want groups.feat~take-in kept with the last merge", done)
}
// 1585/T19: an ask to say a group's end again is answered from the seat's groups, with the ask's time on the message id;
// not written while a member's merged message is not written, with the reason; not for a group the seat does not hold,
// nor when the groups cannot be read.
ask := groupkeys.EndAsk{V: 1, Group: "feat/take-in", By: "operator@shanks", Why: "the indexer never took it in", At: time.Date(2026, 11, 4, 23, 0, 0, 123, time.UTC)}
for _, tc := range []struct {
name string
gate map[string][]byte
of []seat.Group
read error
published bool
written bool
answer string
}{
{"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, true, true, "written again"},
{"a member's message not written", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, false, false, "not written"},
{"a member not merged", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerging, "")}, true, false, "not written"},
{"no merge at the gate", map[string][]byte{}, true, false, "holds no merge"},
{"every message written: written again", []seat.Group{group}, nil, true, true, "written again"},
{"a member's message not written", []seat.Group{group}, nil, false, false, "not written"},
{"no such group at the seat", []seat.Group{{ID: "other", Members: group.Members}}, nil, true, false, "holds no group"},
{"the groups cannot be read", nil, errors.New("no answer"), true, false, "cannot be read"},
} {
pub, done := &idPub{}, memRevisions{}
pub, done := &idPub{}, memStrs{}
if tc.published {
done[groupkeys.PublishedKey("novox/mesh-sdk", 37, c1)], done[groupkeys.PublishedKey("novox/mesh-delivery", 9, c2)] = 1, 1
done[groupkeys.PublishedKey("novox/mesh-sdk", 37)], done[groupkeys.PublishedKey("novox/mesh-delivery", 9)] = c1, c2
}
g := &grouper{send: pub.send, done: done, logger: log.New(&bytes.Buffer{}, "", 0),
merges: func(_ context.Context, key string) ([]byte, uint64, bool, error) {
v, ok := tc.gate[key]
return v, 4, ok, nil
}}
got := g.answerEnd(ctx, "feat/take-in", ask, ask.At.Add(time.Second))
groups: func(context.Context) ([]seat.Group, error) { return tc.of, tc.read }}
got := g.answerEnd(ctx, ask, ask.At.Add(time.Second))
if got.Written != tc.written || !strings.Contains(got.Answer, tc.answer) || !got.Done() || len(pub.ids) != map[bool]int{true: 1}[tc.written] {
t.Errorf("%s: %+v, ids %v", tc.name, got, pub.ids)
}
@@ -7,6 +7,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
@@ -20,28 +21,27 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
git "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-forge-bridge/internal/gitfixture"
groupkeys "mesh-delivery-forge-bridge/internal/groups"
)
// busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream,
// the controller's user, and the bridge's own principal <node>.<module> with the permissions the controller composes for
// it, and no others, so a permission the bridge lacks fails a test here and not on the mesh (issue 1279).
// busConf is the shape of the composed accounts file (the grants part's buscompose): one account with JetStream, the
// controller's user, and the bridge's own principal <node>.<module> with the permissions composed for its manifest, and
// no others, so a permission the bridge lacks fails a test here and not on the mesh (issue 1279).
//
// The gate's buckets mesh-delivery-gate_merges and mesh-delivery-gate_owed are read and watched (the manifest's reads), the
// bucket performed is the bridge's own (its state), and the five forge tools of the gate's acts are invoked (issue 1341, 1342).
//
// The bridge neither reads the checker's owed nor hears its stream (ADR 0521, R4e finding R2-1): the forge module writes
// mesh/check from it. The stream checks is made here so a test can say the checker wrote a status.
//
// The lists are what mesh-controller's broker.PermissionsFor answers (internal/broker/nats.go:577) for a module principal
// with the manifest of module.json: consumes the three gitea.pull events, invokes gitea.gitea_open_pulls,
// gitea.gitea_commit_trees and gitea.gitea_get_file, state heads, performed, inputs and end-asks (ADR 0633 §5's hand end), and the stream forge; `**` is written `>` on the wire (modstreams.go
// WirePattern). The consumer's durable is <node>_<module> on EVENTS (nats.go consumerDurable). The controller's own
// user keeps all rights, as it has.
// From module.json: the seat git's events heard through its durable on EVENTS; what the gate and the checker owe heard
// through its durable on each seat's own stream owed (SEAT_<seat>_owed); the seat git's verbs, the seat mesh-delivery's
// groups and the seat mesh-hand-acts' hand-act called on mesh.seat.<seat>.call.<verb>.<token> (or its plain fallback);
// its own states heads, performed, inputs, end-asks and owed-asks; its stream forge. It reads no state of the gate's or
// the checker's (design 47, acceptance 49 and 53). `**` is written `>` on the wire. The controller's own user keeps all
// rights, as it has.
const busConf = `
listen: 127.0.0.1:-1
jetstream { store_dir: %q }
@@ -53,38 +53,23 @@ accounts {
{ user: "n1.mesh-delivery-forge-bridge", password: "pw"
permissions {
publish { allow: [
"$JS.ACK.EVENTS.n1_mesh-delivery-forge-bridge.>"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_heads.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_heads.>"
"$JS.API.CONSUMER.INFO.EVENTS.n1_mesh-delivery-forge-bridge"
"$JS.API.CONSUMER.MSG.NEXT.EVENTS.n1_mesh-delivery-forge-bridge"
"$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_heads.>"
"$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_heads"
"$JS.FC.KV_mesh-delivery-forge-bridge_heads.>"
"$KV.mesh-delivery-forge-bridge_heads.>"
"mesh.mod.gitea.call.gitea_commit_trees.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_commit_trees.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_open_pulls.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_open_pulls.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_commit_trees"
"mesh.mod.gitea.tool.gitea_commit_trees.*"
"mesh.mod.gitea.call.gitea_get_file.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_get_file.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_get_file"
"mesh.mod.gitea.tool.gitea_get_file.*"
"mesh.mod.gitea.tool.gitea_open_pulls"
"mesh.mod.gitea.tool.gitea_open_pulls.*"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_performed.>"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_inputs.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_performed.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_inputs.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_performed.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_inputs.>"
"$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_performed"
"$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_inputs"
"$JS.FC.KV_mesh-delivery-forge-bridge_performed.>"
"$JS.FC.KV_mesh-delivery-forge-bridge_inputs.>"
"$KV.mesh-delivery-forge-bridge_performed.>"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_inputs.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_inputs.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_inputs.>"
"$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_inputs"
"$JS.FC.KV_mesh-delivery-forge-bridge_inputs.>"
"$KV.mesh-delivery-forge-bridge_inputs.>"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_end-asks.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_end-asks.>"
@@ -92,45 +77,51 @@ accounts {
"$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_end-asks"
"$JS.FC.KV_mesh-delivery-forge-bridge_end-asks.>"
"$KV.mesh-delivery-forge-bridge_end-asks.>"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-gate_merges.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-gate_merges.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-gate_merges.>"
"$JS.API.STREAM.INFO.KV_mesh-delivery-gate_merges"
"$JS.FC.KV_mesh-delivery-gate_merges.>"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-gate_owed.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-gate_owed.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-gate_owed.>"
"$JS.API.STREAM.INFO.KV_mesh-delivery-gate_owed"
"$JS.FC.KV_mesh-delivery-gate_owed.>"
"mesh.mod.gitea.call.gitea_merge_pull_request.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_merge_pull_request.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_merge_pull_request"
"mesh.mod.gitea.tool.gitea_merge_pull_request.*"
"mesh.mod.gitea.call.gitea_commit_status.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_commit_status.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_commit_status"
"mesh.mod.gitea.tool.gitea_commit_status.*"
"mesh.mod.gitea.call.gitea_delivery_view.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_delivery_view.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_delivery_view"
"mesh.mod.gitea.tool.gitea_delivery_view.*"
"mesh.mod.gitea.call.gitea_note_append.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_note_append.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_note_append"
"mesh.mod.gitea.tool.gitea_note_append.*"
"mesh.mod.gitea.call.gitea_add_comment.*.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.call.gitea_add_comment.n1~mesh-delivery-forge-bridge"
"mesh.mod.gitea.tool.gitea_add_comment"
"mesh.mod.gitea.tool.gitea_add_comment.*"
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_owed-asks.>"
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_owed-asks.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_owed-asks.>"
"$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_owed-asks"
"$JS.FC.KV_mesh-delivery-forge-bridge_owed-asks.>"
"$KV.mesh-delivery-forge-bridge_owed-asks.>"
"$JS.ACK.EVENTS.n1_mesh-delivery-forge-bridge.>"
"$JS.API.CONSUMER.INFO.EVENTS.n1_mesh-delivery-forge-bridge"
"$JS.API.CONSUMER.MSG.NEXT.EVENTS.n1_mesh-delivery-forge-bridge"
"$JS.ACK.SEAT_mesh-delivery_owed.n1_mesh-delivery-forge-bridge.>"
"$JS.API.CONSUMER.INFO.SEAT_mesh-delivery_owed.n1_mesh-delivery-forge-bridge"
"$JS.API.CONSUMER.MSG.NEXT.SEAT_mesh-delivery_owed.n1_mesh-delivery-forge-bridge"
"$JS.ACK.SEAT_mesh-delivery-checker_owed.n1_mesh-delivery-forge-bridge.>"
"$JS.API.CONSUMER.INFO.SEAT_mesh-delivery-checker_owed.n1_mesh-delivery-forge-bridge"
"$JS.API.CONSUMER.MSG.NEXT.SEAT_mesh-delivery-checker_owed.n1_mesh-delivery-forge-bridge"
"mesh.seat.git.call.status.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.status"
"mesh.seat.git.call.comment.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.comment"
"mesh.seat.git.call.view.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.view"
"mesh.seat.git.call.note.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.note"
"mesh.seat.git.call.merge-pull.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.merge-pull"
"mesh.seat.git.call.open-pulls.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.open-pulls"
"mesh.seat.git.call.trees.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.trees"
"mesh.seat.git.call.file.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.file"
"mesh.seat.git.call.pull.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.pull"
"mesh.seat.git.call.tags.n1~mesh-delivery-forge-bridge"
"mesh.seat.git.tool.tags"
"mesh.seat.mesh-delivery.call.groups.n1~mesh-delivery-forge-bridge"
"mesh.seat.mesh-delivery.tool.groups"
"mesh.seat.mesh-hand-acts.call.hand-act.n1~mesh-delivery-forge-bridge"
"mesh.seat.mesh-hand-acts.tool.hand-act"
"mesh.stream.mesh-delivery-forge-bridge.event.forge.>"
"mesh.stream.mesh-delivery-forge-bridge.event.owed.ended"
] }
subscribe { allow: [
"_INBOX.n1.mesh-delivery-forge-bridge.>"
"mesh.assignment.n1.mesh-delivery-forge-bridge"
"mesh.mod.gitea.event.forge.delete"
"mesh.mod.gitea.event.pull.closed"
"mesh.mod.gitea.event.pull.merged"
"mesh.mod.gitea.event.pull.updated"
"mesh.mod.mesh-delivery-forge-bridge.call.>"
"mesh.mod.mesh-delivery-forge-bridge.tool.>"
] }
@@ -186,26 +177,32 @@ type bed struct {
ctx context.Context
done <-chan error
reply func(string)
// files sets what the stand-in forge answers to gitea_get_file from then on; it is asked with the tool's arguments.
files func(func(owner, repo, path, ref string) (string, bool))
// files sets what the stand-in seat git answers to file from then on; it is asked with the verb's input.
files func(func(repository, path, ref string) (string, bool))
// answer sets the stand-in's answer to one verb of a seat (`<seat>.<verb>`) from then on.
answer func(verb string, answer string)
// calls are the verbs the stand-in was called with, in order, as `<seat>.<verb> <input>`.
calls func() []string
// restart stops the bridge, runs between, and starts it again on the same bus.
restart func(between func())
}
// getFileReply is the reply envelope for gitea_get_file as mesh-catalog modules/gitea tools/index.ts and client.ts getFile
// answer it ({file: {path, ref, sha, size, content}}), and for a path the forge does not have the error client.ts request
// writes: `Gitea API <path>: 404 <body>`.
func getFileReply(path, ref, content string, found bool) []byte {
// answerOf is a verb's answer in the reply envelope of mesh-sdk go/process/call.go ({result, error, node}).
func answerOf(answer string) []byte { return []byte(`{"result":` + answer + `,"node":"n0"}`) }
// fileAnswer is the seat git's answer to file: the content, or absent.
func fileAnswer(path, ref, content string, found bool) string {
if !found {
b, _ := json.Marshal(map[string]any{"error": "Gitea API /repos/novox/mesh-delivery/contents/" + path + "?ref=" + ref + `: 404 {"message":"GetContentsOrList","url":"https://forge.example/api/swagger"}`})
return b
return `{"absent":true}`
}
b, _ := json.Marshal(map[string]any{"result": map[string]any{"file": map[string]any{"path": path, "ref": ref, "sha": "0123456789abcdef0123456789abcdef01234567",
"size": len(content), "content": content}}, "node": "n0"})
return b
b, _ := json.Marshal(git.FileAnswer{Path: path, Ref: ref, Content: content, Size: int64(len(content))})
return string(b)
}
// startBed is startBridge with a step between the streams and the consumer: prepare writes what the streams hold before
// the bridge's consumer is made, as the controller makes it, from now (ADR 0443 §1), and reply sets the answer of the
// forge's gitea_open_pulls from then on.
// startBed is startBridge with a step between the streams and the consumers: prepare writes what the streams hold before
// the bridge's consumers are made, as the grants part makes them, from now (ADR 0443 §1), and reply sets the answer of the
// seat git's open-pulls from then on. A stand-in holds the seats the bridge calls: git (every verb, trees r0 by default,
// a module.json of {} at every path), mesh-delivery's groups (none by default) and mesh-hand-acts' hand-act.
func startBed(t *testing.T, openReply string, prepare func(ctx context.Context, js jetstream.JetStream)) *bed {
srv := startBus(t)
ctl, err := nats.Connect(srv.ClientURL(), nats.UserInfo("ctl", "pw"))
@@ -219,70 +216,74 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context,
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
t.Cleanup(cancel)
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>"}}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "FORGE", Subjects: []string{"mesh.stream.mesh-delivery-forge-bridge.event.forge.>"}}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "FORGE", Subjects: []string{"mesh.stream.mesh-delivery-forge-bridge.event.>"}}); err != nil {
t.Fatal(err)
}
// The checker's stream checks, which the bridge no longer hears (ADR 0521): a test says on it that the checker wrote a status.
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "CHECKS", Subjects: []string{"mesh.stream.mesh-delivery-checker.event.>"}}); err != nil {
t.Fatal(err)
// Each seat's own stream owed, as its contract keeps it (Stream<E>, SEAT_<seat>_owed), as the grants part makes it: no
// subjects of its own, sourced from EVENTS filtered by the event's subject.
for name, subjects := range map[string]string{seat.StreamOwed: seat.SubjectOwedAll, checker.StreamOwed: checker.SubjectOwedAll} {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: name, MaxAge: 7 * 24 * time.Hour,
Sources: []*jetstream.StreamSource{{Name: "EVENTS", FilterSubject: subjects}}}); err != nil {
t.Fatal(err)
}
}
// The bridge's state bucket, made by the controller from the manifest's state entry before the process starts.
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: headsBucket, History: 1}); err != nil {
t.Fatal(err)
}
// Its other state bucket, and the gate's buckets the bridge reads, made by the controller from the gate's manifest
// (the gate's merges and owed with their histories).
for name, history := range map[string]uint8{performedBucket: 1, inputsBucket: 1, mergesBucket: 10, owedBucket: 5} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil {
// The bridge's states, made from the manifest's state entries and its requires before the process starts.
for _, name := range []string{headsBucket, performedBucket, inputsBucket, groupkeys.EndAsksBucket, groupkeys.OwedAsksBucket} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: 1}); err != nil {
t.Fatal(err)
}
}
if prepare != nil {
prepare(ctx, js)
}
acct := process.Account{Node: "n1", Module: "mesh-delivery-forge-bridge"}
if _, err := js.CreateOrUpdateConsumer(ctx, "EVENTS", jetstream.ConsumerConfig{
Durable: "n1_mesh-delivery-forge-bridge", AckPolicy: jetstream.AckExplicitPolicy, DeliverPolicy: jetstream.DeliverNewPolicy,
FilterSubjects: []string{"mesh.mod.gitea.event.pull.updated", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.closed", "mesh.mod.gitea.event.forge.delete"},
Durable: acct.EventConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, DeliverPolicy: jetstream.DeliverNewPolicy,
FilterSubjects: []string{git.SubjectPullUpdated, git.SubjectPullMerged, git.SubjectPullClosed, git.SubjectForgeDelete},
}); err != nil {
t.Fatal(err)
}
for name, subjects := range map[string]string{seat.StreamOwed: seat.SubjectOwedAll, checker.StreamOwed: checker.SubjectOwedAll} {
if _, err := js.CreateOrUpdateConsumer(ctx, name, acct.SeatEventConsumerConfig([]string{subjects}, false)); err != nil {
t.Fatal(err)
}
}
// A stand-in answers the forge's gitea_open_pulls, which the bridge asks at its start and after a reconnect, on its tool
// subject (mesh-control internal/broker/nats.go invokedSubjects) in the reply envelope of mesh-sdk go/process/call.go.
// The stand-in seats, answering on the caller-named subject and its plain fallback.
var mu sync.Mutex
current := openReply
open, err := ctl.Subscribe("mesh.mod.gitea.tool.gitea_open_pulls", func(m *nats.Msg) {
mu.Lock()
defer mu.Unlock()
_ = m.Respond([]byte(current))
})
if err != nil {
t.Fatal(err)
answers := map[string]string{
"git.open-pulls": openReply, "git.trees": `{"root":"r0","trees":{"":"t0","modules/a":"t1"}}`,
"mesh-delivery.groups": `{"groups":[]}`, "mesh-hand-acts.hand-act": `{"recorded":true}`,
}
t.Cleanup(func() { _ = open.Unsubscribe() })
if err := ctl.Flush(); err != nil {
t.Fatal(err)
for _, verb := range []string{"status", "comment", "view", "note", "merge-pull"} {
answers["git."+verb] = string(gitfixture.Given(t, verb+" answers", "verb:"+verb+".answer"))
}
// A stand-in answers the forge's gitea_get_file: by default a module.json of {} at every path and no other file.
var fmu sync.Mutex
filesOf := func(_, _, path, _ string) (string, bool) { return "{}", strings.HasSuffix(path, "module.json") }
getFile, err := ctl.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) {
var a struct{ Owner, Repo, Path, Ref string }
_ = json.Unmarshal(m.Data, &a)
fmu.Lock()
f := filesOf
fmu.Unlock()
content, found := f(a.Owner, a.Repo, a.Path, a.Ref)
_ = m.Respond(getFileReply(a.Path, a.Ref, content, found))
})
if err != nil {
t.Fatal(err)
filesOf := func(_, path, _ string) (string, bool) { return "{}", strings.HasSuffix(path, "module.json") }
var called []string
for _, subject := range []string{"mesh.seat.*.call.*.*", "mesh.seat.*.tool.*"} {
sub, err := ctl.Subscribe(subject, func(m *nats.Msg) {
parts := strings.Split(m.Subject, ".")
verb := parts[2] + "." + parts[4]
mu.Lock()
called = append(called, verb+" "+string(m.Data))
answer, files := answers[verb], filesOf
mu.Unlock()
if verb == "git.file" {
var in git.FileInput
_ = json.Unmarshal(m.Data, &in)
content, found := files(in.Repository, in.Path, in.Ref)
answer = fileAnswer(in.Path, in.Ref, content, found)
}
_ = m.Respond(answerOf(answer))
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
}
t.Cleanup(func() { _ = getFile.Unsubscribe() })
if err := ctl.Flush(); err != nil {
t.Fatal(err)
}
@@ -293,24 +294,37 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context,
t.Fatal(err)
}
env := map[string]string{"MESH_BROKER_FILE": broker}
runCtx, stop := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
t.Cleanup(stop)
return &bed{srv: srv, ctl: ctl, js: js, ctx: ctx, done: done, reply: func(s string) { mu.Lock(); current = s; mu.Unlock() },
files: func(f func(owner, repo, path, ref string) (string, bool)) { fmu.Lock(); filesOf = f; fmu.Unlock() }}
var stop context.CancelFunc
start := func() {
var runCtx context.Context
runCtx, stop = context.WithCancel(ctx)
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
}
start()
t.Cleanup(func() { stop() })
set := func(verb, answer string) { mu.Lock(); answers[verb] = answer; mu.Unlock() }
return &bed{srv: srv, ctl: ctl, js: js, ctx: ctx, done: done, reply: func(s string) { set("git.open-pulls", s) },
files: func(f func(repository, path, ref string) (string, bool)) { mu.Lock(); filesOf = f; mu.Unlock() },
answer: set,
calls: func() []string { mu.Lock(); defer mu.Unlock(); return append([]string(nil), called...) },
restart: func(between func()) {
stop()
<-done
between()
start()
}}
}
// TestBridgeWritesPullMessages starts the program the way the node does (its entry, run, with MESH_BROKER_FILE).
func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
ctl, js, ctx, _ := startBridge(t, `{"result":{"pulls":[]}}`)
ctl, js, ctx, _ := startBridge(t, `{"pulls":[]}`)
defer branchDeletedCase(t, ctl.ConnectedUrl(), js, ctx)
// gitea.pull.updated as the forge module publishes it: the body of trunk.ts:233-250 on mesh.mod.gitea.event.pull.updated.
ev, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "t", "body": "b", "base": "main",
"head": "feat/x", "head_sha": head, "clone_url": "u", "html_url": "h", "paths": []string{"modules/a/x.go"}, "paths_truncated": false,
"removed": []string{}, "module_dirs": []string{"modules/a"}, "module_dirs_said": true})
if _, err := js.Publish(ctx, "mesh.mod.gitea.event.pull.updated", ev); err != nil {
// pull.updated as the seat git says it (its contract's PullUpdatedEvent).
ev, _ := json.Marshal(git.PullUpdatedEvent{Repository: "novox/mesh-delivery", Number: 12, Title: "t", Base: "main", Branch: "feat/x", Head: head,
Change: &git.Change{Paths: []string{"modules/a/x.go"}, ModuleDirs: []string{"modules/a"}}})
if _, err := js.Publish(ctx, git.SubjectPullUpdated, ev); err != nil {
t.Fatal(err)
}
@@ -357,13 +371,13 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
t.Errorf("the event is not acknowledged: %+v", ci)
}
// branchDeletedCase is DLV-FB1's case of the bridge's whole-module test (design 51): the forge module's forge.delete of
// a branch is a branch-deleted message on FORGE, and a forge.delete from another account than the forge module's is
// refused by the bus.
// branchDeletedCase is DLV-FB1's case of the bridge's whole-module test (design 51): the seat git's forge.delete of a
// branch is a branch-deleted message on FORGE, and a forge.delete from another account than the seat's holder is refused
// by the bus.
func branchDeletedCase(t *testing.T, srv string, js jetstream.JetStream, ctx context.Context) {
t.Helper()
ev, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-sdk", "ref": "feat/x", "ref_type": "branch", "sha": head})
if _, err := js.Publish(ctx, "mesh.mod.gitea.event.forge.delete", ev); err != nil {
ev, _ := json.Marshal(git.ForgeDeleteEvent{Repository: "novox/mesh-sdk", Ref: "feat/x", RefType: "branch"})
if _, err := js.Publish(ctx, git.SubjectForgeDelete, ev); err != nil {
t.Fatal(err)
}
forge, err := js.Stream(ctx, "FORGE")
@@ -388,21 +402,26 @@ func branchDeletedCase(t *testing.T, srv string, js jetstream.JetStream, ctx con
ojs, _ := jetstream.New(other)
octx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
if _, err := ojs.Publish(octx, "mesh.mod.gitea.event.forge.delete", ev); err == nil {
t.Error("a forge.delete from the bridge's account was taken by the bus: only the forge module's account may say it")
if _, err := ojs.Publish(octx, git.SubjectForgeDelete, ev); err == nil {
t.Error("a forge.delete from the bridge's account was taken by the bus: only the seat git's holder may say it")
}
}
const merge = "89abcdef0123456789abcdef0123456789abcdef"
// TestBridgeWritesMergedMessage has a stand-in answer the forge's gitea_commit_trees on its tool subject
// (mesh-control internal/broker/nats.go invokedSubjects: mesh.mod.<module>.tool.<tool>), in the reply envelope of
// mesh-sdk go/process/call.go ({result, error, node}).
// openPull is one entry of the seat git's open-pulls answer.
func openPull(repo string, number int, branch, head string) string {
b, _ := json.Marshal(git.OpenPull{Pull: git.Pull{Repository: "novox/" + repo, Number: int64(number), Base: "main", Branch: branch, Head: head, State: "open"},
Checks: map[string]git.Status{}, Required: []string{}})
return string(b)
}
// TestBridgeWritesMergedMessage: the stand-in seat git answers trees and file on the caller-named subjects.
func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
bed := startBed(t, `{"result":{"pulls":[]}}`, nil)
ctl, js, ctx := bed.ctl, bed.js, bed.ctx
bed := startBed(t, `{"pulls":[]}`, nil)
js, ctx := bed.js, bed.ctx
// The stand-in forge's files are two real files of this repository (this module's module.json and go.sum, read from the
// working tree), in gitea_get_file's real answer shape: the root has a manifest only, modules/a a manifest and go.sum.
// working tree), in the seat git's file answer: the root has a manifest only, modules/a a manifest and go.sum.
manifest, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
@@ -411,7 +430,7 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
if err != nil {
t.Fatal(err)
}
bed.files(func(_, _, path, ref string) (string, bool) {
bed.files(func(_, path, ref string) (string, bool) {
if ref != merge {
t.Errorf("the forge was asked for %s at %s, want the merge commit %s", path, ref, merge)
}
@@ -423,25 +442,10 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
}
return "", false
})
var asked chan []byte = make(chan []byte, 4)
sub, err := ctl.Subscribe("mesh.mod.gitea.tool.gitea_commit_trees", func(m *nats.Msg) {
asked <- m.Data
_ = m.Respond([]byte(`{"result":{"root":"r0","trees":{"":"t0","modules/a":"t1"}},"node":"n0"}`))
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
if err := ctl.Flush(); err != nil {
t.Fatal(err)
}
// gitea.pull.merged as mesh-catalog modules/gitea/index.ts:186-207 publishes it.
ev, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "t", "body": "b",
"head_sha": head, "head": "feat/x", "base": "main", "merge_commit_sha": merge, "merged_at": "2026-10-19T12:00:00Z",
"clone_url": "u", "html_url": "h", "paths": []string{"modules/a/x.go"}, "paths_truncated": false, "removed": []string{},
"module_dirs": []string{"modules/a"}, "module_dirs_said": true})
if _, err := js.Publish(ctx, "mesh.mod.gitea.event.pull.merged", ev); err != nil {
// pull.merged as the seat git says it (its contract's PullMergedEvent).
ev, _ := json.Marshal(git.PullMergedEvent{Repository: "novox/mesh-delivery", Number: 12, Title: "t", Head: head, Branch: "feat/x", Base: "main",
MergeCommit: merge, Change: &git.Change{Paths: []string{"modules/a/x.go"}, ModuleDirs: []string{"modules/a"}}})
if _, err := js.Publish(ctx, git.SubjectPullMerged, ev); err != nil {
t.Fatal(err)
}
@@ -475,16 +479,15 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
}
select {
case body := <-asked:
if string(body) != `{"owner":"novox","repo":"mesh-delivery","sha":"`+merge+`"}` {
t.Errorf("the tool was asked %s", body)
var trees []string
for _, c := range bed.calls() {
if strings.HasPrefix(c, "git.trees ") {
trees = append(trees, c)
}
default:
t.Error("the stand-in was not asked")
}
if len(asked) != 0 {
t.Error("the tool was asked more than once")
in, _ := json.Marshal(git.TreesInput{Repository: "novox/mesh-delivery", Commit: merge})
if len(trees) != 1 || trees[0] != "git.trees "+string(in) {
t.Errorf("the seat git was asked for trees %v, want once at the merge commit", trees)
}
cons, err := js.Consumer(ctx, "EVENTS", "n1_mesh-delivery-forge-bridge")
if err != nil {
@@ -503,10 +506,10 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
// TestBridgeSaysTheOpenPullsAtStart: the forge holds two open pull requests when the bridge starts; one message each is on
// FORGE, and the open message naming both after them (ADR 0443 §5).
func TestBridgeSaysTheOpenPullsAtStart(t *testing.T) { // 1237/AC1
// The entries are gitea_open_pulls's (mesh-catalog modules/gitea/client.ts:369-381, head the branch).
_, js, ctx, _ := startBridge(t, `{"result":{"pulls":[
{"owner":"novox","repo":"mesh-delivery","number":12,"base":"main","head":"feat/x","head_sha":"`+head+`","html_url":"h","required":[],"statuses":[],"checks":{},"labels":[]},
{"owner":"novox","repo":"mesh-catalog","number":300,"base":"main","head":"feat/x","head_sha":"`+strings.Repeat("a", 40)+`","html_url":"h","required":[],"statuses":[],"checks":{},"labels":[]}]},"node":"n0"}`)
// The entries are the seat git's open-pulls answer (its contract's OpenPull).
_, js, ctx, _ := startBridge(t, `{"pulls":[
`+openPull("mesh-delivery", 12, "feat/x", head)+`,
`+openPull("mesh-catalog", 300, "feat/x", strings.Repeat("a", 40))+`]}`)
forge, err := js.Stream(ctx, "FORGE")
if err != nil {
t.Fatal(err)
@@ -533,10 +536,10 @@ func TestBridgeSaysTheOpenPullsAtStart(t *testing.T) { // 1237/AC1
// TestBridgeExitsWhenTheOpenPullsCannotBeRead: the process manager starts it again.
func TestBridgeExitsWhenTheOpenPullsCannotBeRead(t *testing.T) { // 1237/AC3
_, _, _, done := startBridge(t, `{"error":"the forge said 502"}`)
_, _, _, done := startBridge(t, `{"error":"the forge said 502","error-kind":"failed","pulls":[]}`)
select {
case err := <-done:
if err == nil || !strings.Contains(err.Error(), "gitea.gitea_open_pulls") || !strings.Contains(err.Error(), "the forge said 502") {
if err == nil || !strings.Contains(err.Error(), "open-pulls") || !strings.Contains(err.Error(), "the forge said 502") {
t.Errorf("run returned %v", err)
}
case <-time.After(10 * time.Second):
@@ -555,14 +558,12 @@ func TestBridgeReadsTheForgeAtStartAndAfterAReconnectAndHearsNoOlderEvent(t *tes
return "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox." + repo + ".pull"
}
openSubject := "mesh.stream.mesh-delivery-forge-bridge.event.forge.open"
b := startBed(t, `{"result":{"pulls":[
{"owner":"novox","repo":"mesh-delivery","number":12,"base":"main","head":"feat/x","head_sha":"`+head+`","html_url":"h","required":[],"statuses":[],"checks":{},"labels":[]},
{"owner":"novox","repo":"mesh-catalog","number":300,"base":"main","head":"feat/x","head_sha":"`+h2+`","html_url":"h","required":[],"statuses":[],"checks":{},"labels":[]}]},"node":"n0"}`,
b := startBed(t, `{"pulls":[`+openPull("mesh-delivery", 12, "feat/x", head)+`,`+openPull("mesh-catalog", 300, "feat/x", h2)+`]}`,
func(ctx context.Context, js jetstream.JetStream) {
// Older events: a pull request long closed and one long updated, from before the bridge's consumer.
for subject, ev := range map[string]map[string]any{
"mesh.mod.gitea.event.pull.updated": {"owner": "novox", "repo": "mesh-old", "number": 5, "head": "feat/old", "head_sha": h2},
"mesh.mod.gitea.event.pull.closed": {"owner": "novox", "repo": "mesh-older", "number": 6, "head": "feat/older", "head_sha": h2},
for subject, ev := range map[string]any{
git.SubjectPullUpdated: git.PullUpdatedEvent{Repository: "novox/mesh-old", Number: 5, Base: "main", Branch: "feat/old", Head: h2},
git.SubjectPullClosed: git.PullClosedEvent{Repository: "novox/mesh-older", Number: 6, Base: "main", Branch: "feat/older", Head: h2},
} {
raw, _ := json.Marshal(ev)
if _, err := js.Publish(ctx, subject, raw); err != nil {
@@ -651,171 +652,186 @@ func TestBridgeReadsTheForgeAtStartAndAfterAReconnectAndHearsNoOlderEvent(t *tes
}
}
// TestBridgeMergesAGroupOneMemberAtATime: the gate (played by the test) writes merges.g with member A handed and the
// Approve's heads of A and B; a stand-in forge merges A and announces it; the gate then hands B; both end merged on FORGE
// (1342/AC3, ADR 0437 §8's check).
func TestBridgeMergesAGroupOneMemberAtATime(t *testing.T) { // 1342/AC3
b := startBed(t, `{"result":{"pulls":[]}}`, nil)
const headB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
const mergeA, mergeB = "1111111111111111111111111111111111111111", "2222222222222222222222222222222222222222"
merges := make(chan string, 4)
// The stand-in for gitea_merge_pull_request answers in the reply envelope of mesh-sdk go/process/call.go and publishes
// gitea.pull.merged as mesh-catalog modules/gitea/index.ts:186-207 does, with the merge commit of the member merged.
sub, err := b.ctl.Subscribe("mesh.mod.gitea.tool.gitea_merge_pull_request", func(m *nats.Msg) {
var a struct {
Repo string `json:"repo"`
Head string `json:"head"`
}
_ = json.Unmarshal(m.Data, &a)
merges <- string(m.Data)
commit, number := mergeA, 12
if a.Repo == "mesh-sdk" {
commit, number = mergeB, 4
}
ev, _ := json.Marshal(map[string]any{"owner": "novox", "repo": a.Repo, "number": number, "head_sha": a.Head, "head": "feat/x",
"base": "main", "merge_commit_sha": commit, "module_dirs": []string{"modules/a"}, "module_dirs_said": true})
_, _ = b.js.Publish(b.ctx, "mesh.mod.gitea.event.pull.merged", ev)
_ = m.Respond([]byte(`{"result":{"merged":true},"node":"n0"}`))
})
// heard is every message published under the bridge's stream subjects from now, by subject.
func heard(t *testing.T, b *bed) <-chan *nats.Msg {
t.Helper()
ch := make(chan *nats.Msg, 64)
sub, err := b.ctl.ChanSubscribe("mesh.stream.mesh-delivery-forge-bridge.event.>", ch)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
trees, err := b.ctl.Subscribe("mesh.mod.gitea.tool.gitea_commit_trees", func(m *nats.Msg) {
_ = m.Respond([]byte(`{"result":{"root":"r0","trees":{"":"t0","modules/a":"t1"}},"node":"n0"}`))
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = trees.Unsubscribe() })
if err := b.ctl.Flush(); err != nil {
t.Fatal(err)
}
return ch
}
kv, err := b.js.KeyValue(b.ctx, mergesBucket)
if err != nil {
t.Fatal(err)
}
approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): head, meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB}
pullA := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 12, Head: head, State: string(meshdeliveryclient.MergeMerging)}
pullB := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerging)}
put := func(pulls ...seat.MergesByGroupValuePullsItem) {
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls})
if err != nil {
t.Fatal(err)
}
if _, err := kv.Put(b.ctx, "g", v); err != nil {
t.Fatal(err)
}
}
next := func(repo string) {
// next is the next message heard on subject within a bound; the others are passed by.
func next(t *testing.T, ch <-chan *nats.Msg, subject string) []byte {
t.Helper()
bound := time.After(20 * time.Second)
for {
select {
case got := <-merges:
if !strings.Contains(got, `"repo":"`+repo+`"`) {
t.Errorf("the forge was asked %s, want %s", got, repo)
case m := <-ch:
if m.Subject == subject {
return m.Data
}
for _, want := range []string{"novox/mesh-delivery@" + head, "novox/mesh-sdk@" + headB} {
if !strings.Contains(got, want) {
t.Errorf("the merge %s does not carry the approved head %s", got, want)
}
}
case <-time.After(10 * time.Second):
t.Fatalf("%s was not merged within 10 s", repo)
case <-bound:
t.Fatalf("nothing on %s within 20 s", subject)
return nil
}
}
put(pullA)
next("mesh-delivery")
// The gate hands the next member only after the bridge reported the one before merged (ADR 0441 §5).
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && len(forgeMerged(t, b)) < 1; time.Sleep(50 * time.Millisecond) {
}
pullA.State, pullA.Commit = string(meshdeliveryclient.MergeMerged), mergeA
put(pullA, pullB)
next("mesh-sdk")
var got []string
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if got = forgeMerged(t, b); len(got) == 2 {
break
}
}
if len(got) != 2 {
t.Fatalf("FORGE holds merged messages for %v, want both members", got)
}
select {
case extra := <-merges:
t.Errorf("a third merge was asked: %s", extra)
default:
}
}
// forgeMerged lists the repositories FORGE holds a merged message for, sorted.
func forgeMerged(t *testing.T, b *bed) []string {
// refusedRead says the bridge's account cannot read the bucket: a key-value read of another module's state is refused.
func refusedRead(t *testing.T, b *bed, bucket string) {
t.Helper()
forge, err := b.js.Stream(b.ctx, "FORGE")
if _, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: bucket}); err != nil {
t.Fatal(err)
}
nc, err := nats.Connect(b.srv.ClientURL(), nats.UserInfo("n1.mesh-delivery-forge-bridge", "pw"))
if err != nil {
t.Fatal(err)
}
var out []string
for _, repo := range []string{"mesh-delivery", "mesh-sdk"} {
raw, err := forge.GetLastMsgForSubject(b.ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox."+repo+".merged")
if err != nil {
continue
}
if f, err := forgemsg.DecodeForge(raw.Data); err == nil && f.Kind == forgemsg.ForgeMerged {
out = append(out, f.Repository)
defer nc.Close()
js, _ := jetstream.New(nc)
ctx, cancel := context.WithTimeout(b.ctx, 2*time.Second)
defer cancel()
if kv, err := js.KeyValue(ctx, bucket); err == nil {
if _, err := kv.Get(ctx, "any"); err == nil || errors.Is(err, jetstream.ErrKeyNotFound) {
t.Errorf("the bridge's account reads %s", bucket)
}
}
return out
}
// Acceptance 49: the forge bridge sets the group on each member's merged message and says the group's end once, after the
// last member's, from the seat mesh-delivery's verb groups; its account reads no state of the gate's.
func TestBridgeSaysAGroupFromTheSeatsGroups(t *testing.T) { // 1585, ADR 0633 §4
b := startBed(t, `{"pulls":[]}`, nil)
headB := strings.Repeat("b", 40)
mergeB := strings.Repeat("e", 40)
groups, _ := json.Marshal(seat.GroupsAnswer{Groups: []seat.Group{{ID: "feat/x", State: "merging", Members: []seat.Member{
{Repository: "novox/mesh-delivery", Number: 12, Head: head, State: "merged"}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: "merging"}}}}})
b.answer("mesh-delivery.groups", string(groups))
ch := heard(t, b)
for _, m := range []git.PullMergedEvent{
{Repository: "novox/mesh-delivery", Number: 12, Head: head, Branch: "feat/x", Base: "main", MergeCommit: merge, Change: &git.Change{Paths: []string{"modules/a/x.go"}, ModuleDirs: []string{"modules/a"}}},
{Repository: "novox/mesh-sdk", Number: 4, Head: headB, Branch: "feat/x", Base: "main", MergeCommit: mergeB, Change: &git.Change{Paths: []string{"go/x.go"}}},
} {
ev, _ := json.Marshal(m)
if _, err := b.js.Publish(b.ctx, git.SubjectPullMerged, ev); err != nil {
t.Fatal(err)
}
repo := strings.TrimPrefix(m.Repository, "novox/")
f, err := forgemsg.DecodeForge(next(t, ch, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox."+repo+".merged"))
if err != nil || f.Group != "feat/x" || len(f.Members) != 2 || f.Members[1].Number != 4 {
t.Fatalf("%s's merged message: %+v %v, want the group feat/x with both members", repo, f, err)
}
}
end, err := forgemsg.DecodeForgeGroupEnd(next(t, ch, forgeSubject(forgemsg.ForgeGroupEndEvent)))
if err != nil || end.Group != "feat/x" || len(end.Members) != 2 || end.Members[0].Merge != merge || end.Members[1].Merge != mergeB {
t.Errorf("the group's end: %+v %v", end, err)
}
refusedRead(t, b, "mesh-delivery-gate_merges")
}
// Acceptance 53 (design 47 *What the gate owes the forge*): an owed act said on a seat's own stream owed is performed through
// the seat git and its outcome said as owed.ended; said twice it is performed once; one it cannot read is said undecodable;
// one said while the bridge is stopped is performed once after its restart; bridge_owed_again's ask is answered by the
// process; and its account reads neither the gate's nor the checker's state.
func TestBridgePerformsWhatIsOwedOnce(t *testing.T) {
b := startBed(t, `{"pulls":[]}`, nil)
ch := heard(t, b)
ended := func() forgemsg.OwedEnded {
t.Helper()
e, err := forgemsg.DecodeOwedEnded(next(t, ch, forgeSubject(forgemsg.OwedEndedEvent)))
if err != nil {
t.Fatal(err)
}
return e
}
count := func(verb string) int {
n := 0
for _, c := range b.calls() {
if strings.HasPrefix(c, verb+" ") {
n++
}
}
return n
}
gateOwed := gitfixture.Seat(t, seat.Seat, "events", "owed is said, a merge", "event:owed")[0]
subject := seat.SubjectOwed("novox~mesh-sdk", "178")
for range 2 {
if _, err := b.js.Publish(b.ctx, subject, gateOwed); err != nil {
t.Fatal(err)
}
}
if e := ended(); e.Outcome != forgemsg.OwedPerformed || e.Of != seat.Seat {
t.Errorf("the gate's merge: %+v, want performed", e)
}
if _, err := b.js.Publish(b.ctx, subject, []byte("{")); err != nil {
t.Fatal(err)
}
if e := ended(); e.Outcome != forgemsg.OwedUndecodable {
t.Errorf("an act it cannot read: %+v, want undecodable", e)
}
if n := count("git.merge-pull"); n != 1 {
t.Errorf("an act said twice was performed %d times", n)
}
// Said while the bridge is stopped: performed once after its restart.
checkerOwed := gitfixture.Seat(t, checker.Seat, "events", "owed is said", "event:owed")[0]
b.restart(func() {
if _, err := b.js.Publish(b.ctx, checker.SubjectOwed("novox~mesh-sdk", "178"), checkerOwed); err != nil {
t.Fatal(err)
}
})
if e := ended(); e.Of != checker.Seat || e.Outcome != forgemsg.OwedPerformed {
t.Errorf("the checker's status after the restart: %+v, want performed", e)
}
if n := count("git.status"); n != 1 {
t.Errorf("the checker's status was performed %d times, want once", n)
}
// bridge_owed_again asks through the state owed-asks; the process performs the act again and answers there.
var g struct {
ID string `json:"id"`
}
_ = json.Unmarshal(gateOwed, &g)
key, value, err := groupkeys.NewOwedAsk(groupkeys.OwedAgain, seat.Seat, g.ID, "a test", "lead@n1", time.Now())
if err != nil {
t.Fatal(err)
}
asks, err := b.js.KeyValue(b.ctx, groupkeys.OwedAsksBucket)
if err != nil {
t.Fatal(err)
}
w, err := asks.Watch(b.ctx, key)
if err != nil {
t.Fatal(err)
}
defer w.Stop()
if _, err := asks.Put(b.ctx, key, value); err != nil {
t.Fatal(err)
}
bound := time.After(20 * time.Second)
for answered := false; !answered; {
select {
case e := <-w.Updates():
if e == nil {
continue
}
if a, err := groupkeys.DecodeOwedAsk(e.Value()); err == nil && a.Done() {
answered = true
if a.Outcome != forgemsg.OwedPerformed || count("git.merge-pull") != 2 {
t.Errorf("bridge_owed_again: %+v, merge-pull asked %d times; want performed again", a, count("git.merge-pull"))
}
}
case <-bound:
t.Fatal("bridge_owed_again was not answered within 20 s")
}
}
refusedRead(t, b, "mesh-delivery-gate_owed")
refusedRead(t, b, "mesh-delivery-checker_owed")
}
func sha256Sum(b []byte) []byte { s := sha256.Sum256(b); return s[:] }
// TestBridgePerformsNoMeshCheckAct: the checker owes mesh/check on a head and says so on its stream; the bridge calls no
// tool with mesh/check and publishes nothing of its own on EVENTS for it. The forge module writes mesh/check from the
// checker's owed state itself (ADR 0521; R4e round 2 finding R2-1).
func TestBridgePerformsNoMeshCheckAct(t *testing.T) { // R2-1, ADR 0521
b := startBed(t, `{"result":{"pulls":[]}}`, nil)
calls := make(chan string, 8)
// Every forge call is watched, and none answered here: the bed's stand-ins answer the ones the bridge asks.
sub, err := b.ctl.Subscribe("mesh.mod.gitea.>", func(m *nats.Msg) {
if strings.Contains(string(m.Data), "mesh/check") {
calls <- m.Subject + " " + string(m.Data)
}
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
if err := b.ctl.Flush(); err != nil {
t.Fatal(err)
}
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: "mesh-delivery-checker_owed", History: 5})
if err != nil {
t.Fatal(err)
}
key, err := meshdeliveryclient.OwedKey("novox/mesh-delivery", 12, head, "status=mesh/check")
if err != nil {
t.Fatal(err)
}
v, err := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.0123456789ab"})
if err != nil {
t.Fatal(err)
}
if _, err := kv.Put(b.ctx, key, v); err != nil {
t.Fatal(err)
}
if _, err := b.js.Publish(b.ctx, "mesh.stream.mesh-delivery-checker.event.owed.written", []byte(`{"key":"`+key+`"}`)); err != nil {
t.Fatal(err)
}
select {
case c := <-calls:
t.Errorf("a forge tool was called with mesh/check: %s", c)
case <-time.After(2 * time.Second):
}
events, err := b.js.Stream(b.ctx, "EVENTS")
if err != nil {
t.Fatal(err)
}
if _, err := events.GetLastMsgForSubject(b.ctx, "mesh.mod.mesh-delivery-forge-bridge.event.>"); err == nil {
t.Error("the bridge published an event of its own for the checker's mesh/check")
}
}
@@ -22,6 +22,8 @@ import (
"github.com/nats-io/nats.go/jetstream"
git "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-forge-bridge/internal/bridge"
@@ -146,39 +148,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// Everything written on FORGE, an event's and a sync's, keeps the head in the bucket.
rec := &recorder{pub: js, heads: heads, logger: logger}
said := func(ctx context.Context) error { return syncOpen(ctx, call, rec, heads, &turn, logger) }
// What the gate asks of the forge (ADR 0437 §8): its owed acts and its merges, each performed once per revision.
performedKV, err := process.EssentialBucket(ctx, js, "bridge", performedBucket, "perform an act of the gate only once")
performedKV, err := process.EssentialBucket(ctx, js, "bridge", performedBucket, "keep what it performed of what is owed to the forge")
if err != nil {
return err
}
perf := &performer{call: call, pub: rec, done: kvRevisions{performedKV}, logger: logger}
// A delivery group's member names its group, and its end is said once (ADR 0633 §4), from the gate's merges.
groups := &grouper{send: sendVia(js), done: kvRevisions{performedKV}, logger: logger}
watching := make(chan error, 3)
for _, w := range []struct {
bucket string
react func(ctx context.Context, key string, rev uint64, value []byte)
}{{owedBucket, perf.owed}, {mergesBucket, func(ctx context.Context, key string, rev uint64, value []byte) {
perf.merges(ctx, key, rev, value)
groups.ended(ctx, key, rev, value)
}}} {
// The gate's buckets come with the gate and its grant; the bridge does not wait for them (no timer), and is started
// again by its delivery when the gate arrives. A bucket refused or not answered is the same (issue 1945).
kv := process.OptionalBucket(ctx, js, w.bucket, "nothing of it is performed until the bridge starts again", logger.Printf)
if kv == nil {
continue
}
if w.bucket == mergesBucket {
groups.merges = kvMerges(kv)
}
go func() {
watching <- watch(ctx, kv, func(key string, rev uint64, value []byte) {
hctx, cancel := context.WithTimeout(ctx, syncDeadline)
defer cancel()
w.react(hctx, key, rev, value)
})
}()
}
// What the gate and the checker owe the forge, performed through the seat git (design 47 *What the gate owes the forge*).
owe := &owing{call: call, pub: js, performed: kvPerformed{performedKV}, now: time.Now, logger: logger}
// A delivery group's member names its group, and its end is said once (ADR 0633 §4), from the seat mesh-delivery's groups.
groups := &grouper{groups: groupsVia(call), send: sendVia(js), done: kvHeads{performedKV}, logger: logger}
// A person says a group's end again with bridge_group_end_again, which writes the state end-asks (ADR 0633 §4): watched, and
// answered there; a state not there at this start is answered from the next start.
if kv := process.OptionalBucket(ctx, js, groupkeys.EndAsksBucket, "bridge_group_end_again is not answered until the bridge starts again", logger.Printf); kv != nil {
@@ -192,6 +169,43 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if err := said(ctx); err != nil {
return err
}
// What the gate and the checker say they owe, each on its seat's own stream owed through this module's durable there
// (made from its consumes). A stream not there yet is said, and the bridge goes on: its other work does not wait on the
// gate, and what was said meanwhile is caught up from the groups at the next start.
for _, owner := range []struct{ of, kept string }{{seat.Seat, seat.StreamOwed}, {checker.Seat, checker.StreamOwed}} {
cc, err := cred.HearSeatEvents(ctx, js, owner.kept, func(m jetstream.Msg) {
hctx, cancel := context.WithTimeout(ctx, syncDeadline)
defer cancel()
owe.hear(hctx, owner.of, m)
}, logger.Printf)
if err != nil {
logger.Printf("nothing %s owes the forge is performed until the bridge starts again: %v", owner.of, err)
continue
}
defer cc.Stop()
}
// A person performs an act again, drops one or reads the consumers' backlog with the tools bridge_owed_again,
// bridge_owed_drop and bridge_owed, which write the state owed-asks: watched, and answered there.
if kv := process.OptionalBucket(ctx, js, groupkeys.OwedAsksBucket, "bridge_owed_again, bridge_owed_drop and bridge_owed's backlog are not answered until the bridge starts again", logger.Printf); kv != nil {
backlog := owedBacklog(js, cred, time.Now)
go func() {
err := watch(ctx, kv, func(key string, rev uint64, value []byte) {
hctx, cancel := context.WithTimeout(ctx, syncDeadline)
defer cancel()
owe.asked(hctx, kv, key, rev, value, backlog)
})
if err != nil && ctx.Err() == nil {
logger.Printf("the owed asks are no longer watched, so they are not answered until the bridge starts again: %v", err)
}
}()
}
// What was said while the bridge was away and is still unanswered, read from the seat (design 47 *The bridge catches up
// from the seat*). The gate comes after the bridge, so a seat that does not answer yet is said, and the bridge goes on.
if n, err := owe.catchUp(ctx); err != nil {
logger.Printf("nothing owed is caught up from the groups at this start, so an act said while the bridge was away waits for its saying again: %v", err)
} else if n > 0 {
logger.Printf("%d acts owed to the forge, said while the bridge was away, performed from the groups", n)
}
go onReconnect(ctx, again, said, logger)
// What a merge read of each module directory, by its tree (issue 1126): a merge reads only the directories it changed.
inputs := newKept(process.OptionalBucket(ctx, js, inputsBucket, "a merge's module directories are kept in memory only, so the first merge after a start reads every one", logger.Printf), logger)
@@ -213,10 +227,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return fmt.Errorf("%w: the bridge hears no forge event without it, so it stops; where the bus refused it or did not answer, this module's consumes (made by the controller with its grant) have not reached the bus yet", err)
}
defer hub.Stop()
select {
case <-ctx.Done():
return nil
case err := <-watching:
return fmt.Errorf("watching the gate's keys: %w", err)
}
<-ctx.Done()
return nil
}
@@ -2,15 +2,9 @@ package main
import groupkeys "mesh-delivery-forge-bridge/internal/groups"
// The holders and buckets the forge bridge names, taken from its own manifest (module.json: its name, its `reads`, its
// The holders and buckets the forge bridge names, taken from its own manifest (module.json: its name and its
// states); a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder
// (novox/hq ADR 0625 §4, ADR 0634 §1).
const (
forgeBridgeModule = groupkeys.Module
gateModule = "mesh-delivery-gate"
// mergesBucket and owedBucket are the gate's states merges and owed, read by the bridge (reads: mesh-delivery-gate.merges,
// mesh-delivery-gate.owed).
mergesBucket = gateModule + "_merges"
owedBucket = gateModule + "_owed"
)
@@ -7,8 +7,8 @@ import (
"testing"
)
// theNamesAreTheManifests (a subtest of TestAnOwedStatusIsSetOnceForARevision): the holders and buckets the bridge names
// are its manifest's (its module, its `reads`, its states), since no contract names a holder (novox/hq ADR 0634 §1).
// theNamesAreTheManifests (a subtest of TestWhatIsOwedIsPerformedSaidAndKept): the holders and buckets the bridge names
// are its manifest's (its module, its `reads`, its states and its kept data in `requires`), since no contract names a holder (novox/hq ADR 0634 §1).
func theNamesAreTheManifests(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
@@ -20,6 +20,7 @@ func theNamesAreTheManifests(t *testing.T) {
State []struct {
Name string `json:"name"`
} `json:"state"`
Requires []json.RawMessage `json:"requires"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
@@ -27,15 +28,22 @@ func theNamesAreTheManifests(t *testing.T) {
if m.Module != forgeBridgeModule {
t.Errorf("the manifest's module is %q, the bridge names itself %q", m.Module, forgeBridgeModule)
}
for _, read := range []string{gateModule + ".merges", gateModule + ".owed"} {
if !slices.Contains(m.Reads, read) {
t.Errorf("the bridge reads %s, its manifest's reads are %v", read, m.Reads)
}
if len(m.Reads) != 0 {
t.Errorf("the bridge reads no other module's state (acceptance 49), its manifest's reads are %v", m.Reads)
}
var own []string
for _, s := range m.State {
own = append(own, forgeBridgeModule+"_"+s.Name)
}
for _, r := range m.Requires {
var kept struct {
Name string `json:"name"`
Template string `json:"template"`
}
if json.Unmarshal(r, &kept) == nil && kept.Template == "state" {
own = append(own, forgeBridgeModule+"_"+kept.Name)
}
}
for _, b := range []string{headsBucket, performedBucket, inputsBucket} {
if !slices.Contains(own, b) {
t.Errorf("the bridge keeps %s, its manifest's states are %v", b, own)
@@ -0,0 +1,342 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
git "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-forge-bridge/internal/bridge"
"mesh-delivery-forge-bridge/internal/groups"
)
// performedBucket is the module's state performed (manifest `requires`, the template state): one key per owed act it
// performed, the act's outcome and when, with the owed event as heard (novox/hq design 47 *What the gate owes the
// forge*). It is a saving and a read, never what keeps an act from happening twice: every act is safe to repeat at the
// forge. The group ends' bookkeeping (`published.`, `groups.`, groups.go) is in it too, under the same 7 days.
const performedBucket = forgeBridgeModule + "_performed"
// performedKey is the key of one act in performed: the seat that said it and its id, the id in unpadded base64url, since
// an id may hold what a key may not (`#`, `/`).
func performedKey(of, id string) string {
return of + "." + base64.RawURLEncoding.EncodeToString([]byte(id))
}
// Performed is one value of the state performed: the outcome said as owed.ended, and the owed event it answers, so
// bridge_owed_again performs it again from here.
type Performed struct {
Ended forgemsg.OwedEnded `json:"ended"`
Owed json.RawMessage `json:"owed,omitempty"`
}
// performedStore is the state performed as the owing reads and writes it.
type performedStore interface {
Get(ctx context.Context, key string) (value []byte, found bool, err error)
Put(ctx context.Context, key string, value []byte) error
}
type kvPerformed struct{ kv jetstream.KeyValue }
func (s kvPerformed) Get(ctx context.Context, key string) ([]byte, bool, error) {
e, err := s.kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
return e.Value(), true, nil
}
func (s kvPerformed) Put(ctx context.Context, key string, value []byte) error {
_, err := s.kv.Put(ctx, key, value)
return err
}
// owing performs what the gate and the checker owe the forge (design 47 *What the gate owes the forge*): each owed act
// through the seat git's verb under its id, its outcome said as owed.ended on the stream forge and then kept in
// performed. It holds no timer.
type owing struct {
call caller
pub publisher
performed performedStore
now func() time.Time
logger *log.Logger
}
// readerOf is the reading of the seat of's owed event into the seat git's verb.
func readerOf(of string) (func([]byte) (bridge.OwedCall, error), error) {
switch of {
case seat.Seat:
return bridge.GateOwed, nil
case checker.Seat:
return bridge.CheckerOwed, nil
}
return nil, fmt.Errorf("the seat %q says no owed act the bridge performs", of)
}
// heard performs one owed event the seat of said. Its error is only an outcome that could not be said on the stream
// forge: the event is then heard again, and the act, safe to repeat, performed again.
func (o *owing) heard(ctx context.Context, of string, data []byte) (forgemsg.OwedEnded, error) {
read, err := readerOf(of)
if err != nil {
return o.end(ctx, forgemsg.OwedEnded{Of: of, Outcome: forgemsg.OwedUndecodable, Words: err.Error()}, data)
}
c, err := read(data)
if err != nil {
return o.end(ctx, forgemsg.OwedEnded{Of: of, ID: c.ID,
Outcome: forgemsg.OwedUndecodable, Words: err.Error()}, data)
}
if o.held(ctx, c.Of, c.ID) {
// said twice, or said again while its outcome was on its way: performed once, the outcome said once
return forgemsg.OwedEnded{Of: c.Of, ID: c.ID}, nil
}
if words, done := o.switchedDone(ctx, c.Of, c.ID); done {
// performed by the old bridge before the switch: said performed, never done twice
return o.end(ctx, forgemsg.OwedEnded{Of: c.Of, ID: c.ID,
Outcome: forgemsg.OwedPerformed, Words: words}, data)
}
return o.perform(ctx, c, data)
}
// switchedDone says whether a switched act (design 47 *The switch carries what is owed*: the gate's act of a key of its
// old state owed or merges, its id meshdeliveryclient.SwitchedActID) was performed by the old bridge: its own bookkeeping
// in performed, `<state>.<key>`, holds that key at the act's revision or a later one. Any other id, and bookkeeping that
// cannot be read, is not: the act is performed, which changes nothing at the forge.
func (o *owing) switchedDone(ctx context.Context, of, id string) (string, bool) {
state, key, rev, ok := meshdeliveryclient.ParseSwitchedActID(id)
if !ok || of != seat.Seat {
return "", false
}
kept, found, err := o.performed.Get(ctx, state+"."+key)
if err != nil || !found {
return "", false
}
at, err := strconv.ParseUint(strings.TrimSpace(string(kept)), 10, 64)
if err != nil || at < rev {
return "", false
}
return fmt.Sprintf("performed by the bridge before the switch: its %s.%s at revision %d", state, key, at), true
}
// hear is one message of a seat's stream owed: performed and acknowledged, or, when its outcome could not be said,
// left to come again (design 47: the bridge stopped between an act and its owed.ended performs it again).
func (o *owing) hear(ctx context.Context, of string, m delivered) {
if _, err := o.heard(ctx, of, m.Data()); err != nil {
o.logger.Printf("%s, so the act is heard again: %v", m.Subject(), err)
_ = m.NakWithDelay(retryDelay)
return
}
_ = m.Ack()
}
// perform calls the seat git's verb of one act and says what it came to.
func (o *owing) perform(ctx context.Context, c bridge.OwedCall, data []byte) (forgemsg.OwedEnded, error) {
answer, callErr := o.call(ctx, "seat:"+git.Seat+"."+c.Verb, c.Input)
outcome, words := bridge.Outcome(answer, callErr)
if outcome != forgemsg.OwedPerformed {
o.logger.Printf("the owed %s %s of %s#%d (said by %s) is %s: %s", c.Act, c.ID, c.Repository, c.Number, c.Of, outcome, words)
}
return o.end(ctx, forgemsg.OwedEnded{Of: c.Of, ID: c.ID,
Outcome: outcome, Words: words}, data)
}
// end says an act's outcome as owed.ended on the stream forge, then keeps it in performed. An outcome not kept is
// logged: the act may be performed again, which changes nothing at the forge.
func (o *owing) end(ctx context.Context, e forgemsg.OwedEnded, owed []byte) (forgemsg.OwedEnded, error) {
e.At = o.now().UTC()
data, err := forgemsg.EncodeOwedEnded(e)
if err != nil {
return e, fmt.Errorf("the outcome of the owed %s cannot be said: %w", e.ID, err)
}
if _, err := o.pub.Publish(ctx, forgeSubject(forgemsg.OwedEndedEvent), data); err != nil {
return e, fmt.Errorf("the outcome %s of the owed %s could not be said on the stream forge: %w", e.Outcome, e.ID, err)
}
if e.ID == "" {
return e, nil // an undecodable act with no id is said, and has no key to keep
}
if !json.Valid(owed) {
owed = nil
}
v, err := json.Marshal(Performed{Ended: e, Owed: owed})
if err == nil {
err = o.performed.Put(ctx, performedKey(e.Of, e.ID), v)
}
if err != nil {
o.logger.Printf("the outcome %s of the owed %s is said but could not be kept in performed: %v", e.Outcome, e.ID, err)
}
return e, nil
}
// held says whether performed holds an outcome of the act already. One that cannot be read is not held: the act is
// performed again, which changes nothing at the forge.
func (o *owing) held(ctx context.Context, of, id string) bool {
_, found, err := o.performed.Get(ctx, performedKey(of, id))
if err != nil {
o.logger.Printf("performed of the owed %s cannot be read, so it is performed again: %v", id, err)
return false
}
return found
}
// catchUp reads the seat mesh-delivery's groups and performs every act said there with no recorded outcome that
// performed does not hold either (design 47 *The bridge catches up from the seat*). It answers how many it performed.
func (o *owing) catchUp(ctx context.Context) (int, error) {
of, err := groups.Read(ctx, o.call)
if err != nil {
return 0, err
}
n := 0
for _, g := range of {
for _, act := range g.Acts {
if act.Outcome != "" || o.held(ctx, seat.Seat, act.Owed.ID) {
continue // answered, or performed and its outcome on its way to the gate
}
data, err := json.Marshal(act.Owed)
if err != nil {
return n, err
}
if _, err := o.heard(ctx, seat.Seat, data); err != nil {
return n, err
}
n++
}
}
return n, nil
}
// owedOf is the owed event of an act to perform again: as performed keeps it, else, for the gate's, as its groups say it.
func (o *owing) owedOf(ctx context.Context, of, id string) ([]byte, error) {
v, found, err := o.performed.Get(ctx, performedKey(of, id))
if err != nil {
return nil, fmt.Errorf("performed cannot be read: %w", err)
}
if found {
var p Performed
if err := json.Unmarshal(v, &p); err == nil && len(p.Owed) > 0 {
return p.Owed, nil
}
}
if of != seat.Seat {
return nil, fmt.Errorf("the bridge holds no owed act %s of %s: the checker says its unanswered acts again at its start", id, of)
}
raw, err := o.call(ctx, "seat:"+seat.Seat+"."+seat.VerbGroups, seat.GroupsInput{All: seat.GroupsInputAllTrue})
if err != nil {
return nil, fmt.Errorf("the seat %s's groups could not be read: %w", seat.Seat, err)
}
var a seat.GroupsAnswer
if err := json.Unmarshal(raw, &a); err != nil || a.Error != "" {
return nil, fmt.Errorf("the seat %s's groups do not answer: %v %s", seat.Seat, err, a.Error)
}
for _, g := range a.Groups {
for _, act := range g.Acts {
if act.Owed.ID == id {
return json.Marshal(act.Owed)
}
}
}
return nil, fmt.Errorf("neither the bridge nor the seat %s's groups hold an owed act %s", seat.Seat, id)
}
// answer answers one owed ask: an act performed again now, whatever performed holds of it; an act dropped, said as
// owed.ended with the outcome dropped and the ask's why (the tool wrote the hand-act log first); or the consumers' backlog.
func (o *owing) answer(ctx context.Context, a groups.OwedAsk, backlog func(context.Context) []groups.Backlog) groups.OwedAsk {
a.Answered = o.now().UTC()
switch a.Kind {
case groups.OwedBacklog:
a.Backlog, a.Answer = backlog(ctx), "the backlog of each seat's stream owed"
return a
}
data, err := o.owedOf(ctx, a.Of, a.ID)
if err != nil {
a.Answer = err.Error()
return a
}
var e forgemsg.OwedEnded
if a.Kind == groups.OwedDrop {
e, err = o.end(ctx, forgemsg.OwedEnded{Of: a.Of, ID: a.ID, Outcome: forgemsg.OwedDropped, Words: a.Why + " (dropped by " + a.By + ")"}, data)
} else if read, rerr := readerOf(a.Of); rerr != nil {
err = rerr
} else if c, rerr := read(data); rerr != nil {
e, err = o.end(ctx, forgemsg.OwedEnded{Of: a.Of, ID: a.ID, Outcome: forgemsg.OwedUndecodable, Words: rerr.Error()}, data)
} else {
e, err = o.perform(ctx, c, data)
}
if err != nil {
a.Answer = err.Error()
return a
}
a.Outcome, a.Answer = e.Outcome, "said as owed.ended: "+e.Outcome
if e.Words != "" {
a.Answer += ": " + e.Words
}
return a
}
// asked answers one value of the state owed-asks not yet answered, written back by compare-and-set at its revision; an
// answered one (this bridge's own write) is left.
func (o *owing) asked(ctx context.Context, kv jetstream.KeyValue, key string, rev uint64, value []byte, backlog func(context.Context) []groups.Backlog) {
a, err := groups.DecodeOwedAsk(value)
if err != nil {
o.logger.Printf("%s.%s holds what is not an owed ask, left: %v", groups.OwedAsksState, key, err)
return
}
if a.Done() {
return
}
a = o.answer(ctx, a, backlog)
o.logger.Printf("the owed ask %s of %s %s by %s (%s): %s", a.Kind, a.Of, a.ID, a.By, a.Why, a.Answer)
data, err := json.Marshal(a)
if err != nil {
return
}
if _, err := kv.Update(ctx, key, data, rev); err != nil && !errors.Is(err, jetstream.ErrKeyExists) && !errors.Is(err, jetstream.ErrKeyRevisionMismatch) {
o.logger.Printf("%s.%s: the answer could not be written: %v", groups.OwedAsksState, key, err)
}
}
// owedBacklog reads the bridge's durable on each seat's stream owed: its acknowledged floor, what waits and what is held,
// and the time of the first event after the floor.
func owedBacklog(js jetstream.JetStream, cred process.Account, now func() time.Time) func(context.Context) []groups.Backlog {
return func(ctx context.Context) []groups.Backlog {
var out []groups.Backlog
for _, kept := range []string{seat.StreamOwed, checker.StreamOwed} {
row := groups.Backlog{Stream: kept, Consumer: cred.EventConsumerName()}
c, err := js.Consumer(ctx, row.Stream, row.Consumer)
var info *jetstream.ConsumerInfo
if err == nil {
info, err = c.Info(ctx)
}
if err != nil {
row.Error = err.Error()
out = append(out, row)
continue
}
row.LastApplied, row.Waiting, row.Held = info.AckFloor.Stream, info.NumPending, uint64(info.NumAckPending)
if row.Waiting+row.Held > 0 {
if s, err := js.Stream(ctx, row.Stream); err == nil {
if m, err := s.GetMsg(ctx, info.AckFloor.Stream+1); err == nil {
row.Oldest, row.OldestSecs = m.Time.UTC(), int64(now().Sub(m.Time).Seconds())
}
}
}
out = append(out, row)
}
return out
}
}
@@ -156,7 +156,7 @@ type Forge struct {
PredefinedRead bool `json:"predefined-read,omitempty"`
// Group is the delivery group a merged message's pull request was merged in, and Members every approved member of
// that group with its approved head, this one included (novox/hq ADR 0633 §4): the forge bridge sets both from the
// gate's merges state, and the indexer gathers the group's merges until the group's end (ForgeGroupEnd). A merge
// seat mesh-delivery's verb groups, and the indexer gathers the group's merges until the group's end (ForgeGroupEnd). A merge
// with no group carries neither, and an older bridge sends neither; an older reader ignores both.
Group string `json:"group,omitempty"`
Members []meshdeliveryclient.Head `json:"members,omitempty"`
@@ -3,8 +3,10 @@ module mesh-delivery-forge-bridge
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/git v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
@@ -1,160 +0,0 @@
package bridge
import (
"fmt"
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// Call is a call of a forge tool: the tool by its manifest key and its arguments.
type Call struct {
Tool string
Args map[string]any
}
// Act is one thing the bridge does on the forge for a key of the gate: the act's name (an owed key's act, `status=<context>`,
// `view`, `note`, `comment.<seq>`, or `merge`), the pull request and head it is on (Number 0 for a commit no pull
// request names) and the forge call that performs it.
type Act struct {
Name string
Repository string
Number int
Head string
Call Call
}
// The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: meshdeliveryclient.ts
// commitStatusTool (owner, repo, sha, context, state, description, target_url), tools/index.ts gitea_delivery_view
// (owner, repo, number, body), gitea_note_append (owner, repo, sha, ref, line), gitea_add_comment (owner, repo,
// number, body), merge.ts gitea_merge_pull_request (owner, repo, number, head, approved_heads).
const (
commitStatusTool = "gitea.gitea_commit_status"
deliveryViewTool = "gitea.gitea_delivery_view"
noteAppendTool = "gitea.gitea_note_append"
addCommentTool = "gitea.gitea_add_comment"
mergePullTool = "gitea.gitea_merge_pull_request"
)
// MergeAct is the name of the act of merging a pull request.
const MergeAct = "merge"
func ownerRepo(repository string) (owner, repo string) {
owner, repo, _ = strings.Cut(repository, "/")
return owner, repo
}
// OwedAct turns an owed key and its value (mesh-sdk go/delivery OwedKey, EncodeOwed) into the forge call that performs
// it. A key that is no owed key, a value that is no owed act, a value whose act is not the key's, and a view or a comment
// on no pull request are errors, each saying why.
func OwedAct(key string, value []byte) (Act, error) {
repository, number, head, name, err := meshdeliveryclient.ParseOwedKey(key)
if err != nil {
return Act{}, err
}
o, err := meshdeliveryclient.DecodeOwed(value)
if err != nil {
return Act{}, fmt.Errorf("the owed act %s: %w", key, err)
}
kind, context, err := meshdeliveryclient.ParseOwedAct(name)
if err != nil {
return Act{}, err
}
if o.Act != kind {
return Act{}, fmt.Errorf("the owed act %s says %q, not the %q its key names", key, o.Act, kind)
}
owner, repo := ownerRepo(repository)
a := Act{Name: name, Repository: repository, Number: number, Head: head}
needsPull := func() error {
if number <= 0 {
return fmt.Errorf("the owed %s %s is on a commit no pull request names, and it needs the pull request", kind, key)
}
return nil
}
switch kind {
case meshdeliveryclient.OwedStatus:
if o.Context != context {
return Act{}, fmt.Errorf("the owed status %s says the context %q, not the %q its key names", key, o.Context, context)
}
if o.Context == meshdeliveryclient.StatusContextCheck {
// No tool writes mesh/check (R4e finding K1), and the bridge performs no act of it: the forge module writes it from
// the checker's state mesh-delivery-checker.owed, read by a watch (ADR 0521, finding R2-1).
return Act{}, fmt.Errorf("the owed status %s is %s, which the bridge does not perform: the forge module writes it from mesh-delivery-checker.owed",
key, meshdeliveryclient.StatusContextCheck)
}
a.Call = Call{Tool: commitStatusTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "context": o.Context,
"state": o.State, "description": o.Text, "target_url": o.Target}}
case meshdeliveryclient.OwedView:
if err := needsPull(); err != nil {
return Act{}, err
}
a.Call = Call{Tool: deliveryViewTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}}
case meshdeliveryclient.OwedNote:
a.Call = Call{Tool: noteAppendTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "ref": o.Ref, "line": o.Text}}
case meshdeliveryclient.OwedComment:
if err := needsPull(); err != nil {
return Act{}, err
}
a.Call = Call{Tool: addCommentTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}}
}
return a, nil
}
// MergeActs turns the value of a merges key (the delivery client's EncodeMerge) into the merges to perform: one for each
// pull request in the state merging, in the order the value names them, at the head it expects and with the approved
// heads of every member of the group, `<owner>/<repo>@<full sha>` sorted (the merge tool's approved_heads). A pull
// request merged or refused already gets none. A value with no approved heads is an error: the forge's merge tool
// needs them for a group.
func MergeActs(value []byte) ([]Act, error) {
m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil {
return nil, err
}
var handed []seat.MergesByGroupValuePullsItem
for _, p := range m.Pulls {
if meshdeliveryclient.MergeState(p.State) == meshdeliveryclient.MergeMerging {
handed = append(handed, p)
}
}
if len(handed) == 0 {
return nil, nil
}
if len(m.Approved) == 0 {
return nil, fmt.Errorf("the merge of the group %s names no approved heads, so the forge's merge tool has nothing to merge at", m.Group)
}
approved := make([]string, 0, len(m.Approved))
for key, sha := range m.Approved {
repository, _, ok := strings.Cut(key, "@")
if !ok {
return nil, fmt.Errorf("the approved head %q of the group %s is not <repository>@<number>", key, m.Group)
}
approved = append(approved, repository+"@"+sha)
}
slices.Sort(approved)
var acts []Act
for _, p := range handed {
owner, repo := ownerRepo(p.Repository)
acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: int(p.Number), Head: p.Head,
Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": int(p.Number), "head": p.Head,
"approved_heads": approved}}})
}
return acts, nil
}
// RefusedMessage is the refused message the bridge writes on FORGE when the forge refuses an act: the act, the pull
// request and head it was on, and the forge's own words.
func RefusedMessage(a Act, words string) (Message, error) {
event, err := forgemsg.ForgeEvent(a.Repository, forgemsg.ForgeRefused)
if err != nil {
return Message{}, err
}
body, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeRefused, Repository: a.Repository, Number: a.Number,
Head: a.Head, Act: a.Name, Words: words})
if err != nil {
return Message{}, err
}
return Message{Event: event, Data: body}, nil
}
@@ -1,188 +0,0 @@
package bridge
import (
"reflect"
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/keys"
)
const (
headA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
headB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
)
// owedValue is an owed value as the SDK writes it (mesh-sdk go/delivery EncodeOwed), and its key as OwedKey spells it.
func owedValue(t *testing.T, repository string, number int, act string, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
t.Helper()
key, err := meshdeliveryclient.OwedKey(repository, number, headA, act)
if err != nil {
t.Fatal(err)
}
b, err := meshdeliveryclient.EncodeOwed(o)
if err != nil {
t.Fatal(err)
}
return key, b
}
func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1, K1
key, v := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate",
seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: "mesh/merge-gate", State: "pending", Text: "waiting", Target: "https://forge.example/novox/mesh-delivery/pulls/12"})
a, err := OwedAct(key, v)
if err != nil {
t.Fatal(err)
}
want := Call{Tool: "gitea.gitea_commit_status", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA,
"context": "mesh/merge-gate", "state": "pending", "description": "waiting", "target_url": "https://forge.example/novox/mesh-delivery/pulls/12"}}
if !reflect.DeepEqual(a.Call, want) {
t.Errorf("call = %+v, want %+v", a.Call, want)
}
if a.Repository != "novox/mesh-delivery" || a.Number != 12 || a.Head != headA || a.Name != "status=mesh/merge-gate" {
t.Errorf("act = %+v", a)
}
// R4e findings K1 and R2-1 (issue 1301 S4; ADR 0503 §3, ADR 0521): no tool writes mesh/check, since a tool is told no
// caller and node-tools invokes every tool; the forge module writes it from the checker's owed state, and the bridge
// performs no mesh/check act.
ck, cv := checkerKV(t, seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.1"})
if a, err := OwedAct(ck, cv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") {
t.Fatalf("OwedAct(mesh/check) = %+v, %v; want refused, naming where the forge module reads it", a, err)
}
// Nor through the gate's own owed bucket: the context is refused whichever key carries it.
gk, gv := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/check",
seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://x.example"})
if a, err := OwedAct(gk, gv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") {
t.Fatalf("OwedAct(gate's mesh/check) = %+v, %v; want refused", a, err)
}
}
// checkerKV is the status the checker owes, as it writes it (the gate's owed key with the act status=mesh/check).
func checkerKV(t *testing.T, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
t.Helper()
key, err := meshdeliveryclient.OwedKey("novox/mesh-delivery", 12, headA, "status=mesh/check")
if err != nil {
t.Fatal(err)
}
v, err := meshdeliveryclient.EncodeOwed(o)
if err != nil {
t.Fatal(err)
}
return key, v
}
func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2
cases := []struct {
act string
o seat.OwedByRepositoryNumberHeadActValue
want Call
}{
{"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"},
Call{Tool: "gitea.gitea_delivery_view", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "the view"}}},
{"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"},
Call{Tool: "gitea.gitea_note_append", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA, "ref": "mesh-plan", "line": "a line"}}},
{"comment.2", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"},
Call{Tool: "gitea.gitea_add_comment", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "hello"}}},
}
for _, c := range cases {
key, v := owedValue(t, "novox/mesh-delivery", 12, c.act, c.o)
a, err := OwedAct(key, v)
if err != nil {
t.Fatalf("%s: %v", c.act, err)
}
if !reflect.DeepEqual(a.Call, c.want) {
t.Errorf("%s: call = %+v, want %+v", c.act, a.Call, c.want)
}
}
}
func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) {
key, v := owedValue(t, "novox/mesh-delivery", 0, "note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "l", Ref: "mesh-plan"})
if _, err := OwedAct(key, v); err != nil {
t.Errorf("a note on a commit no pull request names: %v", err)
}
key, v = owedValue(t, "novox/mesh-delivery", 0, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"})
if _, err := OwedAct(key, v); err == nil {
t.Error("a view of no pull request was taken")
}
}
func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) {
// A dotted repository's key is unescaped, and its act names the repository whole.
dk, dv := owedValue(t, "novox/my.repo", 3, "comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "x"})
if a, err := OwedAct(dk, dv); err != nil || a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" {
t.Errorf("a dotted repository's act = %+v, %v", a, err)
}
key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"})
v, _ := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "v"})
if _, err := OwedAct(key, v); err == nil {
t.Error("a comment under a view's key was taken")
}
if _, err := OwedAct("nonsense", v); err == nil {
t.Error("a key that is no owed key was taken")
}
if _, err := OwedAct(key, []byte(`{"v":1`)); err == nil {
t.Error("a value that is not json was taken")
}
}
func mergeValue(t *testing.T, m seat.MergesByGroupValue) []byte {
t.Helper()
b, err := meshdeliveryclient.EncodeMerge(m)
if err != nil {
t.Fatal(err)
}
return b
}
func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { // 1342/AC1
approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA}
v := mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []seat.MergesByGroupValuePullsItem{
{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerged), Commit: "c0"},
{Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)},
}})
acts, err := MergeActs(v)
if err != nil {
t.Fatal(err)
}
if len(acts) != 1 {
t.Fatalf("%d acts, want 1 (the merged member is not merged again)", len(acts))
}
want := Call{Tool: "gitea.gitea_merge_pull_request", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "head": headA,
"approved_heads": []string{"novox/mesh-delivery@" + headA, "novox/mesh-sdk@" + headB}}}
if !reflect.DeepEqual(acts[0].Call, want) {
t.Errorf("call = %+v, want %+v", acts[0].Call, want)
}
if acts[0].Name != "merge" || acts[0].Number != 12 || acts[0].Head != headA {
t.Errorf("act = %+v", acts[0])
}
// A merge with no approved heads is refused: the forge's tool would refuse it with nothing to say which.
v = mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "a", Pulls: []seat.MergesByGroupValuePullsItem{
{Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)}}})
if _, err := MergeActs(v); err == nil {
t.Error("a merge with no approved heads was taken")
}
}
func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3, 1342/AC2
a := Act{Name: "merge", Repository: "novox/mesh-delivery", Number: 12, Head: headA}
m, err := RefusedMessage(a, "novox/mesh-delivery#12 is not merged: its head moved")
if err != nil {
t.Fatal(err)
}
if m.Event != "forge.novox.mesh-delivery.refused" {
t.Errorf("event = %q", m.Event)
}
f, err := forgemsg.DecodeForge(m.Data)
if err != nil {
t.Fatal(err)
}
want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA,
Act: "merge", Words: "novox/mesh-delivery#12 is not merged: its head moved"}
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v", f)
}
}
@@ -89,7 +89,7 @@ var errNoAnswer = errors.New("the seat git's answer carries nothing")
// MergedMessage is the merged message of a merge into the trunk, asked again: built as the bridge builds the one it writes
// on FORGE (it is that event's body through ConvertKept), from the pull request's owner, repository, number, branch, head
// and merge commit, which the gate's merges state holds. It reads the forge for every module directory (no kept inputs).
// and merge commit, which the seat git's verb pull answers. It reads the forge for every module directory (no kept inputs).
func MergedMessage(owner, repo string, number int, branch, head, merge string, trees Trees, files Files) (Message, error) {
body, err := json.Marshal(git.PullMergedEvent{Repository: repository(owner, repo), Number: int64(number), Branch: branch, Head: head, Base: Trunk, MergeCommit: merge})
if err != nil {
@@ -0,0 +1,147 @@
package bridge
import (
"encoding/json"
"errors"
"fmt"
git "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// OwedCall is one act owed to the forge as the seat git's verb that performs it: the seat that said it (Of), the act's
// id, and the verb with its input (novox/hq design 47 *What the gate owes the forge*). Every verb is safe to repeat at
// the forge: a status and the view replace what stands, a merge of a pull request merged at that head is answered
// performed, and a comment and a note line carry the id, which the seat's holder skips when it already holds it.
type OwedCall struct {
Of string
ID string
Act string
Repository string
Number int
Verb string
Input any
}
// ErrUndecodable is an owed event the bridge cannot read: it is said as owed.ended `undecodable`, never only logged.
var ErrUndecodable = errors.New("an owed act the bridge cannot read")
func undecodable(why string, args ...any) error {
return fmt.Errorf("%w: %s", ErrUndecodable, fmt.Sprintf(why, args...))
}
// GateOwed reads the seat mesh-delivery's event owed into the seat git's verb that performs it.
func GateOwed(data []byte) (OwedCall, error) {
var o seat.OwedEvent
if err := json.Unmarshal(data, &o); err != nil {
return OwedCall{}, undecodable("the gate's owed is not json: %v", err)
}
c := OwedCall{Of: seat.Seat, ID: o.ID, Act: o.Act, Repository: o.Repository, Number: int(o.Number)}
if err := c.common(o.Head); err != nil {
return c, err
}
commit := o.Commit
if commit == "" {
commit = o.Head
}
needsPull := func() error {
if o.Number <= 0 {
return undecodable("the owed %s %s names no pull request", o.Act, o.ID)
}
return nil
}
switch o.Act {
case seat.OwedActMerge:
// Only a switched act is a merge (§169): from G's delivery the gate owes none (§167), a session calls merge-pull
// itself; a merge of the old state, not yet performed by the old bridge, is not dropped.
if err := needsPull(); err != nil {
return c, err
}
c.Verb, c.Input = git.VerbMergePull, git.MergePullInput{ID: o.ID, Repository: o.Repository, Number: o.Number, Head: o.Head}
case seat.OwedActStatus:
if o.Context == "" || o.State == "" {
return c, undecodable("the owed status %s names no context or state", o.ID)
}
c.Verb, c.Input = git.VerbStatus, git.StatusInput{ID: o.ID, Repository: o.Repository, Commit: commit, Context: o.Context,
State: o.State, Description: o.Description, Target: o.Target}
case seat.OwedActView:
if err := needsPull(); err != nil {
return c, err
}
c.Verb, c.Input = git.VerbView, git.ViewInput{ID: o.ID, Repository: o.Repository, Number: o.Number, Text: o.Text}
case seat.OwedActComment:
if err := needsPull(); err != nil {
return c, err
}
c.Verb, c.Input = git.VerbComment, git.CommentInput{ID: o.ID, Repository: o.Repository, Number: o.Number, Text: o.Text}
case seat.OwedActNote:
if o.Line == "" {
return c, undecodable("the owed note line %s says no line", o.ID)
}
c.Verb, c.Input = git.VerbNote, git.NoteInput{ID: o.ID, Repository: o.Repository, Commit: commit, Line: o.Line}
default:
return c, undecodable("the owed %s is the act %q, none the seat git performs", o.ID, o.Act)
}
return c, nil
}
// CheckerOwed reads the seat mesh-delivery-checker's event owed (its mesh/check status) into the seat git's verb status.
func CheckerOwed(data []byte) (OwedCall, error) {
var o checker.OwedEvent
if err := json.Unmarshal(data, &o); err != nil {
return OwedCall{}, undecodable("the checker's owed is not json: %v", err)
}
c := OwedCall{Of: checker.Seat, ID: o.ID, Act: o.Act, Repository: o.Repository, Number: int(o.Number)}
if err := c.common(o.Head); err != nil {
return c, err
}
if o.Act != checker.OwedActStatus || o.Context == "" || o.State == "" {
return c, undecodable("the checker's owed %s is %q of the context %q in the state %q, not a status", o.ID, o.Act, o.Context, o.State)
}
c.Verb, c.Input = git.VerbStatus, git.StatusInput{ID: o.ID, Repository: o.Repository, Commit: o.Head, Context: o.Context,
State: o.State, Description: o.Description, Target: o.Target}
return c, nil
}
func (c OwedCall) common(head string) error {
switch {
case c.ID == "":
return undecodable("an owed %s of %s names no id", c.Act, c.Repository)
case c.Repository == "":
return undecodable("the owed %s names no repository", c.ID)
case !sha40.MatchString(head):
return undecodable("the owed %s names the head %q, not 40 hex digits", c.ID, head)
}
if _, _, err := splitRepository(c.Repository); err != nil {
return undecodable("the owed %s: %v", c.ID, err)
}
return nil
}
// Outcome is what an act's call of the seat git came to, as owed.ended says it: performed when the forge holds the act
// (done now, or its id already held); refused with the forge's own words when the seat answers a refusal or an error
// of the kind refused; could not be performed with the error when the seat did not answer, answered an error of the
// kind failed, or answered what the contract cannot read.
func Outcome(raw json.RawMessage, callErr error) (outcome, words string) {
if callErr != nil {
return forgemsg.OwedCouldNotBePerformed, callErr.Error()
}
var a git.MergePullAnswer // every act verb's answer is this one's fields or fewer
if err := json.Unmarshal(raw, &a); err != nil {
return forgemsg.OwedCouldNotBePerformed, "the seat git's answer is not its contract's: " + err.Error()
}
switch {
case a.Refused != "":
return forgemsg.OwedRefused, a.Refused
case a.Error != "" && a.ErrorKind == git.ErrorKindRefused:
return forgemsg.OwedRefused, a.Error
case a.Error != "":
// failed, or a kind the contract does not name: the forge did not do it, and did not say no
return forgemsg.OwedCouldNotBePerformed, a.Error
case a.Performed || a.Held:
return forgemsg.OwedPerformed, ""
}
return forgemsg.OwedCouldNotBePerformed, "the seat git's answer says neither performed, held, refused nor an error: " + string(raw)
}
@@ -0,0 +1,95 @@
package bridge
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
git "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"mesh-delivery-forge-bridge/internal/gitfixture"
)
// Design 47 *What the gate owes the forge*: each act the gate or the checker says as owed is the seat git's verb of that
// act, under the act's id; what cannot be read is undecodable, and every answer is one outcome.
func TestAnOwedEventIsTheSeatGitsVerbAndItsAnswerOneOutcome(t *testing.T) {
gate := func(name string) []byte { return gitfixture.Seat(t, "mesh-delivery", "events", name, "event:owed")[0] }
chk := gitfixture.Seat(t, "mesh-delivery-checker", "events", "owed is said", "event:owed")[0]
a40 := strings.Repeat("a", 40)
edit := func(raw []byte, k string, v any) []byte {
var m map[string]any
_ = json.Unmarshal(raw, &m)
if v == nil {
delete(m, k)
} else {
m[k] = v
}
b, _ := json.Marshal(m)
return b
}
for _, c := range []struct {
name string
read func([]byte) (OwedCall, error)
data []byte
verb string
input any
bad string
}{
{"a merge on no pull request", GateOwed, edit(gate("owed is said, a merge"), "number", 0), "", nil, "no pull request"},
{"a status on the head", GateOwed, gate("owed is said, a status"), "status", git.StatusInput{ID: "status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate",
Repository: "novox/mesh-sdk", Commit: a40, Context: "mesh/merge-gate", State: "pending", Description: "waits for the operator's Approve", Target: "https://forge.test/x"}, ""},
{"a view", GateOwed, edit(edit(gate("owed is said, a status"), "act", "view"), "text", "the view"), "view",
git.ViewInput{ID: "status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate", Repository: "novox/mesh-sdk", Number: 178, Text: "the view"}, ""},
{"a comment", GateOwed, edit(edit(gate("owed is said, a status"), "act", "comment"), "text", "hello"), "comment",
git.CommentInput{ID: "status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate", Repository: "novox/mesh-sdk", Number: 178, Text: "hello"}, ""},
{"a note line on its commit", GateOwed, edit(edit(edit(gate("owed is said, a status"), "act", "note"), "line", "a line"), "commit", strings.Repeat("c", 40)), "note",
git.NoteInput{ID: "status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/merge-gate", Repository: "novox/mesh-sdk", Commit: strings.Repeat("c", 40), Line: "a line"}, ""},
{"the checker's status", CheckerOwed, chk, "status", git.StatusInput{ID: "status.novox/mesh-sdk#178.aaaaaaaaaaaa.mesh/check",
Repository: "novox/mesh-sdk", Commit: a40, Context: "mesh/check", State: "success", Description: "passed", Target: "https://forge.test/x"}, ""},
{"not json", GateOwed, []byte("{"), "", nil, "not json"},
{"no id", GateOwed, edit(gate("owed is said, a merge"), "id", nil), "", nil, "no id"},
{"a short head", CheckerOwed, edit(chk, "head", "abc"), "", nil, "40 hex"},
{"a merge of the switch", GateOwed, gate("owed is said, a merge"), "merge-pull", git.MergePullInput{ID: "merge.feat=47-G.0123456789ab.novox/mesh-sdk#178",
Repository: "novox/mesh-sdk", Number: 178, Head: a40}, ""},
{"a comment on no pull request", GateOwed, edit(edit(gate("owed is said, a status"), "act", "comment"), "number", 0), "", nil, "no pull request"},
{"an act of no kind", GateOwed, edit(gate("owed is said, a status"), "act", "label"), "", nil, "none the seat git"},
{"a repository not owner/name", GateOwed, edit(gate("owed is said, a status"), "repository", "mesh-sdk"), "", nil, "owner"},
} {
got, err := c.read(c.data)
switch {
case c.bad != "":
if !errors.Is(err, ErrUndecodable) || !strings.Contains(err.Error(), c.bad) {
t.Errorf("%s: %v, want undecodable naming %q", c.name, err, c.bad)
}
case err != nil || got.Verb != c.verb || !reflect.DeepEqual(got.Input, c.input):
t.Errorf("%s: %s %+v %v, want %s %+v", c.name, got.Verb, got.Input, err, c.verb, c.input)
}
}
answers := func(verb string) []json.RawMessage { return gitfixture.All(t, verb+" answers", "verb:"+verb+".answer") }
failed := func(verb string) json.RawMessage {
return gitfixture.Given(t, verb+" answers an error, never none", "verb:"+verb+".answer")
}
for _, c := range []struct {
name string
raw json.RawMessage
err error
outcome, words string
}{
{"a merge performed", answers("merge-pull")[0], nil, forgemsg.OwedPerformed, ""},
{"a merge refused", answers("merge-pull")[1], nil, forgemsg.OwedRefused, "the head moved"},
{"a comment already held", answers("comment")[0], nil, forgemsg.OwedPerformed, ""},
{"the forge's error, of the kind refused", failed("comment"), nil, forgemsg.OwedRefused, "403"},
{"the forge's error, of the kind failed", failed("view"), nil, forgemsg.OwedCouldNotBePerformed, "502"},
{"no answer in the call's bound", nil, errors.New("nats: timeout"), forgemsg.OwedCouldNotBePerformed, "timeout"},
{"an answer saying nothing", json.RawMessage(`{"id":"x"}`), nil, forgemsg.OwedCouldNotBePerformed, "neither"},
{"an answer not the contract's", json.RawMessage(`[1]`), nil, forgemsg.OwedCouldNotBePerformed, "contract"},
} {
outcome, words := Outcome(c.raw, c.err)
if outcome != c.outcome || !strings.Contains(words, c.words) {
t.Errorf("%s: %q %q, want %q naming %q", c.name, outcome, words, c.outcome, c.words)
}
}
}
@@ -13,9 +13,16 @@ import (
// All is every fixture's body named name of of (such as "file answers", "verb:file.answer"), in the file's order; a
// name with none fails the test.
func All(t testing.TB, name, of string) []json.RawMessage {
t.Helper()
return Seat(t, "git", "verbs", name, of)
}
// Seat is every fixture's body named name of of in a seat's fixtures file (such as "mesh-delivery", "events"), so the
// bridge's fakes of the seats it hears say what those seats say.
func Seat(t testing.TB, seat, file, name, of string) []json.RawMessage {
t.Helper()
_, here, _, _ := runtime.Caller(0)
raw, err := os.ReadFile(filepath.Join(filepath.Dir(here), "../../../../contracts/seats/git/fixtures/verbs.json"))
raw, err := os.ReadFile(filepath.Join(filepath.Dir(here), "../../../../contracts/seats", seat, "fixtures", file+".json"))
if err != nil {
t.Fatal(err)
}
@@ -5,6 +5,7 @@
package groups
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -12,12 +13,13 @@ import (
"fmt"
"slices"
"sort"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The bridge's states the tools read and write.
@@ -26,20 +28,34 @@ const (
Module = "mesh-delivery-forge-bridge"
// PerformedState is the bridge's state of what it performed and published.
PerformedState = "performed"
// EndAsksState is the state of asks to say a group's end again, keyed by the gate's merges key of the group.
// EndAsksState is the state of asks to say a group's end again, keyed by the group escaped as a key.
EndAsksState = "end-asks"
// EndAsksBucket is its bucket on the bus.
EndAsksBucket = Module + "_" + EndAsksState
)
// Key is the key of performed that says a group's end is written: `groups.<merges key>`, its value the revision of the
// gate's merges key it was worked out from.
func Key(mergesKey string) string { return "groups." + mergesKey }
// Key is the key of performed that says a group's end is written: `groups.<group, escaped as a key>`, its value the last
// member's merge commit the end named.
func Key(group string) (string, error) {
part, err := KeyOf(group)
if err != nil {
return "", err
}
return "groups." + part, nil
}
// KeyOf is a group's id (its branch) escaped as a key, as the states performed and end-asks key it.
func KeyOf(group string) (string, error) {
if strings.TrimSpace(group) == "" {
return "", errors.New("a group's id is empty")
}
return keys.EscapeName(group), nil
}
// PublishedKey is the key of performed that says a member's merged message is written: `published.<16 hex digits of the
// sha256 of <repository>#<number>@<merge>>`.
func PublishedKey(repository string, number int, merge string) string {
sum := sha256.Sum256([]byte(fmt.Sprintf("%s#%d@%s", strings.ToLower(repository), number, merge)))
// sha256 of <repository>#<number>>`, its value the merge commit the message named (a pull request merges once).
func PublishedKey(repository string, number int) string {
sum := sha256.Sum256([]byte(fmt.Sprintf("%s#%d", strings.ToLower(repository), number)))
return "published." + hex.EncodeToString(sum[:8])
}
@@ -72,13 +88,13 @@ type EndAsk struct {
// Done says the ask is answered.
func (a EndAsk) Done() bool { return !a.Answered.IsZero() }
// NewEndAsk is the value bridge_group_end_again writes for a group: its key (the gate's merges key) and the value.
// NewEndAsk is the value bridge_group_end_again writes for a group: its key (the group escaped as a key) and the value.
func NewEndAsk(group, why, by string, at time.Time) (string, []byte, error) {
group = strings.TrimSpace(group)
if group == "" {
return "", nil, errors.New("name the group whose end is said again (group)")
}
key, err := meshdeliveryclient.MergesKey(group)
key, err := KeyOf(group)
if err != nil {
return "", nil, err
}
@@ -98,21 +114,24 @@ func DecodeEndAsk(data []byte) (EndAsk, error) {
return a, nil
}
// Row is one group as bridge_groups answers it: the gate's merge of it and what the bridge kept of its end.
// Row is one group as bridge_groups answers it: the seat mesh-delivery's group and what the bridge kept of its end.
type Row struct {
Key string `json:"key"`
Group string `json:"group"`
State string `json:"state,omitempty"`
Members []RowMember `json:"members"`
Ended bool `json:"ended"`
EndedAt uint64 `json:"ended-at-revision,omitempty"`
Open bool `json:"open"`
Words string `json:"words"`
// EndedWith is the last member's merge commit the end named, when it is written.
EndedWith string `json:"ended-with,omitempty"`
Open bool `json:"open"`
Words string `json:"words"`
// LastEndAsk is the last ask to say the group's end again (bridge_group_end_again), its who, why and when, and the
// bridge's answer once it read it; absent when none was asked. The state end-asks keeps the last per group.
LastEndAsk *EndAsk `json:"last-end-ask,omitempty"`
}
// RowMember is one approved member of a group: its merge's state at the gate and whether its merged message is written.
// RowMember is one member of a group: its delivery state at the seat and, once its merged message is written, the merge
// commit it named.
type RowMember struct {
Repository string `json:"repository"`
Number int `json:"number"`
@@ -121,12 +140,12 @@ type RowMember struct {
Published bool `json:"published"`
}
// Rows are the groups the gate's merges hold with more than one member, each with what the bridge kept: ended (its end
// written, the revision it was worked out from) or open (some member is not merged or its merged message not written, or its
// end not written). merges is the gate's merges by key, performed the bridge's performed keys by key; unread are the gate's
// keys that do not read. asks are the end-asks values by the same key; one that does not read is named unread as
// `end-asks.<key>`.
func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[string]uint64, asks map[string][]byte) (rows []Row, unread []string) {
// Rows are the groups the seat mesh-delivery's verb groups answers, each with what the bridge kept: ended (its end written,
// with the last merge it named) or open (some member's merged message is not written, or its end not written). published
// is performed's `published.` values by key (the merge commit), ended its `groups.` values by key; asks are the end-asks
// values by the same key, and one that does not read is named unread as `end-asks.<key>`. A group whose id cannot be a
// key is named unread by its id.
func Rows(of []seat.Group, published, ended map[string]string, asks map[string][]byte) (rows []Row, unread []string) {
lastAsk := map[string]*EndAsk{}
for key, raw := range asks {
a, err := DecodeEndAsk(raw)
@@ -136,37 +155,32 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin
}
lastAsk[key] = &a
}
for key, raw := range merges {
m, err := meshdeliveryclient.DecodeMerge(raw)
for _, g := range of {
if len(g.Members) < 2 {
continue
}
key, err := KeyOf(g.ID)
if err != nil {
unread = append(unread, key)
unread = append(unread, "a group with no id")
continue
}
if len(m.Approved) < 2 {
continue
r := Row{Key: key, Group: g.ID, State: g.State}
allPublished := true
for _, m := range g.Members {
commit, pub := published[PublishedKey(m.Repository, int(m.Number))]
r.Members = append(r.Members, RowMember{Repository: m.Repository, Number: int(m.Number), State: m.State, Commit: commit, Published: pub})
allPublished = allPublished && pub
}
r := Row{Key: key, Group: m.Group}
allMerged := true
for _, p := range m.Pulls {
pub := p.Commit != "" && performed[PublishedKey(p.Repository, int(p.Number), p.Commit)]
r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: int(p.Number), State: p.State, Commit: p.Commit, Published: pub})
if meshdeliveryclient.MergeState(p.State) != meshdeliveryclient.MergeMerged || !pub {
allMerged = false
}
}
if len(m.Pulls) < len(m.Approved) {
allMerged = false
}
r.EndedAt, r.Ended = endedAt[key]
r.EndedWith, r.Ended = ended["groups."+key]
r.LastEndAsk = lastAsk[key]
r.Open = !r.Ended
switch {
case r.Ended:
r.Words = "its end is written"
case allMerged:
r.Words = "every member is merged and its message written, and its end is not written: bridge_group_end_again says it"
case allPublished:
r.Words = "every member's merged message is written, and its end is not written: bridge_group_end_again says it"
default:
r.Words = "gathered: not every member is merged and its merged message written"
r.Words = "gathered: not every member's merged message is written"
}
rows = append(rows, r)
}
@@ -177,25 +191,46 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin
func sortRows(rows []Row) { sort.Slice(rows, func(i, j int) bool { return rows[i].Key < rows[j].Key }) }
// MembersOf are a merge's approved members with their heads, in the order of their keys.
func MembersOf(m seat.MergesByGroupValue) []meshdeliveryclient.Head {
var out []meshdeliveryclient.Head
for _, k := range slices.Sorted(func(yield func(string) bool) {
for k := range m.Approved {
if !yield(k) {
return
}
}
}) {
at := strings.LastIndex(k, "@")
if at < 0 {
continue
}
n, err := strconv.Atoi(k[at+1:])
if err != nil {
continue
}
out = append(out, meshdeliveryclient.Head{Repository: k[:at], Number: n, Head: m.Approved[k]})
// MembersOf are a group's members with their heads, in the group's order.
func MembersOf(g seat.Group) []meshdeliveryclient.Head {
out := make([]meshdeliveryclient.Head, 0, len(g.Members))
for _, m := range g.Members {
out = append(out, meshdeliveryclient.Head{Repository: m.Repository, Number: int(m.Number), Head: m.Head})
}
return out
}
// Named is the group of id among the groups the seat answered, when it has more than one member and names the pull
// request number of repository among them (a lone pull request is no group).
func Named(of []seat.Group, id, repository string, number int) (seat.Group, bool) {
for _, g := range of {
if g.ID != id || len(g.Members) < 2 {
continue
}
if slices.ContainsFunc(g.Members, func(m seat.Member) bool {
return trunk.SameRepository(m.Repository, repository) && int(m.Number) == number
}) {
return g, true
}
}
return seat.Group{}, false
}
// Read is every delivery group the seat mesh-delivery's verb groups answers, the ended ones too (all), called through call
// by the verb's manifest key: the bridge reads a group from the seat, never from the gate's state (design 47 *A group's
// merges are taken in together*). An answer saying an error, or one the contract cannot type, is an error: a failed read
// is never read as no group.
func Read(ctx context.Context, call func(ctx context.Context, key string, body any) (json.RawMessage, error)) ([]seat.Group, error) {
raw, err := call(ctx, "seat:"+seat.Seat+"."+seat.VerbGroups, seat.GroupsInput{All: seat.GroupsInputAllTrue})
if err != nil {
return nil, fmt.Errorf("the seat %s did not answer %s: %w", seat.Seat, seat.VerbGroups, err)
}
var a seat.GroupsAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the seat %s's answer of %s is not its contract's: %w", seat.Seat, seat.VerbGroups, err)
}
if a.Error != "" {
return nil, fmt.Errorf("the seat %s refused %s: %s", seat.Seat, seat.VerbGroups, a.Error)
}
return a.Groups, nil
}
@@ -0,0 +1,95 @@
package groups
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
// What a person asks of the bridge's performing of what is owed (novox/hq design 47 *What the gate owes the forge*): the
// tools bridge_owed (its consumers' backlog), bridge_owed_again and bridge_owed_drop write an ask into the state
// owed-asks and wait for the bridge's answer there, as bridge_group_end_again does, since the bridge is the other program
// and serves no request subject (ADR 0437 §4).
const (
// OwedAsksState is the state of asks about what is owed, one key per kind of ask and act.
OwedAsksState = "owed-asks"
// OwedAsksBucket is its bucket on the bus.
OwedAsksBucket = Module + "_" + OwedAsksState
)
// The kinds of an owed ask.
const (
OwedAgain = "again"
OwedDrop = "drop"
OwedBacklog = "backlog"
)
// Backlog is one durable consumer of a seat's stream owed as the bridge reads it: the last event applied (its stream
// sequence), the events waiting and those delivered and not yet acknowledged, and the oldest of them.
type Backlog struct {
Stream string `json:"stream"`
Consumer string `json:"consumer"`
LastApplied uint64 `json:"last-applied"`
Waiting uint64 `json:"waiting"`
Held uint64 `json:"held"`
Oldest time.Time `json:"oldest,omitzero"`
OldestSecs int64 `json:"oldest-secs,omitempty"`
Error string `json:"error,omitempty"`
}
// OwedAsk is a value of owed-asks: what was asked (of the act Of said under ID, for again and drop), why, who and when,
// and once the bridge answered, when, what it came to (the outcome said as owed.ended) and its words; a backlog ask is
// answered with the consumers' backlog.
type OwedAsk struct {
Kind string `json:"kind"`
Of string `json:"of,omitempty"`
ID string `json:"id,omitempty"`
Why string `json:"why,omitempty"`
By string `json:"by,omitempty"`
At time.Time `json:"at"`
Answered time.Time `json:"answered,omitzero"`
Outcome string `json:"outcome,omitempty"`
Answer string `json:"answer,omitempty"`
Backlog []Backlog `json:"backlog,omitempty"`
}
// Done says the bridge answered it.
func (a OwedAsk) Done() bool { return !a.Answered.IsZero() }
// NewOwedAsk is an ask's key and value: a backlog ask under the key backlog, an act's under its kind, its seat and its id
// (unpadded base64url, as an id may hold what a key may not). A drop says why.
func NewOwedAsk(kind, of, id, why, by string, at time.Time) (string, []byte, error) {
a := OwedAsk{Kind: kind, Of: of, ID: id, Why: strings.TrimSpace(why), By: by, At: at.UTC()}
key := kind
switch kind {
case OwedBacklog:
a.Of, a.ID = "", ""
case OwedAgain, OwedDrop:
if of == "" || id == "" {
return "", nil, fmt.Errorf("an owed act is named by the seat that said it and its id")
}
if kind == OwedDrop && a.Why == "" {
return "", nil, fmt.Errorf("a drop says why")
}
key = kind + "." + of + "." + base64.RawURLEncoding.EncodeToString([]byte(id))
default:
return "", nil, fmt.Errorf("no owed ask %q: again, drop or backlog", kind)
}
v, err := json.Marshal(a)
return key, v, err
}
// DecodeOwedAsk reads a value of owed-asks.
func DecodeOwedAsk(data []byte) (OwedAsk, error) {
var a OwedAsk
if err := json.Unmarshal(data, &a); err != nil {
return a, err
}
switch a.Kind {
case OwedAgain, OwedDrop, OwedBacklog:
return a, nil
}
return a, fmt.Errorf("no owed ask %q", a.Kind)
}
@@ -27,6 +27,14 @@ type state struct {
History int `json:"history"`
}
// kept is an entry of requires in its long form: kept data on the bus (novox/hq ADR 0595).
type kept struct {
Name string `json:"name"`
Storage string `json:"storage"`
Template string `json:"template"`
Options map[string]int `json:"options"`
}
type artifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
@@ -46,6 +54,7 @@ type manifest struct {
Consumes []string `json:"consumes"`
Streams []stream `json:"streams"`
State []state `json:"state"`
Requires []kept `json:"requires"`
Reads []string `json:"reads"`
Tools []string `json:"tools"`
Invokes []string `json:"invokes"`
@@ -87,20 +96,22 @@ func TestManifestDeclarations(t *testing.T) { // 1235/AC1
if m.Module != "mesh-delivery-forge-bridge" {
t.Errorf("module = %q", m.Module)
}
if !reflect.DeepEqual(m.Streams, []stream{{Name: "forge", Events: []string{"forge.**"}}}) {
if !reflect.DeepEqual(m.Streams, []stream{{Name: "forge", Events: []string{"forge.**", "owed.ended"}}}) {
t.Errorf("streams = %+v", m.Streams)
}
// The checker's owed is the forge module's to write (ADR 0521, R4e finding R2-1): the bridge neither hears nor reads it.
// The forge's events are the seat git's (design 47 *The seat git*); forge.delete: a branch's end, said as a
// What the gate and the checker owe the forge is each seat's event owed, heard on its own stream (design 47 *What the
// gate owes the forge*), never read from their state. The forge's events are the seat git's (design 47 *The seat git*); forge.delete: a branch's end, said as a
// branch-deleted kind (design 51, DLV-FB1).
if want := []string{"git.forge-delete", "git.pull-closed", "git.pull-merged", "git.pull-updated"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
if want := []string{"git.forge-delete", "git.pull-closed", "git.pull-merged", "git.pull-updated", "mesh-delivery-checker.owed", "mesh-delivery.owed"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
if want := []string{"mesh-delivery-gate.merges", "mesh-delivery-gate.owed"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want)
// A group is read from the seat mesh-delivery's verb groups (design 47, acceptance 49): the bridge reads no state of
// another module's.
if len(m.Reads) != 0 {
t.Errorf("reads = %v, want none", m.Reads)
}
if want := []string{"gitea.gitea_add_comment", "gitea.gitea_commit_status", "gitea.gitea_delivery_view", "gitea.gitea_merge_pull_request",
"gitea.gitea_note_append", "seat:git.file", "seat:git.open-pulls", "seat:git.pull", "seat:git.tags", "seat:git.trees"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
if want := []string{"seat:git.comment", "seat:git.file", "seat:git.merge-pull", "seat:git.note", "seat:git.open-pulls",
"seat:git.pull", "seat:git.status", "seat:git.tags", "seat:git.trees", "seat:git.view", "seat:mesh-delivery.groups", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
@@ -109,19 +120,22 @@ func TestManifestDeclarations(t *testing.T) { // 1235/AC1
if m.SecretsOwner != "mesh-delivery-forge-bridge" {
t.Errorf("secrets-owner = %q", m.SecretsOwner)
}
// Three buckets: the head last written on FORGE for each pull request, so a sync writes only what differs (ADR 0443
// §5), the revision of each key of the gate's buckets the bridge performed, so a key is performed once per
// revision (ADR 0437 §8; issue 1341), and what a merge read of each module directory by its tree, so a merge reads
// the forge only for the directories it changed (issue 1126).
if !reflect.DeepEqual(m.State, []state{{Name: "heads", History: 1}, {Name: "performed", History: 1}, {Name: "inputs", History: 1}, {Name: "end-asks", History: 1}}) { // 1279/AC6, 1341, 1126, 1585
t.Errorf("state = %+v, want the buckets heads, performed, inputs and end-asks", m.State)
// The head last written on FORGE for each pull request, so a sync writes only what differs (ADR 0443 §5), and what a
// merge read of each module directory by its tree, so a merge reads the forge only for the directories it changed
// (issue 1126). What it performed of what is owed is kept data in the new form, one key per act, kept 7 days as the
// owners' streams owed are; its size is the template's own (design 47 *What the gate owes the forge*, ADR 0595 §3).
if !reflect.DeepEqual(m.State, []state{{Name: "heads", History: 1}, {Name: "inputs", History: 1}, {Name: "end-asks", History: 1}, {Name: "owed-asks", History: 1}}) { // 1279/AC6, 1126, 1585
t.Errorf("state = %+v, want the buckets heads, inputs, end-asks and owed-asks", m.State)
}
if want := []kept{{Name: "performed", Storage: "key-value", Template: "state", Options: map[string]int{"history": 1, "ttl-seconds": 604800}}}; !reflect.DeepEqual(m.Requires, want) {
t.Errorf("requires = %+v, want performed as %+v", m.Requires, want)
}
// 1585/T18: the tools read the groups and ask the bridge to say a group's end again, through its state end-asks. The tool bridge_merged_message answers a merge's merged message again (issue 1585); bridge_tags reads a package's tags for the builder's floor (design 51).
if want := []string{"bridge_group_end_again", "bridge_groups", "bridge_merged_message", "bridge_tags"}; !reflect.DeepEqual(sorted(m.Tools), want) {
if want := []string{"bridge_group_end_again", "bridge_groups", "bridge_merged_message", "bridge_owed", "bridge_owed_again", "bridge_owed_drop", "bridge_tags"}; !reflect.DeepEqual(sorted(m.Tools), want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
// No other state, no bus, no seat, no event (ADR 0437 §4; ADR 0521: no check.status).
for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "seat", "seats"} {
// No other state, no seat, no event (ADR 0437 §4; ADR 0521: no check.status); its kept data on the bus is the one entry of requires above.
for _, k := range []string{"logs", "emits", "claims", "provides", "uses", "defines-seats", "seat", "seats"} {
if _, ok := top[k]; ok {
t.Errorf("the manifest declares %q: the bridge has none", k)
}
+25 -14
View File
@@ -6,39 +6,50 @@
"git.pull-updated",
"git.pull-merged",
"git.pull-closed",
"git.forge-delete"
"git.forge-delete",
"mesh-delivery.owed",
"mesh-delivery-checker.owed"
],
"streams": [
{ "name": "forge", "events": ["forge.**"] }
{ "name": "forge", "events": ["forge.**", "owed.ended"] }
],
"state": [
{ "name": "heads", "history": 1 },
{ "name": "performed", "history": 1 },
{ "name": "inputs", "history": 1 },
{ "name": "end-asks", "history": 1 }
{ "name": "end-asks", "history": 1 },
{ "name": "owed-asks", "history": 1 }
],
"tools": [
"bridge_tags",
"bridge_groups",
"bridge_group_end_again",
"bridge_merged_message"
],
"reads": [
"mesh-delivery-gate.merges",
"mesh-delivery-gate.owed"
"bridge_merged_message",
"bridge_owed",
"bridge_owed_again",
"bridge_owed_drop"
],
"invokes": [
"gitea.gitea_merge_pull_request",
"gitea.gitea_commit_status",
"gitea.gitea_delivery_view",
"gitea.gitea_note_append",
"gitea.gitea_add_comment",
"seat:git.status",
"seat:git.comment",
"seat:git.view",
"seat:git.note",
"seat:git.merge-pull",
"seat:mesh-delivery.groups",
"seat:mesh-hand-acts.hand-act",
"seat:git.open-pulls",
"seat:git.trees",
"seat:git.file",
"seat:git.pull",
"seat:git.tags"
],
"requires": [
{
"name": "performed",
"storage": "key-value",
"template": "state",
"options": { "history": 1, "ttl-seconds": 604800 }
}
],
"own-secrets": {
"broker": "${dir:state}/broker"
},