Merge pull request 'Gate reads the forge bridge's own messages and passes a group's end by; operator-channel.asks narrows its answer (issues 1585, 595; ADR 0633)' (#615) from feat/take-in into main
This commit was merged in pull request #615.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ type fakeMerges struct {
|
||||
manual bool
|
||||
failed error
|
||||
// said are the messages the bridge wrote on FORGE, in order.
|
||||
said []sdk.Forge
|
||||
said []forgemsg.Forge
|
||||
}
|
||||
|
||||
func newFakeMerges(f *fakeForge) *fakeMerges {
|
||||
@@ -76,11 +77,11 @@ func (m *fakeMerges) Put(group string, v sdk.Merge) error {
|
||||
err = f.refuse
|
||||
}
|
||||
f.mu.Unlock()
|
||||
msg := sdk.Forge{Repository: p.Repository, Number: p.Number, Head: p.Head.Head}
|
||||
msg := forgemsg.Forge{Repository: p.Repository, Number: p.Number, Head: p.Head.Head}
|
||||
if err != nil {
|
||||
msg.Kind, msg.Act, msg.Words = sdk.ForgeRefused, "merge", err.Error()
|
||||
msg.Kind, msg.Act, msg.Words = forgemsg.ForgeRefused, "merge", err.Error()
|
||||
} else {
|
||||
msg.Kind, msg.Merge, msg.Root = sdk.ForgeMerged, strings.Repeat("c", 40), "root"
|
||||
msg.Kind, msg.Merge, msg.Root = forgemsg.ForgeMerged, strings.Repeat("c", 40), "root"
|
||||
msg.Trees = map[string]string{"app": "tree"}
|
||||
}
|
||||
m.say(msg)
|
||||
@@ -89,12 +90,12 @@ func (m *fakeMerges) Put(group string, v sdk.Merge) error {
|
||||
}
|
||||
|
||||
// say writes a message on FORGE and has the gate hear it, as the runtime does.
|
||||
func (m *fakeMerges) say(msg sdk.Forge) {
|
||||
body, err := sdk.EncodeForge(msg)
|
||||
func (m *fakeMerges) say(msg forgemsg.Forge) {
|
||||
body, err := forgemsg.EncodeForge(msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
event, err := sdk.ForgeEvent(msg.Repository, msg.Kind)
|
||||
event, err := forgemsg.ForgeEvent(msg.Repository, msg.Kind)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/asks"
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -582,7 +583,7 @@ func TestAPassingVerdictAsksTheApproveAndItsApproveMergesWithinSecondsOverARealB
|
||||
t.Fatalf("the gate asked the forge to merge %d times: the bridge merges", n)
|
||||
}
|
||||
// The bridge says it merged; the mesh's source then holds the merge, in one write.
|
||||
b.forgeSays(t, sdk.Forge{Kind: sdk.ForgeMerged, Repository: "novox/app", Number: 7, Head: head, Merge: strings.Repeat("b", 40),
|
||||
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/app", Number: 7, Head: head, Merge: strings.Repeat("b", 40),
|
||||
Root: "r", Trees: map[string]string{"app": "t"}})
|
||||
// The forge's own word that the pull request merged follows the bridge's (its webhook): the delivery ends merged.
|
||||
b.publish(t, "mesh.mod.gitea.event.pull.merged", merged("novox/app", 7, head, strings.Repeat("b", 40)))
|
||||
@@ -677,13 +678,13 @@ func (b *testBus) answer(t *testing.T, key, option string) time.Time {
|
||||
}
|
||||
|
||||
// forgeSays is the forge bridge's message on FORGE, as its stream carries it.
|
||||
func (b *testBus) forgeSays(t *testing.T, f sdk.Forge) {
|
||||
func (b *testBus) forgeSays(t *testing.T, f forgemsg.Forge) {
|
||||
t.Helper()
|
||||
raw, err := sdk.EncodeForge(f)
|
||||
raw, err := forgemsg.EncodeForge(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event, err := sdk.ForgeEvent(f.Repository, f.Kind)
|
||||
event, err := forgemsg.ForgeEvent(f.Repository, f.Kind)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -742,7 +743,7 @@ func TestAGroupOfTwoWithGreenChecksGetsOneAskAndOnItsApproveBothMergeOneAfterThe
|
||||
t.Fatalf("the second member was handed before the first merged: %+v", again.Pulls)
|
||||
}
|
||||
one := first.Pulls[0]
|
||||
b.forgeSays(t, sdk.Forge{Kind: sdk.ForgeMerged, Repository: one.Repository, Number: one.Number, Head: one.Head.Head,
|
||||
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: one.Repository, Number: one.Number, Head: one.Head.Head,
|
||||
Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
var both sdk.Merge
|
||||
rt.until("the second member to be handed", eventWithin, func() bool { both = mergeAt(b, "feat/x"); return len(both.Pulls) == 2 })
|
||||
@@ -750,7 +751,7 @@ func TestAGroupOfTwoWithGreenChecksGetsOneAskAndOnItsApproveBothMergeOneAfterThe
|
||||
t.Fatalf("the second merge handed is %+v", both.Pulls)
|
||||
}
|
||||
two := both.Pulls[1]
|
||||
b.forgeSays(t, sdk.Forge{Kind: sdk.ForgeMerged, Repository: two.Repository, Number: two.Number, Head: two.Head.Head,
|
||||
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: two.Repository, Number: two.Number, Head: two.Head.Head,
|
||||
Merge: strings.Repeat("b", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
rt.until("source.main to hold both merges", eventWithin, func() bool {
|
||||
e, err := b.kvs["source"].Get(ctx, sdk.SourceMainKey)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
)
|
||||
|
||||
// The holder, end to end over fakes (novox/hq ADR 0239 *How it is checked*, cut by ADR 0539 §17): a pull request's head
|
||||
@@ -241,9 +241,9 @@ func TestAMergedGroupEndsMergedWithSourceMainMoved(t *testing.T) {
|
||||
t.Fatalf("source.main is %+v, wanted both merges", w.source.main)
|
||||
}
|
||||
// A merge that leaves no module.json carries no trees (issue 1367): the gate reads it, and refuses nothing.
|
||||
msg := sdk.Forge{Kind: sdk.ForgeMerged, Repository: "novox/a", Number: 9, Head: strings.Repeat("e", 40),
|
||||
msg := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/a", Number: 9, Head: strings.Repeat("e", 40),
|
||||
Merge: strings.Repeat("f", 40), Root: "r"}
|
||||
body, err := sdk.EncodeForge(msg)
|
||||
body, err := forgemsg.EncodeForge(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("a merged message with no trees is refused: %v", err)
|
||||
}
|
||||
@@ -251,6 +251,17 @@ func TestAMergedGroupEndsMergedWithSourceMainMoved(t *testing.T) {
|
||||
if len(w.h.refusals) != 0 {
|
||||
t.Errorf("the gate refused a merged message with no trees: %v", w.h.refusals)
|
||||
}
|
||||
// A group's end is the indexer's: the gate ignores it before decoding, and says no refusal (1585/T25).
|
||||
w.h.Forged("mesh-delivery-forge-bridge.forge.group.merged", []byte(`{"v":1,"group":"feat/y"}`))
|
||||
if len(w.h.refusals) != 0 {
|
||||
t.Errorf("the gate refused a group's end: %v", w.h.refusals)
|
||||
}
|
||||
// The repository forge/group's merged message ends the same way: it is decoded as a forge message, and this one,
|
||||
// which is none, is refused.
|
||||
w.h.Forged("mesh-delivery-forge-bridge.forge.forge.group.merged", []byte(`{"v":1,"group":"feat/y"}`))
|
||||
if len(w.h.refusals) != 1 {
|
||||
t.Errorf("forge/group's merged message was passed by as a group's end: refusals %v", w.h.refusals)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0419 §2, issue 978: a description's `after:` line is not read, and says so once on its pull request. The
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -765,6 +766,6 @@ func allGreenFor(w *world, ms ...member) {
|
||||
|
||||
// bridgeMerged is the forge bridge's word that it merged a member.
|
||||
func bridgeMerged(w *world, repository string, number int, head, merge string) {
|
||||
w.merges.say(sdk.Forge{Kind: sdk.ForgeMerged, Repository: repository, Number: number, Head: head, Merge: merge, Root: "root",
|
||||
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Number: number, Head: head, Merge: merge, Root: "root",
|
||||
Trees: map[string]string{"app": "tree"}})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -100,7 +101,7 @@ func TestAnAnswerHandsTheFirstMemberOnlyAndTheBridgesMergedHandsTheNext(t *testi
|
||||
if len(w.merges.puts) != 1 {
|
||||
t.Fatalf("the next member was handed before the bridge said the first merged: %d merges written", len(w.merges.puts))
|
||||
}
|
||||
w.merges.say(sdk.Forge{Kind: sdk.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
w.settleAll()
|
||||
if len(w.merges.puts) != 2 {
|
||||
t.Fatalf("the second member was not handed after the first merged: %d merges written", len(w.merges.puts))
|
||||
@@ -115,7 +116,7 @@ func TestAnAnswerHandsTheFirstMemberOnlyAndTheBridgesMergedHandsTheNext(t *testi
|
||||
// AC2: the bridge's refused hands no member, and the group is said urgent, naming the member and the forge's words.
|
||||
func TestTheBridgesRefusalHandsNoMemberAndIsSaidUrgent(t *testing.T) {
|
||||
w := answeredFirst(t)
|
||||
w.merges.say(sdk.Forge{Kind: sdk.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
|
||||
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
|
||||
Words: "the forge refused gitea_merge_pull_request: mesh/repo-check (missing)"})
|
||||
w.later(time30s)
|
||||
w.settleAll()
|
||||
@@ -137,7 +138,7 @@ func TestTheBridgesRefusalHandsNoMemberAndIsSaidUrgent(t *testing.T) {
|
||||
// the group, even after the first member merged.
|
||||
func TestTheNextMemberIsNotHandedWhileARequiredStatusOfTheGroupIsNotASuccess(t *testing.T) {
|
||||
w := answeredFirst(t)
|
||||
w.merges.say(sdk.Forge{Kind: sdk.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing meanwhile
|
||||
w.settleAll()
|
||||
if len(w.merges.puts) != 1 {
|
||||
@@ -193,7 +194,7 @@ func TestARefusalThatRepeatsWithNoTrunkMoveIsNotAskedAgainWithoutEnd(t *testing.
|
||||
w := answeredFirst(t)
|
||||
d := w.h.deliveries[IDOf(gCat, gHk)]
|
||||
refuse := func(took string) {
|
||||
w.h.ForgeRefused(sdk.Forge{Kind: sdk.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
|
||||
w.h.ForgeRefused(forgemsg.Forge{Kind: forgemsg.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
|
||||
Words: "novox/mesh-catalog#2 is not merged: its verdict names no trunk commit it was judged against, and main is now f70a99dbf485. " +
|
||||
"The check of head d2d2d2d2d2d2 merged into trunk f70a99dbf485 was asked; the last check took " + took + ". Ask again once both statuses pass"})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -312,23 +313,33 @@ func (h *Holder) mergeValue(name, key string, approved map[string]string, target
|
||||
return m, len(m.Pulls) > 0 && state == sdk.MergeMerged
|
||||
}
|
||||
|
||||
// forgeGroupEnd is the whole event of a group's end on FORGE, the forge bridge's event name under its module: compared
|
||||
// exactly, so the merged message of a repository named forge/group, which ends the same way, is read as one.
|
||||
const forgeGroupEnd = sdk.ForgeBridgeModule + "." + forgemsg.ForgeGroupEndEvent
|
||||
|
||||
// Forged takes a message of the forge bridge on FORGE: the gate follows a merge and the refusal of a merge, and nothing else.
|
||||
//
|
||||
// The end of a group's merges (`forge.group.merged`, a body this gate has no use for: the gate wrote the merges itself) is
|
||||
// the indexer's, and is ignored before it is decoded, so no refusal is said for it (issue 1585, ADR 0633 clause 4).
|
||||
func (h *Holder) Forged(event string, body []byte) {
|
||||
f, err := sdk.DecodeForge(body)
|
||||
if event == forgeGroupEnd {
|
||||
return
|
||||
}
|
||||
f, err := forgemsg.DecodeForge(body)
|
||||
if err != nil {
|
||||
h.Refuse(fmt.Errorf("a message of the forge bridge is not readable: %v", err))
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case f.Kind == sdk.ForgeMerged:
|
||||
case f.Kind == forgemsg.ForgeMerged:
|
||||
h.ForgeMerged(f)
|
||||
case f.Kind == sdk.ForgeRefused && f.Act == "merge":
|
||||
case f.Kind == forgemsg.ForgeRefused && f.Act == "merge":
|
||||
h.ForgeRefused(f)
|
||||
}
|
||||
}
|
||||
|
||||
// memberOfForge is the delivery a forge message is about: at the head it names.
|
||||
func (h *Holder) memberOfForge(f sdk.Forge) *Delivery {
|
||||
func (h *Holder) memberOfForge(f forgemsg.Forge) *Delivery {
|
||||
if d := h.deliveries[IDOf(f.Repository, f.Head)]; d != nil && d.Number == f.Number {
|
||||
return d
|
||||
}
|
||||
@@ -337,7 +348,7 @@ func (h *Holder) memberOfForge(f sdk.Forge) *Delivery {
|
||||
|
||||
// ForgeMerged takes the bridge's word that a pull request the gate handed to it merged: the merge is announced once,
|
||||
// with the ask, its asker the teller and the heads (issue 1025), and the next member's turn comes at the next look.
|
||||
func (h *Holder) ForgeMerged(f sdk.Forge) {
|
||||
func (h *Holder) ForgeMerged(f forgemsg.Forge) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
d := h.memberOfForge(f)
|
||||
@@ -354,7 +365,7 @@ func (h *Holder) ForgeMerged(f sdk.Forge) {
|
||||
// ForgeRefused takes the bridge's word that the forge refused a merge: nothing after it is merged, and the refusal is said
|
||||
// as a stalled line (ADR 0441 §3, §4). The merge is handed again only at the next event that decides whether it may
|
||||
// merge (mergeAgainNow).
|
||||
func (h *Holder) ForgeRefused(f sdk.Forge) {
|
||||
func (h *Holder) ForgeRefused(f forgemsg.Forge) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
d := h.memberOfForge(f)
|
||||
|
||||
@@ -3,7 +3,8 @@ module mesh-delivery-gate
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-20261104235927-c78941e80b59
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-20261104235927-c78941e80b59 h1:pFb+lP0FxS3bF6N3gHKtjzyixI9b1vC/NLViT/lVzRQ=
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-20261104235927-c78941e80b59/go.mod h1:Xxg9VI4y755tznAS2PygQFe2V3/aIzjRn3/06+Br9nQ=
|
||||
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=
|
||||
|
||||
@@ -1416,3 +1416,76 @@ func (h *Holder) AsksNow() []AskView {
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// AsksQuery narrows what the tool `operator-channel.asks` answers (issue 595): Key one ask, State open (open and held) or
|
||||
// ended, Since the asks opened at or after that time, Bound the most bytes of the answer's asks. The zero query keeps the
|
||||
// whole answer, as before.
|
||||
type AsksQuery struct {
|
||||
Key, State string
|
||||
Since time.Time
|
||||
Bound int
|
||||
}
|
||||
|
||||
// AsksAnswer is the answer of a narrowed query: the asks kept, how many the filters matched, and, when the bound cut the
|
||||
// answer short, that it did and how.
|
||||
type AsksAnswer struct {
|
||||
Asks []AskView `json:"asks"`
|
||||
Matched int `json:"matched"`
|
||||
Cut string `json:"cut,omitempty"`
|
||||
}
|
||||
|
||||
// AsksQueryOf reads the arguments of the tool; an empty set is the zero query.
|
||||
func AsksQueryOf(a map[string]any) (AsksQuery, error) {
|
||||
q := AsksQuery{Key: strArg(a, "key"), State: strArg(a, "state")}
|
||||
if q.State != "" && q.State != "open" && q.State != "ended" {
|
||||
return q, errorf("state is open or ended, not %q", q.State)
|
||||
}
|
||||
if since := strArg(a, "since"); since != "" {
|
||||
t, err := time.Parse(time.RFC3339, since)
|
||||
if err != nil {
|
||||
return q, errorf("since is a time like 2026-11-04T22:00:00Z: %v", err)
|
||||
}
|
||||
q.Since = t
|
||||
}
|
||||
if v, ok := a["bound"].(float64); ok {
|
||||
if v < 1 {
|
||||
return q, errorf("bound is the most bytes of the answer, at least 1")
|
||||
}
|
||||
q.Bound = int(v)
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// Narrow keeps of the views (open first, newest first) the asks the query names, and cuts the answer at its byte bound: the
|
||||
// asks that fit, in order, and a said cut when one more did not.
|
||||
func (q AsksQuery) Narrow(all []AskView) AsksAnswer {
|
||||
var out AsksAnswer
|
||||
out.Asks = []AskView{}
|
||||
var kept []AskView
|
||||
for _, v := range all {
|
||||
open := v.State == StateOpen || v.State == StateHeld
|
||||
switch {
|
||||
case q.Key != "" && v.Key != q.Key,
|
||||
q.State == "open" && !open,
|
||||
q.State == "ended" && open,
|
||||
!q.Since.IsZero() && v.Opened < q.Since.UTC().Format(time.RFC3339):
|
||||
continue
|
||||
}
|
||||
kept = append(kept, v)
|
||||
}
|
||||
out.Matched = len(kept)
|
||||
used := 0
|
||||
for i, v := range kept {
|
||||
if q.Bound > 0 {
|
||||
raw, _ := json.Marshal(v)
|
||||
if used+len(raw)+1 > q.Bound {
|
||||
out.Cut = fmt.Sprintf("the answer is cut at its bound of %d bytes: %d of %d asks are shown, the %d after them are not "+
|
||||
"(narrow with key, state or since, or raise bound)", q.Bound, i, len(kept), len(kept)-i)
|
||||
break
|
||||
}
|
||||
used += len(raw) + 1
|
||||
}
|
||||
out.Asks = append(out.Asks, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -140,3 +141,55 @@ func TestAHeldAskNamesEveryKindAndWhyWhenNoChannelIsReady(t *testing.T) {
|
||||
t.Fatalf("%q", why)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 595: operator-channel.asks answers one ask, the open or the ended, those opened from a time, and no more bytes than a
|
||||
// bound, said when it cut the answer short; with no argument it keeps the whole answer.
|
||||
func TestTheAsksVerbNarrowsItsAnswer(t *testing.T) {
|
||||
view := func(key, state, opened string) AskView {
|
||||
return AskView{Key: key, Asker: "mesh-delivery", Headline: "Approve " + key, State: state, Opened: opened}
|
||||
}
|
||||
all := []AskView{view("a.3", StateOpen, "2026-11-04T10:00:00Z"), view("a.2", StateHeld, "2026-11-03T10:00:00Z"),
|
||||
view("a.1", "ended", "2026-11-02T10:00:00Z"), view("a.0", "ended", "2026-11-01T10:00:00Z")}
|
||||
one, _ := json.Marshal(all[0])
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
want []string
|
||||
matched int
|
||||
cut bool
|
||||
err bool
|
||||
}{
|
||||
{"no argument is the zero query", map[string]any{}, nil, 0, false, false},
|
||||
{"key", map[string]any{"key": "a.1"}, []string{"a.1"}, 1, false, false},
|
||||
{"state open takes open and held", map[string]any{"state": "open"}, []string{"a.3", "a.2"}, 2, false, false},
|
||||
{"state ended", map[string]any{"state": "ended"}, []string{"a.1", "a.0"}, 2, false, false},
|
||||
{"since", map[string]any{"since": "2026-11-02T12:00:00Z"}, []string{"a.3", "a.2"}, 2, false, false},
|
||||
{"bound cuts and says so", map[string]any{"bound": float64(2*len(one) + 10), "state": "open"}, []string{"a.3", "a.2"}, 2, false, false},
|
||||
{"a bound under the asks cuts", map[string]any{"bound": float64(len(one) + 5)}, []string{"a.3"}, 4, true, false},
|
||||
{"a state that is neither", map[string]any{"state": "done"}, nil, 0, false, true},
|
||||
{"a time that is not one", map[string]any{"since": "yesterday"}, nil, 0, false, true},
|
||||
} {
|
||||
q, err := AsksQueryOf(tc.args)
|
||||
if (err != nil) != tc.err {
|
||||
t.Errorf("%s: err = %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
if tc.err {
|
||||
continue
|
||||
}
|
||||
if tc.want == nil {
|
||||
if q != (AsksQuery{}) {
|
||||
t.Errorf("%s: %+v is not the zero query, which keeps today's answer", tc.name, q)
|
||||
}
|
||||
continue
|
||||
}
|
||||
got := q.Narrow(all)
|
||||
var keys []string
|
||||
for _, v := range got.Asks {
|
||||
keys = append(keys, v.Key)
|
||||
}
|
||||
if !reflect.DeepEqual(keys, tc.want) || got.Matched != tc.matched || (got.Cut != "") != tc.cut {
|
||||
t.Errorf("%s: asks %v matched %d cut %q, want %v, %d, cut %v", tc.name, keys, got.Matched, got.Cut, tc.want, tc.matched, tc.cut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,8 +734,24 @@ func tools(h *Holder, l *listening) []stdio.Tool {
|
||||
{Name: "operator-channel.asks",
|
||||
Description: "The asks this router holds (novox/hq ADR 0259): open (or held, with since when and why no channel can carry it yet) first, then the last month's — each " +
|
||||
"one's asker, headline, state, when it opened and expires, the channels it was offered on, how it " +
|
||||
"ended (the warrant: who chose what, through which channel), and whether its asker was told.",
|
||||
Run: func(map[string]any) (any, error) { return h.AsksNow(), nil }},
|
||||
"ended (the warrant: who chose what, through which channel), and whether its asker was told. With no argument every " +
|
||||
"ask, which is large; filter it: key for one ask, state open or ended, since (a time, RFC 3339) for the asks opened " +
|
||||
"from then, and bound, the most bytes of the answer, which says when it cut it short.",
|
||||
Input: map[string]any{
|
||||
"key": str("one ask, by its key (asker.id); none for every ask"),
|
||||
"state": str("open (open and held) or ended; none for both"),
|
||||
"since": str("only the asks opened at or after this time, RFC 3339"),
|
||||
"bound": map[string]any{"type": "integer", "description": "the most bytes of the asks in the answer; said when it cuts the answer short"}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
q, err := AsksQueryOf(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if q == (AsksQuery{}) {
|
||||
return h.AsksNow(), nil
|
||||
}
|
||||
return q.Narrow(h.AsksNow()), nil
|
||||
}},
|
||||
{Name: "messenger_identities",
|
||||
Description: "The accounts linked as the operator, one per channel kind: the account, the name its " +
|
||||
"service shows, when it was linked, and from when it can approve (an hour after linking).",
|
||||
|
||||
Reference in New Issue
Block a user