Tool runner: the cap's reason fails the call; the test proves the ending; the refused-answer limit keys on the caller's prefix and forgets old entries (issue 1966, review round 1)

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-09 13:59:13 +01:00
parent f3a9cc276e
commit 312e29478e
4 changed files with 73 additions and 27 deletions
+13 -3
View File
@@ -1229,15 +1229,25 @@ func (c *Conn) answerRefused(err error) bool {
if len(m) < 2 || !strings.HasPrefix(strings.Trim(m[1], `"`), "_INBOX.") {
return false
}
// Keyed without the reply's last token (a new one per request), so a caller's burst is one line.
subject := strings.Trim(m[1], `"`)
if dot := strings.LastIndex(subject, "."); dot > len("_INBOX.") {
subject = subject[:dot]
}
c.mu.Lock()
if c.inboxSaid == nil {
c.inboxSaid = map[string]time.Time{}
}
last, seen := c.inboxSaid[subject]
say := !seen || time.Since(last) >= time.Minute
now := time.Now()
for k, at := range c.inboxSaid {
if now.Sub(at) >= time.Minute {
delete(c.inboxSaid, k)
}
}
_, seen := c.inboxSaid[subject]
say := !seen
if say {
c.inboxSaid[subject] = time.Now()
c.inboxSaid[subject] = now
}
c.mu.Unlock()
if say && c.Logf != nil {
+8 -3
View File
@@ -94,17 +94,22 @@ func TestARefusedAnswerIsSaidOncePerMinute(t *testing.T) {
refuse := func(subject string) bool {
return c.answerRefused(fmt.Errorf("%w: Permissions Violation for Publish to %q", nats.ErrPermissionViolation, subject))
}
if !refuse("_INBOX.user.abc") || !refuse("_INBOX.user.abc") || !refuse("_INBOX.user.def") {
// A reply subject ends in a token new to every request: a burst under one prefix is one line.
if !refuse("_INBOX.user.abc.t1") || !refuse("_INBOX.user.abc.t2") || !refuse("_INBOX.user.def.t1") {
t.Fatal("a refused answer was not taken for one")
}
if len(said) != 2 {
t.Fatalf("want one line per subject, got %v", said)
t.Fatalf("want one line per caller, got %v", said)
}
c.inboxSaid["_INBOX.user.abc"] = time.Now().Add(-2 * time.Minute)
refuse("_INBOX.user.abc")
c.inboxSaid["_INBOX.user.old"] = time.Now().Add(-2 * time.Minute)
refuse("_INBOX.user.abc.t3")
if len(said) != 3 || !strings.Contains(said[2], "_INBOX.user.abc") {
t.Fatalf("a minute later it is said again, got %v", said)
}
if _, kept := c.inboxSaid["_INBOX.user.old"]; kept || len(c.inboxSaid) != 2 {
t.Fatalf("entries older than a minute are dropped on write, kept %v", c.inboxSaid)
}
if refuse("mesh.mod.x.tool.y") {
t.Fatal("a refused publish that is no answer was taken for one")
}