Answer an unreadable assignment as not yet, never a refusal (issue 928)

This commit is contained in:
fix-vault-pas@novox
2026-10-16 19:30:15 +02:00
parent 3817099b2c
commit 0dab4475cd
4 changed files with 65 additions and 3 deletions
+7 -2
View File
@@ -143,8 +143,8 @@ func (d Declared) May(caller, name string) (bool, string) {
}
raw, err := d.Read(node + "." + module)
if err != nil {
return false, fmt.Sprintf("the assignment %s.%s could not be read, so what it names is not known: %v", node,
module, err)
return false, fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read, so what it names is not known: %v",
node, module, err)
}
if raw == nil {
return false, fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", module,
@@ -173,3 +173,8 @@ func (d Declared) May(caller, name string) (bool, string) {
}
return false, fmt.Sprintf("%s's plan on %s names no secret %s", module, node, secret)
}
// notYetPrefix begins a reason that is not a refusal (issue 928): what the rule needs could not be read just now, a
// bus error rather than an answer. The vault says "holds no secret named ... yet", which the client waits on and asks
// again, and records no refusal.
const notYetPrefix = "not yet: "
@@ -0,0 +1,48 @@
package main
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/seal"
)
// Issue 928: an assignment the vault could not read (the controller was being pushed, "no responders") is not a
// refusal the client keeps for ten minutes: it is "not yet", the answer the client waits on and asks again.
func TestAnAssignmentThatCannotBeReadIsNotYetAndNotARefusal(t *testing.T) {
down := errors.New("nats: no responders available for request")
read := func(string) ([]byte, error) { return nil, down }
v := newVault(t, Declared{Read: read})
asker, _ := seal.GenerateKeyPair()
for _, name := range []string{"own/g14/claude-code/token", "provision/novox/gitea-read/g14/claude-code"} {
_, err := v.Get("provisioner.g14", name, asker.PublicKey)
if err == nil {
t.Fatalf("%s: answered while the assignment cannot be read and the vault holds nothing", name)
}
if !strings.Contains(err.Error(), "holds no secret named") || !strings.Contains(err.Error(), "no responders") {
t.Errorf("%s: the answer is %q, not the not-yet the client waits on, with the reason", name, err)
}
}
hs, _ := v.Store.Handed(10)
for _, h := range hs {
if h.Outcome == "refused" {
t.Errorf("a read error was recorded as a refusal: %+v", h)
}
}
}
// A pair row asked for while the consumer's assignment cannot be read is not yet either, and a row whose
// assignment reads as absent stays "not on".
func TestAPairRowWhoseAssignmentCannotBeReadIsNotYet(t *testing.T) {
down := errors.New("nats: no responders available for request")
parts := []string{"pair", "novox", "db", "g14", "claude-code"}
why := pairRowRefusal(func(string) ([]byte, error) { return nil, down }, "g14", parts)
if !strings.HasPrefix(why, notYetPrefix) || !strings.Contains(why, "no responders") {
t.Errorf("a read error answered %q, want a not-yet carrying the reason", why)
}
why = pairRowRefusal(func(string) ([]byte, error) { return nil, nil }, "g14", parts)
if strings.HasPrefix(why, notYetPrefix) || !strings.Contains(why, "is not on g14") {
t.Errorf("an absent assignment answered %q, want the refusal 'not on'", why)
}
}
+4 -1
View File
@@ -780,7 +780,10 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
return "the vault cannot read the assignments, so it cannot tell what " + node + "'s plans name"
}
raw, err := read(consumer + "." + module)
if err != nil || raw == nil {
if err != nil {
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", consumer, module, err)
}
if raw == nil {
return fmt.Sprintf("%s is not on %s", module, consumer)
}
var a plannedProvisions
@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
@@ -237,6 +238,11 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) {
return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why)
}
} else if ok, why := askers.May(caller, name); !ok {
if reason, notYet := strings.CutPrefix(why, notYetPrefix); notYet {
// Not a refusal (issue 928): the client waits and asks again, and nothing is recorded against it.
v.log("not yet %s to %s: %s", name, caller, reason)
return Answer{}, fmt.Errorf("the vault holds no secret named %s yet: %s", name, reason)
}
v.log("refused %s to %s: %s", name, caller, why)
_ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why})
return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why)