Take-in and forge contracts live with their owners: the indexer's takein package, the forge bridge's forgemsg package; mesh-sdk pinned at main (issue 1585, ADR 0633)

The operator, 2026-11-04: logic one module owns lives inside that module as a package of its own, so the take-in step
changes no mesh-sdk. TakeIn, its keys and event names, PendingWait and PendingAnswer move to
modules/mesh-delivery-indexer/takein (its merges a takein.Merge of its own); Forge whole, with its group fields,
ForgeGroupEnd and GroupMerge, moves to modules/mesh-delivery-forge-bridge/forgemsg. Both are nested Go modules that their
readers take by replace, as buildrun is. Every importer moves in this commit: the indexer, the bridge, the watchman and
the view. Forge's private checks (the repository name, the version) are copied small; mesh-sdk main still holds an unused
Forge until its own fold removes it.

The indexer, bridge, watchman, view and checker pin mesh-sdk 5a38a12 (origin/main), no feat/take-in commit.

The bridge's integration bus now grants the state end-asks that bridge_group_end_again added: without it every test of
the package failed on a permissions violation. Test budget: two new test functions (TestForge, TestTakeInEvents) and the
two the join added are paid by merging four pairs in internal/bridge; the repository stays at 806 unit tests.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-05 00:05:01 +01:00
parent a395b49be2
commit 4e4601a1eb
67 changed files with 1547 additions and 425 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-delivery-checker
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,7 +1,7 @@
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg=
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -9,6 +9,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -120,8 +121,8 @@ func TestARefusedActIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1341/AC
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 := delivery.DecodeForge(pub.data[0])
if err != nil || m.Kind != delivery.ForgeRefused || m.Act != "comment.1" || m.Number != 12 || m.Head != headA || m.Words != "the pull request is locked" {
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
@@ -188,8 +189,8 @@ func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/
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 := delivery.DecodeForge(pub.data[0])
if err != nil || m.Kind != delivery.ForgeRefused || m.Repository != "novox/mesh-delivery" || m.Number != 12 || m.Head != headA ||
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)
}
@@ -10,6 +10,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
groupkeys "mesh-delivery-forge-bridge/internal/groups"
@@ -58,7 +59,7 @@ func kvMerges(kv jetstream.KeyValue) mergesRead {
// 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 delivery.Forge) (delivery.Merge, uint64, bool) {
func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (delivery.Merge, uint64, bool) {
if g == nil || g.merges == nil || f.Branch == "" {
return delivery.Merge{}, 0, false
}
@@ -110,9 +111,9 @@ func membersOf(m delivery.Merge) []delivery.Head {
// 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, delivery.Forge, bool) {
f, err := delivery.DecodeForge(data)
if err != nil || f.Kind != delivery.ForgeMerged {
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)
@@ -120,7 +121,7 @@ func (g *grouper) member(ctx context.Context, data []byte) ([]byte, delivery.For
return data, f, false
}
f.Group, f.Members = m.Group, membersOf(m)
out, err := delivery.EncodeForge(f)
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)
return data, f, false
@@ -129,7 +130,7 @@ func (g *grouper) member(ctx context.Context, data []byte) ([]byte, delivery.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 delivery.Forge) {
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)
return
@@ -163,7 +164,7 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte,
return false
}
}
end := delivery.ForgeGroupEnd{Group: m.Group}
end := forgemsg.ForgeGroupEnd{Group: m.Group}
for _, member := range membersOf(m) {
at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool {
return delivery.SameRepository(p.Repository, member.Repository) && p.Number == member.Number
@@ -174,14 +175,14 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte,
if _, ok, err := g.done.Get(ctx, groupkeys.PublishedKey(member.Repository, member.Number, m.Pulls[at].Commit)); err != nil || !ok {
return false // its merged message is not written yet: said when it is (published)
}
end.Members = append(end.Members, delivery.GroupMerge{Repository: member.Repository, Number: member.Number, Merge: m.Pulls[at].Commit})
end.Members = append(end.Members, forgemsg.GroupMerge{Repository: member.Repository, Number: member.Number, Merge: m.Pulls[at].Commit})
}
// The members in the order the gate merged them, so the last commit names the end.
slices.SortStableFunc(end.Members, func(a, b delivery.GroupMerge) int {
slices.SortStableFunc(end.Members, func(a, b forgemsg.GroupMerge) int {
return slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == a.Merge }) -
slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == b.Merge })
})
data, err := delivery.EncodeForgeGroupEnd(end)
data, err := forgemsg.EncodeForgeGroupEnd(end)
if err != nil {
g.logger.Printf("the end of the group %q cannot be written: %v", m.Group, err)
return false
@@ -190,7 +191,7 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte,
if again != "" {
id += "." + again
}
if err := g.send(ctx, forgeSubject(delivery.ForgeGroupEndEvent), data, id); err != nil {
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)
return false
}
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
groupkeys "mesh-delivery-forge-bridge/internal/groups"
@@ -50,7 +51,7 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
return v, 7, ok, nil
}}
mergedOf := func(repo string, number int, head, commit string) []byte {
data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/take-in", Number: number, Head: head, Merge: commit, Root: "r"})
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 {
t.Fatal(err)
}
@@ -63,7 +64,7 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
// The first member: its message names the group and both members; the group is not ended.
gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), dlv(delivery.MergeMerging, ""))
data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1))
if back, err := delivery.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 {
t.Fatalf("a member's message: %+v %v", back, err)
}
g.published(ctx, f)
@@ -82,7 +83,7 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
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 := delivery.DecodeForgeGroupEnd(pub.data[0])
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)
}
@@ -20,6 +20,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -36,7 +37,7 @@ import (
//
// 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, and the stream forge; `**` is written `>` on the wire (modstreams.go
// 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.
const busConf = `
@@ -83,6 +84,12 @@ accounts {
"$JS.FC.KV_mesh-delivery-forge-bridge_inputs.>"
"$KV.mesh-delivery-forge-bridge_performed.>"
"$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.>"
"$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_end-asks.>"
"$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.>"
@@ -321,13 +328,13 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
if err != nil {
t.Fatal(err)
}
f, err := delivery.DecodeForge(raw.Data)
f, err := forgemsg.DecodeForge(raw.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
want := forgemsg.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
Action: "updated", Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}}
if !delivery.SameForge(f, want) {
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
}
@@ -416,16 +423,16 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
if err != nil {
t.Fatal(err)
}
f, err := delivery.DecodeForge(raw.Data)
f, err := forgemsg.DecodeForge(raw.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
want := forgemsg.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"},
Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)},
// No mesh package: only the indexer computes what a fingerprint takes (ADR 0593 §2).
Locks: map[string]map[string]string{"modules/a": {"go.sum": hex.EncodeToString(sha256Sum(sum))}}}
if !delivery.SameForge(f, want) {
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
}
select {
@@ -478,7 +485,7 @@ func TestBridgeSaysTheOpenPullsAtStart(t *testing.T) { // 1237/AC1
if err != nil {
t.Fatal(err)
}
if f, err := delivery.DecodeForge(raw.Data); err != nil || f.Action != "updated" || f.DirsKnown || f.Branch != "feat/x" {
if f, err := forgemsg.DecodeForge(raw.Data); err != nil || f.Action != "updated" || f.DirsKnown || f.Branch != "feat/x" {
t.Errorf("%s: %+v err %v", repo, f, err)
}
}
@@ -548,20 +555,20 @@ func TestBridgeReadsTheForgeAtStartAndAfterAReconnectAndHearsNoOlderEvent(t *tes
t.Fatalf("FORGE holds %d messages (%v), want %d", info.State.Msgs, err, want)
}
}
named := func(seq uint64) []delivery.OpenPull {
named := func(seq uint64) []forgemsg.OpenPull {
t.Helper()
raw, err := forge.GetMsg(b.ctx, seq)
if err != nil || raw.Subject != openSubject {
t.Fatalf("message %d: %v %v, want the open message", seq, raw, err)
}
f, err := delivery.DecodeForge(raw.Data)
f, err := forgemsg.DecodeForge(raw.Data)
if err != nil {
t.Fatal(err)
}
return f.Open
}
bothOpen := []delivery.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
same := func(got []delivery.OpenPull) bool {
bothOpen := []forgemsg.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
same := func(got []forgemsg.OpenPull) bool {
return len(got) == 2 && got[0] == bothOpen[0] && got[1] == bothOpen[1]
}
@@ -713,7 +720,7 @@ func forgeMerged(t *testing.T, b *bed) []string {
if err != nil {
continue
}
if f, err := delivery.DecodeForge(raw.Data); err == nil && f.Kind == delivery.ForgeMerged {
if f, err := forgemsg.DecodeForge(raw.Data); err == nil && f.Kind == forgemsg.ForgeMerged {
out = append(out, f.Repository)
}
}
@@ -21,6 +21,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"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"
@@ -95,7 +96,7 @@ func handleGrouped(ctx context.Context, m delivered, pub publisher, trees bridge
if id := m.Headers().Get("x-event-id"); id != "" {
opts = append(opts, jetstream.WithMsgID(id))
}
var member delivery.Forge
var member forgemsg.Forge
grouped := false
if g != nil {
msg.Data, member, grouped = g.member(ctx, msg.Data)
@@ -18,7 +18,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"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"
)
@@ -85,7 +85,7 @@ func TestReadableEventIsWrittenOnForgeAndAcked(t *testing.T) { // 1235/AC2 (unit
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull" {
t.Fatalf("published on %v", pub.subjects)
}
f, err := delivery.DecodeForge(pub.data[0])
f, err := forgemsg.DecodeForge(pub.data[0])
if err != nil || f.Number != 12 || f.Branch != "feat/x" || f.Head != head || f.Action != "updated" || !f.DirsKnown {
t.Errorf("message %+v err %v", f, err)
}
@@ -172,7 +172,7 @@ func TestMergeCallsTheToolOnceAndWritesTheMergedMessage(t *testing.T) { // 1236/
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.merged" {
t.Fatalf("published on %v", pub.subjects)
}
f, err := delivery.DecodeForge(pub.data[0])
f, err := forgemsg.DecodeForge(pub.data[0])
if err != nil || f.Root != "r0" || f.Trees[""] != "t0" || f.Trees["modules/a"] != "t1" || f.Merge == "" {
t.Errorf("message %+v err %v", f, err)
}
@@ -10,7 +10,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"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"
)
@@ -41,7 +41,7 @@ func (r *recorder) Publish(ctx context.Context, subject string, payload []byte,
if err != nil {
return ack, err
}
f, derr := delivery.DecodeForge(payload)
f, derr := forgemsg.DecodeForge(payload)
if derr != nil {
return ack, nil
}
@@ -101,7 +101,7 @@ func syncOpen(ctx context.Context, call caller, pub publisher, heads headStore,
}
var differing []bridge.Message
for _, m := range msgs {
f, err := delivery.DecodeForge(m.Data)
f, err := forgemsg.DecodeForge(m.Data)
if err != nil {
differing = append(differing, m)
continue
@@ -13,7 +13,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// openAnswer is gitea_open_pulls's answer as mesh-catalog modules/gitea/client.ts:369-381 builds its entries inside {pulls}.
@@ -43,7 +43,7 @@ func TestSyncOpenWritesOneUpdatedMessagePerOpenPull(t *testing.T) { // 1237/AC1,
}
want := []string{"mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull", "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-catalog.pull"}
for i, w := range want {
f, err := delivery.DecodeForge(pub.data[i])
f, err := forgemsg.DecodeForge(pub.data[i])
if pub.subjects[i] != w || err != nil || f.Action != "updated" || f.DirsKnown || f.Branch != "feat/x" {
t.Errorf("message %d on %s = %+v err %v", i, pub.subjects[i], f, err)
}
@@ -112,8 +112,8 @@ func TestSyncOpenWritesPullsForNewHeadsThenOneOpenMessageNamingAll(t *testing.T)
if pub.subjects[1] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.open" {
t.Fatalf("last message on %s, want the open message after the pull messages", pub.subjects[1])
}
f, err := delivery.DecodeForge(pub.data[1])
want := []delivery.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
f, err := forgemsg.DecodeForge(pub.data[1])
want := []forgemsg.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
if err != nil || len(f.Open) != 2 || f.Open[0] != want[0] || f.Open[1] != want[1] {
t.Errorf("open message = %+v err %v, want it to name %v, the pull request whose head did not differ among them", f, err, want)
}
@@ -140,10 +140,10 @@ func TestWhatIsWrittenOnFORGEIsKeptInTheBucket(t *testing.T) { // 1279/AC6
pub := &fakePub{}
heads := newHeads()
r := &recorder{pub: pub, heads: heads, logger: log.New(&bytes.Buffer{}, "", 0)}
write := func(f delivery.Forge) {
write := func(f forgemsg.Forge) {
t.Helper()
ev, _ := delivery.ForgeEvent(f.Repository, f.Kind)
raw, err := delivery.EncodeForge(f)
ev, _ := forgemsg.ForgeEvent(f.Repository, f.Kind)
raw, err := forgemsg.EncodeForge(f)
if err != nil {
t.Fatal(err)
}
@@ -151,19 +151,19 @@ func TestWhatIsWrittenOnFORGEIsKeptInTheBucket(t *testing.T) { // 1279/AC6
t.Fatal(err)
}
}
p := delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated}
p := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated}
write(p)
if heads.get("novox.mesh-delivery.12") != head {
t.Fatalf("bucket %v", heads.vals)
}
p.Action = delivery.PullClosed
p.Action = forgemsg.PullClosed
write(p)
if _, kept := heads.vals["novox.mesh-delivery.12"]; kept {
t.Errorf("a closed pull request is still kept: %v", heads.vals)
}
p.Action = delivery.PullUpdated
p.Action = forgemsg.PullUpdated
write(p)
write(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}})
write(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}})
if _, kept := heads.vals["novox.mesh-delivery.12"]; kept {
t.Errorf("a merged pull request is still kept: %v", heads.vals)
}
@@ -0,0 +1,413 @@
// Package forgemsg is the forge bridge's contract on its own stream `forge` (novox/hq ADR 0633 §4): its messages (Forge,
// one per pull request, merge, refusal or list of open pull requests) and a delivery group's end (ForgeGroupEnd). The
// forge bridge writes them; the indexer and the gate read them. It is the forge bridge's own package: one module owns
// these types, so they live with it and not in mesh-sdk (the operator, 2026-11-04).
package forgemsg
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Version is the version of every value this package writes and the only one it reads: mesh-sdk's delivery values'.
const Version = delivery.Version
// ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names.
const ForgeStream = "forge"
// The kinds of forge message, the last part of a forge event's name.
const (
ForgePull = "pull"
ForgeMerged = "merged"
ForgeRefused = "refused"
ForgeOpen = "open"
)
// OpenPull names an open pull request into the trunk: the repository as `<owner>/<name>` and the number.
type OpenPull struct {
Repository string `json:"repository"`
Number int `json:"number"`
}
// OpenEvent is the name of an open message: `forge.open`. It names no repository: it says every open pull request of
// every repository the forge bridge reads (novox/hq ADR 0443 §5).
func OpenEvent() string { return "forge." + ForgeOpen }
// The actions of a pull message.
const (
PullOpened = "opened"
PullUpdated = "updated"
PullClosed = "closed"
)
// checkRepository refuses a repository that is not `<owner>/<name>`: exactly one slash, each part a valid name.
func checkRepository(repository string) error {
owner, name, ok := strings.Cut(repository, "/")
if !ok || strings.Contains(name, "/") {
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
if err := checkName("repository owner", owner); err != nil {
return fmt.Errorf("delivery: the repository %q: %w", repository, err)
}
if err := checkName("repository name", name); err != nil {
return fmt.Errorf("delivery: the repository %q: %w", repository, err)
}
if strings.Contains(owner, "/") {
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
return nil
}
func checkForgeKind(kind string) error {
switch kind {
case ForgePull, ForgeMerged, ForgeRefused, ForgeOpen:
return nil
}
return fmt.Errorf("delivery: the forge kind %q is not pull, merged, refused or open", kind)
}
// ForgeEvent is the name of a forge message of the given kind: `forge.<owner>.<name>.<kind>`.
func ForgeEvent(repository, kind string) (string, error) {
if err := checkForgeKind(kind); err != nil {
return "", err
}
if kind == ForgeOpen {
return "", fmt.Errorf("delivery: an open message names no repository, its event is %s", OpenEvent())
}
if err := checkRepository(repository); err != nil {
return "", err
}
owner, name, _ := strings.Cut(repository, "/")
return "forge." + owner + "." + name + "." + kind, nil
}
// ParseForgeEvent is the inverse of ForgeEvent.
func ParseForgeEvent(event string) (repository, kind string, err error) {
if event == OpenEvent() {
return "", ForgeOpen, nil
}
parts := strings.Split(event, ".")
if len(parts) != 4 || parts[0] != "forge" {
return "", "", fmt.Errorf("delivery: %q is not a forge event, forge.<owner>.<name>.<kind>", event)
}
repository = parts[1] + "/" + parts[2]
if _, err := ForgeEvent(repository, parts[3]); err != nil {
return "", "", fmt.Errorf("delivery: %q is not a forge event: %w", event, err)
}
return repository, parts[3], nil
}
// Forge is a message of the forge bridge's stream, trimmed to repository, branch, pull request and head. A pull
// message says Action; a merged message carries the merge commit, the tree of every directory holding a
// module.json and the repository's root tree; a refused message names the owed act it answers and the forge's own
// words. A pull message may also carry the module directories its head changes (Dirs, in the order the forge said) and
// DirsKnown, true when the forge could say them: known and empty is DirsKnown with no Dirs, not known is neither
// (the forge's module_dirs and module_dirs_said, mesh-catalog modules/gitea/trunk.ts). Other messages carry neither.
type Forge struct {
V int `json:"v"`
Kind string `json:"kind"`
Repository string `json:"repository"`
Branch string `json:"branch,omitempty"`
Number int `json:"number,omitempty"`
Head string `json:"head,omitempty"`
Action string `json:"action,omitempty"`
Dirs []string `json:"dirs,omitempty"`
DirsKnown bool `json:"dirs-known,omitempty"`
Merge string `json:"merge,omitempty"`
Trees map[string]string `json:"trees,omitempty"`
Root string `json:"root,omitempty"`
// Manifests are the module.json bytes, exact, of each module directory: of the merge's trees on a merged message,
// of Dirs at the head on a pull message. Locks are, of each such directory, its lock files' hex sha256 by path
// relative to it (novox/hq ADR 0465 clause 1). Older writers send neither.
Manifests map[string]string `json:"manifests,omitempty"`
Locks map[string]map[string]string `json:"locks,omitempty"`
// Paths are, of each module directory whose manifest names paths on every artifact, each named path's git object
// id at the same commit as its manifest, by path relative to the directory (novox/hq ADR 0499 clause 3). The
// indexer names the paths; a message without them gives such a module none. Older writers send none.
Paths map[string]map[string]string `json:"paths,omitempty"`
// Packages are, of each module directory, the mesh packages its lock file names (MeshPackagesOf; novox/hq ADR 0573
// §2): the indexer keeps them in the module's inputs and takes each one's latest published version into its
// fingerprint. Absent for a directory that uses none, and from an older writer.
Packages map[string][]string `json:"packages,omitempty"`
Act string `json:"act,omitempty"`
Words string `json:"words,omitempty"`
Open []OpenPull `json:"open,omitempty"`
// RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is
// the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is
// refused, loudly, rather than read with them dropped.
RetiredPlacements json.RawMessage `json:"placements,omitempty"`
RetiredRead bool `json:"placements-read,omitempty"`
// Predefined is the catalogue's predefined.json, exact, at the merge commit of a merged message (novox/hq ADR 0565
// §2): what the indexer keeps as its state `predefined`. PredefinedRead says the bridge read the merge and it holds
// no predefined.json. Carried on a merged message only; an older bridge sends neither, and the indexer keeps what it
// held.
Predefined json.RawMessage `json:"predefined,omitempty"`
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
// with no group carries neither, and an older bridge sends neither; an older reader ignores both.
Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
}
// checkGroup refuses a group on anything but a merged message, a group without its members or members without a
// group, a member that is not a repository's pull request at a head, a member named twice, and members that leave out
// the message's own pull request.
func (f Forge) checkGroup() error {
if f.Group == "" && len(f.Members) == 0 {
return nil
}
if f.Kind != ForgeMerged {
return fmt.Errorf("delivery: only a merged message names its delivery group, not a %s message", f.Kind)
}
if f.Group == "" || len(f.Members) == 0 {
return fmt.Errorf("delivery: a merged message names its group and the group's members together, or neither")
}
self := false
seen := map[string]bool{}
for _, m := range f.Members {
if err := checkRepository(m.Repository); err != nil {
return err
}
if m.Number <= 0 || m.Head == "" {
return fmt.Errorf("delivery: the member %s of the group %q needs its pull request number and head", m.Repository, f.Group)
}
k := fmt.Sprintf("%s#%d", m.Repository, m.Number)
if seen[k] {
return fmt.Errorf("delivery: the group %q names its member %s twice", f.Group, k)
}
seen[k] = true
self = self || (delivery.SameRepository(m.Repository, f.Repository) && m.Number == f.Number)
}
if !self {
return fmt.Errorf("delivery: the members of the group %q leave out the merged pull request %s#%d", f.Group, f.Repository, f.Number)
}
return nil
}
// checkPredefined refuses the predefined list on anything but a merged message, a list that is not one JSON object, and
// a list said both present and absent.
func (f Forge) checkPredefined() error {
if len(f.Predefined) == 0 && !f.PredefinedRead {
return nil
}
if f.Kind != ForgeMerged {
return fmt.Errorf("delivery: only a merged message carries the predefined list, not a %s message", f.Kind)
}
if len(f.Predefined) > 0 && f.PredefinedRead {
return fmt.Errorf("delivery: a merged message carries the predefined list and says the merge holds none (predefined-read)")
}
var list map[string]json.RawMessage
if len(f.Predefined) > 0 && json.Unmarshal(f.Predefined, &list) != nil {
return fmt.Errorf("delivery: the predefined list a merged message carries is not one JSON object")
}
return nil
}
func (f Forge) check() error {
if err := checkForgeKind(f.Kind); err != nil {
return err
}
if f.Kind == ForgeOpen {
return f.checkOpen()
}
if len(f.Open) > 0 {
return fmt.Errorf("delivery: only an open message names open pull requests")
}
if len(f.RetiredPlacements) > 0 || f.RetiredRead {
return fmt.Errorf("delivery: a forge message carries placement files, which no repository holds since ADR 0567: " +
"placement is the seat mesh-placement's state")
}
if err := f.checkPredefined(); err != nil {
return err
}
if err := f.checkGroup(); err != nil {
return err
}
if err := checkRepository(f.Repository); err != nil {
return err
}
switch f.Kind {
case ForgePull:
if f.Number <= 0 {
return fmt.Errorf("delivery: a pull message needs its pull request number")
}
if f.Head == "" {
return fmt.Errorf("delivery: a pull message needs its head")
}
switch f.Action {
case PullOpened, PullUpdated, PullClosed:
default:
return fmt.Errorf("delivery: the pull action %q is not opened, updated or closed", f.Action)
}
if len(f.Dirs) > 0 && !f.DirsKnown {
return fmt.Errorf("delivery: a pull message with module directories must say it knows them (dirs-known)")
}
dirs := map[string]bool{}
for _, d := range f.Dirs {
dirs[d] = true
}
if err := f.checkInputs(func(d string) bool { return dirs[d] }, "its module directories"); err != nil {
return err
}
case ForgeMerged:
if len(f.Dirs) > 0 || f.DirsKnown {
return fmt.Errorf("delivery: a merged message carries no module directories")
}
if f.Number <= 0 {
return fmt.Errorf("delivery: a merged message needs its pull request number")
}
if f.Head == "" {
return fmt.Errorf("delivery: a merged message needs its head")
}
if f.Merge == "" {
return fmt.Errorf("delivery: a merged message needs its merge commit")
}
if f.Root == "" {
return fmt.Errorf("delivery: a merged message needs the repository's root tree")
}
// No trees is a merge that leaves the repository with no module.json anywhere (a documents repository, or one
// whose last module was removed): it is written all the same, so the indexer takes the merge in and drops the
// repository's modules (novox/hq issue 1126). An older writer always sent at least one tree, so none is unambiguous;
// such a message carries no manifests, locks or paths (checkInputs below: each names a directory of its trees).
for dir, tree := range f.Trees {
if tree == "" {
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
}
}
if err := f.checkInputs(func(d string) bool { _, ok := f.Trees[d]; return ok }, "its trees"); err != nil {
return err
}
case ForgeRefused:
if len(f.Dirs) > 0 || f.DirsKnown {
return fmt.Errorf("delivery: a refused message carries no module directories")
}
if len(f.Manifests) > 0 || len(f.Locks) > 0 || len(f.Paths) > 0 {
return fmt.Errorf("delivery: a refused message carries no manifests, lock hashes or named paths")
}
if f.Act == "" {
return fmt.Errorf("delivery: a refused message needs the act it answers")
}
if f.Words == "" {
return fmt.Errorf("delivery: a refused message needs the forge's words")
}
}
return nil
}
// checkInputs refuses a manifest or a lock hash of a directory that known does not know (of is what it knows them
// by), an empty manifest and an empty lock path or hash, naming the directory.
func (f Forge) checkInputs(known func(dir string) bool, of string) error {
for dir, m := range f.Manifests {
if !known(dir) {
return fmt.Errorf("delivery: the manifest of the directory %q is not of %s", dir, of)
}
if m == "" {
return fmt.Errorf("delivery: the manifest of the directory %q is empty", dir)
}
}
for dir, locks := range f.Locks {
if !known(dir) {
return fmt.Errorf("delivery: the lock hashes of the directory %q are not of %s", dir, of)
}
for path, hash := range locks {
if path == "" || hash == "" {
return fmt.Errorf("delivery: the lock %q of the directory %q has an empty path or hash", path, dir)
}
}
}
for dir, paths := range f.Paths {
if !known(dir) {
return fmt.Errorf("delivery: the named paths of the directory %q are not of %s", dir, of)
}
for path, object := range paths {
if path == "" || object == "" {
return fmt.Errorf("delivery: the named path %q of the directory %q has an empty path or object", path, dir)
}
}
}
return nil
}
// checkOpen refuses an open message that names a repository, a branch, a head or anything of the other kinds, or
// an open pull request that is not `<owner>/<name>` and a number.
func (f Forge) checkOpen() error {
if f.Repository != "" || f.Branch != "" || f.Number != 0 || f.Head != "" || f.Action != "" || len(f.Dirs) > 0 || f.DirsKnown ||
f.Merge != "" || len(f.Trees) > 0 || len(f.Manifests) > 0 || len(f.Locks) > 0 || len(f.Paths) > 0 || f.Root != "" || f.Act != "" || f.Words != "" || f.Group != "" || len(f.Members) > 0 {
return fmt.Errorf("delivery: an open message carries only the open pull requests it names")
}
for _, p := range f.Open {
if err := checkRepository(p.Repository); err != nil {
return err
}
if p.Number <= 0 {
return fmt.Errorf("delivery: the open pull request of %s has no number", p.Repository)
}
}
return nil
}
// EncodeForge is f as JSON at Version; it refuses a message missing what its kind needs.
func EncodeForge(f Forge) ([]byte, error) {
if err := f.check(); err != nil {
return nil, err
}
f.V = Version
return json.Marshal(f)
}
// DecodeForge reads a forge message; it refuses an unknown version (ErrUnknownVersion) and what EncodeForge refuses.
func DecodeForge(data []byte) (Forge, error) {
var f Forge
if err := decode(data, &f); err != nil {
return Forge{}, err
}
if err := f.check(); err != nil {
return Forge{}, err
}
return f, nil
}
// SameForge says whether two forge messages are the same value.
func SameForge(a, b Forge) bool { return reflect.DeepEqual(a, b) }
var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
// checkName refuses a name that is empty or holds a dot, a wildcard, whitespace or a character a bus key refuses
// (mesh-sdk delivery's rule for a name).
func checkName(kind, name string) error {
if name == "" {
return fmt.Errorf("delivery: the %s name is empty", kind)
}
if !nameOK.MatchString(name) {
return fmt.Errorf("delivery: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name)
}
return nil
}
// decode reads a value of Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk delivery's
// values are read.
func decode(data []byte, into any) error {
var probe struct {
V *int `json:"v"`
}
if err := json.Unmarshal(data, &probe); err != nil {
return fmt.Errorf("delivery: value is not JSON: %w", err)
}
if probe.V == nil {
return fmt.Errorf("%w: no version in the value, this reader reads %d", delivery.ErrUnknownVersion, Version)
}
if *probe.V != Version {
return fmt.Errorf("%w: the value is version %d, this reader reads %d", delivery.ErrUnknownVersion, *probe.V, Version)
}
if err := json.Unmarshal(data, into); err != nil {
return fmt.Errorf("delivery: value does not fit its shape: %w", err)
}
return nil
}
@@ -0,0 +1,193 @@
package forgemsg
import (
"encoding/json"
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
const oddManifest = "{\n\t\"module\": \"a\",\n \"z\": 1, \"a\": [ \"x\",\"y\" ],\n \"why\": \"é — <b> & \\n\"\n}\n\n"
func sampleForges() []Forge {
return []Forge{
{Kind: ForgePull, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 36, Head: "aaaa", Action: "opened"},
{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc",
Trees: map[string]string{"modules/a": "t1"}, Root: "t0"},
{Kind: ForgeRefused, Repository: "novox/mesh-sdk", Act: "novox/mesh-sdk.36.aaaa.view", Words: "403 forbidden"},
{Kind: ForgeOpen, Open: []OpenPull{{Repository: "novox/mesh-sdk", Number: 36}, {Repository: "novox/mesh-control", Number: 7}}},
// 1585/T1: a group member's merged message names its group and every approved member (ADR 0633 §4).
{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 37, Head: "dddd", Merge: "eeee", Root: "t0",
Group: "feat/take-in", Members: []delivery.Head{{Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}, {Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}}},
}
}
func mergedWithInputs() Forge {
return Forge{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc", Root: "t0",
Trees: map[string]string{"a": "t1", "b": "t2"},
Manifests: map[string]string{"a": oddManifest},
Locks: map[string]map[string]string{"a": {"go.sum": strings.Repeat("ab", 32)}}}
}
// TestForge holds the forge bridge's contract, moved whole from mesh-sdk go/delivery with its tests (1200/AC1, AC3,
// AC4; 1279/AC5; 1442/AC1-AC4; 1585/T1).
func TestForge(t *testing.T) {
t.Run("events", func(t *testing.T) {
ev, err := ForgeEvent("novox/mesh-sdk", ForgeMerged)
if err != nil || ev != "forge.novox.mesh-sdk.merged" {
t.Fatalf("forge event = %q, %v", ev, err)
}
if repo, kind, err := ParseForgeEvent(ev); err != nil || repo != "novox/mesh-sdk" || kind != ForgeMerged {
t.Fatalf("parse forge event = %q %q %v", repo, kind, err)
}
for _, bad := range []struct{ repo, kind string }{
{"", "pull"}, {"novox", "pull"}, {"a/b/c", "pull"}, {"/b", "pull"}, {"a/", "pull"},
{"a.b/c", "pull"}, {"a/b c", "pull"}, {"a/b", "closed"}, {"a/b", ""}, {"a/b", ForgeOpen},
} {
if _, err := ForgeEvent(bad.repo, bad.kind); err == nil {
t.Errorf("forge event %q %q accepted", bad.repo, bad.kind)
}
}
for _, bad := range []string{"", "forge.a.b", "forge.a.b.closed", "x.a.b.pull", "forge.a.b.pull.x", "forge..b.pull", ForgeGroupEndEvent} {
if _, _, err := ParseForgeEvent(bad); err == nil {
t.Errorf("parse forge event %q accepted", bad)
}
}
if repo, kind, err := ParseForgeEvent(OpenEvent()); OpenEvent() != "forge.open" || err != nil || repo != "" || kind != ForgeOpen {
t.Errorf("open event %q = %q %q %v", OpenEvent(), repo, kind, err)
}
})
t.Run("round trip", func(t *testing.T) {
fs := append(sampleForges(), mergedWithInputs())
for _, f := range fs {
data, err := EncodeForge(f)
if err != nil {
t.Fatalf("%s: %v", f.Kind, err)
}
var probe map[string]any
if err := json.Unmarshal(data, &probe); err != nil || probe["v"] != float64(1) {
t.Fatalf("%s: json %s does not carry v:1", f.Kind, data)
}
f.V = Version
if got, err := DecodeForge(data); err != nil || !SameForge(got, f) {
t.Errorf("forge %s round trip: %+v %v", f.Kind, got, err)
}
}
// An empty list is an open message too: nothing is open.
if data, err := EncodeForge(Forge{Kind: ForgeOpen, Open: []OpenPull{}}); err != nil {
t.Errorf("empty open: %v", err)
} else if f, err := DecodeForge(data); err != nil || f.Kind != ForgeOpen || len(f.Open) != 0 {
t.Errorf("empty open round trip: %+v %v", f, err)
}
ge := ForgeGroupEnd{Group: "feat/take-in", Members: []GroupMerge{{Repository: "novox/mesh-sdk", Number: 37, Merge: "eeee"}}}
data, err := EncodeForgeGroupEnd(ge)
ge.V = Version
if got, err2 := DecodeForgeGroupEnd(data); err != nil || err2 != nil || !SameForgeGroupEnd(got, ge) {
t.Errorf("group end round trip: %+v %v %v", got, err, err2)
}
// An older bridge's message reads, and is written back byte for byte.
old := `{"v":1,"kind":"merged","repository":"novox/mesh-sdk","number":36,"head":"aaaa","merge":"cccc","trees":{"a":"t1"},"root":"t0"}`
if f, err := DecodeForge([]byte(old)); err != nil || f.Manifests != nil {
t.Fatalf("%+v, %v", f, err)
} else if data, err := EncodeForge(f); err != nil || string(data) != old {
t.Errorf("%s, %v, want exactly %s", data, err, old)
}
// A merge leaving the repository with no module.json is a message too (issue 1126).
f := sampleForges()[1]
f.Trees = nil
if data, err := EncodeForge(f); err != nil {
t.Errorf("a merged message with no trees is refused: %v", err)
} else if got, err := DecodeForge(data); err != nil || len(got.Trees) != 0 {
t.Errorf("a merged message with no trees comes back as %+v, %v", got, err)
}
for name, dec := range map[string]func([]byte) error{
"forge": func(b []byte) error { _, err := DecodeForge(b); return err },
"group end": func(b []byte) error { _, err := DecodeForgeGroupEnd(b); return err },
} {
for _, in := range []string{`{"group":"g"}`, `{"v":2,"group":"g"}`} {
if err := dec([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) {
t.Errorf("%s decode of %s = %v, want delivery.ErrUnknownVersion", name, in, err)
}
}
}
})
t.Run("refused", func(t *testing.T) {
at := func(i int, mut func(*Forge)) Forge { f := sampleForges()[i]; mut(&f); return f }
in := func(mut func(*Forge)) Forge { f := mergedWithInputs(); mut(&f); return f }
pullIn := func(mut func(*Forge)) Forge {
f := Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened,
Dirs: []string{"a"}, DirsKnown: true, Manifests: map[string]string{"a": oddManifest},
Locks: map[string]map[string]string{"a": {"go.sum": strings.Repeat("0", 64)}}}
mut(&f)
return f
}
for _, c := range []struct {
name, want string
f Forge
}{
{"merged without merge commit", "merge", at(1, func(f *Forge) { f.Merge = "" })},
{"merged without root", "root", at(1, func(f *Forge) { f.Root = "" })},
{"merged with a manifest and no trees", "modules/a", at(1, func(f *Forge) { f.Trees = nil; f.Manifests = map[string]string{"modules/a": "{}"} })},
{"merged with an empty tree", "modules/a", at(1, func(f *Forge) { f.Trees = map[string]string{"modules/a": ""} })},
{"pull with an unknown action", "reopened", at(0, func(f *Forge) { f.Action = "reopened" })},
{"pull without head", "head", at(0, func(f *Forge) { f.Head = "" })},
{"refused without words", "words", at(2, func(f *Forge) { f.Words = "" })},
{"refused without act", "act", at(2, func(f *Forge) { f.Act = "" })},
{"unknown kind", "closed", Forge{Kind: "closed", Repository: "a/b"}},
{"bad repository", "nope", Forge{Kind: ForgePull, Repository: "nope", Number: 1, Head: "h", Action: "opened"}},
{"open with a repository", "only the open", Forge{Kind: ForgeOpen, Repository: "a/b", Open: []OpenPull{{Repository: "a/b", Number: 1}}}},
{"open with a bad entry", "", Forge{Kind: ForgeOpen, Open: []OpenPull{{Repository: "a", Number: 1}}}},
{"open with a zero number", "no number", Forge{Kind: ForgeOpen, Open: []OpenPull{{Repository: "a/b"}}}},
{"pull naming open pull requests", "only an open message", Forge{Kind: ForgePull, Repository: "a/b", Number: 1, Head: "h", Action: "updated", Open: []OpenPull{{Repository: "a/b", Number: 1}}}},
{"manifest outside trees", `"zz"`, in(func(f *Forge) { f.Manifests["zz"] = "{}" })},
{"locks outside trees", `"yy"`, in(func(f *Forge) { f.Locks["yy"] = map[string]string{"go.sum": "h"} })},
{"empty manifest", `"b"`, in(func(f *Forge) { f.Manifests["b"] = "" })},
{"empty lock hash", `"a"`, in(func(f *Forge) { f.Locks["a"]["go.sum"] = "" })},
{"named paths outside trees", `"other"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"other": {"src": "o"}} })},
{"a named path with no object", `"src"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"a": {"src": ""}} })},
{"placement files", "mesh-placement", in(func(f *Forge) { f.RetiredPlacements = []byte(`{"anchor":"{}"}`) })},
{"placements read", "mesh-placement", in(func(f *Forge) { f.RetiredRead = true })},
{"predefined not an object", "one JSON object", in(func(f *Forge) { f.Predefined = []byte(`["domain"]`) })},
{"predefined both", "says the merge holds none", in(func(f *Forge) { f.Predefined = []byte(`{}`); f.PredefinedRead = true })},
{"pull with the predefined list", "only a merged message", Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened, PredefinedRead: true}},
{"pull manifest outside dirs", `"b"`, pullIn(func(f *Forge) { f.Manifests["b"] = "{}" })},
{"pull locks outside dirs", `"c"`, pullIn(func(f *Forge) { f.Locks["c"] = map[string]string{"go.sum": "h"} })},
{"refused with manifests", "no manifests", Forge{Kind: ForgeRefused, Repository: "a/b", Act: "x", Words: "no", Manifests: map[string]string{"a": "{}"}}},
{"open with locks", "only the open", Forge{Kind: ForgeOpen, Locks: map[string]map[string]string{"a": {"go.sum": "h"}}}},
// 1585/T1: a group only on a merged message, with its members, the message's own pull request among them.
{"pull with a group", "only a merged message names its delivery group", at(0, func(f *Forge) {
f.Group, f.Members = "g", []delivery.Head{{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}}
})},
{"merged with a group and no members", "together", at(4, func(f *Forge) { f.Members = nil })},
{"merged with members and no group", "together", at(4, func(f *Forge) { f.Group = "" })},
{"merged with members leaving itself out", "leave out", at(4, func(f *Forge) { f.Members = f.Members[1:] })},
{"merged with a member twice", "twice", at(4, func(f *Forge) { f.Members = append(f.Members, f.Members[0]) })},
{"open with a group", "only the open pull requests", Forge{Kind: ForgeOpen, Group: "g"}},
} {
if _, err := EncodeForge(c.f); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.want)
}
}
// A member's repository in another case still names the merged pull request (10c91fba).
f := sampleForges()[4]
f.Members[0].Repository = "Novox/Mesh-SDK"
if _, err := EncodeForge(f); err != nil {
t.Errorf("a member in another case: %v", err)
}
for _, c := range []struct {
name, want string
e ForgeGroupEnd
}{
{"group end without members", "no member", ForgeGroupEnd{Group: "g"}},
{"group end without a group", "names its group", ForgeGroupEnd{Members: []GroupMerge{{Repository: "a/b", Number: 1, Merge: "m"}}}},
{"group end with a member without its merge", "merge commit", ForgeGroupEnd{Group: "g", Members: []GroupMerge{{Repository: "a/b", Number: 1}}}},
{"group end with a member twice", "twice", ForgeGroupEnd{Group: "g", Members: []GroupMerge{{Repository: "a/b", Number: 1, Merge: "m"}, {Repository: "a/b", Number: 1, Merge: "m"}}}},
} {
if _, err := EncodeForgeGroupEnd(c.e); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.want)
}
}
})
}
@@ -0,0 +1,5 @@
module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
@@ -0,0 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,78 @@
package forgemsg
import (
"encoding/json"
"fmt"
"reflect"
)
// ForgeGroupEndEvent is the name of a delivery group's end on the forge bridge's stream (novox/hq ADR 0633 §4): the
// bridge publishes it once every approved member of the group is merged, after the last member's merged message. It
// has three parts, so no repository's forge event (`forge.<owner>.<name>.<kind>`) is ever this name.
const ForgeGroupEndEvent = "forge.group.merged"
// GroupMerge is one merged member of a delivery group: the repository as `<owner>/<name>`, its pull request and the
// merge commit the forge made of it.
type GroupMerge struct {
Repository string `json:"repository"`
Number int `json:"number"`
Merge string `json:"merge"`
}
// ForgeGroupEnd is the body of ForgeGroupEndEvent: the group and every member merged in it, with its merge commit.
// The indexer takes the members it holds in as one source revision once it hears it (ADR 0633 §4).
type ForgeGroupEnd struct {
V int `json:"v"`
Group string `json:"group"`
Members []GroupMerge `json:"members"`
}
func (e ForgeGroupEnd) check() error {
if e.Group == "" {
return fmt.Errorf("delivery: a group's end names its group")
}
if len(e.Members) == 0 {
return fmt.Errorf("delivery: the end of the group %q names no member merged in it", e.Group)
}
seen := map[string]bool{}
for _, m := range e.Members {
if err := checkRepository(m.Repository); err != nil {
return err
}
if m.Number <= 0 || m.Merge == "" {
return fmt.Errorf("delivery: the member %s of the group %q's end needs its pull request number and merge commit", m.Repository, e.Group)
}
k := fmt.Sprintf("%s#%d", m.Repository, m.Number)
if seen[k] {
return fmt.Errorf("delivery: the end of the group %q names its member %s twice", e.Group, k)
}
seen[k] = true
}
return nil
}
// EncodeForgeGroupEnd is e as JSON at Version; it refuses an end with no group or no member, and a member without its
// pull request or merge commit.
func EncodeForgeGroupEnd(e ForgeGroupEnd) ([]byte, error) {
if err := e.check(); err != nil {
return nil, err
}
e.V = Version
return json.Marshal(e)
}
// DecodeForgeGroupEnd reads a group's end; it refuses an unknown version (ErrUnknownVersion) and what
// EncodeForgeGroupEnd refuses.
func DecodeForgeGroupEnd(data []byte) (ForgeGroupEnd, error) {
var e ForgeGroupEnd
if err := decode(data, &e); err != nil {
return ForgeGroupEnd{}, err
}
if err := e.check(); err != nil {
return ForgeGroupEnd{}, err
}
return e, nil
}
// SameForgeGroupEnd says whether two group ends are the same value.
func SameForgeGroupEnd(a, b ForgeGroupEnd) bool { return reflect.DeepEqual(a, b) }
+4 -1
View File
@@ -3,12 +3,13 @@ module mesh-delivery-forge-bridge
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.0 // indirect
@@ -20,3 +21,5 @@ require (
golang.org/x/sys v0.48.0 // indirect
golang.org/x/time v0.15.0 // indirect
)
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ./forgemsg
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -5,6 +5,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -145,11 +146,11 @@ func MergeActs(value []byte) ([]Act, error) {
// 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 := delivery.ForgeEvent(a.Repository, delivery.ForgeRefused)
event, err := forgemsg.ForgeEvent(a.Repository, forgemsg.ForgeRefused)
if err != nil {
return Message{}, err
}
body, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeRefused, Repository: a.Repository, Number: a.Number,
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
@@ -5,6 +5,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -111,18 +112,12 @@ func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) {
}
}
func TestOwedActOfADottedRepositoryIsUnescaped(t *testing.T) {
key, v := owedValue(t, "novox/my.repo", 3, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "x"})
a, err := OwedAct(key, v)
if err != nil {
t.Fatal(err)
}
if a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" {
t.Errorf("act = %+v", a)
}
}
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", delivery.Owed{Act: delivery.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", delivery.Owed{Act: delivery.OwedView, Text: "v"})
v, _ := delivery.EncodeOwed(delivery.Owed{Act: delivery.OwedComment, Text: "v"})
if _, err := OwedAct(key, v); err == nil {
@@ -166,13 +161,11 @@ func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { /
if acts[0].Name != "merge" || acts[0].Number != 12 || acts[0].Head != headA {
t.Errorf("act = %+v", acts[0])
}
}
func TestMergeActsOfAMergeWithoutApprovedHeadsAreRefused(t *testing.T) {
v := mergeValue(t, delivery.Merge{Group: "g", Ask: "a", Pulls: []delivery.MergePull{
// A merge with no approved heads is refused: the forge's tool would refuse it with nothing to say which.
v = mergeValue(t, delivery.Merge{Group: "g", Ask: "a", Pulls: []delivery.MergePull{
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}}})
if _, err := MergeActs(v); err == nil {
t.Error("a merge with no approved heads was taken: the forge's tool would refuse it with nothing to say which")
t.Error("a merge with no approved heads was taken")
}
}
@@ -185,13 +178,13 @@ func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3,
if m.Event != "forge.novox.mesh-delivery.refused" {
t.Errorf("event = %q", m.Event)
}
f, err := delivery.DecodeForge(m.Data)
f, err := forgemsg.DecodeForge(m.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA,
want := forgemsg.Forge{V: delivery.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 !delivery.SameForge(f, want) {
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v", f)
}
}
@@ -15,7 +15,7 @@ import (
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// ErrUnreadable is wrapped by every error that says an event cannot be turned into a message; it is never
@@ -110,11 +110,11 @@ func ConvertKept(subject string, data []byte, trees Trees, files Files, kept Kep
var action string
switch subject {
case UpdatedSubject:
action = delivery.PullUpdated
action = forgemsg.PullUpdated
case ClosedSubject:
action = delivery.PullClosed
action = forgemsg.PullClosed
case MergedSubject:
action = delivery.PullClosed // unless it merged into the trunk, below
action = forgemsg.PullClosed // unless it merged into the trunk, below
default:
return Message{}, fmt.Errorf("%w: %s", ErrNotHandled, subject)
}
@@ -136,29 +136,29 @@ func ConvertKept(subject string, data []byte, trees Trees, files Files, kept Kep
return merged(subject, p, trees, files, kept)
}
said := ""
if action == delivery.PullUpdated && p.Base != "" && p.Base != Trunk {
if action == forgemsg.PullUpdated && p.Base != "" && p.Base != Trunk {
// The checker judges pull requests into main only, and source.main names main's head only (novox/hq ADR 0484 §11): a
// pull request into another base is no candidate, as it is none at the start (OpenPulls) and at its merge. It is
// written closed, so a candidate an older head made is taken out, and said (issue 1422).
action = delivery.PullClosed
action = forgemsg.PullClosed
said = fmt.Sprintf("%s/%s#%d at %s is into %s, not %s: written closed, so it is no candidate; the checker judges pull requests into %s only (novox/hq ADR 0484 §11)",
p.Owner, p.Repo, p.Number, p.HeadSHA[:12], p.Base, Trunk, Trunk)
}
f := delivery.Forge{Kind: delivery.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number,
f := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number,
Head: p.HeadSHA, Action: action}
// The directories are the forge's word only when it said it could read them (module_dirs_said).
if action == delivery.PullUpdated && p.ModuleDirsSaid {
if action == forgemsg.PullUpdated && p.ModuleDirsSaid {
f.Dirs, f.DirsKnown = p.ModuleDirs, true
var err error
if f.Manifests, f.Locks, _, err = inputsRead(p.Owner, p.Repo, p.HeadSHA, p.ModuleDirs, files); err != nil {
return Message{}, fmt.Errorf("%w: %s: the manifests of %s/%s at the head %s: %v", ErrRetry, subject, p.Owner, p.Repo, p.HeadSHA, err)
}
}
event, err := delivery.ForgeEvent(f.Repository, delivery.ForgePull)
event, err := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgePull)
if err != nil {
return Message{}, unreadable(subject, "%v", err)
}
body, err := delivery.EncodeForge(f)
body, err := forgemsg.EncodeForge(f)
if err != nil {
return Message{}, unreadable(subject, "%v", err)
}
@@ -313,11 +313,11 @@ func merged(subject string, p pull, trees Trees, files Files, kept Kept) (Messag
if len(locks) == 0 {
locks = nil
}
event, err := delivery.ForgeEvent(repository, delivery.ForgeMerged)
event, err := forgemsg.ForgeEvent(repository, forgemsg.ForgeMerged)
if err != nil {
return Message{}, unreadable(subject, "%v", err)
}
f := delivery.Forge{Kind: delivery.ForgeMerged, Repository: repository, Branch: p.Head, Number: p.Number,
f := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Branch: p.Head, Number: p.Number,
Head: p.HeadSHA, Merge: p.MergeSHA, Root: root, Trees: dirs, Manifests: manifests, Locks: locks}
if repository == CatalogRepository {
// The catalogue's predefined list at the merge (novox/hq ADR 0565 §2): the indexer keeps it for mesh-config.
@@ -331,7 +331,7 @@ func merged(subject string, p pull, trees Trees, files Files, kept Kept) (Messag
f.Predefined = json.RawMessage(raw)
}
}
body, err := delivery.EncodeForge(f)
body, err := forgemsg.EncodeForge(f)
if err != nil {
return Message{}, unreadable(subject, "%v", err)
}
@@ -373,12 +373,12 @@ func OpenPulls(answer []byte) (msgs []Message, skipped []string, err error) {
skipped = append(skipped, name+": it names no owner, repo, number, branch or 40-hex head")
continue
}
f := delivery.Forge{Kind: delivery.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number,
Head: p.HeadSHA, Action: delivery.PullUpdated}
event, err := delivery.ForgeEvent(f.Repository, delivery.ForgePull)
f := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number,
Head: p.HeadSHA, Action: forgemsg.PullUpdated}
event, err := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgePull)
var body []byte
if err == nil {
body, err = delivery.EncodeForge(f)
body, err = forgemsg.EncodeForge(f)
}
if err != nil {
skipped = append(skipped, name+": "+err.Error())
@@ -392,12 +392,12 @@ func OpenPulls(answer []byte) (msgs []Message, skipped []string, err error) {
// Track says what a message the bridge wrote changes in the head it keeps for each pull request (the bucket heads,
// novox/hq ADR 0443 §5): the key `<owner>.<name>.<number>`, and the head to keep under it, or "" to take the key away
// (a closed pull request, a merged one). ok is false for a message that keeps no head.
func Track(f delivery.Forge) (key, head string, ok bool) {
func Track(f forgemsg.Forge) (key, head string, ok bool) {
key = strings.ReplaceAll(f.Repository, "/", ".") + "." + strconv.Itoa(f.Number)
switch {
case f.Kind == delivery.ForgePull && f.Action != delivery.PullClosed:
case f.Kind == forgemsg.ForgePull && f.Action != forgemsg.PullClosed:
return key, f.Head, true
case f.Kind == delivery.ForgePull || f.Kind == delivery.ForgeMerged:
case f.Kind == forgemsg.ForgePull || f.Kind == forgemsg.ForgeMerged:
return key, "", true
}
return "", "", false
@@ -413,22 +413,22 @@ func OpenMessage(answer []byte) (Message, error) {
if err := json.Unmarshal(answer, &a); err != nil || a.Pulls == nil {
return Message{}, fmt.Errorf("the answer of gitea_open_pulls is not {pulls: [...]}: %v", err)
}
open := []delivery.OpenPull{}
open := []forgemsg.OpenPull{}
for _, p := range *a.Pulls {
if p.Base != Trunk || p.Owner == "" || p.Repo == "" || p.Number <= 0 {
continue
}
open = append(open, delivery.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number})
open = append(open, forgemsg.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number})
}
slices.SortFunc(open, func(a, b delivery.OpenPull) int {
slices.SortFunc(open, func(a, b forgemsg.OpenPull) int {
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
return c
}
return a.Number - b.Number
})
body, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeOpen, Open: open})
body, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeOpen, Open: open})
if err != nil {
return Message{}, fmt.Errorf("the open pull requests cannot be said: %w", err)
}
return Message{Event: delivery.OpenEvent(), Data: body}, nil
return Message{Event: forgemsg.OpenEvent(), Data: body}, nil
}
@@ -6,6 +6,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -54,13 +55,13 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion;
if msg.Event != "forge.novox.mesh-delivery.pull" {
t.Errorf("event = %q", msg.Event)
}
f, err := delivery.DecodeForge(msg.Data)
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}}
if !delivery.SameForge(f, want) {
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}}
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
}
}
@@ -70,7 +71,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3
if err != nil {
t.Fatal(err)
}
f, _ := delivery.DecodeForge(msg.Data)
f, _ := forgemsg.DecodeForge(msg.Data)
if f.DirsKnown || len(f.Dirs) != 0 {
t.Errorf("dirs = %v known = %v, want not known", f.Dirs, f.DirsKnown)
}
@@ -79,7 +80,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3
if err != nil {
t.Fatal(err)
}
f, _ = delivery.DecodeForge(msg.Data)
f, _ = forgemsg.DecodeForge(msg.Data)
if !f.DirsKnown || len(f.Dirs) != 0 {
t.Errorf("dirs = %v known = %v, want known and empty", f.Dirs, f.DirsKnown)
}
@@ -88,7 +89,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3
if err != nil {
t.Fatal(err)
}
if f, _ = delivery.DecodeForge(msg.Data); f.DirsKnown || len(f.Dirs) != 0 {
if f, _ = forgemsg.DecodeForge(msg.Data); f.DirsKnown || len(f.Dirs) != 0 {
t.Errorf("dirs %v known %v without module_dirs_said", f.Dirs, f.DirsKnown)
}
}
@@ -101,13 +102,13 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4
if msg.Event != "forge.novox.mesh-delivery.pull" {
t.Errorf("event = %q", msg.Event)
}
f, err := delivery.DecodeForge(msg.Data)
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: delivery.PullClosed}
if !delivery.SameForge(f, want) {
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: forgemsg.PullClosed}
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
}
// Issue 1422, ADR 0484 §11: a head pushed to a pull request into another base is written closed, read nothing for, and said;
@@ -116,12 +117,12 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4
if err != nil {
t.Fatal(err)
}
if f, err = delivery.DecodeForge(msg.Data); err != nil || !delivery.SameForge(f, want) || !strings.Contains(msg.Said, "is into nox-mesh, not main") {
if f, err = forgemsg.DecodeForge(msg.Data); err != nil || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "is into nox-mesh, not main") {
t.Errorf("an update into nox-mesh is %+v (%v), said %q; want it closed and said", f, err, msg.Said)
}
for _, base := range []any{"main", nil} {
msg, err = Convert(updatedSubject, updated(map[string]any{"base": base}), nil, anyFiles)
if f, _ = delivery.DecodeForge(msg.Data); err != nil || f.Action != delivery.PullUpdated || msg.Said != "" {
if f, _ = forgemsg.DecodeForge(msg.Data); err != nil || f.Action != forgemsg.PullUpdated || msg.Said != "" {
t.Errorf("an update with base %v is %+v (%v), said %q; want it updated", base, f, err, msg.Said)
}
}
@@ -170,12 +171,18 @@ func mustDrop(body []byte, key string) []byte {
return b
}
func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235
func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235, 1236
for _, s := range []string{"mesh.mod.gitea.event.something.else", "mesh.mod.other.event.pull.updated"} {
if _, err := Convert(s, updated(nil), nil, anyFiles); !errors.Is(err, ErrNotHandled) {
t.Errorf("%s: err = %v, want ErrNotHandled", s, err)
}
}
// 1236: a merged event without a full merge commit is unreadable, and the forge is not asked.
var calls []call
_, err := Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil), anyFiles)
if !errors.Is(err, ErrUnreadable) || len(calls) != 0 {
t.Errorf("merged without a merge commit: err = %v calls = %v", err, calls)
}
}
const merge = "89abcdef0123456789abcdef0123456789abcdef"
@@ -222,13 +229,13 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1
if msg.Event != "forge.novox.mesh-delivery.merged" {
t.Errorf("event = %q", msg.Event)
}
f, err := delivery.DecodeForge(msg.Data)
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Merge: merge, Root: "r0", Trees: answer, Manifests: map[string]string{"modules/a": "{}", "modules/b": "{}"}}
if !delivery.SameForge(f, want) {
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
}
if len(calls) != 1 || calls[0] != (call{"novox", "mesh-delivery", merge}) {
@@ -242,13 +249,13 @@ func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2
if err != nil {
t.Fatal(err)
}
f, err := delivery.DecodeForge(msg.Data)
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
t.Fatal(err)
}
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: delivery.PullClosed}
if msg.Event != "forge.novox.mesh-delivery.pull" || !delivery.SameForge(f, want) {
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: forgemsg.PullClosed}
if msg.Event != "forge.novox.mesh-delivery.pull" || !forgemsg.SameForge(f, want) {
t.Errorf("event %q message %+v, want a closed pull message", msg.Event, f)
}
if len(calls) != 0 {
@@ -277,22 +284,14 @@ func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f, err := delivery.DecodeForge(msg.Data)
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
f, err := forgemsg.DecodeForge(msg.Data)
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Merge: merge, Root: "r0"}
if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !delivery.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") {
if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") {
t.Errorf("event %q message %+v said %q, %v; want a merged message with no trees", msg.Event, f, msg.Said, err)
}
}
func TestMergedWithoutAMergeCommitIsUnreadable(t *testing.T) { // 1236
var calls []call
_, err := Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil), anyFiles)
if !errors.Is(err, ErrUnreadable) || len(calls) != 0 {
t.Errorf("err = %v calls = %v", err, calls)
}
}
// openAnswer is gitea_open_pulls's answer as mesh-catalog modules/gitea/client.ts:369-381 builds each entry
// (owner, repo, number, base, head, head_sha, html_url, required, statuses, checks, labels) inside {pulls}.
func openAnswer(pulls ...map[string]any) []byte {
@@ -311,12 +310,12 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/
if err != nil || len(skipped) != 0 || len(msgs) != 2 {
t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err)
}
for i, want := range []delivery.Forge{
{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated},
{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: delivery.PullUpdated},
for i, want := range []forgemsg.Forge{
{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated},
{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated},
} {
f, err := delivery.DecodeForge(msgs[i].Data)
if err != nil || !delivery.SameForge(f, want) {
f, err := forgemsg.DecodeForge(msgs[i].Data)
if err != nil || !forgemsg.SameForge(f, want) {
t.Errorf("message %d = %+v err %v, want %+v", i, f, err, want)
}
if f.DirsKnown {
@@ -328,14 +327,11 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/
}
}
func TestOpenPullIntoAnotherBranchGetsNoMessage(t *testing.T) { // 1237/AC4
msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "feat/y", "feat/x", head)))
if err != nil || len(msgs) != 0 || len(skipped) != 0 {
t.Errorf("msgs %d skipped %v err %v, want none", len(msgs), skipped, err)
func TestOpenPullsUnreadableEntryIsNamedAndTheRestGo(t *testing.T) { // 1237, 1237/AC4
// 1237/AC4: an open pull request into another branch gets no message.
if msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "feat/y", "feat/x", head))); err != nil || len(msgs) != 0 || len(skipped) != 0 {
t.Errorf("into another branch: msgs %d skipped %v err %v, want none", len(msgs), skipped, err)
}
}
func TestOpenPullsUnreadableEntryIsNamedAndTheRestGo(t *testing.T) { // 1237
msgs, skipped, err := OpenPulls(openAnswer(openEntry("a", 1, "main", "b", "short"), openEntry("mesh-delivery", 12, "main", "feat/x", head)))
if err != nil || len(msgs) != 1 || len(skipped) != 1 || !strings.Contains(skipped[0], "novox/a#1") {
t.Errorf("msgs %d skipped %v err %v", len(msgs), skipped, err)
@@ -355,15 +351,15 @@ func TestOpenMessageNamesEveryOpenPullIntoTheTrunk(t *testing.T) { // 1279/AC6
if err != nil || m.Event != "forge.open" {
t.Fatalf("message %+v err %v", m, err)
}
f, err := delivery.DecodeForge(m.Data)
f, err := forgemsg.DecodeForge(m.Data)
// Every one into the trunk, whether its head reads or not (it is open); none into another branch; in a fixed order.
want := []delivery.OpenPull{{Repository: "novox/a", Number: 1}, {Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
if err != nil || f.Kind != delivery.ForgeOpen || len(f.Open) != 3 || f.Open[0] != want[0] || f.Open[1] != want[1] || f.Open[2] != want[2] {
want := []forgemsg.OpenPull{{Repository: "novox/a", Number: 1}, {Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
if err != nil || f.Kind != forgemsg.ForgeOpen || len(f.Open) != 3 || f.Open[0] != want[0] || f.Open[1] != want[1] || f.Open[2] != want[2] {
t.Errorf("open message = %+v err %v, want %v", f, err, want)
}
// Nothing open is a message too: the indexer then drops every candidate.
m, err = OpenMessage([]byte(`{"pulls":[]}`))
if f, derr := delivery.DecodeForge(m.Data); err != nil || derr != nil || f.Kind != delivery.ForgeOpen || len(f.Open) != 0 {
if f, derr := forgemsg.DecodeForge(m.Data); err != nil || derr != nil || f.Kind != forgemsg.ForgeOpen || len(f.Open) != 0 {
t.Errorf("empty answer: %+v %v %v", f, err, derr)
}
for _, bad := range []string{`{"pulls":null}`, `{}`, `x`} {
@@ -374,24 +370,24 @@ func TestOpenMessageNamesEveryOpenPullIntoTheTrunk(t *testing.T) { // 1279/AC6
}
func TestTrackSaysWhichHeadOfWhichPullRequestWasWritten(t *testing.T) { // 1279/AC6
pullMsg := func(action string) delivery.Forge {
return delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: action}
pullMsg := func(action string) forgemsg.Forge {
return forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: action}
}
for _, a := range []string{delivery.PullOpened, delivery.PullUpdated} {
for _, a := range []string{forgemsg.PullOpened, forgemsg.PullUpdated} {
if key, h, ok := Track(pullMsg(a)); key != "novox.mesh-delivery.12" || h != head || !ok {
t.Errorf("%s: Track = %q %q %v, want the head kept under a bucket key", a, key, h, ok)
}
}
// A closed pull request and a merged one are no longer kept: their key goes.
if key, h, ok := Track(pullMsg(delivery.PullClosed)); key != "novox.mesh-delivery.12" || h != "" || !ok {
if key, h, ok := Track(pullMsg(forgemsg.PullClosed)); key != "novox.mesh-delivery.12" || h != "" || !ok {
t.Errorf("closed: Track = %q %q %v", key, h, ok)
}
merged := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}}
merged := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}}
if key, h, ok := Track(merged); key != "novox.mesh-delivery.12" || h != "" || !ok {
t.Errorf("merged: Track = %q %q %v", key, h, ok)
}
// The open message and the rest hold no head.
if _, _, ok := Track(delivery.Forge{Kind: delivery.ForgeOpen}); ok {
if _, _, ok := Track(forgemsg.Forge{Kind: forgemsg.ForgeOpen}); ok {
t.Error("an open message was tracked")
}
}
@@ -13,7 +13,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// The bytes the forge answers in these tests are two real files of this repository, read from the working tree: the
@@ -69,7 +69,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
if err != nil {
t.Fatal(err)
}
f, err := delivery.DecodeForge(msg.Data)
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
t.Fatal(err)
}
@@ -90,7 +90,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
if err != nil {
t.Fatal(err)
}
if f, err = delivery.DecodeForge(msg.Data); err != nil {
if f, err = forgemsg.DecodeForge(msg.Data); err != nil {
t.Fatal(err)
}
if len(f.RetiredPlacements) > 0 || f.RetiredRead || strings.Contains(string(msg.Data), `"placements`) {
@@ -108,7 +108,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1
}
forge.have[PredefinedFile] = []byte(`{"font-size":{"type":"integer","value":13}}`)
msg, _ = Convert(mergedSubject, mergedBody(map[string]any{"repo": "mesh-catalog"}), treesOf(&calls, "r0", map[string]string{"modules/a": "t1"}, nil), forge.get)
if f, err = delivery.DecodeForge(msg.Data); err != nil || string(f.Predefined) != string(forge.have[PredefinedFile]) || f.PredefinedRead {
if f, err = forgemsg.DecodeForge(msg.Data); err != nil || string(f.Predefined) != string(forge.have[PredefinedFile]) || f.PredefinedRead {
t.Errorf("predefined = %s (%v)", f.Predefined, err)
}
forge.calls = nil
@@ -133,7 +133,7 @@ func TestPullWithKnownDirsCarriesTheSameAtTheHead(t *testing.T) { // 1448/AC2
if err != nil {
t.Fatal(err)
}
f, _ := delivery.DecodeForge(msg.Data)
f, _ := forgemsg.DecodeForge(msg.Data)
if !reflect.DeepEqual(f.Manifests, map[string]string{"modules/a": string(manifest)}) || len(f.Locks) != 1 || len(f.Packages) != 0 {
t.Errorf("manifests %v locks %v packages %v", f.Manifests, f.Locks, f.Packages)
}
@@ -146,7 +146,7 @@ func TestPullWithKnownDirsCarriesTheSameAtTheHead(t *testing.T) { // 1448/AC2
if err != nil {
t.Fatal(err)
}
f, _ = delivery.DecodeForge(msg.Data)
f, _ = forgemsg.DecodeForge(msg.Data)
if f.Manifests != nil || f.Locks != nil || len(forge.calls) != 0 {
t.Errorf("manifests %v locks %v calls %v", f.Manifests, f.Locks, forge.calls)
}
@@ -175,7 +175,7 @@ func TestAForgeErrorOrAMissingManifestIsRetryAndALockNotFoundIsLeftOut(t *testin
if err != nil {
t.Fatal(err)
}
if f, _ := delivery.DecodeForge(msg.Data); f.Locks != nil {
if f, _ := forgemsg.DecodeForge(msg.Data); f.Locks != nil {
t.Errorf("locks = %v, want none for lock files the forge does not have", f.Locks)
}
}
@@ -4,7 +4,7 @@ import (
"os"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// ADR 0495 clause 1 with the lead's decision L8 of R4e: the forge bridge is no reader of a manifest's `paths`. A merge of a
@@ -26,7 +26,7 @@ func TestAMergedMessageCarriesNoNamedPathsTheBridgeDerived(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f, err := delivery.DecodeForge(msg.Data)
f, err := forgemsg.DecodeForge(msg.Data)
if err != nil {
t.Fatal(err)
}
@@ -19,6 +19,8 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -102,15 +104,15 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
}
})
h1, h2 := "0123456789abcdef0123456789abcdef01234567", "89abcdef0123456789abcdef0123456789abcdef"
for _, f := range []delivery.Forge{
{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true},
{Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: delivery.PullUpdated, Dirs: []string{"modules/c"}, DirsKnown: true},
for _, f := range []forgemsg.Forge{
{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true},
{Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated, Dirs: []string{"modules/c"}, DirsKnown: true},
} {
data, err := delivery.EncodeForge(f)
data, err := forgemsg.EncodeForge(f)
if err != nil {
t.Fatal(err)
}
event, _ := delivery.ForgeEvent(f.Repository, delivery.ForgePull)
event, _ := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgePull)
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+event, data); err != nil {
t.Fatal(err)
}
@@ -143,43 +145,43 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
t.Fatal(err)
}
const ix = delivery.IndexerModule
m1 := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1,
m1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1,
Merge: "1111111111111111111111111111111111111111", Root: "r0",
Trees: map[string]string{"modules/" + ix: "tree-indexer-2", "modules/a": "tree-a-2"},
Manifests: map[string]string{"modules/" + ix: string(manifest), "modules/a": `{"module":"a","version":"1"}`},
Locks: map[string]map[string]string{"modules/" + ix: {"go.sum": "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"}}}
merged := func(f delivery.Forge) uint64 {
merged := func(f forgemsg.Forge) uint64 {
t.Helper()
data, err := delivery.EncodeForge(f)
data, err := forgemsg.EncodeForge(f)
if err != nil {
t.Fatal(err)
}
event, _ := delivery.ForgeEvent(f.Repository, delivery.ForgeMerged)
event, _ := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgeMerged)
ack, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+event, data)
if err != nil {
t.Fatal(err)
}
return ack.Sequence
}
m1Key := delivery.PendingKey(merged(m1))
m1Key := takein.PendingKey(merged(m1))
takeIns, err := js.Stream(ctx, "TAKE_IN")
if err != nil {
t.Fatal(err)
}
// said is the last take-in event under kind and key, within 20 s.
said := func(kind, key string) (delivery.TakeIn, bool) {
event, _ := delivery.TakeInEvent(kind, key)
said := func(kind, key string) (takein.TakeIn, bool) {
event, _ := takein.TakeInEvent(kind, key)
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if msg, err := takeIns.GetLastMsgForSubject(ctx, process.ModuleStreamSubject(ix, event)); err == nil {
if ti, err := delivery.DecodeTakeIn(msg.Data); err == nil {
if ti, err := takein.DecodeTakeIn(msg.Data); err == nil {
return ti, true
}
}
}
return delivery.TakeIn{}, false
return takein.TakeIn{}, false
}
held, ok := said(delivery.TakeInPending, m1Key)
if !ok || held.What != delivery.TakeInRevision || held.WaitsFor == "" || held.WaitsFor == first || len(held.Merges) != 1 {
held, ok := said(takein.TakeInPending, m1Key)
if !ok || held.What != takein.TakeInRevision || held.WaitsFor == "" || held.WaitsFor == first || len(held.Merges) != 1 {
t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the revision waiting for another build than %s", m1Key, held, ok, first)
}
named := held.WaitsFor
@@ -226,14 +228,14 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
// A group's first member arrives meanwhile; its end is still to come (the knowledge base's remark: it stays across the restart).
members := []delivery.Head{{Repository: "novox/mesh-catalog", Number: 40, Head: "4040404040404040404040404040404040404040"},
{Repository: "novox/mesh-controller", Number: 41, Head: "4141414141414141414141414141414141414141"}}
g1 := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head,
g1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head,
Merge: "4000000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/c": "tree-c-2"},
Manifests: map[string]string{"modules/c": `{"module":"c","version":"1"}`}, Group: "feat/g", Members: members}
g2 := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-controller", Branch: "feat/g", Number: 41, Head: members[1].Head,
g2 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-controller", Branch: "feat/g", Number: 41, Head: members[1].Head,
Merge: "4100000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/k": "tree-k-1"},
Manifests: map[string]string{"modules/k": `{"module":"k","version":"1"}`}, Group: "feat/g", Members: members}
g1Key, groupKey := delivery.PendingKey(merged(g1)), delivery.GroupTakeInKey("feat/g")
if gathered, ok := said(delivery.TakeInPending, groupKey); !ok || gathered.What != delivery.TakeInGroup || len(gathered.Merges) != 1 {
g1Key, groupKey := takein.PendingKey(merged(g1)), takein.GroupTakeInKey("feat/g")
if gathered, ok := said(takein.TakeInPending, groupKey); !ok || gathered.What != takein.TakeInGroup || len(gathered.Merges) != 1 {
t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the group with its first member", groupKey, gathered, ok)
}
// indexer_pending, the tool over the same state, names both waits.
@@ -241,29 +243,29 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
if err != nil {
t.Fatal(err)
}
waits := func() []delivery.PendingWait {
waits := func() []takein.PendingWait {
t.Helper()
answer, err := index.PendingTools(kvStore{pendingKV}, nil, time.Now)[0].Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
return answer.(delivery.PendingAnswer).Waits
return answer.(takein.PendingAnswer).Waits
}
var w []delivery.PendingWait
var w []takein.PendingWait
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if w = waits(); len(w) == 2 && w[0].WaitsFor == named && w[1].Said != "" {
break
}
}
if len(w) != 2 || w[0].Key != m1Key || w[0].WaitsFor != named || !slices.Contains(w[0].Holds, "a") || !slices.Contains(w[0].Holds, ix) ||
w[1].Key != groupKey || w[1].What != delivery.TakeInGroup || len(w[1].Merges) != 1 || !strings.HasSuffix(w[1].Said, "pending."+groupKey) {
w[1].Key != groupKey || w[1].What != takein.TakeInGroup || len(w[1].Merges) != 1 || !strings.HasSuffix(w[1].Said, "pending."+groupKey) {
t.Fatalf("indexer_pending = %+v, want the revision %s waiting for %s holding a and the indexer, then the group gathered", w, m1Key, named)
}
// The named indexer starts (the node-engine now runs that build): it takes the revision in, and leaves the group.
running.Store(&named)
restart()
if taken, ok := said(delivery.TakeInTaken, m1Key); !ok || len(taken.Merges) != 1 || taken.Merges[0].Merge != m1.Merge {
if taken, ok := said(takein.TakeInTaken, m1Key); !ok || len(taken.Merges) != 1 || taken.Merges[0].Merge != m1.Merge {
t.Fatalf("take-in.taken.%s = %+v (said %v) within 20 s of the start of %s", m1Key, taken, ok, named)
}
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
@@ -286,15 +288,15 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
// I8b: the group's second member and its end: one take-in of both, said under the group's key.
merged(g2)
end, err := delivery.EncodeForgeGroupEnd(delivery.ForgeGroupEnd{Group: "feat/g", Members: []delivery.GroupMerge{
end, err := forgemsg.EncodeForgeGroupEnd(forgemsg.ForgeGroupEnd{Group: "feat/g", Members: []forgemsg.GroupMerge{
{Repository: g1.Repository, Number: g1.Number, Merge: g1.Merge}, {Repository: g2.Repository, Number: g2.Number, Merge: g2.Merge}}})
if err != nil {
t.Fatal(err)
}
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+delivery.ForgeGroupEndEvent, end); err != nil {
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+forgemsg.ForgeGroupEndEvent, end); err != nil {
t.Fatal(err)
}
if taken, ok := said(delivery.TakeInTaken, groupKey); !ok || taken.What != delivery.TakeInGroup || len(taken.Merges) != 2 {
if taken, ok := said(takein.TakeInTaken, groupKey); !ok || taken.What != takein.TakeInGroup || len(taken.Merges) != 2 {
t.Fatalf("take-in.taken.%s = %+v (said %v) within 20 s of the group's end, want both members", groupKey, taken, ok)
}
var hasC, hasK bool
@@ -307,7 +309,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
t.Errorf("after the group's end: modules.c %v, modules.k %v, indexer_pending %+v: want both taken in and nothing waiting", hasC, hasK, w)
}
// One take-in: neither member was said as a revision of its own.
single, _ := delivery.TakeInEvent(delivery.TakeInPending, g1Key)
single, _ := takein.TakeInEvent(takein.TakeInPending, g1Key)
if _, err := takeIns.GetLastMsgForSubject(ctx, process.ModuleStreamSubject(ix, single)); err == nil {
t.Errorf("the group's first member %s was said pending as a revision of its own", g1Key)
}
@@ -443,7 +445,7 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
builds.end(t, "novox/mesh-controller", "build-agent", "modules/build-agent")
})
const merge = "89abcdef0123456789abcdef0123456789abcdef"
data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: "0123456789abcdef0123456789abcdef01234567", Merge: merge, Root: "r0", Trees: map[string]string{"modules/a": "newtree"},
Manifests: map[string]string{"modules/a": `{"module":"a","version":"1"}`}})
if err != nil {
@@ -497,7 +499,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
t.Fatal(err)
}
})
forge := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301,
forge := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301,
Head: "0123456789abcdef0123456789abcdef01234567", Merge: "89abcdef0123456789abcdef0123456789abcdef", Root: "r0",
Trees: map[string]string{"modules/avahi": "newtree"},
Manifests: map[string]string{"modules/avahi": string(manifest)},
@@ -505,7 +507,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
for name, raw := range judged {
forge.Manifests["modules/"+name], forge.Trees["modules/"+name] = raw, "tree-"+name
}
data, err := delivery.EncodeForge(forge)
data, err := forgemsg.EncodeForge(forge)
if err != nil {
t.Fatal(err)
}
@@ -599,7 +601,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
js, ctx, _, modules := startIndexer(t, func(ctx context.Context, builds buildLog) {
builds.end(t, "novox/mesh-catalog", "avahi", "modules/avahi")
})
data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301,
data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301,
Head: "0123456789abcdef0123456789abcdef01234567", Merge: "89abcdef0123456789abcdef0123456789abcdef", Root: "r0",
Trees: map[string]string{"modules/avahi": "newtree"},
Manifests: map[string]string{"modules/avahi": string(manifest)},
@@ -686,7 +688,7 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
t.Fatal(err)
}
})
pull, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: head, Action: delivery.PullUpdated,
pull, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: head, Action: forgemsg.PullUpdated,
Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": string(manifest)}})
if err != nil {
t.Fatal(err)
@@ -856,7 +858,7 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
place(ctx, t, "n1", "mesh-build-go")
place(ctx, t, "n2")
})
data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 302,
data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 302,
Head: "0123456789abcdef0123456789abcdef01234567", Merge: "89abcdef0123456789abcdef0123456789abcdef", Root: "r0",
Trees: map[string]string{"modules/avahi": "newtree"},
Manifests: map[string]string{"modules/avahi": string(manifest)},
@@ -25,6 +25,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -157,14 +158,14 @@ type delivered interface {
// handleGroupEnd takes a group's end in (index.EndGroup): a body that does not read is terminated and said; an end
// whose members are not all held yet, or while the build this program runs is not known, is left for redelivery.
func handleGroupEnd(ctx context.Context, m delivered, ix *index.Indexer, h *index.Heard, logger *log.Logger) {
end, err := delivery.DecodeForgeGroupEnd(m.Data())
end, err := forgemsg.DecodeForgeGroupEnd(m.Data())
if err != nil {
logger.Printf("%s: not a group's end, dropped: %v", m.Subject(), err)
h.Outcome, h.Words = index.HeardDropped, err.Error()
_ = m.Term()
return
}
h.Kind, h.Branch = delivery.ForgeGroupEndEvent, end.Group
h.Kind, h.Branch = forgemsg.ForgeGroupEndEvent, end.Group
applied, words, err := ix.EndGroup(ctx, end)
h.Written, h.Late = applied.Written, applied.Late
if err != nil {
@@ -197,11 +198,11 @@ func handle(ctx context.Context, m delivered, ix *index.Indexer, heard index.Sto
}
}()
// A delivery group's end (novox/hq ADR 0633 §4) has a body of its own: told apart by its name, before the decode.
if strings.HasSuffix(m.Subject(), "."+delivery.ForgeGroupEndEvent) {
if strings.HasSuffix(m.Subject(), "."+forgemsg.ForgeGroupEndEvent) {
handleGroupEnd(ctx, m, ix, &h, logger)
return
}
f, err := delivery.DecodeForge(m.Data())
f, err := forgemsg.DecodeForge(m.Data())
if err != nil {
logger.Printf("%s: not a forge message, dropped: %v", m.Subject(), err)
h.Outcome, h.Words = index.HeardDropped, err.Error()
@@ -213,7 +214,7 @@ func handle(ctx context.Context, m delivered, ix *index.Indexer, heard index.Sto
h.Commit = f.Merge
}
var applied index.Applied
if f.Kind == delivery.ForgeMerged && h.Sequence > 0 {
if f.Kind == forgemsg.ForgeMerged && h.Sequence > 0 {
// A merge is taken in only by the indexer its revision names (ADR 0633 §1): kept in pending until then.
applied, h.Words, err = ix.Arrive(ctx, h.Sequence, f)
} else {
@@ -11,6 +11,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
@@ -73,8 +74,8 @@ const head = "0123456789abcdef0123456789abcdef01234567"
func TestAMessageIsIndexedAndAcked(t *testing.T) { // 1238/AC2 (unit part)
st := &memStore{vals: map[string][]byte{}}
ix := &index.Indexer{Store: st, Builds: index.NewBuilds()}
data, _ := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true})
data, _ := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true})
m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull", data: data}
heard := &memStore{vals: map[string][]byte{}}
handle(context.Background(), m, ix, heard, log.New(&bytes.Buffer{}, "", 0))
@@ -84,7 +85,7 @@ func TestAMessageIsIndexedAndAcked(t *testing.T) { // 1238/AC2 (unit part)
}
// Issue 1126 note 76: what the indexer heard of FORGE is readable (indexer_heard): subject, sequence, deliveries, time,
// repository, commit and what it did.
if h := heardOf(t, heard); h.Subject != m.subject || h.Sequence != 41 || h.Delivered != 2 || h.Published.Hour() != 13 || h.Kind != delivery.ForgePull ||
if h := heardOf(t, heard); h.Subject != m.subject || h.Sequence != 41 || h.Delivered != 2 || h.Published.Hour() != 13 || h.Kind != forgemsg.ForgePull ||
h.Repository != "novox/mesh-delivery" || h.Number != 12 || h.Commit != head || h.Outcome != index.HeardApplied || h.Handled.IsZero() {
t.Errorf("heard = %+v", h)
}
@@ -111,12 +112,12 @@ func TestAMessageThatIsNoForgeMessageIsTerminatedAndLogged(t *testing.T) { // 12
}{{`{"v":1,"group":"feat/x","members":[{"repository":"novox/mesh-sdk","number":3,"merge":"` + head + `"}]}`, 0, ""},
{`{"v":1,"group":"feat/x"}`, 1, "not a group's end"}} {
logs.Reset()
m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event." + delivery.ForgeGroupEndEvent, data: []byte(c.data)}
m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event." + forgemsg.ForgeGroupEndEvent, data: []byte(c.data)}
handle(context.Background(), m, &index.Indexer{Store: st, Builds: index.NewBuilds()}, heard, log.New(&logs, "", 0))
if m.termd != c.termd || m.acked+m.termd != 1 || !strings.Contains(logs.String(), c.logged) {
t.Errorf("group end %s: terminated %d acked %d log %q", c.data, m.termd, m.acked, logs.String())
}
if h := heardOf(t, heard); h.Kind != delivery.ForgeGroupEndEvent && c.termd == 0 {
if h := heardOf(t, heard); h.Kind != forgemsg.ForgeGroupEndEvent && c.termd == 0 {
t.Errorf("heard of a group's end = %+v", h)
}
}
@@ -9,6 +9,7 @@ import (
"path"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/fingerprint"
@@ -18,7 +19,7 @@ import (
// commitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea): with `paths`, by module directory
// (the root spelled ""), the paths relative to it, it answers {root, trees, paths} with paths by directory then path, each
// path's git object id; a path not at the commit fails the call naming it (the interface agreed in R4e; mesh-sdk
// delivery.Forge.Paths; novox/hq ADR 0499 clause 3).
// forgemsg.Forge.Paths; novox/hq ADR 0499 clause 3).
const commitTreesTool = "gitea.gitea_commit_trees"
// toolCaller calls a module's tool, `<module>.<tool>`, as the indexer's own account (mesh-sdk process.Conn.Call).
@@ -258,8 +259,8 @@ const (
)
// openVia asks the forge which pull requests into the trunk are open now (gitea_open_pulls), every repository's.
func openVia(call toolCaller) func(ctx context.Context) ([]delivery.OpenPull, error) {
return func(context.Context) ([]delivery.OpenPull, error) {
func openVia(call toolCaller) func(ctx context.Context) ([]forgemsg.OpenPull, error) {
return func(context.Context) ([]forgemsg.OpenPull, error) {
raw, err := call(openPullsTool, map[string]any{})
if err != nil {
return nil, err
@@ -275,10 +276,10 @@ func openVia(call toolCaller) func(ctx context.Context) ([]delivery.OpenPull, er
if err := json.Unmarshal(raw, &o); err != nil || o.Pulls == nil {
return nil, fmt.Errorf("the answer of %s is not {pulls}: %v", openPullsTool, err)
}
out := []delivery.OpenPull{}
out := []forgemsg.OpenPull{}
for _, p := range *o.Pulls {
if p.Base == trunkBranch && p.Number > 0 {
out = append(out, delivery.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number})
out = append(out, forgemsg.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number})
}
}
return out, nil
@@ -13,6 +13,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -71,10 +72,10 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
}
serveForge(t, js.Conn(), listing, goSumContent)
})
merged := func(repo string, number int, merge string, f delivery.Forge) {
merged := func(repo string, number int, merge string, f forgemsg.Forge) {
t.Helper()
f.Kind, f.Repository, f.Branch, f.Number, f.Head, f.Merge = delivery.ForgeMerged, repo, "feat/x", number, "0123456789abcdef0123456789abcdef01234567", merge
data, err := delivery.EncodeForge(f)
f.Kind, f.Repository, f.Branch, f.Number, f.Head, f.Merge = forgemsg.ForgeMerged, repo, "feat/x", number, "0123456789abcdef0123456789abcdef01234567", merge
data, err := forgemsg.EncodeForge(f)
if err != nil {
t.Fatal(err)
}
@@ -101,10 +102,10 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
"tsconfig.json": "5555555555555555555555555555555555555555"}}
}
// mesh-sdk at m1, then the catalogue's two modules: their go.sum names mesh-sdk, read by the indexer itself.
merged("novox/mesh-sdk", 70, m1, delivery.Forge{Root: sdkRoot1, Trees: map[string]string{"": sdkRoot1},
merged("novox/mesh-sdk", 70, m1, forgemsg.Forge{Root: sdkRoot1, Trees: map[string]string{"": sdkRoot1},
Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("6666666666666666666666666666666666666666")})
module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m1 })
merged("novox/mesh-catalog", 301, catalogMerge, delivery.Forge{Root: "r0",
merged("novox/mesh-catalog", 301, catalogMerge, forgemsg.Forge{Root: "r0",
Trees: map[string]string{"modules/importer": "t1", "modules/plain": "t2"},
Manifests: map[string]string{"modules/importer": named("importer"), "modules/plain": named("plain")},
Locks: map[string]map[string]string{"modules/importer": {"go.sum": goSumHash}, "modules/plain": {"go.sum": goSumHash}}})
@@ -140,11 +141,11 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// over its named tree, is not written (issue 1758, ADR 0499 clause 6; the operator, 2026-10-25: a module's value moves
// only with the module).
const goOnly = "4444444444444444444444444444444444444444"
merged("novox/mesh-sdk", 72, goOnly, delivery.Forge{Root: "cccccccccccccccccccccccccccccccccccccccc",
merged("novox/mesh-sdk", 72, goOnly, forgemsg.Forge{Root: "cccccccccccccccccccccccccccccccccccccccc",
Trees: map[string]string{"": "cccccccccccccccccccccccccccccccccccccccc"}, Manifests: map[string]string{"": string(sdkManifest)},
Paths: sdkObjects("6666666666666666666666666666666666666666")})
// mesh-sdk merges a change of buildstep alone (its version moves: src changed too).
merged("novox/mesh-sdk", 71, m2, delivery.Forge{Root: sdk2, Trees: map[string]string{"": sdk2},
merged("novox/mesh-sdk", 71, m2, forgemsg.Forge{Root: sdk2, Trees: map[string]string{"": sdk2},
Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("7777777777777777777777777777777777777777")})
sdkAt2 := module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m2 })
if sdkAt2.Inputs2 == nil || !strings.HasPrefix(sdkAt2.Inputs2.Tree, delivery.NamedTreePrefix) {
@@ -6,6 +6,7 @@ import (
"fmt"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"github.com/nats-io/nats.go"
@@ -65,13 +66,13 @@ func sayAccepted(js jetstream.JetStream, cred process.Account, logf func(string,
// sayTakeIn publishes a take-in event on the indexer's own stream take-in (novox/hq ADR 0633 §5):
// `mesh.stream.<module>.event.take-in.<kind>.<key>`. A publish that fails is the caller's to say.
func sayTakeIn(js jetstream.JetStream, cred process.Account) func(context.Context, delivery.TakeIn) error {
return func(ctx context.Context, t delivery.TakeIn) error {
event, err := delivery.TakeInEvent(t.Kind, t.Key)
func sayTakeIn(js jetstream.JetStream, cred process.Account) func(context.Context, takein.TakeIn) error {
return func(ctx context.Context, t takein.TakeIn) error {
event, err := takein.TakeInEvent(t.Kind, t.Key)
if err != nil {
return err
}
data, err := delivery.EncodeTakeIn(t)
data, err := takein.EncodeTakeIn(t)
if err != nil {
return err
}
+10 -2
View File
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
@@ -24,6 +24,14 @@ require (
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun
require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
)
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ./takein
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ../mesh-delivery-forge-bridge/forgemsg
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -7,12 +7,13 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
func manifestPull(t *testing.T, number int, head, manifest string) delivery.Forge {
f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: number, Head: head,
Action: delivery.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true}
func manifestPull(t *testing.T, number int, head, manifest string) forgemsg.Forge {
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: number, Head: head,
Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true}
if manifest != "" {
f.Manifests = map[string]string{"modules/avahi": manifest}
}
@@ -11,6 +11,8 @@ import (
"sync/atomic"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -98,7 +100,7 @@ type Indexer struct {
// ReadOpen asks the forge which pull requests into the trunk are open now (gitea_open_pulls). A catalogue pull message
// that finds the candidates of another pull request asks it and deletes those of a pull request no longer open, so a
// closed message the indexer missed heals at the next one (issue 1126); nil asks none.
ReadOpen func(ctx context.Context) ([]delivery.OpenPull, error)
ReadOpen func(ctx context.Context) ([]forgemsg.OpenPull, error)
mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach
source sourceNow
@@ -106,7 +108,7 @@ type Indexer struct {
toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go)
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
moving map[string]forgemsg.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
@@ -125,7 +127,7 @@ type Indexer struct {
// takes every merge in at once, as before the take-in step.
Pending Store
// SayTakeIn publishes a take-in event on the indexer's own stream take-in (ADR 0633 §5); nil says none.
SayTakeIn func(ctx context.Context, t delivery.TakeIn) error
SayTakeIn func(ctx context.Context, t takein.TakeIn) error
pendingMu sync.Mutex // one take-in step at a time: a merge, a group's end, the start and a hand act (pending.go)
}
@@ -139,23 +141,23 @@ func (ix *Indexer) logf(format string, args ...any) {
// pull request, a closed one or a merged message takes it out; a group left with none is deleted. A change is written
// by compare-and-set on the revision read, and read again on a conflict. A lone pull request is a group of one. A
// merged message also writes the fingerprint of each module of its repository (modules.go).
func (ix *Indexer) Apply(ctx context.Context, f delivery.Forge) error {
func (ix *Indexer) Apply(ctx context.Context, f forgemsg.Forge) error {
_, err := ix.ApplyMessage(ctx, f)
return err
}
// ApplyMessage is Apply, and answers which modules a merged message wrote and kept (for the indexer's heard state). Each
// applied merged message is said in one line: its repository, commit and pull request, and the modules written.
func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied, error) {
func (ix *Indexer) ApplyMessage(ctx context.Context, f forgemsg.Forge) (Applied, error) {
ix.kickJudge(ctx)
var applied Applied
if f.Kind == delivery.ForgeOpen {
if f.Kind == forgemsg.ForgeOpen {
return applied, ix.applyOpen(ctx, f)
}
if f.Kind != delivery.ForgePull && f.Kind != delivery.ForgeMerged {
if f.Kind != forgemsg.ForgePull && f.Kind != forgemsg.ForgeMerged {
return applied, nil
}
if f.Kind == delivery.ForgeMerged && ix.Modules != nil {
if f.Kind == forgemsg.ForgeMerged && ix.Modules != nil {
w, err := ix.applyMerged(ctx, f)
applied = Applied{Written: w.written, Late: w.late}
if err != nil {
@@ -184,7 +186,7 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied,
}
var digests map[string]string
var held map[string]delivery.Module
if f.Kind == delivery.ForgePull {
if f.Kind == forgemsg.ForgePull {
var err error
if digests, err = ix.keepManifests(ctx, f); err != nil {
return applied, err
@@ -251,8 +253,8 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied,
// applyOpen takes an open message into the candidates (novox/hq ADR 0443 §5): every pull request it does not name is
// taken out of its group, and a group left with none is deleted. What the message names is left as it is. A pull request
// opened after the bridge read the forge is heard after this message and added again by its own.
func (ix *Indexer) applyOpen(ctx context.Context, f delivery.Forge) error {
named := make(map[delivery.OpenPull]bool, len(f.Open))
func (ix *Indexer) applyOpen(ctx context.Context, f forgemsg.Forge) error {
named := make(map[forgemsg.OpenPull]bool, len(f.Open))
for _, p := range f.Open {
named[p] = true
}
@@ -270,7 +272,7 @@ func (ix *Indexer) applyOpen(ctx context.Context, f delivery.Forge) error {
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
}
kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p delivery.CandidatePull) bool {
return !named[delivery.OpenPull{Repository: p.Repository, Number: p.Number}]
return !named[forgemsg.OpenPull{Repository: p.Repository, Number: p.Number}]
})
switch {
case len(kept) == len(c.Pulls):
@@ -337,14 +339,14 @@ func write(ctx context.Context, st Store, key string, compute func(value []byte,
// its manifest and why it has no fingerprint of the new form (NoneNoPreview), and the candidate its FPSet (issue 1451); a
// pull request whose head is held again without its directories keeps the Fps it had. digests are the manifests' digests by
// directory; held are the modules the modules bucket holds, which name the module of a changed directory (moduleOfDir).
func (ix *Indexer) next(cur *delivery.Candidate, f delivery.Forge, digests map[string]string, held map[string]delivery.Module) *delivery.Candidate {
func (ix *Indexer) next(cur *delivery.Candidate, f forgemsg.Forge, digests map[string]string, held map[string]delivery.Module) *delivery.Candidate {
var pulls []delivery.CandidatePull
var base uint64
if cur != nil {
pulls, base = slices.Clone(cur.Pulls), cur.Base
}
at := slices.IndexFunc(pulls, func(p delivery.CandidatePull) bool { return p.Repository == f.Repository && p.Number == f.Number })
if f.Kind == delivery.ForgeMerged || f.Action == delivery.PullClosed {
if f.Kind == forgemsg.ForgeMerged || f.Action == forgemsg.PullClosed {
if at < 0 {
return cur
}
@@ -10,6 +10,7 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -27,7 +28,7 @@ var (
// writePredefined keeps the predefined list a merged message carries, unless the key names a later merge: a merge older
// than the one it names never replaces it (ADR 0348 §7). The same list at the same merge writes nothing (ADR 0289 §1).
func (ix *Indexer) writePredefined(ctx context.Context, f delivery.Forge) error {
func (ix *Indexer) writePredefined(ctx context.Context, f forgemsg.Forge) error {
if ix.Predefined == nil || len(f.Predefined) == 0 {
return nil
}
@@ -203,8 +204,8 @@ func (ix *Indexer) dropConfigCandidate(ctx context.Context, repository string, n
// configCandidateOfPull is a pull message's part in config-candidates: a head whose manifests the message carries is written,
// a closed one deleted. A head held again without its directories leaves the key as it is.
func (ix *Indexer) configCandidateOfPull(ctx context.Context, f delivery.Forge, held map[string]delivery.Module) error {
if f.Action == delivery.PullClosed {
func (ix *Indexer) configCandidateOfPull(ctx context.Context, f forgemsg.Forge, held map[string]delivery.Module) error {
if f.Action == forgemsg.PullClosed {
return ix.dropConfigCandidate(ctx, f.Repository, f.Number)
}
if !f.DirsKnown {
@@ -220,7 +221,7 @@ func (ix *Indexer) configCandidateOfPull(ctx context.Context, f delivery.Forge,
// configCandidatesAfterMerge writes again every open pull request's key that names a module the merge changed, since what
// it adds is judged against the trunk (ADR 0565 §2), from the head's manifests its candidate keeps; and deletes the merged
// pull request's.
func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f delivery.Forge, changed []string) error {
func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f forgemsg.Forge, changed []string) error {
if ix.ConfigCandidates == nil {
return nil
}
@@ -7,6 +7,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -77,7 +78,7 @@ func TestTheControlRecords(t *testing.T) {
t.Errorf("a head declaring nothing new = %+v, want an empty set", c)
}
closed := pull(catalogueRepo, 7, "feat/y", strings.Repeat("d", 40))
closed.Action = delivery.PullClosed
closed.Action = forgemsg.PullClosed
if err := ix.Apply(ctx, closed); err != nil {
t.Fatal(err)
}
@@ -5,6 +5,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -91,9 +92,9 @@ func buildRecord(t *testing.T, repo, module, path, at string) []byte {
return logged(t, "build-"+module, module, repo, path, "", "", at)
}
func pull(repo string, number int, branch, head string, dirs ...string) delivery.Forge {
return delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: repo, Branch: branch, Number: number, Head: head,
Action: delivery.PullUpdated, Dirs: dirs, DirsKnown: dirs != nil}
func pull(repo string, number int, branch, head string, dirs ...string) forgemsg.Forge {
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: repo, Branch: branch, Number: number, Head: head,
Action: forgemsg.PullUpdated, Dirs: dirs, DirsKnown: dirs != nil}
}
func newIndexer(t *testing.T) (*Indexer, *memStore, *strings.Builder) {
@@ -212,13 +213,13 @@ func TestClosedAndMergedTakeAPullOutAndTheLastDeletesTheKey(t *testing.T) { // 1
ctx := context.Background()
_ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
_ = ix.Apply(ctx, pull("novox/mesh-catalog", 300, "feat/x", h2, "modules/c"))
gone := func(repo string, n int, head string) delivery.Forge {
gone := func(repo string, n int, head string) forgemsg.Forge {
if how == "closed" {
f := pull(repo, n, "feat/x", head)
f.Action = delivery.PullClosed
f.Action = forgemsg.PullClosed
return f
}
return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}}
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}}
}
if err := ix.Apply(ctx, gone("novox/mesh-delivery", 12, h1)); err != nil {
t.Fatal(err)
@@ -330,8 +331,8 @@ func TestAnOddBranchNameKeysEscaped(t *testing.T) {
}
}
func open(pulls ...delivery.OpenPull) delivery.Forge {
return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeOpen, Open: pulls}
func open(pulls ...forgemsg.OpenPull) forgemsg.Forge {
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeOpen, Open: pulls}
}
// 1279/AC7 (ADR 0443 §5): on an open message, candidates lose every pull request it does not name, a group left empty is
@@ -339,7 +340,7 @@ func open(pulls ...delivery.OpenPull) delivery.Forge {
func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/AC7
ix, st, _ := newIndexer(t)
ctx := context.Background()
for _, f := range []delivery.Forge{
for _, f := range []forgemsg.Forge{
pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"), pull("novox/mesh-catalog", 300, "feat/x", h2, "modules/c"),
pull("novox/mesh-delivery", 13, "feat/y", h2, "modules/a"),
pull("novox/mesh-catalog", 280, "feat/old", h3, "modules/c"),
@@ -351,7 +352,7 @@ func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/A
before, _ := read(t, st, "feat/x")
// Open: mesh-delivery#12 only (and a pull request no group holds, which changes nothing).
if err := ix.Apply(ctx, open(delivery.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, delivery.OpenPull{Repository: "novox/mesh-catalog", Number: 999})); err != nil {
if err := ix.Apply(ctx, open(forgemsg.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, forgemsg.OpenPull{Repository: "novox/mesh-catalog", Number: 999})); err != nil {
t.Fatal(err)
}
c, ok := read(t, st, "feat/x")
@@ -374,7 +375,7 @@ func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/A
}
// The same open message again changes nothing (no write), and an empty one drops every group.
writes := st.writes
if err := ix.Apply(ctx, open(delivery.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, delivery.OpenPull{Repository: "novox/mesh-catalog", Number: 300})); err != nil {
if err := ix.Apply(ctx, open(forgemsg.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, forgemsg.OpenPull{Repository: "novox/mesh-catalog", Number: 300})); err != nil {
t.Fatal(err)
}
if st.writes != writes {
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -48,7 +49,7 @@ func (ix *Indexer) keepManifest(ctx context.Context, module, raw string) (string
// keepManifests writes the manifest of each directory a message carries, and answers the digest of each by directory.
// A manifest that cannot be kept is left out and said in the log, so the module reads as one with no manifest.
func (ix *Indexer) keepManifests(ctx context.Context, f delivery.Forge) (map[string]string, error) {
func (ix *Indexer) keepManifests(ctx context.Context, f forgemsg.Forge) (map[string]string, error) {
digests := make(map[string]string, len(f.Manifests))
for dir, raw := range f.Manifests {
module := manifestModule(raw) // the name the manifest gives itself: one digest always has one module
@@ -5,6 +5,7 @@ import (
"os"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -37,7 +38,7 @@ func manifestsIndexer(t *testing.T, recs ...[2]any) (*Indexer, *memStore, *memSt
return ix, mods, manifests
}
func withManifests(f delivery.Forge, manifests map[string]string, locks map[string]map[string]string) delivery.Forge {
func withManifests(f forgemsg.Forge, manifests map[string]string, locks map[string]map[string]string) forgemsg.Forge {
f.Manifests, f.Locks = manifests, locks
return f
}
@@ -81,8 +82,8 @@ func sameManifestAppliedAgainIsWrittenOnce(t *testing.T) { // 1449/AC2
if err := ix.Apply(context.Background(), f); err != nil {
t.Errorf("a second merge: %v", err)
}
pull := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: 301, Head: h1,
Action: delivery.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": avahi}}
pull := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: 301, Head: h1,
Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": avahi}}
if err := ix.Apply(context.Background(), pull); err != nil {
t.Errorf("a pull message: %v", err)
}
@@ -7,6 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -31,20 +32,20 @@ import (
//
// A merge never moves a module backwards (order.go): a merged message older than the repository's taken merge writes and
// deletes nothing, and is said; it is asked once, before anything is written.
func (ix *Indexer) applyMerged(ctx context.Context, f delivery.Forge) (*mergeWrite, error) {
func (ix *Indexer) applyMerged(ctx context.Context, f forgemsg.Forge) (*mergeWrite, error) {
return ix.applyManifests(ctx, f, true)
}
// applyManifests is applyMerged; drop says whether the modules of the repository the message holds no manifest for are
// deleted. A message that names only some of a repository's modules (a registered module read on its own, registered.go)
// drops none.
func (ix *Indexer) applyManifests(ctx context.Context, f delivery.Forge, drop bool) (*mergeWrite, error) {
func (ix *Indexer) applyManifests(ctx context.Context, f forgemsg.Forge, drop bool) (*mergeWrite, error) {
return ix.applyMoving(ctx, f, drop, nil)
}
// applyMoving is applyManifests; moves names the modules keys the message may take from the repository that holds them
// (a move handed over, moves.go). Every other module held of another repository is left, said and kept waiting.
func (ix *Indexer) applyMoving(ctx context.Context, f delivery.Forge, drop bool, moves map[string]bool) (*mergeWrite, error) {
func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool, moves map[string]bool) (*mergeWrite, error) {
w := newMergeWrite(f)
if drop {
if err := ix.lateMerge(ctx, w); err != nil || w.late != "" {
@@ -180,7 +181,7 @@ func withV(m delivery.Module) delivery.Module { m.V = delivery.Version; return m
// namedObjects are, by module directory, the git objects of the paths each manifest of the merge names for every artifact
// (catalogue Manifest.NamedPaths): those the message carries, else asked of the forge at the merge commit in one call
// (Named). A forge that cannot say gives, for each directory asked, its words, so the module's reason names them.
func (ix *Indexer) namedObjects(ctx context.Context, f delivery.Forge) (map[string]map[string]string, map[string]string) {
func (ix *Indexer) namedObjects(ctx context.Context, f forgemsg.Forge) (map[string]map[string]string, map[string]string) {
objects := map[string]map[string]string{}
need := map[string][]string{}
for dir, raw := range f.Manifests {
@@ -220,7 +221,7 @@ func (ix *Indexer) namedObjects(ctx context.Context, f delivery.Forge) (map[stri
// held(key, module) says whether the trunk holds it. A module of another repository, or of none known, is never touched.
func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[string]delivery.Module, trunkHolds func(key string, m delivery.Module) bool) error {
var handed []string
elsewhere := map[string]delivery.Forge{}
elsewhere := map[string]forgemsg.Forge{}
for _, key := range slices.Sorted(maps.Keys(held)) {
cur := held[key]
if cur.Repository == "" || !delivery.SameRepository(cur.Repository, repository) || trunkHolds(key, cur) {
@@ -354,7 +355,7 @@ func unreadInputs(tree string, locks, paths map[string]string, packages []string
// goSumPackages are the version line's packages of the merge's directory dir (ADR 0573 §2, ADR 0593 §2): the mesh Go
// modules of other repositories its go.sum names, and those of its own repository its go.mod replaces by a directory
// (siblingPackages), which no go.sum line names. A directory with no go.sum is no Go module and has none.
func (ix *Indexer) goSumPackages(ctx context.Context, f delivery.Forge, dir string, cur delivery.Module, has bool) []string {
func (ix *Indexer) goSumPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string {
others := ix.otherPackages(ctx, f, dir, cur, has)
if f.Locks[dir]["go.sum"] == "" && len(f.Packages[dir]) == 0 {
return others
@@ -368,7 +369,7 @@ func (ix *Indexer) goSumPackages(ctx context.Context, f delivery.Forge, dir stri
// siblingPackages are the mesh Go modules of the merge's own repository the go.mod of dir replaces by a directory, read
// from the forge (GoMod); where it cannot be read, the held module's own-repository packages stand, said.
func (ix *Indexer) siblingPackages(ctx context.Context, f delivery.Forge, dir string, cur delivery.Module, has bool) []string {
func (ix *Indexer) siblingPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string {
held := func() []string {
if !has || cur.Inputs2 == nil {
return nil
@@ -395,7 +396,7 @@ func (ix *Indexer) siblingPackages(ctx context.Context, f delivery.Forge, dir st
// otherPackages are the mesh Go modules of other repositories the go.sum of the merge's directory dir names: those the
// message carries (an older bridge), else the held module's while its go.sum's hash is unchanged, else the go.sum read
// from the forge (GoSum).
func (ix *Indexer) otherPackages(ctx context.Context, f delivery.Forge, dir string, cur delivery.Module, has bool) []string {
func (ix *Indexer) otherPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string {
if carried := f.Packages[dir]; len(carried) > 0 {
return fingerprint.OtherPackages(carried, f.Repository) // its own repository's come with its tree
}
@@ -10,6 +10,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -71,12 +72,12 @@ func rec(t *testing.T, id, module, repo, path, ref, state, at, inputs string) [2
// mergedMsg is a merged message of the repository with these module directories' trees, each with a manifest naming its
// module, the directory's last element ("r" for the root), as the forge bridge sends it (one manifest per directory that
// holds a module.json).
func mergedMsg(repo string, trees map[string]string) delivery.Forge {
func mergedMsg(repo string, trees map[string]string) forgemsg.Forge {
manifests := map[string]string{}
for dir := range trees {
manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}`
}
return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests}
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests}
}
func nameOfDir(dir string) string {
@@ -459,7 +460,7 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) {
gone.Trees, gone.Manifests = nil, nil
for _, tc := range []struct {
name string
merges []delivery.Forge
merges []forgemsg.Forge
repository string // modules.a's repository at the end, "" for none
deletedKeyA bool
// Issue 1367: a fresh indexer, no wait in memory, the source naming heads; trunk is the module directories the forge
@@ -472,20 +473,20 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) {
stored bool
bGoneAtEnd bool
}{
{name: "named while its trunk holds it: left", merges: []delivery.Forge{held, named}, repository: from},
{name: "named, then dropped by its trunk: handed over in place", merges: []delivery.Forge{held, named, dropped}, repository: to},
{name: "dropped by its trunk, then named: deleted, then written new", merges: []delivery.Forge{held, dropped, named}, repository: to, deletedKeyA: true},
{name: "dropped by its trunk with no merge naming it: deleted", merges: []delivery.Forge{held, dropped}, deletedKeyA: true},
{name: "named while its trunk holds it: left", merges: []forgemsg.Forge{held, named}, repository: from},
{name: "named, then dropped by its trunk: handed over in place", merges: []forgemsg.Forge{held, named, dropped}, repository: to},
{name: "dropped by its trunk, then named: deleted, then written new", merges: []forgemsg.Forge{held, dropped, named}, repository: to, deletedKeyA: true},
{name: "dropped by its trunk with no merge naming it: deleted", merges: []forgemsg.Forge{held, dropped}, deletedKeyA: true},
{name: "no module.json left, another trunk holds it, nothing waits in memory: written from that trunk, never deleted",
merges: []delivery.Forge{held, gone}, heads: true, trunk: []string{"modules/a", "modules/other"}, repository: to, bGoneAtEnd: true},
merges: []forgemsg.Forge{held, gone}, heads: true, trunk: []string{"modules/a", "modules/other"}, repository: to, bGoneAtEnd: true},
{name: "no module.json left, no other trunk holds it: deleted",
merges: []delivery.Forge{held, gone}, heads: true, deletedKeyA: true, bGoneAtEnd: true},
merges: []forgemsg.Forge{held, gone}, heads: true, deletedKeyA: true, bGoneAtEnd: true},
{name: "taken in by repository, a head with no module.json: handed over from the other trunk",
merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: from, answer: "handed over", stored: true, repository: to, bGoneAtEnd: true},
merges: []forgemsg.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: from, answer: "handed over", stored: true, repository: to, bGoneAtEnd: true},
{name: "taken in by repository, a head with modules: answered by name",
merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: to, answer: "by name", repository: from},
merges: []forgemsg.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: to, answer: "by name", repository: from},
{name: "taken in by repository, no head named: answered so",
merges: []delivery.Forge{held}, heads: true, takeIn: "novox/nobody", answer: "names no head", repository: from},
merges: []forgemsg.Forge{held}, heads: true, takeIn: "novox/nobody", answer: "names no head", repository: from},
} {
t.Run(tc.name, func(t *testing.T) {
mods := &deletesSeen{memStore: newMem()}
@@ -7,6 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -14,9 +15,9 @@ import (
// holds it at the head the latest source names: the module directories there are asked of the forge (Trunk) and the manifest of
// each directory named like the module is read (Read). It answers the merge to hand the module over with, or none: a forge
// that cannot say leaves the module to be dropped as it was, said (issue 1367).
func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository string) (delivery.Forge, bool) {
func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository string) (forgemsg.Forge, bool) {
if ix.Trunk == nil || ix.Read == nil {
return delivery.Forge{}, false
return forgemsg.Forge{}, false
}
ix.mu.Lock()
heads := maps.Clone(ix.source.heads)
@@ -45,7 +46,7 @@ func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository st
if read.Trees[dir] == "" || manifestModule(raw) != module {
continue
}
f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: other, Merge: head, Root: read.Root,
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: other, Merge: head, Root: read.Root,
Trees: map[string]string{dir: read.Trees[dir]}, Manifests: map[string]string{dir: raw}}
if l := read.Locks[dir]; len(l) > 0 {
f.Locks = map[string]map[string]string{dir: l}
@@ -57,7 +58,7 @@ func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository st
return f, true
}
}
return delivery.Forge{}, false
return forgemsg.Forge{}, false
}
// nameOfDirectory is the last element of a module directory.
@@ -76,8 +77,8 @@ func nameOfDirectory(dir string) string { return dir[strings.LastIndex(dir, "/")
// waitToMove keeps, by modules key, the part of a merged message naming one module directory the indexer holds of another
// repository. A later merge of the same repository replaces it.
func (ix *Indexer) waitToMove(key string, f delivery.Forge, dir string) {
one := delivery.Forge{V: f.V, Kind: f.Kind, Repository: f.Repository, Branch: f.Branch, Number: f.Number, Merge: f.Merge, Root: f.Root,
func (ix *Indexer) waitToMove(key string, f forgemsg.Forge, dir string) {
one := forgemsg.Forge{V: f.V, Kind: f.Kind, Repository: f.Repository, Branch: f.Branch, Number: f.Number, Merge: f.Merge, Root: f.Root,
Trees: map[string]string{dir: f.Trees[dir]}, Manifests: map[string]string{dir: f.Manifests[dir]}}
if l := f.Locks[dir]; len(l) > 0 {
one.Locks = map[string]map[string]string{dir: l}
@@ -91,7 +92,7 @@ func (ix *Indexer) waitToMove(key string, f delivery.Forge, dir string) {
ix.mu.Lock()
defer ix.mu.Unlock()
if ix.moving == nil {
ix.moving = map[string]delivery.Forge{}
ix.moving = map[string]forgemsg.Forge{}
}
ix.moving[key] = one
}
@@ -117,7 +118,7 @@ func (ix *Indexer) handOver(ctx context.Context, key string) error {
}
// handOverFrom writes the module at key from the merge f, which names it, in place.
func (ix *Indexer) handOverFrom(ctx context.Context, key string, f delivery.Forge) error {
func (ix *Indexer) handOverFrom(ctx context.Context, key string, f forgemsg.Forge) error {
w, err := ix.applyMoving(ctx, f, false, map[string]bool{key: true})
if err != nil {
return fmt.Errorf("modules.%s, moving to %s: %w", key, f.Repository, err)
@@ -7,6 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -46,7 +47,7 @@ func (w *mergeWrite) taken(digest string) {
w.fresh[digest] = true
}
func newMergeWrite(f delivery.Forge) *mergeWrite {
func newMergeWrite(f forgemsg.Forge) *mergeWrite {
return &mergeWrite{repository: f.Repository, merge: f.Merge}
}
@@ -7,6 +7,7 @@ import (
"path"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -70,7 +71,7 @@ func moduleAt(held map[string]delivery.Module, repository, dir string) (string,
// moduleOfDir is the module of a changed directory of a pull request: the module its manifest in the message names, else
// the one the modules bucket holds at that directory of the repository, else the directory's last element (the
// catalogue's rule for nearly every manifest).
func moduleOfDir(held map[string]delivery.Module, f delivery.Forge, dir string) string {
func moduleOfDir(held map[string]delivery.Module, f forgemsg.Forge, dir string) string {
if name := manifestModule(f.Manifests[dir]); name != "" {
return name
}
@@ -10,6 +10,8 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -21,7 +23,7 @@ import (
// build of the indexer writes only what building and moving that indexer needs, and the rest stays pending for it.
const (
// PendingState is the indexer's state of held merges: the bucket mesh-delivery-indexer_pending, keyed
// delivery.PendingKey, so the key order is the merge order. An old `state` entry (the indexer is foundation, ADR
// takein.PendingKey, so the key order is the merge order. An old `state` entry (the indexer is foundation, ADR
// 0595 §11), history 1, reached by the indexer alone.
PendingState = "pending"
PendingBucket = delivery.IndexerModule + "_" + PendingState
@@ -66,23 +68,23 @@ type PendingEntry struct {
Key string
Revision uint64
Value Pending
Forge delivery.Forge
Forge forgemsg.Forge
}
// DecodePending reads one pending value and its merged message.
func DecodePending(data []byte) (Pending, delivery.Forge, error) {
func DecodePending(data []byte) (Pending, forgemsg.Forge, error) {
var p Pending
if err := json.Unmarshal(data, &p); err != nil {
return p, delivery.Forge{}, fmt.Errorf("not a pending value: %w", err)
return p, forgemsg.Forge{}, fmt.Errorf("not a pending value: %w", err)
}
if p.V != 1 {
return p, delivery.Forge{}, fmt.Errorf("a pending value of version %d, not 1", p.V)
return p, forgemsg.Forge{}, fmt.Errorf("a pending value of version %d, not 1", p.V)
}
f, err := delivery.DecodeForge(p.Forge)
f, err := forgemsg.DecodeForge(p.Forge)
if err != nil {
return p, f, fmt.Errorf("its merged message: %w", err)
}
if f.Kind != delivery.ForgeMerged {
if f.Kind != forgemsg.ForgeMerged {
return p, f, fmt.Errorf("a pending value holds a %s message, not a merged one", f.Kind)
}
return p, f, nil
@@ -143,7 +145,7 @@ func WaitsOf(entries []PendingEntry) Waits {
w.Groups[e.Value.Group] = append(w.Groups[e.Value.Group], e)
for _, m := range e.Forge.Members {
if _, ok := blocked[strings.ToLower(m.Repository)]; !ok {
blocked[strings.ToLower(m.Repository)] = delivery.GroupTakeInKey(e.Value.Group)
blocked[strings.ToLower(m.Repository)] = takein.GroupTakeInKey(e.Value.Group)
}
}
continue
@@ -158,10 +160,10 @@ func WaitsOf(entries []PendingEntry) Waits {
}
// mergesOf are the merges of entries, oldest first.
func mergesOf(entries []PendingEntry) []delivery.GroupMerge {
out := make([]delivery.GroupMerge, len(entries))
func mergesOf(entries []PendingEntry) []takein.Merge {
out := make([]takein.Merge, len(entries))
for i, e := range entries {
out[i] = delivery.GroupMerge{Repository: e.Forge.Repository, Number: e.Forge.Number, Merge: e.Forge.Merge}
out[i] = takein.Merge{Repository: e.Forge.Repository, Number: e.Forge.Number, Merge: e.Forge.Merge}
}
return out
}
@@ -177,8 +179,8 @@ func sinceOf(entries []PendingEntry) time.Time {
}
// groupWait is the take-in body of a group being gathered.
func groupWait(kind, group string, held []PendingEntry) delivery.TakeIn {
t := delivery.TakeIn{Kind: kind, Key: delivery.GroupTakeInKey(group), What: delivery.TakeInGroup, Group: group,
func groupWait(kind, group string, held []PendingEntry) takein.TakeIn {
t := takein.TakeIn{Kind: kind, Key: takein.GroupTakeInKey(group), What: takein.TakeInGroup, Group: group,
Merges: mergesOf(held), Since: sinceOf(held)}
if len(held) > 0 {
t.Members = held[len(held)-1].Forge.Members
@@ -187,8 +189,8 @@ func groupWait(kind, group string, held []PendingEntry) delivery.TakeIn {
}
// revisionWait is the take-in body of a revision: its key is its oldest merge's.
func revisionWait(kind string, ready []PendingEntry, waitsFor string) delivery.TakeIn {
return delivery.TakeIn{Kind: kind, Key: ready[0].Key, What: delivery.TakeInRevision, Merges: mergesOf(ready),
func revisionWait(kind string, ready []PendingEntry, waitsFor string) takein.TakeIn {
return takein.TakeIn{Kind: kind, Key: ready[0].Key, What: takein.TakeInRevision, Merges: mergesOf(ready),
WaitsFor: waitsFor, Since: sinceOf(ready)}
}
@@ -196,7 +198,7 @@ func revisionWait(kind string, ready []PendingEntry, waitsFor string) delivery.T
// (issue 1790), and until then every merge is taken in at once, so the build this program runs is not needed. A read
// that fails otherwise counts as on the bus: the step that follows answers the error.
func (ix *Indexer) pendingOnBus(ctx context.Context) bool {
_, _, _, err := ix.Pending.Get(ctx, delivery.PendingKey(0))
_, _, _, err := ix.Pending.Get(ctx, takein.PendingKey(0))
return !errors.Is(err, process.ErrNotYet)
}
@@ -216,7 +218,7 @@ func (ix *Indexer) ownBuildNow(ctx context.Context) string {
// sayTakeIn publishes a take-in event and keeps what it said on the entries, so indexer_pending reads the last event of
// each wait from the state. A publish that fails is said in the log: the next evaluation says it again.
func (ix *Indexer) sayTakeIn(ctx context.Context, t delivery.TakeIn) bool {
func (ix *Indexer) sayTakeIn(ctx context.Context, t takein.TakeIn) bool {
if ix.SayTakeIn == nil {
return true
}
@@ -229,7 +231,7 @@ func (ix *Indexer) sayTakeIn(ctx context.Context, t delivery.TakeIn) bool {
// markSaid writes on each entry the wait it was said under, by compare-and-set; a lost one is left (its next
// evaluation says it again).
func (ix *Indexer) markSaid(ctx context.Context, entries []PendingEntry, t *delivery.TakeIn, change func(*Pending)) {
func (ix *Indexer) markSaid(ctx context.Context, entries []PendingEntry, t *takein.TakeIn, change func(*Pending)) {
for _, e := range entries {
p := e.Value
if t != nil {
@@ -253,7 +255,7 @@ func (ix *Indexer) markSaid(ctx context.Context, entries []PendingEntry, t *deli
// holds is evaluated (Evaluate): taken in whole when its revision names the build this program runs, or held for the
// indexer it names. It answers what was taken in, and a line saying what waits. A message is never written while the
// build this program runs is not known (ErrOwnBuildUnknown).
func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Applied, string, error) {
func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f forgemsg.Forge) (Applied, string, error) {
if ix.Pending == nil || ix.Modules == nil || !ix.pendingOnBus(ctx) {
// The state this delivery brings is not on the bus yet (issue 1790): the merge is taken in at once, as before,
// whether or not the build this program runs is known.
@@ -266,11 +268,11 @@ func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Ap
}
ix.pendingMu.Lock()
defer ix.pendingMu.Unlock()
raw, err := delivery.EncodeForge(f)
raw, err := forgemsg.EncodeForge(f)
if err != nil {
return Applied{}, "", fmt.Errorf("%w: %v", ErrUnreadable, err)
}
key := delivery.PendingKey(seq)
key := takein.PendingKey(seq)
data, err := encodePending(Pending{Forge: raw, Group: f.Group, Since: ix.now(), LastWriter: own})
if err != nil {
return Applied{}, "", err
@@ -290,7 +292,7 @@ func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Ap
return Applied{}, "", err
}
if held := WaitsOf(entries).Groups[f.Group]; len(held) > 0 {
t := groupWait(delivery.TakeInPending, f.Group, held)
t := groupWait(takein.TakeInPending, f.Group, held)
if ix.sayTakeIn(ctx, t) {
ix.markSaid(ctx, held, &t, nil)
}
@@ -304,7 +306,7 @@ func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Ap
// one revision. A member named and neither held nor taken in yet answers an error, so the end is delivered again once
// the member arrived. An end of a group taken in already (every member's merge at or behind its repository's taken
// merge) changes nothing and is said: it is never delivered again for ever.
func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Applied, string, error) {
func (ix *Indexer) EndGroup(ctx context.Context, end forgemsg.ForgeGroupEnd) (Applied, string, error) {
if ix.Pending == nil || !ix.pendingOnBus(ctx) {
return Applied{}, "no pending state: the group's merges were taken in one by one", nil
}
@@ -327,7 +329,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Ap
held = append(held, e)
}
}
isHeld := func(m delivery.GroupMerge) bool {
isHeld := func(m forgemsg.GroupMerge) bool {
return slices.ContainsFunc(held, func(e PendingEntry) bool {
return delivery.SameRepository(e.Forge.Repository, m.Repository) && e.Forge.Number == m.Number
})
@@ -357,7 +359,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Ap
}
// The group's wait ends first (take-in.taken under its group key); a revision it then is, pending for another
// indexer, is said pending under its oldest merge's key by the evaluation that follows.
t := groupWait(delivery.TakeInTaken, end.Group, held)
t := groupWait(takein.TakeInTaken, end.Group, held)
said := &t
if !ix.sayTakeIn(ctx, t) {
said = nil // said again by the take-in or the hold that follows (saidWaits)
@@ -368,7 +370,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Ap
}
// takenAlready says whether a group member's merge is at or behind its repository's taken merge.
func (ix *Indexer) takenAlready(ctx context.Context, m delivery.GroupMerge) (bool, error) {
func (ix *Indexer) takenAlready(ctx context.Context, m forgemsg.GroupMerge) (bool, error) {
if ix.Taken == nil {
return false, nil
}
@@ -622,21 +624,21 @@ func byWords(by string) string {
}
// saidWaits are the taken events of every wait the entries were said under that is not ended yet.
func saidWaits(entries []PendingEntry) []delivery.TakeIn {
func saidWaits(entries []PendingEntry) []takein.TakeIn {
by := map[string][]PendingEntry{}
for _, e := range entries {
if e.Value.Said != "" && e.Value.SaidKind == delivery.TakeInPending {
if e.Value.Said != "" && e.Value.SaidKind == takein.TakeInPending {
by[e.Value.Said] = append(by[e.Value.Said], e)
}
}
var out []delivery.TakeIn
var out []takein.TakeIn
for _, key := range slices.Sorted(maps.Keys(by)) {
held := by[key]
if strings.HasPrefix(key, "g.") {
out = append(out, groupWait(delivery.TakeInTaken, held[0].Value.Group, held))
out = append(out, groupWait(takein.TakeInTaken, held[0].Value.Group, held))
continue
}
t := revisionWait(delivery.TakeInTaken, held, held[0].Value.WaitsFor)
t := revisionWait(takein.TakeInTaken, held, held[0].Value.WaitsFor)
t.Key = key
out = append(out, t)
}
@@ -666,18 +668,18 @@ func (ix *Indexer) holdFor(ctx context.Context, ready []PendingEntry, names, own
}
}
}
t := revisionWait(delivery.TakeInPending, ready, names)
t := revisionWait(takein.TakeInPending, ready, names)
var ended, unmarked []PendingEntry
said, changed := false, false
for _, e := range ready {
switch {
case e.Value.Said == t.Key && e.Value.SaidKind == delivery.TakeInPending:
case e.Value.Said == t.Key && e.Value.SaidKind == takein.TakeInPending:
said = true
changed = changed || e.Value.WaitsFor != names
if e.Value.WaitsFor != names {
unmarked = append(unmarked, e)
}
case e.Value.Said != "" && e.Value.SaidKind == delivery.TakeInPending:
case e.Value.Said != "" && e.Value.SaidKind == takein.TakeInPending:
ended = append(ended, e)
unmarked = append(unmarked, e)
default:
@@ -722,28 +724,28 @@ func copyKey(ctx context.Context, from *overlayStore, to Store, key string) erro
// PendingWaits is what indexer_pending answers of the pending state's entries (ADR 0633 §5): the ready merges as one
// revision, each group being gathered, and the merges behind a group, each with the modules it holds back and the
// last take-in event said of it.
func PendingWaits(entries []PendingEntry) []delivery.PendingWait {
func PendingWaits(entries []PendingEntry) []takein.PendingWait {
w := WaitsOf(entries)
var out []delivery.PendingWait
var out []takein.PendingWait
if len(w.Ready) > 0 {
out = append(out, waitOf(w.Ready, delivery.TakeInRevision, ""))
out = append(out, waitOf(w.Ready, takein.TakeInRevision, ""))
}
for _, g := range slices.Sorted(maps.Keys(w.Groups)) {
out = append(out, waitOf(w.Groups[g], delivery.TakeInGroup, ""))
out = append(out, waitOf(w.Groups[g], takein.TakeInGroup, ""))
}
for _, g := range slices.Sorted(maps.Keys(w.Behind)) {
out = append(out, waitOf(w.Behind[g], delivery.TakeInRevision, g))
out = append(out, waitOf(w.Behind[g], takein.TakeInRevision, g))
}
slices.SortStableFunc(out, func(a, b delivery.PendingWait) int { return a.Since.Compare(b.Since) })
slices.SortStableFunc(out, func(a, b takein.PendingWait) int { return a.Since.Compare(b.Since) })
return out
}
func waitOf(entries []PendingEntry, what, behind string) delivery.PendingWait {
pw := delivery.PendingWait{Key: entries[0].Key, What: what, Merges: mergesOf(entries), Behind: behind, Since: sinceOf(entries),
func waitOf(entries []PendingEntry, what, behind string) takein.PendingWait {
pw := takein.PendingWait{Key: entries[0].Key, What: what, Merges: mergesOf(entries), Behind: behind, Since: sinceOf(entries),
WaitsFor: entries[len(entries)-1].Value.WaitsFor, Holds: []string{}}
if what == delivery.TakeInGroup {
if what == takein.TakeInGroup {
pw.Group = entries[0].Value.Group
pw.Key = delivery.GroupTakeInKey(pw.Group)
pw.Key = takein.GroupTakeInKey(pw.Group)
pw.Members = entries[len(entries)-1].Forge.Members
pw.WaitsFor = ""
}
@@ -755,7 +757,7 @@ func waitOf(entries []PendingEntry, what, behind string) delivery.PendingWait {
}
}
if e.Value.Said != "" && e.Value.SaidAt.After(pw.SaidAt) {
pw.Said, pw.SaidAt = delivery.TakeInStream+"."+e.Value.SaidKind+"."+e.Value.Said, e.Value.SaidAt
pw.Said, pw.SaidAt = takein.TakeInStream+"."+e.Value.SaidKind+"."+e.Value.Said, e.Value.SaidAt
}
}
slices.Sort(pw.Holds)
@@ -785,7 +787,7 @@ func (ix *Indexer) TakeInPendingByHand(ctx context.Context, key, by, why string)
switch {
case strings.HasPrefix(key, "g."):
for g, held := range w.Groups {
if delivery.GroupTakeInKey(g) == key {
if takein.GroupTakeInKey(g) == key {
take = held
}
}
@@ -838,7 +840,7 @@ func (ix *Indexer) DropPending(ctx context.Context, key, by, why string) TakenIn
return r
}
ix.logf("take-in: %s (%s#%d at %s) dropped by hand, asked by %s: %s; nothing of it is taken in", key, e.Forge.Repository, e.Forge.Number, e.Forge.Merge, by, why)
if e.Value.Said != "" && e.Value.SaidKind == delivery.TakeInPending {
if e.Value.Said != "" && e.Value.SaidKind == takein.TakeInPending {
var same []PendingEntry
for _, o := range entries {
if o.Key != key && o.Value.Said == e.Value.Said {
@@ -9,6 +9,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -19,8 +21,8 @@ type takeInBed struct {
ix *Indexer
mods *memStore
pending *memStore
said []delivery.TakeIn
base delivery.Forge
said []takein.TakeIn
base forgemsg.Forge
logs *strings.Builder
}
@@ -36,7 +38,7 @@ func newTakeInBed(t *testing.T) *takeInBed {
b := &takeInBed{ix: ix, mods: mods, pending: newMem(), base: f, logs: &strings.Builder{}}
ix.Verdicts = newMem()
ix.Pending = b.pending
ix.SayTakeIn = func(_ context.Context, t delivery.TakeIn) error { b.said = append(b.said, t); return nil }
ix.SayTakeIn = func(_ context.Context, t takein.TakeIn) error { b.said = append(b.said, t); return nil }
logf := ix.Logf
ix.Logf = func(format string, args ...any) {
b.logs.WriteString(format + "\n")
@@ -65,7 +67,7 @@ func (b *takeInBed) commit(t *testing.T, module string) string {
}
// merge is a catalogue merge at commit c whose trees of avahi and the indexer are given ("" keeps the base's).
func (b *takeInBed) merge(number int, c, avahi, indexer string) delivery.Forge {
func (b *takeInBed) merge(number int, c, avahi, indexer string) forgemsg.Forge {
f := b.base
f.Number, f.Merge, f.Head = number, c, c
f.Trees = map[string]string{}
@@ -81,7 +83,7 @@ func (b *takeInBed) merge(number int, c, avahi, indexer string) delivery.Forge {
return f
}
func (b *takeInBed) arrive(t *testing.T, seq uint64, f delivery.Forge) {
func (b *takeInBed) arrive(t *testing.T, seq uint64, f forgemsg.Forge) {
t.Helper()
if _, _, err := b.ix.Arrive(context.Background(), seq, f); err != nil {
t.Fatalf("arrive %d: %v", seq, err)
@@ -144,7 +146,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
if n := len(b.ix.Verdicts.(*memStore).vals); n > verdicts+1 {
t.Errorf("verdicts %d, want at most %d: only the new indexer manifest's", n, verdicts+1)
}
if want := []string{delivery.PendingKey(2)}; !reflect.DeepEqual(b.keys(), want) || !slices.Equal(b.events(), []string{"pending " + delivery.PendingKey(2)}) || b.said[0].WaitsFor != named {
if want := []string{takein.PendingKey(2)}; !reflect.DeepEqual(b.keys(), want) || !slices.Equal(b.events(), []string{"pending " + takein.PendingKey(2)}) || b.said[0].WaitsFor != named {
t.Fatalf("pending %v, said %+v: want m.2 pending for %s", b.keys(), b.said, named)
}
// T5: the merge delivered again writes no second key and says nothing again.
@@ -162,7 +164,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
if len(b.keys()) != 0 || b.fp(t, delivery.IndexerModule) != own || b.commit(t, "avahi") != sha('e') {
t.Fatalf("revert: pending %v, indexer %s, avahi at %s: want all taken in", b.keys(), b.fp(t, delivery.IndexerModule), b.commit(t, "avahi"))
}
if want := []string{"pending " + delivery.PendingKey(2), "taken " + delivery.PendingKey(2)}; !slices.Equal(b.events(), want) {
if want := []string{"pending " + takein.PendingKey(2), "taken " + takein.PendingKey(2)}; !slices.Equal(b.events(), want) {
t.Errorf("said %v, want %v", b.events(), want)
}
})
@@ -192,7 +194,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
if _, _, err := next.TakeInAtStart(ctx, func(context.Context) bool { return true }); err != nil {
t.Fatal(err)
}
if asked < 2 || !reflect.DeepEqual(b.keys(), []string{delivery.PendingKey(5)}) || b.commit(t, "avahi") != sha('c') {
if asked < 2 || !reflect.DeepEqual(b.keys(), []string{takein.PendingKey(5)}) || b.commit(t, "avahi") != sha('c') {
t.Errorf("start: asked %d, pending %v, avahi at %s: want the revision taken in, the group left", asked, b.keys(), b.commit(t, "avahi"))
}
})
@@ -204,7 +206,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
b.ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) {
return strings.IndexByte(order, commit[0]) <= strings.IndexByte(order, in[0]), nil
}
member := func(repo string, number int, c string) delivery.Forge {
member := func(repo string, number int, c string) forgemsg.Forge {
f := b.merge(number, c, "", "")
f.Group = "feat/take-in"
f.Members = []delivery.Head{{Repository: catalogueRepo, Number: 40, Head: sha('1')}, {Repository: "novox/mesh-sdk", Number: 41, Head: sha('2')}}
@@ -223,10 +225,10 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
alone.Repository, alone.Trees, alone.Manifests, alone.Locks = "novox/hq", map[string]string{}, nil, nil
b.arrive(t, 2, alone)
b.arrive(t, 3, b.merge(51, sha('6'), "tree-avahi-9", ""))
if !reflect.DeepEqual(b.keys(), []string{delivery.PendingKey(1), delivery.PendingKey(3)}) {
if !reflect.DeepEqual(b.keys(), []string{takein.PendingKey(1), takein.PendingKey(3)}) {
t.Fatalf("pending %v: want the member and the merge behind it", b.keys())
}
end := delivery.ForgeGroupEnd{Group: "feat/take-in", Members: []delivery.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('7')}, {Repository: "novox/mesh-sdk", Number: 41, Merge: sha('b')}}}
end := forgemsg.ForgeGroupEnd{Group: "feat/take-in", Members: []forgemsg.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('7')}, {Repository: "novox/mesh-sdk", Number: 41, Merge: sha('b')}}}
if _, _, err := b.ix.EndGroup(ctx, end); err == nil {
t.Fatal("an end before its last member is taken in")
}
@@ -250,10 +252,10 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
g.Group, g.Trees = "feat/two", b.merge(0, "", "", "tree-indexer-7").Trees
g.Members = g.Members[:1]
b.arrive(t, 9, g)
if _, _, err := b.ix.EndGroup(ctx, delivery.ForgeGroupEnd{Group: "feat/two", Members: []delivery.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('c')}}}); err != nil {
if _, _, err := b.ix.EndGroup(ctx, forgemsg.ForgeGroupEnd{Group: "feat/two", Members: []forgemsg.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('c')}}}); err != nil {
t.Fatal(err)
}
if want := []string{"pending g.feat/two", "taken g.feat/two", "pending " + delivery.PendingKey(9)}; !slices.Equal(b.events(), want) {
if want := []string{"pending g.feat/two", "taken g.feat/two", "pending " + takein.PendingKey(9)}; !slices.Equal(b.events(), want) {
t.Errorf("a group naming another indexer: said %v, want %v", b.events(), want)
}
})
@@ -266,12 +268,12 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
b.ix.ownMu.Unlock()
// The other indexer claims the merge between this one's read and its claim.
b.pending.beforeWrite = func() {
v := b.pending.vals[delivery.PendingKey(2)]
v := b.pending.vals[takein.PendingKey(2)]
var p Pending
_ = json.Unmarshal(v, &p)
p.Taking = "the other indexer"
data, _ := json.Marshal(p)
b.pending.put(delivery.PendingKey(2), data)
b.pending.put(takein.PendingKey(2), data)
}
if _, _, err := b.ix.TakeInAtStart(ctx, func(context.Context) bool { return false }); err != nil {
t.Fatal(err)
@@ -284,32 +286,32 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
b := newTakeInBed(t)
b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2"))
b.arrive(t, 3, b.merge(22, sha('d'), "tree-avahi-4", "tree-indexer-2"))
var answer delivery.PendingAnswer
var answer takein.PendingAnswer
for _, tool := range PendingTools(b.pending, nil, nil) {
if tool.Name == PendingTool {
out, err := tool.Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
answer = out.(delivery.PendingAnswer)
answer = out.(takein.PendingAnswer)
}
}
if len(answer.Waits) != 1 || answer.Waits[0].Key != delivery.PendingKey(2) || !slices.Equal(answer.Waits[0].Holds, []string{"avahi", delivery.IndexerModule}) ||
answer.Waits[0].Said != "take-in.pending."+delivery.PendingKey(2) || len(answer.Waits[0].Keys) != 2 {
if len(answer.Waits) != 1 || answer.Waits[0].Key != takein.PendingKey(2) || !slices.Equal(answer.Waits[0].Holds, []string{"avahi", delivery.IndexerModule}) ||
answer.Waits[0].Said != "take-in.pending."+takein.PendingKey(2) || len(answer.Waits[0].Keys) != 2 {
t.Fatalf("indexer_pending = %+v", answer)
}
b.said = nil
if r := b.ix.DropPending(ctx, delivery.PendingKey(2), "lead@shanks", "replaced by m.3"); !r.Stored {
if r := b.ix.DropPending(ctx, takein.PendingKey(2), "lead@shanks", "replaced by m.3"); !r.Stored {
t.Fatalf("drop: %+v", r)
}
if len(b.said) < 2 || !b.said[0].Dropped || b.said[0].Key != delivery.PendingKey(2) || b.said[0].By != "lead@shanks" || b.said[1].Key != delivery.PendingKey(3) {
if len(b.said) < 2 || !b.said[0].Dropped || b.said[0].Key != takein.PendingKey(2) || b.said[0].By != "lead@shanks" || b.said[1].Key != takein.PendingKey(3) {
t.Fatalf("drop said %+v: want m.2 taken (dropped, by) and m.3 pending", b.said)
}
b.said = nil
if r := b.ix.TakeInPendingByHand(ctx, delivery.PendingKey(3), "lead@shanks", "the named indexer can never run"); !r.Stored {
if r := b.ix.TakeInPendingByHand(ctx, takein.PendingKey(3), "lead@shanks", "the named indexer can never run"); !r.Stored {
t.Fatalf("hand take-in: %+v", r)
}
if len(b.keys()) != 0 || b.commit(t, "avahi") != sha('d') || len(b.said) != 1 || b.said[0].Kind != delivery.TakeInTaken || b.said[0].By != "lead@shanks" {
if len(b.keys()) != 0 || b.commit(t, "avahi") != sha('d') || len(b.said) != 1 || b.said[0].Kind != takein.TakeInTaken || b.said[0].By != "lead@shanks" {
t.Errorf("hand take-in: pending %v, avahi at %s, said %+v", b.keys(), b.commit(t, "avahi"), b.said)
}
})
@@ -5,8 +5,8 @@ import (
"fmt"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// PendingTools are the indexer's tools of what waits for a take-in (novox/hq ADR 0633 §5): indexer_pending reads the
@@ -46,7 +46,7 @@ func PendingTools(pending Store, ask func(key string, value []byte) (TakeInAsk,
if err != nil {
return nil, err
}
return delivery.PendingAnswer{Waits: PendingWaits(entries), Unread: unread}, nil
return takein.PendingAnswer{Waits: PendingWaits(entries), Unread: unread}, nil
}},
{Name: TakeInPendingTool,
Description: "Take a wait in by hand with the running indexer and its judge (ADR 0633 §5): the revision under its m. key, " +
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/fingerprint"
@@ -38,7 +39,7 @@ func avahiOnTools(t *testing.T) string {
}
// avahiPull is the pull message of the catalogue's pull request 301 touching avahi, with its manifest.
func avahiPull(t *testing.T, head string) delivery.Forge {
func avahiPull(t *testing.T, head string) forgemsg.Forge {
t.Helper()
f := manifestPull(t, 301, head, avahiOnTools(t))
f.Manifests = map[string]string{"modules/avahi": avahiOnTools(t)}
@@ -7,6 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -125,7 +126,7 @@ func (ix *Indexer) indexAt(ctx context.Context, r Registration, force, reread bo
t.why = fmt.Sprintf("the manifest at %q of %s at %s names the module %q", dir, repository, commit, name)
return t, nil
}
f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repository, Merge: commit, Root: read.Root,
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repository, Merge: commit, Root: read.Root,
Trees: map[string]string{dir: read.Trees[dir]},
Manifests: map[string]string{dir: raw}}
if l := read.Locks[dir]; len(l) > 0 {
@@ -396,7 +397,7 @@ func (ix *Indexer) TakeInRepository(ctx context.Context, repository string) Take
out.Answer = "the modules state cannot be read: " + err.Error()
return out
}
f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: out.Repository, Merge: head}
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: out.Repository, Merge: head}
if _, err := ix.ApplyMessage(ctx, f); err != nil {
out.Answer = fmt.Sprintf("the merge of %s at %.12s could not be taken in: %v", repository, head, err)
return out
@@ -8,6 +8,7 @@ import (
"slices"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -19,13 +20,13 @@ import (
// say keeps the key as it is and says so.
// writeTaken writes the repository's taken key for the merge f carries, unless it names that merge or a later one.
func (ix *Indexer) writeTaken(ctx context.Context, f delivery.Forge) error {
func (ix *Indexer) writeTaken(ctx context.Context, f forgemsg.Forge) error {
return ix.writeTakenKnown(ctx, f, "")
}
// writeTakenKnown is writeTaken where the merge is known not to be older than notOlder (lateMerge asked it, issue 1126): a
// key naming that merge is written over without asking the forge again.
func (ix *Indexer) writeTakenKnown(ctx context.Context, f delivery.Forge, notOlder string) error {
func (ix *Indexer) writeTakenKnown(ctx context.Context, f forgemsg.Forge, notOlder string) error {
if ix.Taken == nil || f.Merge == "" {
return nil
}
@@ -101,7 +102,7 @@ func (ix *Indexer) FillTaken(ctx context.Context) error {
}
}
for _, repository := range slices.Sorted(maps.Keys(pending)) {
err := ix.writeTaken(ctx, delivery.Forge{Repository: repository, Merge: pending[repository]})
err := ix.writeTaken(ctx, forgemsg.Forge{Repository: repository, Merge: pending[repository]})
if err == nil {
ix.mu.Lock()
if ix.untaken[repository] == pending[repository] {
@@ -10,6 +10,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -91,7 +92,7 @@ func newFormIndexer(t *testing.T, mods ...mod) (*Indexer, *memStore) {
}
// newFormIndexerUnmerged is the indexer and the catalogue merge of these modules, not yet applied.
func newFormIndexerUnmerged(t *testing.T, mods ...mod) (*Indexer, *memStore, delivery.Forge) {
func newFormIndexerUnmerged(t *testing.T, mods ...mod) (*Indexer, *memStore, forgemsg.Forge) {
t.Helper()
var recs [][2]any
trees, manifests := map[string]string{}, map[string]string{}
@@ -468,7 +469,7 @@ func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) {
tools, _ := moduleOf(t, mods, "mesh-tools")
sdkMerge := func(commit, tree string) delivery.Module {
t.Helper()
f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1,
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1,
Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1"}`}}
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
@@ -9,6 +9,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -319,7 +320,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatalf("the start pass: %+v, this judge's own refusal %+v (%v)", verdict(atStart), verdict(ownRefusal), err)
}
byPull := stale("tools", `{"module":"tools","version":"8"}`)
pull := delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main",
pull := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main",
Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8"}`}}
if _, err := ix.ApplyMessage(ctx, pull); err != nil || verdict(byPull).Refused() || verdict(byPull).Judge != ix.judgeID() {
t.Fatalf("a pull carrying a digest an older judge refused: %+v %v", verdict(byPull), err)
@@ -12,6 +12,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -147,7 +148,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
}
// 1585/T11 (ADR 0633 §1, §5): its one stream, take-in, an old streams entry (the indexer is foundation, ADR 0595 §11),
// carries the take-in events.
if want := []streamDecl{{Name: delivery.TakeInStream, Events: []string{"take-in.**"}, MaxAgeDays: 7}}; !reflect.DeepEqual(m.Streams, want) {
if want := []streamDecl{{Name: takein.TakeInStream, Events: []string{"take-in.**"}, MaxAgeDays: 7}}; !reflect.DeepEqual(m.Streams, want) {
t.Errorf("streams = %+v, want %+v", m.Streams, want)
}
// No other bus section, no other seat (ADR 0437 §4).
@@ -0,0 +1,5 @@
module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
@@ -0,0 +1,14 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -0,0 +1,251 @@
// Package takein is the indexer's take-in contract (novox/hq ADR 0633 §1, §5): the events it publishes on its own stream
// `take-in` while a source revision waits for the indexer it names or a delivery group is gathered, their keys, and the
// answer of its tool indexer_pending. The indexer writes them; the watchman and the view read them. It is the indexer's
// own package: one module owns these types, so they live with it and not in mesh-sdk (the operator, 2026-11-04).
package takein
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Merge is one merge a take-in event or a wait holds: the repository as `<owner>/<name>`, its pull request and the
// merge commit the forge made of it.
type Merge struct {
Repository string `json:"repository"`
Number int `json:"number"`
Merge string `json:"merge"`
}
// checkRepository refuses a repository that is not `<owner>/<name>`: one slash, neither part empty.
func checkRepository(repository string) error {
owner, name, ok := strings.Cut(repository, "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
return nil
}
// decode reads a value of delivery.Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk
// delivery's values are read.
func decode(data []byte, into any) error {
var probe struct {
V *int `json:"v"`
}
if err := json.Unmarshal(data, &probe); err != nil {
return fmt.Errorf("delivery: value is not JSON: %w", err)
}
if probe.V == nil || *probe.V != delivery.Version {
return fmt.Errorf("%w: this reader reads %d", delivery.ErrUnknownVersion, delivery.Version)
}
if err := json.Unmarshal(data, into); err != nil {
return fmt.Errorf("delivery: value does not fit its shape: %w", err)
}
return nil
}
// The take-in events (novox/hq ADR 0633 §1, §5): the indexer publishes them on its own stream, which keeps the last
// message per subject, `take-in.<kind>.<key>`. A wait starts with `take-in.pending` and ends with `take-in.taken`,
// whoever ended it (the named indexer, a revert, a hand take-in or a drop). The stream's name is the indexer's manifest's;
// these names are its events, relative to it.
const (
TakeInStream = "take-in"
TakeInPending = "pending"
TakeInTaken = "taken"
)
// What waits in a take-in event: a source revision for the indexer build it names, or a delivery group for its end.
const (
TakeInRevision = "revision"
TakeInGroup = "group"
)
// PendingKey is the key of one held merge in the indexer's pending state, and the key a revision's take-in events
// carry (its oldest merge's): `m.<the merge's stream sequence, 20 digits>`, so the key order is the merge order.
func PendingKey(seq uint64) string { return fmt.Sprintf("m.%020d", seq) }
// GroupTakeInKey is the key a group's take-in events carry while it is gathered: `g.<group>`, with every character a
// bus subject cannot carry (a space, `*`, `>`) written `_`. The event's body names the group exactly.
func GroupTakeInKey(group string) string {
return "g." + strings.Map(func(r rune) rune {
if r == '*' || r == '>' || r <= ' ' || r == 0x7f {
return '_'
}
return r
}, group)
}
// TakeInEvent is the name of a take-in event of the given kind and key: `take-in.<pending|taken>.<key>`.
func TakeInEvent(kind, key string) (string, error) {
if kind != TakeInPending && kind != TakeInTaken {
return "", fmt.Errorf("delivery: the take-in kind %q is not pending or taken", kind)
}
if err := checkTakeInKey(key); err != nil {
return "", err
}
return TakeInStream + "." + kind + "." + key, nil
}
// ParseTakeInEvent is the inverse of TakeInEvent.
func ParseTakeInEvent(event string) (kind, key string, err error) {
parts := strings.SplitN(event, ".", 3)
if len(parts) != 3 || parts[0] != TakeInStream {
return "", "", fmt.Errorf("delivery: %q is not a take-in event, take-in.<pending|taken>.<key>", event)
}
if _, err := TakeInEvent(parts[1], parts[2]); err != nil {
return "", "", fmt.Errorf("delivery: %q is not a take-in event: %w", event, err)
}
return parts[1], parts[2], nil
}
func checkTakeInKey(key string) error {
if !strings.HasPrefix(key, "m.") && !strings.HasPrefix(key, "g.") {
return fmt.Errorf("delivery: the take-in key %q is neither m.<sequence> nor g.<group>", key)
}
for _, tok := range strings.Split(key, ".") {
if tok == "" || strings.ContainsAny(tok, "*> \t\r\n") {
return fmt.Errorf("delivery: the take-in key %q is not a bus subject's tokens", key)
}
}
return nil
}
// TakeIn is the body of a take-in event. Kind is the event's kind and Key its key. What says what waits: a revision
// (Key its oldest merge's PendingKey, WaitsFor the indexer build it names) or a group being gathered (Key its
// GroupTakeInKey, Members every approved member with its head, Merges the members held so far). Merges are the merges
// it holds, oldest first; Since is when the wait began. A `taken` event names the same once the wait ended; By and Why
// name a hand act (a hand take-in, or a drop, Dropped), and are carried by nothing else.
type TakeIn struct {
V int `json:"v"`
Kind string `json:"kind"`
Key string `json:"key"`
What string `json:"what"`
Merges []Merge `json:"merges"`
Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
Since time.Time `json:"since"`
By string `json:"by,omitempty"`
Why string `json:"why,omitempty"`
Dropped bool `json:"dropped,omitempty"`
}
func (t TakeIn) check() error {
if _, err := TakeInEvent(t.Kind, t.Key); err != nil {
return err
}
if len(t.Merges) == 0 {
return fmt.Errorf("delivery: the take-in event %s names no merge", t.Key)
}
for _, m := range t.Merges {
if err := checkRepository(m.Repository); err != nil {
return err
}
if m.Merge == "" {
return fmt.Errorf("delivery: the take-in event %s names a merge of %s without its commit", t.Key, m.Repository)
}
}
if t.Since.IsZero() {
return fmt.Errorf("delivery: the take-in event %s does not say since when it waits", t.Key)
}
switch t.What {
case TakeInRevision:
if !strings.HasPrefix(t.Key, "m.") {
return fmt.Errorf("delivery: a revision's take-in key is its oldest merge's m.<sequence>, not %q", t.Key)
}
if t.Kind == TakeInPending && t.WaitsFor == "" {
return fmt.Errorf("delivery: a pending revision %s names the indexer build it waits for", t.Key)
}
case TakeInGroup:
if t.Group == "" {
return fmt.Errorf("delivery: a group's take-in event %s names its group", t.Key)
}
if t.Key != GroupTakeInKey(t.Group) {
return fmt.Errorf("delivery: the take-in key of the group %q is %s, not %s", t.Group, GroupTakeInKey(t.Group), t.Key)
}
if t.WaitsFor != "" {
return fmt.Errorf("delivery: a group being gathered waits for its end, not for an indexer build")
}
default:
return fmt.Errorf("delivery: a take-in event's what %q is not revision or group", t.What)
}
if t.Kind != TakeInTaken && (t.By != "" || t.Why != "" || t.Dropped) {
return fmt.Errorf("delivery: only a taken event names the hand act that ended a wait")
}
if (t.Why != "" || t.Dropped) && t.By == "" {
return fmt.Errorf("delivery: a hand act's take-in event names who acted (by)")
}
return nil
}
// EncodeTakeIn is t as JSON at Version; it refuses what its kind and what do not carry.
func EncodeTakeIn(t TakeIn) ([]byte, error) {
if err := t.check(); err != nil {
return nil, err
}
t.V = delivery.Version
return json.Marshal(t)
}
// DecodeTakeIn reads a take-in event's body; it refuses an unknown version (ErrUnknownVersion) and what EncodeTakeIn
// refuses.
func DecodeTakeIn(data []byte) (TakeIn, error) {
var t TakeIn
if err := decode(data, &t); err != nil {
return TakeIn{}, err
}
if err := t.check(); err != nil {
return TakeIn{}, err
}
return t, nil
}
// SameTakeIn says whether two take-in bodies are the same value.
func SameTakeIn(a, b TakeIn) bool { return reflect.DeepEqual(a, b) }
// PendingWait is one wait the indexer's tool indexer_pending answers (novox/hq ADR 0633 §5), as the view reads it to
// name a module waiting for its revision's indexer. Key and What are a take-in event's: a revision under its oldest
// merge's key, waiting for the indexer build WaitsFor (empty while it is worked out), or a group being gathered under
// its group key, with Members every approved member and Merges those held so far. A revision that waits behind a
// group being gathered names that group's key in Behind. Keys are the pending keys of its merges, in Merges' order (indexer_drop_pending drops one). Holds are the modules it holds back: every module of its
// merges' trees. Said is the last take-in event said of it (`take-in.<kind>.<key>`) and SaidAt when.
type PendingWait struct {
Key string `json:"key"`
What string `json:"what"`
Merges []Merge `json:"merges"`
Keys []string `json:"keys"`
Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
Behind string `json:"behind,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
Since time.Time `json:"since"`
Holds []string `json:"holds"`
Said string `json:"said,omitempty"`
SaidAt time.Time `json:"said-at,omitzero"`
}
// PendingAnswer is indexer_pending's answer: every wait, oldest first, and the pending keys that do not read.
type PendingAnswer struct {
Waits []PendingWait `json:"waits"`
Unread []string `json:"unread,omitempty"`
}
// DecodePendingAnswer reads indexer_pending's answer, refusing a wait that is not a revision or a group, or names no
// key.
func DecodePendingAnswer(data []byte) (PendingAnswer, error) {
var a PendingAnswer
if err := json.Unmarshal(data, &a); err != nil {
return a, fmt.Errorf("delivery: not an answer of indexer_pending: %w", err)
}
for _, w := range a.Waits {
if w.Key == "" || (w.What != TakeInRevision && w.What != TakeInGroup) {
return a, fmt.Errorf("delivery: indexer_pending answers a wait %q of %q, not a revision or a group", w.Key, w.What)
}
}
return a, nil
}
@@ -0,0 +1,95 @@
package takein
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// sampleTakeIns are a pending revision, a group being gathered and a hand take-in's taken (1585/T2).
func sampleTakeIns(since time.Time) []TakeIn {
m := []Merge{{Repository: "novox/mesh-delivery", Number: 9, Merge: "eeee"}}
return []TakeIn{
{Kind: TakeInPending, Key: PendingKey(12), What: TakeInRevision, Merges: m, WaitsFor: "src2:abc", Since: since},
{Kind: TakeInPending, Key: GroupTakeInKey("feat/take-in"), What: TakeInGroup, Merges: m, Group: "feat/take-in",
Members: []delivery.Head{{Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}, {Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}}, Since: since},
{Kind: TakeInTaken, Key: PendingKey(12), What: TakeInRevision, Merges: m, WaitsFor: "src2:abc", Since: since,
By: "lead-47@shanks", Why: "the named indexer can never run"},
}
}
// TestTakeInEvents holds the indexer's take-in contract, moved from mesh-sdk go/delivery with its tests (1585/T2, T12).
func TestTakeInEvents(t *testing.T) {
// The take-in events, `take-in.<pending|taken>.<key>`, a group's key with its dots and no subject wildcard.
if PendingKey(42) != "m.00000000000000000042" || GroupTakeInKey("feat/a.b *>") != "g.feat/a.b___" {
t.Errorf("take-in keys %q %q", PendingKey(42), GroupTakeInKey("feat/a.b *>"))
}
for _, c := range []struct{ kind, key string }{{TakeInPending, PendingKey(7)}, {TakeInTaken, GroupTakeInKey("feat/a.b")}} {
ev, err := TakeInEvent(c.kind, c.key)
if err != nil || ev != "take-in."+c.kind+"."+c.key {
t.Fatalf("take-in event = %q, %v", ev, err)
}
if k, key, err := ParseTakeInEvent(ev); err != nil || k != c.kind || key != c.key {
t.Errorf("parse take-in event %q = %q %q %v", ev, k, key, err)
}
}
for _, bad := range []string{"", "take-in.pending", "take-in.held.m.1", "take-in.taken.x.1", "take-in.taken.m..1", "forge.taken.m.1"} {
if _, _, err := ParseTakeInEvent(bad); err == nil {
t.Errorf("parse take-in event %q accepted", bad)
}
}
// The bodies round-trip: a pending revision, a group being gathered, and a hand take-in's taken with its by.
since := time.Date(2026, 11, 4, 23, 0, 0, 0, time.UTC)
for _, ti := range sampleTakeIns(since) {
data, err := EncodeTakeIn(ti)
if err != nil {
t.Fatalf("take-in %s %s: %v", ti.Kind, ti.What, err)
}
ti.V = delivery.Version
if got, err := DecodeTakeIn(data); err != nil || !SameTakeIn(got, ti) {
t.Errorf("take-in %s round trip: %+v %v", ti.Key, got, err)
}
}
for _, in := range []string{`{"key":"m.1"}`, `{"v":2,"key":"m.1"}`} {
if _, err := DecodeTakeIn([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) {
t.Errorf("decode of %s = %v, want delivery.ErrUnknownVersion", in, err)
}
}
// A body that does not hold is refused, naming why.
for _, c := range []struct {
name, want string
i int
mut func(*TakeIn)
}{
{"group take-in without its group", "names its group", 1, func(t *TakeIn) { t.Group = "" }},
{"group take-in under another group's key", "g.other", 1, func(t *TakeIn) { t.Key = GroupTakeInKey("other") }},
{"pending revision waiting for no build", "waits for", 0, func(t *TakeIn) { t.WaitsFor = "" }},
{"pending with a hand act", "only a taken event", 0, func(t *TakeIn) { t.By = "someone@node" }},
{"hand act without its by", "by", 2, func(t *TakeIn) { t.By = "" }},
{"take-in without merges", "no merge", 0, func(t *TakeIn) { t.Merges = nil }},
{"take-in without since", "since", 0, func(t *TakeIn) { t.Since = time.Time{} }},
{"a merge of no repository", "owner", 0, func(t *TakeIn) { t.Merges = []Merge{{Repository: "nope", Merge: "m"}} }},
} {
ti := sampleTakeIns(since)[c.i]
c.mut(&ti)
if _, err := EncodeTakeIn(ti); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.want)
}
}
// indexer_pending's answer reads back; a wait of neither kind is refused.
pa := PendingAnswer{Waits: []PendingWait{{Key: PendingKey(12), What: TakeInRevision, Merges: sampleTakeIns(since)[0].Merges,
Keys: []string{PendingKey(12)}, WaitsFor: "src2:abc", Since: since, Holds: []string{"mesh-delivery-indexer"}, Said: "take-in.pending." + PendingKey(12), SaidAt: since}}}
if data, err := json.Marshal(pa); err != nil {
t.Fatal(err)
} else if got, err := DecodePendingAnswer(data); err != nil || !reflect.DeepEqual(got, pa) {
t.Errorf("pending answer round trip: %+v %v", got, err)
}
if _, err := DecodePendingAnswer([]byte(`{"waits":[{"key":"m.1","what":"held"}]}`)); err == nil {
t.Error("a pending answer with a wait of neither kind accepted")
}
}
@@ -12,6 +12,7 @@ import (
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -340,10 +341,10 @@ func main() {
}
// pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5).
func pendingOf() (delivery.PendingAnswer, error) {
func pendingOf() (takein.PendingAnswer, error) {
raw, err := stdio.Ask(delivery.IndexerModule+".indexer_pending", map[string]any{})
if err != nil {
return delivery.PendingAnswer{}, err
return takein.PendingAnswer{}, err
}
return delivery.DecodePendingAnswer(raw)
return takein.DecodePendingAnswer(raw)
}
@@ -9,6 +9,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -106,10 +107,10 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
// wait's since; a held module the revision does not move is named in the first tier with that wait alone, a group's wait
// says its group, and a tool that cannot be asked is said and names no module.
heldSince := time.Date(2026, 11, 4, 22, 0, 0, 0, time.UTC)
r.Pending = func() (delivery.PendingAnswer, error) {
return delivery.PendingAnswer{Waits: []delivery.PendingWait{
{Key: "m.00000000000000000042", What: delivery.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}},
{Key: "g.feat", What: delivery.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil
r.Pending = func() (takein.PendingAnswer, error) {
return takein.PendingAnswer{Waits: []takein.PendingWait{
{Key: "m.00000000000000000042", What: takein.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}},
{Key: "g.feat", What: takein.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil
}
out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)})
held, _ := out.(view.Tiers)
@@ -126,7 +127,7 @@ func tiersToolSaysEachModulesWait(t *testing.T) {
if nm := held.Tiers[0][len(held.Tiers[0])-1]; nm.Module != "newmod" || len(nm.Waits) != 1 || nm.Waits[0].Edge != view.TakeInEdge {
t.Errorf("the first tier = %+v, want newmod, which the revision does not move, named with its take-in wait alone", held.Tiers[0])
}
r.Pending = func() (delivery.PendingAnswer, error) { return delivery.PendingAnswer{}, errors.New("no answer") }
r.Pending = func() (takein.PendingAnswer, error) { return takein.PendingAnswer{}, errors.New("no answer") }
if out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || !strings.Contains(out.(view.Tiers).Pending, "no answer") ||
strings.Contains(fmt.Sprint(out.(view.Tiers).Tiers), view.TakeInEdge) {
t.Errorf("view_tiers with no answer of indexer_pending = %+v, %v", out, err)
+7 -2
View File
@@ -3,7 +3,7 @@ module mesh-delivery-view
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
@@ -21,6 +21,11 @@ require (
golang.org/x/time v0.15.0 // indirect
)
require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
)
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ../mesh-delivery-indexer/takein
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -14,6 +14,7 @@ import (
"sync/atomic"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -59,7 +60,7 @@ type Reader struct {
// ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer.
GraphState, SourceState, ProvisionsState StateRead
// Pending reads the indexer's waits through its tool indexer_pending (ADR 0633 §5); unset names no module held back.
Pending func() (delivery.PendingAnswer, error)
Pending func() (takein.PendingAnswer, error)
// BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none.
BuildLog LogSince
}
@@ -9,6 +9,7 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -227,7 +228,7 @@ func (r Reader) heldBack(out *Tiers) (map[string]TierWait, []string) {
}
for _, w := range a.Waits {
words := "the indexer holds back " + w.Key + ", which waits for the indexer build " + w.WaitsFor
if w.What == delivery.TakeInGroup {
if w.What == takein.TakeInGroup {
words = "the indexer gathers the group " + w.Group + " and its end has not come"
} else if w.Behind != "" {
words = "the indexer holds back " + w.Key + " behind " + w.Behind
@@ -22,6 +22,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
@@ -379,10 +380,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The indexer's stream take-in (ADR 0633 §5): a revision or a group it holds back is a wait, ended by take-in.taken. The
// durable consumer <node>_<module> is made by the controller from this module's consumes; the whole stream is heard at
// the start, so a wait that began while the watchman was away is read with its own since.
switch tc, err := cred.EventConsumer(ctx, js, catalogue.StreamBusName(delivery.TakeInStream)); {
switch tc, err := cred.EventConsumer(ctx, js, catalogue.StreamBusName(takein.TakeInStream)); {
case err != nil:
logger.Printf("the consumer %s_%s on %s does not exist yet, so no take-in wait is said until the watchman starts again: %v",
cred.Node, cred.Module, catalogue.StreamBusName(delivery.TakeInStream), err)
cred.Node, cred.Module, catalogue.StreamBusName(takein.TakeInStream), err)
default:
// The replay has caught up when a message says none is pending after it, or when the consumer had none pending at its
// bind (the stream holds nothing it has not acknowledged): the waits past their bound are said then, not before.
+4 -1
View File
@@ -3,12 +3,13 @@ module mesh-delivery-watchman
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104221203-10c91fba4a28
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.1 // indirect
@@ -22,3 +23,5 @@ require (
)
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ../mesh-delivery-indexer/takein
+2 -4
View File
@@ -1,7 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104221203-10c91fba4a28 h1:Zoi+myMvKaRYdM8w/96W6GnoAiTvgwvEF1QPA6HelaU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104221203-10c91fba4a28/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -6,6 +6,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
)
@@ -36,11 +37,11 @@ func TestTheWatchmansFindings(t *testing.T) {
}{
{"ADR 0633 §5 (1585/T21): a revision waiting for an indexer build is a finding past 60 minutes from its since, not within; a group past 30", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.TakeInCaughtUp() // a live message, heard after the replay
w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0))
if o.ttls[signalKey("take-in", revKey)] != TakeInRevisionBound || len(o.findings) != 0 {
t.Fatalf("a revision's wait: ttls %v, findings %v", o.ttls, o.findings)
}
w.TakeIn(takeInEvent(t, delivery.TakeInPending, groupKey), takeInBody(t, delivery.TakeInPending, groupKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, groupKey), takeInBody(t, takein.TakeInPending, groupKey, t0))
if o.ttls[signalKey("take-in", groupKey)] != TakeInGroupBound {
t.Fatalf("a group's wait: ttls %v", o.ttls)
}
@@ -53,41 +54,41 @@ func TestTheWatchmansFindings(t *testing.T) {
{"ADR 0633 §5 (1585/T21): the take-in.taken of a named indexer, a hand take-in or a drop clears the finding", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.TakeInCaughtUp() // a live message, heard after the replay
for _, by := range []string{"", "operator@shanks"} {
w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0))
expire(w, c, signalKey("take-in", revKey), TakeInRevisionBound)
if len(o.findings) != 1 {
t.Fatalf("findings past the bound: %v", o.findings)
}
taken, err := delivery.EncodeTakeIn(delivery.TakeIn{Kind: delivery.TakeInTaken, Key: revKey, What: delivery.TakeInRevision, Since: t0,
Merges: []delivery.GroupMerge{{Repository: "novox/a", Merge: "c0ffee"}}, By: by, Why: map[bool]string{true: "dropped", false: ""}[by != ""], Dropped: by != ""})
taken, err := takein.EncodeTakeIn(takein.TakeIn{Kind: takein.TakeInTaken, Key: revKey, What: takein.TakeInRevision, Since: t0,
Merges: []takein.Merge{{Repository: "novox/a", Merge: "c0ffee"}}, By: by, Why: map[bool]string{true: "dropped", false: ""}[by != ""], Dropped: by != ""})
if err != nil {
t.Fatal(err)
}
w.TakeIn(takeInEvent(t, delivery.TakeInTaken, revKey), taken)
w.TakeIn(takeInEvent(t, takein.TakeInTaken, revKey), taken)
c.t = t0
}
}, map[string]string{}},
{"ADR 0633 §5 (1585/T21): a group's take-in.taken ends its 30 minutes, and the revision then pending under m.<seq> starts its own 60", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.TakeInCaughtUp() // a live message, heard after the replay
w.TakeIn(takeInEvent(t, delivery.TakeInPending, groupKey), takeInBody(t, delivery.TakeInPending, groupKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, groupKey), takeInBody(t, takein.TakeInPending, groupKey, t0))
expire(w, c, signalKey("take-in", groupKey), 25*time.Minute)
w.TakeIn(takeInEvent(t, delivery.TakeInTaken, groupKey), takeInBody(t, delivery.TakeInTaken, groupKey, t0))
w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, c.t))
w.TakeIn(takeInEvent(t, takein.TakeInTaken, groupKey), takeInBody(t, takein.TakeInTaken, groupKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, c.t))
if len(o.findings) != 0 || o.ttls[signalKey("take-in", revKey)] != TakeInRevisionBound {
t.Fatalf("after the hand-over: findings %v, ttls %v", o.findings, o.ttls)
}
expire(w, c, signalKey("take-in", revKey), TakeInRevisionBound)
}, map[string]string{FindingKey(ScopeDelivery, revKey, KindTakeInWaits): Warning}},
{"ADR 0633 §5 (1585/T21): a pending event heard again at the start, since past its bound, is said once the replay has caught up, not before", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0.Add(-2*time.Hour)))
w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0.Add(-2*time.Hour)))
if len(o.findings) != 0 || len(o.pulses) != 0 {
t.Fatalf("a replayed old wait before the replay caught up: findings %v, pulses %v", o.findings, o.pulses)
}
w.TakeInCaughtUp()
}, map[string]string{FindingKey(ScopeDelivery, revKey, KindTakeInWaits): Warning}},
{"ADR 0633 §5 (1585/T21): a replayed pending followed by its taken never raises", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0.Add(-2*time.Hour)))
w.TakeIn(takeInEvent(t, delivery.TakeInTaken, revKey), takeInBody(t, delivery.TakeInTaken, revKey, t0.Add(-2*time.Hour)))
w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0.Add(-2*time.Hour)))
w.TakeIn(takeInEvent(t, takein.TakeInTaken, revKey), takeInBody(t, takein.TakeInTaken, revKey, t0.Add(-2*time.Hour)))
w.TakeInCaughtUp()
if o.findingWrites != 0 || len(o.pulses) != 0 {
t.Fatalf("a wait that ended in the replay wrote %d findings and left pulses %v", o.findingWrites, o.pulses)
@@ -95,17 +96,17 @@ func TestTheWatchmansFindings(t *testing.T) {
}, map[string]string{}},
{"ADR 0633 §5 (1585/T21): a drop clears the wait, and the merges left under it said pending again with the same since are raised from it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
w.TakeInCaughtUp() // a live message, heard after the replay
w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0))
expire(w, c, signalKey("take-in", revKey), TakeInRevisionBound)
if len(o.findings) != 1 {
t.Fatalf("findings past the bound: %v", o.findings)
}
w.TakeIn(takeInEvent(t, delivery.TakeInTaken, revKey), takeInBody(t, delivery.TakeInTaken, revKey, t0))
w.TakeIn(takeInEvent(t, takein.TakeInTaken, revKey), takeInBody(t, takein.TakeInTaken, revKey, t0))
if len(o.findings) != 0 {
t.Fatalf("the drop left %v", o.findings)
}
next := "m.00000000000000000043"
w.TakeIn(takeInEvent(t, delivery.TakeInPending, next), takeInBody(t, delivery.TakeInPending, next, t0))
w.TakeIn(takeInEvent(t, takein.TakeInPending, next), takeInBody(t, takein.TakeInPending, next, t0))
}, map[string]string{FindingKey(ScopeDelivery, "m.00000000000000000043", KindTakeInWaits): Warning}},
{"ADR 0578 §1: a module whose value carries a refusal of its manifest is a finding until it is accepted", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
value := func(verdict string, problems ...string) []byte {
@@ -609,7 +610,7 @@ const (
func takeInEvent(t *testing.T, kind, key string) string {
t.Helper()
e, err := delivery.TakeInEvent(kind, key)
e, err := takein.TakeInEvent(kind, key)
if err != nil {
t.Fatal(err)
}
@@ -619,14 +620,14 @@ func takeInEvent(t *testing.T, kind, key string) string {
// takeInBody is a take-in event's body for a key: a group's when it starts with g.
func takeInBody(t *testing.T, kind, key string, since time.Time) []byte {
t.Helper()
body := delivery.TakeIn{Kind: kind, Key: key, What: delivery.TakeInRevision, Since: since,
Merges: []delivery.GroupMerge{{Repository: "novox/a", Number: 1, Merge: "c0ffee"}}}
body := takein.TakeIn{Kind: kind, Key: key, What: takein.TakeInRevision, Since: since,
Merges: []takein.Merge{{Repository: "novox/a", Number: 1, Merge: "c0ffee"}}}
if key[0] == 'g' {
body.What, body.Group = delivery.TakeInGroup, "feat/take-in"
} else if kind == delivery.TakeInPending {
body.What, body.Group = takein.TakeInGroup, "feat/take-in"
} else if kind == takein.TakeInPending {
body.WaitsFor = "src2:abc"
}
b, err := delivery.EncodeTakeIn(body)
b, err := takein.EncodeTakeIn(body)
if err != nil {
t.Fatal(err)
}
@@ -7,7 +7,7 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
)
// KindTakeInWaits is a source revision or a delivery group the indexer holds back and has not taken in within its bound
@@ -28,12 +28,12 @@ const takeInRow = "take-in"
// a pending one starts the wait for its key, whose bound runs from the event's since, and a taken one ends it. A group's
// taken event ends the group's wait only: the revision it becomes is its own key and starts its own, longer wait.
func (w *Watchman) TakeIn(event string, body []byte) {
kind, key, err := delivery.ParseTakeInEvent(event)
kind, key, err := takein.ParseTakeInEvent(event)
if err != nil {
w.say("a take-in event is left: %v", err)
return
}
t, err := delivery.DecodeTakeIn(body)
t, err := takein.DecodeTakeIn(body)
if err != nil {
w.say("the body of %s cannot be read, left: %v", event, err)
return
@@ -41,14 +41,14 @@ func (w *Watchman) TakeIn(event string, body []byte) {
w.mu.Lock()
defer w.mu.Unlock()
finding := FindingKey(ScopeDelivery, key, KindTakeInWaits)
if kind == delivery.TakeInTaken {
if kind == takein.TakeInTaken {
delete(w.takeInHeld, key)
w.unsignal(takeInRow, key)
w.unfind(finding)
return
}
bound, what := TakeInRevisionBound, "a source revision waits for the indexer build "+t.WaitsFor
if t.What == delivery.TakeInGroup {
if t.What == takein.TakeInGroup {
bound, what = TakeInGroupBound, "a delivery group is gathered and its end has not come"
}
var merges []string