Merge pull request 'operator-channel.asks answers open asks within a bound when asked with no argument (issue 1974)' (#621) from fix/1974-asks-default-bounded into main

This commit was merged in pull request #621.
This commit is contained in:
2026-11-06 21:05:59 +00:00
3 changed files with 36 additions and 21 deletions
+22 -4
View File
@@ -1418,27 +1418,35 @@ func (h *Holder) AsksNow() []AskView {
} }
// AsksQuery narrows what the tool `operator-channel.asks` answers (issue 595): Key one ask, State open (open and held) or // 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 // ended (all takes both), Since the asks opened at or after that time, Bound the most bytes of the answer's asks. With no
// whole answer, as before. // argument at all the query is the open asks within DefaultAsksBound (issue 1974), and the answer says so.
type AsksQuery struct { type AsksQuery struct {
Key, State string Key, State string
Since time.Time Since time.Time
Bound int Bound int
// Defaulted is set by AsksQueryOf when no key, state or since was given: the open asks only.
Defaulted bool
} }
// DefaultAsksBound is the most bytes of asks an answer holds when the caller gives no bound (issue 1974: the whole of a
// month's asks was 737,934 characters, more than a session reads).
const DefaultAsksBound = 32768
// AsksAnswer is the answer of a narrowed query: the asks kept, how many the filters matched, and, when the bound cut the // 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. // answer short, that it did and how.
type AsksAnswer struct { type AsksAnswer struct {
Asks []AskView `json:"asks"` Asks []AskView `json:"asks"`
Matched int `json:"matched"` Matched int `json:"matched"`
Cut string `json:"cut,omitempty"` Cut string `json:"cut,omitempty"`
// Default says that no key, state or since was given, so only the open asks are answered, and how to ask for more.
Default string `json:"default,omitempty"`
} }
// AsksQueryOf reads the arguments of the tool; an empty set is the zero query. // AsksQueryOf reads the arguments of the tool; an empty set is the zero query.
func AsksQueryOf(a map[string]any) (AsksQuery, error) { func AsksQueryOf(a map[string]any) (AsksQuery, error) {
q := AsksQuery{Key: strArg(a, "key"), State: strArg(a, "state")} q := AsksQuery{Key: strArg(a, "key"), State: strArg(a, "state")}
if q.State != "" && q.State != "open" && q.State != "ended" { if q.State != "" && q.State != "open" && q.State != "ended" && q.State != "all" {
return q, errorf("state is open or ended, not %q", q.State) return q, errorf("state is open, ended or all, not %q", q.State)
} }
if since := strArg(a, "since"); since != "" { if since := strArg(a, "since"); since != "" {
t, err := time.Parse(time.RFC3339, since) t, err := time.Parse(time.RFC3339, since)
@@ -1453,6 +1461,12 @@ func AsksQueryOf(a map[string]any) (AsksQuery, error) {
} }
q.Bound = int(v) q.Bound = int(v)
} }
if q.Key == "" && q.State == "" && q.Since.IsZero() {
q.State, q.Defaulted = "open", true
}
if q.Bound == 0 {
q.Bound = DefaultAsksBound
}
return q, nil return q, nil
} }
@@ -1461,6 +1475,10 @@ func AsksQueryOf(a map[string]any) (AsksQuery, error) {
func (q AsksQuery) Narrow(all []AskView) AsksAnswer { func (q AsksQuery) Narrow(all []AskView) AsksAnswer {
var out AsksAnswer var out AsksAnswer
out.Asks = []AskView{} out.Asks = []AskView{}
if q.Defaulted {
out.Default = "no key, state or since was given, so only the open asks are shown: state ended or all for the ended ones, " +
"key for one ask, since for the asks opened from a time"
}
var kept []AskView var kept []AskView
for _, v := range all { for _, v := range all {
open := v.State == StateOpen || v.State == StateHeld open := v.State == StateOpen || v.State == StateHeld
+8 -9
View File
@@ -143,7 +143,7 @@ func TestAHeldAskNamesEveryKindAndWhyWhenNoChannelIsReady(t *testing.T) {
} }
// Issue 595: operator-channel.asks answers one ask, the open or the ended, those opened from a time, and no more bytes than a // 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. // bound, said when it cut the answer short; with no argument it answers the open asks only, within a default bound, and says so (issue 1974).
func TestTheAsksVerbNarrowsItsAnswer(t *testing.T) { func TestTheAsksVerbNarrowsItsAnswer(t *testing.T) {
view := func(key, state, opened string) AskView { view := func(key, state, opened string) AskView {
return AskView{Key: key, Asker: "mesh-delivery", Headline: "Approve " + key, State: state, Opened: opened} return AskView{Key: key, Asker: "mesh-delivery", Headline: "Approve " + key, State: state, Opened: opened}
@@ -159,13 +159,15 @@ func TestTheAsksVerbNarrowsItsAnswer(t *testing.T) {
cut bool cut bool
err bool err bool
}{ }{
{"no argument is the zero query", map[string]any{}, nil, 0, false, false}, {"no argument is the open asks, said", map[string]any{}, []string{"a.3", "a.2"}, 2, false, false},
{"state all is every ask", map[string]any{"state": "all"}, []string{"a.3", "a.2", "a.1", "a.0"}, 4, false, false},
{"a key finds an ended ask whatever its state", map[string]any{"key": "a.0"}, []string{"a.0"}, 1, false, false},
{"key", map[string]any{"key": "a.1"}, []string{"a.1"}, 1, 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 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}, {"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}, {"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}, {"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 bound under the asks cuts", map[string]any{"bound": float64(len(one) + 5)}, []string{"a.3"}, 2, true, false},
{"a state that is neither", map[string]any{"state": "done"}, nil, 0, false, true}, {"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}, {"a time that is not one", map[string]any{"since": "yesterday"}, nil, 0, false, true},
} { } {
@@ -177,13 +179,10 @@ func TestTheAsksVerbNarrowsItsAnswer(t *testing.T) {
if tc.err { if tc.err {
continue 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) got := q.Narrow(all)
if (got.Default != "") != (tc.args["key"] == nil && tc.args["state"] == nil && tc.args["since"] == nil) || q.Bound == 0 {
t.Errorf("%s: default %q, bound %d: only no argument is defaulted, and every query is bounded", tc.name, got.Default, q.Bound)
}
var keys []string var keys []string
for _, v := range got.Asks { for _, v := range got.Asks {
keys = append(keys, v.Key) keys = append(keys, v.Key)
+6 -8
View File
@@ -734,12 +734,13 @@ func tools(h *Holder, l *listening) []stdio.Tool {
{Name: "operator-channel.asks", {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 " + 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 " + "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. With no argument every " + "ended (the warrant: who chose what, through which channel), and whether its asker was told. With no argument " +
"ask, which is large; filter it: key for one ask, state open or ended, since (a time, RFC 3339) for the asks opened " + "only the open asks, within 32768 bytes; the answer says what it cut and how to ask for more: key for one ask " +
"from then, and bound, the most bytes of the answer, which says when it cut it short.", "(any state), state open, ended or all, since (a time, RFC 3339) for the asks opened from then, and bound, the " +
"most bytes of the answer.",
Input: map[string]any{ Input: map[string]any{
"key": str("one ask, by its key (asker.id); none for every ask"), "key": str("one ask, by its key (asker.id); none for the open asks"),
"state": str("open (open and held) or ended; none for both"), "state": str("open (open and held), ended or all; none is open when no key or since is given, else both"),
"since": str("only the asks opened at or after this time, RFC 3339"), "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"}}, "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) { Run: func(a map[string]any) (any, error) {
@@ -747,9 +748,6 @@ func tools(h *Holder, l *listening) []stdio.Tool {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if q == (AsksQuery{}) {
return h.AsksNow(), nil
}
return q.Narrow(h.AsksNow()), nil return q.Narrow(h.AsksNow()), nil
}}, }},
{Name: "messenger_identities", {Name: "messenger_identities",