From 060596296006b1b27f5d90fc5b70ba8da3ef749b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 11:34:04 +0100 Subject: [PATCH] Forge bridge: bridge_merged_message answers a merge's merged message again, built as the bridge builds the one it publishes (issue 1585) Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../mesh-delivery-forge-bridge-tools/main.go | 84 ++++++++++++++++++- .../main_test.go | 76 ++++++++++++++++- .../cmd/mesh-delivery-forge-bridge/call.go | 51 +---------- .../cmd/mesh-delivery-forge-bridge/groups.go | 29 +------ .../internal/bridge/call.go | 79 +++++++++++++++++ .../internal/groups/groups.go | 25 ++++++ .../manifest_test.go | 4 +- .../mesh-delivery-forge-bridge/module.json | 3 +- 8 files changed, 269 insertions(+), 82 deletions(-) create mode 100644 modules/mesh-delivery-forge-bridge/internal/bridge/call.go diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go index 86faa05a..74a3f561 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go @@ -8,7 +8,9 @@ import ( "context" "encoding/json" "fmt" + "maps" "os" + "slices" "strconv" "strings" "sync" @@ -17,6 +19,9 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" + + "mesh-delivery-forge-bridge/internal/bridge" "mesh-delivery-forge-bridge/internal/groups" ) @@ -24,15 +29,34 @@ import ( const ( GroupsTool = "bridge_groups" EndAgainTool = "bridge_group_end_again" + // MergedTool answers the merged message of a merge on request (issue 1585). + MergedTool = "bridge_merged_message" ) // 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)) []stdio.Tool { +func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []string, error), merged func(repository, merge string) (json.RawMessage, error)) []stdio.Tool { str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } return []stdio.Tool{ + {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 " + + "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: the gate holds no merge with that commit, or the forge did not answer.", + 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"}}}, + Run: func(args map[string]any) (any, error) { + raw, err := merged(str(args, "repository"), str(args, "merge")) + if err != nil { + return nil, err + } + 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, " + @@ -81,6 +105,43 @@ 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. +func MergedOf(raws map[string][]byte, repository, merge string, trees bridge.Trees, files bridge.Files) (json.RawMessage, error) { + owner, repo, ok := strings.Cut(repository, "/") + if !ok || owner == "" || repo == "" { + return nil, fmt.Errorf("%q is not owner/name", repository) + } + for _, key := range slices.Sorted(maps.Keys(raws)) { + m, err := delivery.DecodeMerge(raws[key]) + if err != nil { + continue + } + at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { + return delivery.SameRepository(p.Repository, repository) && p.Commit == merge + }) + if at < 0 { + continue + } + p := m.Pulls[at] + msg, err := bridge.MergedMessage(owner, repo, p.Number, m.Group, p.Head.Head, merge, trees, files) + if err != nil { + return nil, 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, err + } + if _, approved := m.Approved[delivery.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, err + } + return nil, fmt.Errorf("the gate's merges hold no merge of %s with the commit %s, so its pull request is not known", repository, merge) +} + // 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) { @@ -175,10 +236,29 @@ func askEnd(s stdio.KeptState, bound time.Duration) asker { } } +// askCaller calls a tool as this bundle's module, through the runtime (stdio.Ask), the module's own invokes granting it. +func askCaller(_ context.Context, key string, body any) (json.RawMessage, error) { + return stdio.Ask(key, body) +} + func main() { merges, performed, asks := stdio.State(delivery.GateModule+".merges"), 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() ([]groups.Row, []string, error) { return read(merges, performed, asks) }, + func(repository, merge string) (json.RawMessage, error) { + keys, err := merges.Keys() + if err != nil { + return nil, 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 + } + } + ctx := context.Background() + return MergedOf(raws, repository, merge, bridge.TreesVia(ctx, askCaller), bridge.FilesVia(ctx, askCaller)) + }) if err := stdio.Serve("", tools); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] %v\n", err) os.Exit(1) diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go index 2d1b9e6f..b36dc76c 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main_test.go @@ -1,13 +1,18 @@ package main import ( + "bytes" "encoding/json" + "errors" "strings" "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" + "mesh-delivery-forge-bridge/internal/bridge" + "mesh-delivery-forge-bridge/internal/groups" ) @@ -51,7 +56,7 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { func() ([]groups.Row, []string, error) { rows, unread := groups.Rows(merges, performed, map[string]uint64{"ended": 7}, asks) return rows, unread, nil - }) + }, nil) call := func(name string, args map[string]any) any { for _, tl := range tools { if tl.Name == name { @@ -114,3 +119,72 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { } } } + +// 1585: bridge_merged_message answers the merged message as the bridge publishes it: for a lone pull request the same bytes +// the event gives through Convert, for a member of a delivery group those with its group and members set as the process +// sets them; a merge the gate does not hold, or a forge that does not answer, is said. +func TestTheBridgeToolAnswersTheMergedMessageAsPublished(t *testing.T) { + h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40) + c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40) + trees := func(owner, repo, sha string) (string, map[string]string, error) { + return "root-" + sha[:4], map[string]string{"": "r", "modules/a": "ta-" + sha[:4]}, nil + } + files := func(owner, repo, path, ref string) ([]byte, error) { + if strings.HasSuffix(path, "module.json") { + return []byte(`{"module":"a"}`), nil + } + return nil, bridge.ErrNotFound + } + published := func(repo string, number int, head, branch, merge string) forgemsg.Forge { + body, _ := json.Marshal(map[string]any{"owner": "novox", "repo": repo, "number": number, "head": branch, "head_sha": head, "base": "main", "merge_commit_sha": merge}) + msg, err := bridge.Convert(bridge.MergedSubject, body, trees, files) + if err != nil { + t.Fatal(err) + } + f, err := forgemsg.DecodeForge(msg.Data) + if err != nil { + t.Fatal(err) + } + return f + } + lone, _ := delivery.EncodeMerge(delivery.Merge{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1}, + Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 5, Head: h1}, State: delivery.MergeMerged, Commit: c1}}}) + group, _ := delivery.EncodeMerge(delivery.Merge{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2}, + Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 6, Head: h1}, State: delivery.MergeMerged, Commit: c1}, + {Head: delivery.Head{Repository: "novox/b", Number: 7, Head: h2}, State: delivery.MergeMerging}}}) + for _, c := range []struct { + name string + raws map[string][]byte + repository string + merge string + 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", []delivery.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"}, + } { + t.Run(c.name, func(t *testing.T) { + raw, err := MergedOf(c.raws, c.repository, c.merge, trees, files) + if c.fail != "" { + if err == nil || !strings.Contains(err.Error(), c.fail) { + t.Fatalf("err %v, want %q", err, c.fail) + } + return + } + want, _ := forgemsg.EncodeForge(c.want) + 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, func(string, string, string) (string, map[string]string, error) { + return "", nil, errors.New("the forge does not answer") + }, files); err == nil || !strings.Contains(err.Error(), "does not answer") { + t.Errorf("a forge that does not answer: %v", err) + } +} diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/call.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/call.go index 9310a73d..abf7f86b 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/call.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/call.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "regexp" "strings" "time" @@ -66,58 +65,12 @@ func callerOn(nc *nats.Conn, user string) caller { } } -// commitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea, issue 1234): arguments owner, -// repo and sha, answer {root, trees}, the root directory spelled "" (1234/AC4). -const commitTreesTool = "gitea.gitea_commit_trees" - // treesVia asks the forge for a commit's trees through call. func treesVia(ctx context.Context, call caller) bridge.Trees { - return func(owner, repo, sha string) (string, map[string]string, error) { - raw, err := call(ctx, commitTreesTool, map[string]string{"owner": owner, "repo": repo, "sha": sha}) - if err != nil { - return "", nil, err - } - var a struct { - Root string `json:"root"` - Trees map[string]string `json:"trees"` - } - if err := json.Unmarshal(raw, &a); err != nil { - return "", nil, fmt.Errorf("the answer of %s is not {root, trees}: %w", commitTreesTool, err) - } - if a.Root == "" { - return "", nil, fmt.Errorf("the answer of %s names no root tree", commitTreesTool) - } - return a.Root, a.Trees, nil - } + return bridge.TreesVia(ctx, bridge.Caller(call)) } -// getFileTool is the forge's tool for one file at a ref (mesh-catalog modules/gitea tools/index.ts gitea_get_file): -// arguments owner, repo, path and ref, answer {file: {path, ref, sha, size, content}}, the content decoded to text -// (client.ts getFile). A path the forge does not have fails with `Gitea API : 404 ` (client.ts request). -const getFileTool = "gitea.gitea_get_file" - -// notFound is how the forge's error says 404 (client.ts request: `Gitea API ${path}: ${status} ${text}`; client.ts:670 -// recognises it as /: 404 /). -var notFound = regexp.MustCompile(`: 404 `) - // filesVia asks the forge for a file through call. A file the forge says is not there is bridge.ErrNotFound. func filesVia(ctx context.Context, call caller) bridge.Files { - return func(owner, repo, path, ref string) ([]byte, error) { - raw, err := call(ctx, getFileTool, map[string]string{"owner": owner, "repo": repo, "path": path, "ref": ref}) - if err != nil { - if notFound.MatchString(err.Error()) { - return nil, fmt.Errorf("%w: %v", bridge.ErrNotFound, err) - } - return nil, err - } - var a struct { - File *struct { - Content *string `json:"content"` - } `json:"file"` - } - if err := json.Unmarshal(raw, &a); err != nil || a.File == nil || a.File.Content == nil { - return nil, fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err) - } - return []byte(*a.File.Content), nil - } + return bridge.FilesVia(ctx, bridge.Caller(call)) } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go index 82b1eb12..c8026a15 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go @@ -5,8 +5,6 @@ import ( "errors" "log" "slices" - "strconv" - "strings" "github.com/nats-io/nats.go/jetstream" @@ -86,29 +84,6 @@ func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (delivery.Merge return m, rev, true } -// membersOf are a merge's approved members with their heads, in the order of their keys. -func membersOf(m delivery.Merge) []delivery.Head { - var out []delivery.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, delivery.Head{Repository: k[:at], Number: n, Head: m.Approved[k]}) - } - return out -} - // 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. func (g *grouper) member(ctx context.Context, data []byte) ([]byte, forgemsg.Forge, bool) { @@ -120,7 +95,7 @@ func (g *grouper) member(ctx context.Context, data []byte) ([]byte, forgemsg.For if !ok { return data, f, false } - f.Group, f.Members = m.Group, membersOf(m) + f.Group, f.Members = m.Group, 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) @@ -165,7 +140,7 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, } } end := forgemsg.ForgeGroupEnd{Group: m.Group} - for _, member := range membersOf(m) { + for _, member := range groupkeys.MembersOf(m) { at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return delivery.SameRepository(p.Repository, member.Repository) && p.Number == member.Number }) diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/call.go b/modules/mesh-delivery-forge-bridge/internal/bridge/call.go new file mode 100644 index 00000000..d1160c79 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/call.go @@ -0,0 +1,79 @@ +package bridge + +import ( + "context" + "encoding/json" + "fmt" + "regexp" +) + +// Caller calls a module's tool by its manifest key (`.`) with a JSON body and returns the tool's result: +// the bridge's process calls through the bus as its own principal, its tools bundle through the runtime (stdio.Ask). +type Caller func(ctx context.Context, key string, body any) (json.RawMessage, error) + +// CommitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea, issue 1234): arguments owner, +// repo and sha, answer {root, trees}, the root directory spelled "" (1234/AC4). +const CommitTreesTool = "gitea.gitea_commit_trees" + +// TreesVia asks the forge for a commit's trees through call. +func TreesVia(ctx context.Context, call Caller) Trees { + return func(owner, repo, sha string) (string, map[string]string, error) { + raw, err := call(ctx, CommitTreesTool, map[string]string{"owner": owner, "repo": repo, "sha": sha}) + if err != nil { + return "", nil, err + } + var a struct { + Root string `json:"root"` + Trees map[string]string `json:"trees"` + } + if err := json.Unmarshal(raw, &a); err != nil { + return "", nil, fmt.Errorf("the answer of %s is not {root, trees}: %w", CommitTreesTool, err) + } + if a.Root == "" { + return "", nil, fmt.Errorf("the answer of %s names no root tree", CommitTreesTool) + } + return a.Root, a.Trees, nil + } +} + +// GetFileTool is the forge's tool for one file at a ref (mesh-catalog modules/gitea tools/index.ts gitea_get_file): +// arguments owner, repo, path and ref, answer {file: {path, ref, sha, size, content}}, the content decoded to text +// (client.ts getFile). A path the forge does not have fails with `Gitea API : 404 ` (client.ts request). +const GetFileTool = "gitea.gitea_get_file" + +// notFound is how the forge's error says 404 (client.ts request: `Gitea API ${path}: ${status} ${text}`; client.ts:670 +// recognises it as /: 404 /). +var notFound = regexp.MustCompile(`: 404 `) + +// FilesVia asks the forge for a file through call. A file the forge says is not there is ErrNotFound. +func FilesVia(ctx context.Context, call Caller) Files { + return func(owner, repo, path, ref string) ([]byte, error) { + raw, err := call(ctx, GetFileTool, map[string]string{"owner": owner, "repo": repo, "path": path, "ref": ref}) + if err != nil { + if notFound.MatchString(err.Error()) { + return nil, fmt.Errorf("%w: %v", ErrNotFound, err) + } + return nil, err + } + var a struct { + File *struct { + Content *string `json:"content"` + } `json:"file"` + } + if err := json.Unmarshal(raw, &a); err != nil || a.File == nil || a.File.Content == nil { + return nil, fmt.Errorf("the answer of %s is not {file: {content}}: %v", GetFileTool, err) + } + return []byte(*a.File.Content), nil + } +} + +// 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). +func MergedMessage(owner, repo string, number int, branch, head, merge string, trees Trees, files Files) (Message, error) { + body, err := json.Marshal(pull{Owner: owner, Repo: repo, Number: number, Head: branch, HeadSHA: head, Base: Trunk, MergeSHA: merge}) + if err != nil { + return Message{}, err + } + return ConvertKept(MergedSubject, body, trees, files, nil) +} diff --git a/modules/mesh-delivery-forge-bridge/internal/groups/groups.go b/modules/mesh-delivery-forge-bridge/internal/groups/groups.go index bd09dbb7..8b731a47 100644 --- a/modules/mesh-delivery-forge-bridge/internal/groups/groups.go +++ b/modules/mesh-delivery-forge-bridge/internal/groups/groups.go @@ -10,7 +10,9 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" + "strconv" "strings" "time" @@ -171,3 +173,26 @@ 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 delivery.Merge) []delivery.Head { + var out []delivery.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, delivery.Head{Repository: k[:at], Number: n, Head: m.Approved[k]}) + } + return out +} diff --git a/modules/mesh-delivery-forge-bridge/manifest_test.go b/modules/mesh-delivery-forge-bridge/manifest_test.go index f702fa2f..59b8ed3c 100644 --- a/modules/mesh-delivery-forge-bridge/manifest_test.go +++ b/modules/mesh-delivery-forge-bridge/manifest_test.go @@ -112,8 +112,8 @@ func TestManifestDeclarations(t *testing.T) { // 1235/AC1 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) } - // 1585/T18: the tools read the groups and ask the bridge to say a group's end again, through its state end-asks. - if want := []string{"bridge_group_end_again", "bridge_groups"}; !reflect.DeepEqual(sorted(m.Tools), 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). + if want := []string{"bridge_group_end_again", "bridge_groups", "bridge_merged_message"}; !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). diff --git a/modules/mesh-delivery-forge-bridge/module.json b/modules/mesh-delivery-forge-bridge/module.json index 338d9cdc..4b677cce 100644 --- a/modules/mesh-delivery-forge-bridge/module.json +++ b/modules/mesh-delivery-forge-bridge/module.json @@ -18,7 +18,8 @@ ], "tools": [ "bridge_groups", - "bridge_group_end_again" + "bridge_group_end_again", + "bridge_merged_message" ], "reads": [ "mesh-delivery-gate.merges",