mesh-delivery-sdk: a part's findings state is written through the seat mesh-conditions' client (findings.Writer over its bucket), and the family-not-heard finding is encoded by it (ADR 0564 §2, ADR 0635 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// Package findings is a part's own bucket `findings` as the state the seat mesh-conditions' client writes into
|
||||
// (meshconditionsclient.FindingState), so a part gives mesh-sdk's views the FindingWriter of the seat's layout without
|
||||
// each writing the same adapter over its bucket (novox/hq ADR 0564 §2, ADR 0635 §4).
|
||||
package findings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// ErrUnbound says the bucket is not on the bus yet, so the finding is not written (the part says it in its log).
|
||||
var ErrUnbound = errors.New("the findings state is not bound yet")
|
||||
|
||||
// State is the bucket the part's state `findings` is, given when it is bound (process.AwaitedBucket.KV): nil is not bound yet.
|
||||
type State struct {
|
||||
Ctx context.Context
|
||||
KV func() jetstream.KeyValue
|
||||
}
|
||||
|
||||
var _ meshconditionsclient.FindingState = State{}
|
||||
|
||||
// Put writes one value under its key as JSON.
|
||||
func (s State) Put(key string, value any) (uint64, error) {
|
||||
kv := s.KV()
|
||||
if kv == nil {
|
||||
return 0, ErrUnbound
|
||||
}
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return kv.Put(s.Ctx, key, data)
|
||||
}
|
||||
|
||||
// Delete clears one key.
|
||||
func (s State) Delete(key string) error {
|
||||
kv := s.KV()
|
||||
if kv == nil {
|
||||
return ErrUnbound
|
||||
}
|
||||
return kv.Delete(s.Ctx, key)
|
||||
}
|
||||
|
||||
// Writer is the part's views.FindingWriter over its bucket; the function is what views.ProcessReader.Findings and the
|
||||
// emitter's Findings take.
|
||||
func Writer(ctx context.Context, kv func() jetstream.KeyValue) views.FindingWriter {
|
||||
return meshconditionsclient.FindingWriter{State: State{Ctx: ctx, KV: kv}}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package findings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// fakeKV is the two calls a findings state makes of its bucket.
|
||||
type fakeKV struct {
|
||||
jetstream.KeyValue
|
||||
held map[string][]byte
|
||||
}
|
||||
|
||||
func (f *fakeKV) Put(_ context.Context, key string, value []byte) (uint64, error) {
|
||||
f.held[key] = value
|
||||
return uint64(len(f.held)), nil
|
||||
}
|
||||
|
||||
func (f *fakeKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
|
||||
delete(f.held, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A part's finding is written in the seat mesh-conditions' layout under the key the condition keeper reads, cleared by the
|
||||
// same finding, and refused while the bucket is not bound (the part says that in its log).
|
||||
func TestAFindingIsWrittenInTheSeatsLayoutAndClearedAndRefusedUnbound(t *testing.T) {
|
||||
kv := &fakeKV{held: map[string][]byte{}}
|
||||
var bound jetstream.KeyValue
|
||||
w := Writer(context.Background(), func() jetstream.KeyValue { return bound })
|
||||
f := views.Finding{Observer: "mesh-delivery-teller", Kind: views.UnansweredKind, ID: "mesh-delivery-teller.asks", Summary: "s", Headline: "h",
|
||||
Explanation: "e", At: time.Date(2026, 11, 5, 15, 0, 0, 0, time.UTC)}
|
||||
if err := w.Put(f); !errors.Is(err, ErrUnbound) {
|
||||
t.Fatalf("an unbound bucket took the finding: %v", err)
|
||||
}
|
||||
bound = kv
|
||||
if err := w.Put(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got struct{ Key, Kind, Observer, Source string }
|
||||
for key, value := range kv.held {
|
||||
if err := json.Unmarshal(value, &got); err != nil || got.Key != key || got.Kind != views.UnansweredKind ||
|
||||
got.Observer != f.Observer || got.Source != f.Observer+"."+f.Kind {
|
||||
t.Fatalf("%s holds %s (%v)", key, value, err)
|
||||
}
|
||||
}
|
||||
if len(kv.held) != 1 {
|
||||
t.Fatalf("the bucket holds %d findings, want 1", len(kv.held))
|
||||
}
|
||||
if err := w.Delete(f); err != nil || len(kv.held) != 0 {
|
||||
t.Fatalf("the finding is not cleared: %v, %d left", err, len(kv.held))
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
|
||||
|
||||
require (
|
||||
|
||||
+12
-8
@@ -7,11 +7,13 @@
|
||||
package heard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// Kind is the finding's kind.
|
||||
@@ -23,7 +25,7 @@ func ID(observer, family string) string { return observer + "." + family }
|
||||
|
||||
// Key is the finding's key in the observer's findings state.
|
||||
func Key(observer, family string) string {
|
||||
return health.FindingKey(health.ScopeModule, ID(observer, family), Kind)
|
||||
return meshconditionsclient.Key(meshconditionsclient.ScopeModule, ID(observer, family), Kind)
|
||||
}
|
||||
|
||||
// Words are the finding's words: its summary, headline and explanation. consequence says what the observer misses while
|
||||
@@ -41,14 +43,16 @@ func Words(observer, family, consequence string, why error) (summary, headline,
|
||||
return summary, headline, explanation
|
||||
}
|
||||
|
||||
// Finding is observer's finding that it does not hear family, as health.Finding encodes it.
|
||||
// Finding is observer's finding that it does not hear family, in the layout of the seat mesh-conditions' state findings.
|
||||
func Finding(observer, family, consequence string, why error, at time.Time) (string, []byte, error) {
|
||||
key := Key(observer, family)
|
||||
summary, headline, explanation := Words(observer, family, consequence, why)
|
||||
value, err := health.EncodeFinding(health.Finding{Key: key, Kind: Kind, Subject: health.Subject{Scope: health.ScopeModule, ID: ID(observer, family)},
|
||||
Severity: health.Warning, Summary: summary, Headline: headline, Explanation: explanation,
|
||||
Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, Source: observer + "." + Kind, Observer: observer, At: at.UTC()})
|
||||
return key, value, err
|
||||
v := meshconditionsclient.FindingFor(views.Finding{Observer: observer, Kind: Kind, ID: ID(observer, family), Summary: summary,
|
||||
Headline: headline, Explanation: explanation, At: at})
|
||||
if err := meshconditionsclient.CheckFinding(v); err != nil {
|
||||
return v.Key, nil, err
|
||||
}
|
||||
value, err := json.Marshal(v)
|
||||
return v.Key, value, err
|
||||
}
|
||||
|
||||
// Writer writes and deletes the finding in the observer's findings state. Put or Delete nil writes nothing, said in the
|
||||
|
||||
Reference in New Issue
Block a user