G V-8: the finding writer takes the generated finding type through views.FindingBody and exports CheckFinding

views.FindingBody becomes the interface of the accessors the seat generator emits
(Kind/SetKind per field, one Subject/SetSubject pair); the writer is given the contract's
constructors of the finding, finding-cleared, findings-stated and findings-asked bodies and
holds none of their layouts. WriterFindings takes only the constructor and lays the views
reader's four findings out itself (scope module, id escaped, urgent for view-too-large,
source views, subject the module on its node). views.CheckFinding(b) error is the one check
of the key's parts, the kind and the owner and waiting-on forms, run before every write.

Builds: design 47 G §686, §687, §689, §640, §688, A-1 (.work/47-G/dev2-ledger.md)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:33:23 +01:00
parent 72e1cb77de
commit 3a438786e3
5 changed files with 383 additions and 147 deletions
+126
View File
@@ -0,0 +1,126 @@
package views_test
import (
"time"
"git.novox.be/novox/mesh-sdk/go/views"
)
// The test's own stand-in for the types the seat generator emits for the seat `mesh-conditions` (design 47 G §686,
// §640): mesh-sdk keeps no such layout, so the writer's tests bring one, with the accessors the generator emits.
type testSubject struct {
Also []string `json:"also,omitempty"`
ID string `json:"id"`
Machine string `json:"machine,omitempty"`
Scope string `json:"scope"`
}
type testFinding struct {
AtVal time.Time `json:"at"`
Explanation string `json:"explanation,omitempty"`
HeadlineVal string `json:"headline,omitempty"`
IDField string `json:"id"`
KindField string `json:"kind"`
ObserverVal string `json:"observer"`
Owner string `json:"owner,omitempty"`
Run string `json:"run,omitempty"`
ScopeField string `json:"scope"`
SeverityVal string `json:"severity"`
SinceVal time.Time `json:"since"`
SourceVal string `json:"source"`
SubjectVal testSubject `json:"subject"`
SummaryVal string `json:"summary"`
WaitingOn string `json:"waiting-on,omitempty"`
}
var _ views.FindingBody = (*testFinding)(nil)
func (f *testFinding) Kind() string { return f.KindField }
func (f *testFinding) SetKind(v string) { f.KindField = v }
func (f *testFinding) Scope() string { return f.ScopeField }
func (f *testFinding) SetScope(v string) { f.ScopeField = v }
func (f *testFinding) ID() string { return f.IDField }
func (f *testFinding) SetID(v string) { f.IDField = v }
func (f *testFinding) At() time.Time { return f.AtVal }
func (f *testFinding) Since() time.Time { return f.SinceVal }
func (f *testFinding) SetSince(v time.Time) { f.SinceVal = v }
func (f *testFinding) Severity() string { return f.SeverityVal }
func (f *testFinding) SetSeverity(v string) { f.SeverityVal = v }
func (f *testFinding) Summary() string { return f.SummaryVal }
func (f *testFinding) SetSummary(v string) { f.SummaryVal = v }
func (f *testFinding) Headline() string { return f.HeadlineVal }
func (f *testFinding) SetHeadline(v string) { f.HeadlineVal = v }
func (f *testFinding) Source() string { return f.SourceVal }
func (f *testFinding) SetSource(v string) { f.SourceVal = v }
func (f *testFinding) Observer() string { return f.ObserverVal }
func (f *testFinding) SetObserver(v string) { f.ObserverVal = v }
func (f *testFinding) SetAt(v time.Time) { f.AtVal = v }
func (f *testFinding) SetSubject(a []string, id, m, s string) {
f.SubjectVal = testSubject{Also: a, ID: id, Machine: m, Scope: s}
}
func (f *testFinding) Subject() ([]string, string, string, string) {
return f.SubjectVal.Also, f.SubjectVal.ID, f.SubjectVal.Machine, f.SubjectVal.Scope
}
type testCleared struct {
At time.Time `json:"at"`
ID string `json:"id"`
Kind string `json:"kind"`
Scope string `json:"scope"`
Why string `json:"why"`
}
func (c *testCleared) SetKind(v string) { c.Kind = v }
func (c *testCleared) SetScope(v string) { c.Scope = v }
func (c *testCleared) SetID(v string) { c.ID = v }
func (c *testCleared) SetAt(v time.Time) { c.At = v }
func (c *testCleared) SetWhy(v string) { c.Why = v }
type testPending struct {
Bound string `json:"bound"`
Key string `json:"key"`
Since time.Time `json:"since"`
}
type testStated struct {
Asked string `json:"asked,omitempty"`
At time.Time `json:"at"`
Findings []testFinding `json:"findings"`
Kinds []string `json:"kinds,omitempty"`
Pending []testPending `json:"pending"`
}
func newTestStated() *testStated {
return &testStated{Findings: []testFinding{}, Pending: []testPending{}}
}
func (s *testStated) SetAt(v time.Time) { s.At = v }
func (s *testStated) SetAsked(v string) { s.Asked = v }
func (s *testStated) SetKinds(v []string) { s.Kinds = v }
func (s *testStated) AddFinding(b views.FindingBody) {
s.Findings = append(s.Findings, *b.(*testFinding))
}
func (s *testStated) AddPending(k string, since time.Time, bound string) {
s.Pending = append(s.Pending, testPending{Bound: bound, Key: k, Since: since})
}
type testAsked struct {
AskID string `json:"ask"`
Mod string `json:"module,omitempty"`
}
func (a *testAsked) Ask() string { return a.AskID }
func (a *testAsked) Module() string { return a.Mod }
var bodies = struct {
NewFinding func() views.FindingBody
NewCleared func() views.ClearedBody
NewStated func() views.StatedBody
NewAsked func() views.AskedBody
}{
func() views.FindingBody { return &testFinding{} },
func() views.ClearedBody { return &testCleared{} },
func() views.StatedBody { return newTestStated() },
func() views.AskedBody { return &testAsked{} },
}
+31 -7
View File
@@ -616,22 +616,46 @@ func (r *Reader) Tools(prefix string) []stdio.Tool {
}
}
// FindingScope is the scope of a views reader's findings: they are findings of the part's module (design 47 G §687).
const FindingScope = "module"
// WriterFindings writes a views reader's findings through the part's mesh-sdk writer, as kinds of the reader's own
// (design 47 G §639, §580): the Findings of a reader, never a writer of its own. Body is the part's: it lays a Finding
// out as the finding of the seat `mesh-conditions` (its scope, severity, source and subject are the seat's contract's,
// not this package's); the writer's statement covers the reader's open findings beside its observe's.
// (design 47 G §639, §580, §687): the Findings of a reader, never a writer of its own. mesh-sdk lays each finding out
// through the body's setters, and the part supplies only New, the generated contract's constructor of a finding body:
// `kind` the finding's, `scope` module, `id` the finding's id escaped as a key's name is, `severity` warning but urgent
// for view-too-large, `source` views, `subject` the part's module on its node, `summary` and `headline` the finding's
// texts. The writer's statement covers the reader's open findings beside its observe's.
type WriterFindings struct {
Writer *Writer
Body func(Finding) FindingBody
New func() FindingBody
}
func (o WriterFindings) body(f Finding) FindingBody {
b := o.New()
b.SetKind(f.Kind)
b.SetScope(FindingScope)
b.SetID(keys.EscapeName(f.ID))
b.SetSeverity("warning")
if f.Kind == TooLargeKind {
b.SetSeverity("urgent")
}
b.SetSource("views")
b.SetSubject(nil, o.Writer.cfg.Module, o.Writer.cfg.Node, FindingScope)
b.SetSummary(f.Summary)
b.SetHeadline(f.Headline)
if !f.At.IsZero() {
b.SetAt(f.At)
}
return b
}
// Put writes one finding.
func (o WriterFindings) Put(f Finding) error {
return o.Writer.Write(context.Background(), o.Body(f))
return o.Writer.Write(context.Background(), o.body(f))
}
// Delete clears one finding.
func (o WriterFindings) Delete(f Finding) error {
b := o.Body(f)
return o.Writer.Clear(context.Background(), b.Scope, b.ID, b.Kind, "the part no longer observes it")
b := o.body(f)
return o.Writer.Clear(context.Background(), b.Scope(), b.ID(), b.Kind(), "the part no longer observes it")
}
+151 -114
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"slices"
"strings"
@@ -30,56 +31,67 @@ const (
ObserveRetryEvery = time.Minute
)
// FindingSubject is what a finding is about (the `Subject` of design 47 G §439's finding).
type FindingSubject struct {
Scope string `json:"scope"`
ID string `json:"id"`
Machine string `json:"machine,omitempty"`
Also []string `json:"also,omitempty"`
// FindingBody is one finding as the seat `mesh-conditions` lays it out (design 47 G §686, §640): the writer keeps no copy
// of that layout, it takes the type the seat generator emits through these accessors. A getter and a setter for each field
// the writer reads or fills; the subject is one pair, its fields as plain values in the generated struct's order (design 47
// G A-1). A body also marshals as JSON to its layout, which is how it is kept and said.
type FindingBody interface {
Kind() string
SetKind(string)
Scope() string
SetScope(string)
ID() string
SetID(string)
At() time.Time
SetAt(time.Time)
Since() time.Time
SetSince(time.Time)
Severity() string
SetSeverity(string)
Summary() string
SetSummary(string)
Headline() string
SetHeadline(string)
Source() string
SetSource(string)
Subject() (also []string, id, machine, scope string)
SetSubject(also []string, id, machine, scope string)
Observer() string
SetObserver(string)
}
// FindingEvidence is one observation, as it was said.
type FindingEvidence struct {
At time.Time `json:"at"`
Said string `json:"said"`
// ClearedBody is the body of `finding-cleared`, as the generated contract's type takes its fields (design 47 G A-1).
type ClearedBody interface {
SetKind(string)
SetScope(string)
SetID(string)
SetAt(time.Time)
SetWhy(string)
}
// FindingAction is one answer the operator may give from a notification.
type FindingAction struct {
Label string `json:"label"`
Verb string `json:"verb"`
Machine string `json:"machine,omitempty"`
Arguments map[string]string `json:"arguments,omitempty"`
Level string `json:"level,omitempty"`
// StatedBody is the body of `findings-stated`, as the generated contract's type takes its fields (design 47 G A-1):
// every finding the observer observes now, and each key it cannot judge yet, with its since and its bound (a duration
// such as 5m).
type StatedBody interface {
SetAt(time.Time)
SetAsked(string)
// SetKinds are the kinds the statement covers (design 47 G §580); none means every kind.
SetKinds([]string)
AddFinding(FindingBody)
AddPending(key string, since time.Time, bound string)
}
// FindingBody is one finding as an observer says it: the layout whole of design 47 G §439's list, the body of
// `finding-written`, of each entry of `findings-stated` and the value of its key `<Scope>.<ID>.<Kind>` in the state `findings`.
type FindingBody struct {
Kind string `json:"kind"`
Scope string `json:"scope"`
ID string `json:"id"`
At time.Time `json:"at"`
Since time.Time `json:"since"`
Severity string `json:"severity"`
Summary string `json:"summary"`
Headline string `json:"headline,omitempty"`
Explanation string `json:"explanation,omitempty"`
Subject FindingSubject `json:"subject"`
Evidence []FindingEvidence `json:"evidence,omitempty"`
Source string `json:"source"`
Observer string `json:"observer"`
Owner string `json:"owner,omitempty"`
WaitingOn string `json:"waiting-on,omitempty"`
Actions []FindingAction `json:"actions,omitempty"`
Needs string `json:"needs,omitempty"`
Resolver string `json:"resolver,omitempty"`
Ref string `json:"ref,omitempty"`
Run string `json:"run,omitempty"`
// AskedBody is the body of `findings-asked` the writer reads: the ask's id and the module asked, none asking every
// observer (design 47 G A-1).
type AskedBody interface {
Ask() string
Module() string
}
// Key is the finding's key in the state `findings`: `<scope>.<id>.<kind>` (novox/hq ADR 0564 §2).
func (f FindingBody) Key() string { return FindingKey(f.Scope, f.ID, f.Kind) }
// FindingKey is the key of the finding of scope, id and kind: `<scope>.<id>.<kind>` (novox/hq ADR 0564 §2).
func FindingKey(scope, id, kind string) string { return scope + "." + id + "." + kind }
func keyOf(b FindingBody) string { return FindingKey(b.Scope(), b.ID(), b.Kind()) }
// ViewKinds are the finding kinds a views reader writes through the part's writer (design 47 G §639).
var ViewKinds = []string{ViewWaitsKind, UnansweredKind, UnsaidKind, TooLargeKind}
@@ -99,18 +111,6 @@ func formOfWho(field, v string) error {
return fmt.Errorf("the finding's %s %q is none of a session (who@node), a seat's name or a commit's author (forge:<account>)", field, v)
}
// FindingKey is the key of the finding of scope, id and kind.
func FindingKey(scope, id, kind string) string { return scope + "." + id + "." + kind }
// FindingCleared is the body of `finding-cleared`: an observer no longer observes the finding.
type FindingCleared struct {
Kind string `json:"kind"`
Scope string `json:"scope"`
ID string `json:"id"`
At time.Time `json:"at"`
Why string `json:"why"`
}
// PendingKey is a key the observer cannot judge yet since its start: its `since` and its bound (a duration such as 5m).
type PendingKey struct {
Key string `json:"key"`
@@ -118,24 +118,6 @@ type PendingKey struct {
Bound string `json:"bound"`
}
// FindingsStated is the body of `findings-stated`: every finding the observer observes now, and what it cannot judge yet.
type FindingsStated struct {
At time.Time `json:"at"`
Asked string `json:"asked,omitempty"`
// Kinds are the kinds this statement covers (design 47 G §580); none means every kind.
Kinds []string `json:"kinds,omitempty"`
Findings []FindingBody `json:"findings"`
Pending []PendingKey `json:"pending"`
}
// FindingsAsked is the body of `findings-asked`: the keeper asks every observer, or the one named, to state its findings.
type FindingsAsked struct {
Ask string `json:"ask"`
At time.Time `json:"at"`
By string `json:"by"`
Module string `json:"module,omitempty"`
}
// Observed is what an observer's own truth says now: the findings it observes and the keys it cannot judge yet.
type Observed struct {
Findings []FindingBody
@@ -171,6 +153,15 @@ type WriterConfig struct {
Conn *nats.Conn
// Subjects are the four events' subjects.
Subjects WriterSubjects
// NewFinding, NewCleared, NewStated and NewAsked make an empty body of the generated contract's types (design 47 G
// §686, §640, A-1): the writer holds no layout of its own. All four are needed.
NewFinding func() FindingBody
NewCleared func() ClearedBody
NewStated func() StatedBody
NewAsked func() AskedBody
// Node is the machine the module runs on, which a views reader's findings name in their subject (design 47 G §687);
// none given, MESH_NODE.
Node string
// Observe answers the findings the observer observes now and its pending keys, or an error. It is called under the
// writer's one lock, so it must not call Write or Clear.
Observe func(ctx context.Context) (Observed, error)
@@ -214,9 +205,14 @@ func NewWriter(cfg WriterConfig) (*Writer, error) {
return nil, errors.New("a finding writer needs the observe function")
case cfg.Subjects.Written == "" || cfg.Subjects.Cleared == "" || cfg.Subjects.Stated == "" || cfg.Subjects.Asked == "":
return nil, errors.New("a finding writer needs the subjects of finding-written, finding-cleared, findings-stated and findings-asked")
case cfg.NewFinding == nil || cfg.NewCleared == nil || cfg.NewStated == nil || cfg.NewAsked == nil:
return nil, errors.New("a finding writer needs the generated contract's constructors of the finding, finding-cleared, findings-stated and findings-asked bodies")
case cfg.JS == nil && (cfg.Publish == nil || cfg.Listen == nil):
return nil, errors.New("a finding writer needs the bus connection's JetStream")
}
if cfg.Node == "" {
cfg.Node = os.Getenv("MESH_NODE")
}
if cfg.Now == nil {
cfg.Now = func() time.Time { return time.Now().UTC() }
}
@@ -354,16 +350,16 @@ func (w *Writer) signal() {
// heard is an ask: answered when it names no module or this one.
func (w *Writer) heard(data []byte) {
var a FindingsAsked
if err := json.Unmarshal(data, &a); err != nil {
a := w.cfg.NewAsked()
if err := json.Unmarshal(data, a); err != nil {
w.cfg.Logf("an ask for findings could not be read: %v", err)
return
}
if a.Module != "" && a.Module != w.cfg.Module {
if a.Module() != "" && a.Module() != w.cfg.Module {
return
}
w.ctl.Lock()
w.asks, w.want = append(w.asks, a.Ask), true
w.asks, w.want = append(w.asks, a.Ask()), true
w.ctl.Unlock()
w.signal()
}
@@ -428,12 +424,16 @@ func (w *Writer) now() time.Time { return w.cfg.Now().UTC() }
// unsaid is a finding as compared to say whether what it says changed: not the time it was last seen, nor what saw it.
func unsaid(f FindingBody) string {
f.At, f.Run = time.Time{}, ""
raw, _ := json.Marshal(f)
var m map[string]any
_ = json.Unmarshal(raw, &m)
delete(m, "at")
delete(m, "run")
raw, _ = json.Marshal(m)
return string(raw)
}
func (w *Writer) read(key string) (*FindingBody, error) {
func (w *Writer) read(key string) (FindingBody, error) {
e, err := w.cfg.State.Get(key)
if err != nil {
return nil, err
@@ -441,45 +441,73 @@ func (w *Writer) read(key string) (*FindingBody, error) {
if e == nil {
return nil, nil
}
var old FindingBody
if err := json.Unmarshal(e.Value, &old); err != nil {
old := w.cfg.NewFinding()
if err := json.Unmarshal(e.Value, old); err != nil {
return nil, fmt.Errorf("the finding %s in the state findings: %w", key, err)
}
return &old, nil
return old, nil
}
// CheckFinding refuses a finding the keeper could not make a condition of, naming the field of the first rule it breaks:
// the key's three parts (`scope`, `id` and `kind`, each given, the kind with no dot, since the kind is the key's last
// part), and the forms of `owner` and `waiting-on` (design 47 G §678, §689). The writer refuses such a finding before it
// writes it, and the keeper judges the findings others write with it.
func CheckFinding(b FindingBody) error {
switch {
case strings.TrimSpace(b.Scope()) == "":
return errors.New("the finding's scope is not given")
case strings.TrimSpace(b.ID()) == "":
return errors.New("the finding's id is not given")
case strings.TrimSpace(b.Kind()) == "":
return errors.New("the finding's kind is not given")
case strings.Contains(b.Kind(), "."):
return fmt.Errorf("the finding's kind %q holds a dot, and the kind is the last part of its key", b.Kind())
}
raw, err := json.Marshal(b)
if err != nil {
return fmt.Errorf("the finding could not be read: %w", err)
}
var who struct {
Owner string `json:"owner"`
WaitingOn string `json:"waiting-on"`
}
if err := json.Unmarshal(raw, &who); err != nil {
return fmt.Errorf("the finding could not be read: %w", err)
}
if err := formOfWho("owner", who.Owner); err != nil {
return err
}
return formOfWho("waiting-on", who.WaitingOn)
}
// complete fills what the observer leaves to the writer and keeps the key's `since` where the key exists (design 47 G §384).
func (w *Writer) complete(f FindingBody) (FindingBody, *FindingBody, error) {
if f.Kind == "" || f.Scope == "" || f.ID == "" {
return f, nil, fmt.Errorf("a finding needs its kind, scope and id: %q.%q.%q", f.Scope, f.ID, f.Kind)
}
if !w.owns(f.Kind) {
return f, nil, fmt.Errorf("the finding kind %q is not one this writer owns (%v)", f.Kind, w.cfg.Kinds)
}
if err := formOfWho("owner", f.Owner); err != nil {
func (w *Writer) complete(f FindingBody) (FindingBody, FindingBody, error) {
if err := CheckFinding(f); err != nil {
return f, nil, err
}
if err := formOfWho("waiting-on", f.WaitingOn); err != nil {
return f, nil, err
if !w.owns(f.Kind()) {
return f, nil, fmt.Errorf("the finding kind %q is not one this writer owns (%v)", f.Kind(), w.cfg.Kinds)
}
old, err := w.read(f.Key())
old, err := w.read(keyOf(f))
if err != nil {
return f, nil, err
}
if f.Observer == "" {
f.Observer = w.cfg.Module
if f.Observer() == "" {
f.SetObserver(w.cfg.Module)
}
if f.At.IsZero() {
f.At = w.now()
at := f.At()
if at.IsZero() {
at = w.now()
}
f.At = f.At.UTC()
f.SetAt(at.UTC())
since := f.Since()
switch {
case old != nil && !old.Since.IsZero():
f.Since = old.Since
case f.Since.IsZero():
f.Since = f.At
case old != nil && !old.Since().IsZero():
since = old.Since()
case since.IsZero():
since = f.At()
}
f.Since = f.Since.UTC()
f.SetSince(since.UTC())
return f, old, nil
}
@@ -492,14 +520,14 @@ func (w *Writer) Write(ctx context.Context, f FindingBody) error {
if err != nil {
return err
}
if _, err := w.cfg.State.Put(f.Key(), f); err != nil {
return fmt.Errorf("writing the finding %s: %w", f.Key(), err)
if _, err := w.cfg.State.Put(keyOf(f), f); err != nil {
return fmt.Errorf("writing the finding %s: %w", keyOf(f), err)
}
if old != nil && unsaid(*old) == unsaid(f) {
if old != nil && unsaid(old) == unsaid(f) {
return nil
}
raw, _ := json.Marshal(f)
w.say(ctx, w.cfg.Subjects.Written, "finding-written", f.Key(), raw)
w.say(ctx, w.cfg.Subjects.Written, "finding-written", keyOf(f), raw)
return nil
}
@@ -515,7 +543,13 @@ func (w *Writer) Clear(ctx context.Context, scope, id, kind, why string) error {
if err := w.cfg.State.Delete(key); err != nil {
return fmt.Errorf("clearing the finding %s: %w", key, err)
}
raw, _ := json.Marshal(FindingCleared{Kind: kind, Scope: scope, ID: id, At: w.now(), Why: why})
cleared := w.cfg.NewCleared()
cleared.SetKind(kind)
cleared.SetScope(scope)
cleared.SetID(id)
cleared.SetAt(w.now())
cleared.SetWhy(why)
raw, _ := json.Marshal(cleared)
w.say(ctx, w.cfg.Subjects.Cleared, "finding-cleared", key, raw)
return nil
}
@@ -531,21 +565,24 @@ func (w *Writer) State(ctx context.Context, asked string) error {
return fmt.Errorf("observe: %w", err)
}
keep := map[string]bool{}
body := FindingsStated{At: w.now(), Asked: asked, Kinds: w.statedKinds(), Findings: []FindingBody{}, Pending: []PendingKey{}}
body := w.cfg.NewStated()
body.SetAt(w.now())
body.SetAsked(asked)
body.SetKinds(w.statedKinds())
for _, f := range obs.Findings {
f, _, err := w.complete(f)
if err != nil {
return err
}
if _, err := w.cfg.State.Put(f.Key(), f); err != nil {
return fmt.Errorf("writing the finding %s: %w", f.Key(), err)
if _, err := w.cfg.State.Put(keyOf(f), f); err != nil {
return fmt.Errorf("writing the finding %s: %w", keyOf(f), err)
}
keep[f.Key()] = true
body.Findings = append(body.Findings, f)
keep[keyOf(f)] = true
body.AddFinding(f)
}
for _, p := range obs.Pending {
keep[p.Key] = true
body.Pending = append(body.Pending, p)
body.AddPending(p.Key, p.Since, p.Bound)
}
keys, err := w.cfg.State.Keys()
if err != nil {
@@ -557,7 +594,7 @@ func (w *Writer) State(ctx context.Context, asked string) error {
if i := strings.LastIndex(k, "."); !keep[k] && i >= 0 && slices.Contains(ViewKinds, k[i+1:]) {
if old, err := w.read(k); err == nil && old != nil {
keep[k] = true
body.Findings = append(body.Findings, *old)
body.AddFinding(old)
}
}
if !keep[k] && w.ownsKey(k) {
+4 -3
View File
@@ -43,7 +43,8 @@ func TestWriterSaysItsEventsAndHearsAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
state := &fakeState{m: map[string][]byte{}}
w, err := views.NewWriter(views.WriterConfig{Module: "m", State: state, JS: js, Conn: nc,
w, err := views.NewWriter(views.WriterConfig{Module: "m", State: state, JS: js, Conn: nc, Node: "n1",
NewFinding: bodies.NewFinding, NewCleared: bodies.NewCleared, NewStated: bodies.NewStated, NewAsked: bodies.NewAsked,
Subjects: views.WriterSubjects{Written: "obs.event.written", Cleared: "obs.event.cleared", Stated: "obs.event.stated", Asked: "obs.ask"},
Observe: func(context.Context) (views.Observed, error) {
return views.Observed{Findings: []views.FindingBody{finding("a", "now")}}, nil
@@ -55,7 +56,7 @@ func TestWriterSaysItsEventsAndHearsAsksOnTheBus(t *testing.T) {
t.Fatal(err)
}
defer w.Close()
next := func(want string) views.FindingsStated {
next := func(want string) testStated {
t.Helper()
m, err := listen.Next(jetstream.FetchMaxWait(10 * time.Second))
if err != nil {
@@ -64,7 +65,7 @@ func TestWriterSaysItsEventsAndHearsAsksOnTheBus(t *testing.T) {
if m.Subject() != want {
t.Fatalf("said %s, want %s", m.Subject(), want)
}
var st views.FindingsStated
var st testStated
if want == "obs.event.stated" {
if err := json.Unmarshal(m.Data(), &st); err != nil {
t.Fatal(err)
+71 -23
View File
@@ -86,7 +86,8 @@ func newRigKinds(t *testing.T, kinds ...string) *rig {
r := &rig{t: t, state: &fakeState{m: map[string][]byte{}}, now: time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC), ch: make(chan struct{}, 100)}
r.observe = func(context.Context) (views.Observed, error) { return views.Observed{}, nil }
w, err := views.NewWriter(views.WriterConfig{
Module: "m", State: r.state, Subjects: subjects, Kinds: kinds,
Module: "m", State: r.state, Subjects: subjects, Kinds: kinds, Node: "n1",
NewFinding: bodies.NewFinding, NewCleared: bodies.NewCleared, NewStated: bodies.NewStated, NewAsked: bodies.NewAsked,
Observe: func(ctx context.Context) (views.Observed, error) { return r.observe(ctx) },
Logf: func(f string, a ...any) {
r.mu.Lock()
@@ -136,9 +137,9 @@ func (r *rig) events() []said {
return slices.Clone(r.sent)
}
func finding(id, summary string) views.FindingBody {
return views.FindingBody{Kind: "k", Scope: "module", ID: id, Severity: "warning", Summary: summary, Source: "probe",
Subject: views.FindingSubject{Scope: "module", ID: id}}
func finding(id, summary string) *testFinding {
return &testFinding{KindField: "k", ScopeField: "module", IDField: id, SeverityVal: "warning", SummaryVal: summary, SourceVal: "probe",
SubjectVal: testSubject{Scope: "module", ID: id}}
}
// A finding written says its event with the layout whole after the key, keeps the key's since, and says again only when
@@ -163,11 +164,11 @@ func TestWriterWritesAndClearsFindingsAsEvents(t *testing.T) {
if len(ev) != 2 || ev[0].subject != subjects.Written || ev[1].subject != subjects.Written {
t.Fatalf("events: %+v", ev)
}
var got views.FindingBody
var got testFinding
if err := json.Unmarshal(ev[1].data, &got); err != nil {
t.Fatal(err)
}
if got.Summary != "two" || !got.Since.Equal(first) || got.Observer != "m" || got.Key() != "module.a.k" || !got.At.Equal(r.now) {
if got.SummaryVal != "two" || !got.SinceVal.Equal(first) || got.ObserverVal != "m" || views.FindingKey(got.ScopeField, got.IDField, got.KindField) != "module.a.k" || !got.AtVal.Equal(r.now) {
t.Fatalf("body: %+v", got)
}
if e, _ := r.state.Get("module.a.k"); e == nil {
@@ -180,7 +181,7 @@ func TestWriterWritesAndClearsFindingsAsEvents(t *testing.T) {
t.Fatal(err)
}
ev = r.events()
var cl views.FindingCleared
var cl testCleared
if len(ev) != 4 || ev[3].subject != subjects.Cleared || json.Unmarshal(ev[3].data, &cl) != nil || cl.ID != "never" || cl.Why != "held by the keeper" {
t.Fatalf("clear events: %+v", ev)
}
@@ -221,7 +222,7 @@ func TestWriterStatesAtStartAndOnAsksAndRecovers(t *testing.T) {
r.state.Put("module.old.k", finding("old", "x"))
r.state.Put("module.wait.k", finding("wait", "x"))
first := r.now
r.state.Put("module.a.k", func() views.FindingBody { f := finding("a", "before"); f.Since = first.Add(-time.Hour); return f }())
r.state.Put("module.a.k", func() *testFinding { f := finding("a", "before"); f.SinceVal = first.Add(-time.Hour); return f }())
calls := 0
r.observe = func(context.Context) (views.Observed, error) {
calls++
@@ -239,12 +240,12 @@ func TestWriterStatesAtStartAndOnAsksAndRecovers(t *testing.T) {
if calls != 3 || len(r.waits) != 2 || r.waits[0] != views.ObserveRetryEvery {
t.Fatalf("observe called %d times, waits %v", calls, r.waits)
}
var st views.FindingsStated
var st testStated
if err := json.Unmarshal(stated.data, &st); err != nil {
t.Fatal(err)
}
if stated.subject != subjects.Stated || st.Asked != "" || len(st.Findings) != 1 || st.Findings[0].Summary != "now" ||
!st.Findings[0].Since.Equal(first.Add(-time.Hour)) || len(st.Pending) != 1 || st.Pending[0].Key != "module.wait.k" {
if stated.subject != subjects.Stated || st.Asked != "" || len(st.Findings) != 1 || st.Findings[0].SummaryVal != "now" ||
!st.Findings[0].SinceVal.Equal(first.Add(-time.Hour)) || len(st.Pending) != 1 || st.Pending[0].Key != "module.wait.k" {
t.Fatalf("statement: %s", stated.data)
}
keys, _ := r.state.Keys()
@@ -265,8 +266,8 @@ func TestWriterStatesAtStartAndOnAsksAndRecovers(t *testing.T) {
}
}
func FindingsStatedOf(t *testing.T, e said) views.FindingsStated {
var st views.FindingsStated
func FindingsStatedOf(t *testing.T, e said) testStated {
var st testStated
if err := json.Unmarshal(e.data, &st); err != nil {
t.Fatal(err)
}
@@ -322,15 +323,16 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
ctx := context.Background()
r := newRigKinds(t, "k")
other := finding("x", "another bundle's")
other.Kind = "other"
r.state.m[other.Key()] = []byte(`{}`)
other.KindField = "other"
otherKey := views.FindingKey(other.ScopeField, other.IDField, other.KindField)
r.state.m[otherKey] = []byte(`{}`)
if err := r.w.Write(ctx, other); err == nil || !strings.Contains(err.Error(), `"other"`) {
t.Errorf("a write of another kind: %v", err)
}
if err := r.w.Clear(ctx, "module", "x", "other", "why"); err == nil || !strings.Contains(err.Error(), `"other"`) {
t.Errorf("a clear of another kind: %v", err)
}
for field, f := range map[string]views.FindingBody{"owner": {Owner: "Not A Form"}, "waiting-on": {WaitingOn: "x y"}} {
for field, f := range map[string]testFinding{"owner": {Owner: "Not A Form"}, "waiting-on": {WaitingOn: "x y"}} {
b := finding("o", "s")
b.Owner, b.WaitingOn = f.Owner, f.WaitingOn
if err := r.w.Write(ctx, b); err == nil || !strings.Contains(err.Error(), field) {
@@ -345,9 +347,7 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
}
}
// a views reader writes through the part's writer, and the statement keeps its open findings
rf := views.WriterFindings{Writer: r.w, Body: func(f views.Finding) views.FindingBody {
return views.FindingBody{Kind: f.Kind, Scope: "module", ID: f.ID, Severity: "warning", Summary: f.Summary, Source: "views"}
}}
rf := views.WriterFindings{Writer: r.w, New: bodies.NewFinding}
if err := rf.Put(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s", Summary: "waits"}); err != nil {
t.Fatal(err)
}
@@ -357,16 +357,16 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
if err := r.w.State(ctx, ""); err != nil {
t.Fatal(err)
}
if _, ok := r.state.m["module.m.o.s.view-waits"]; !ok {
if _, ok := r.state.m["module.m=2eo=2es.view-waits"]; !ok {
t.Errorf("the statement deleted the reader's finding: %v", r.state.m)
}
if _, ok := r.state.m[other.Key()]; !ok {
if _, ok := r.state.m[otherKey]; !ok {
t.Errorf("the statement deleted another bundle's key")
}
if _, ok := r.state.m["module.ok.k"]; ok {
t.Errorf("a key of its own kind neither stated nor pending stays")
}
var stated views.FindingsStated
var stated testStated
for _, e := range r.events() {
if e.subject == subjects.Stated {
_ = json.Unmarshal(e.data, &stated)
@@ -378,7 +378,55 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
if err := rf.Delete(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s"}); err != nil {
t.Fatal(err)
}
if _, ok := r.state.m["module.m.o.s.view-waits"]; ok {
if _, ok := r.state.m["module.m=2eo=2es.view-waits"]; ok {
t.Errorf("the reader's finding was not cleared")
}
}
// A views reader's four findings are laid out by mesh-sdk through the body's setters (design 47 G §687): the finding's
// kind, the scope module, the id escaped as a key's name is, a warning but urgent for view-too-large, the source views, the
// subject the module on its node; and CheckFinding refuses a finding breaking a rule before it is written, naming the field
// (§689).
func TestReaderFindingsAreLaidOutAndCheckFindingRefuses(t *testing.T) {
r := newRig(t)
rf := views.WriterFindings{Writer: r.w, New: bodies.NewFinding}
for _, kind := range views.ViewKinds {
if err := rf.Put(views.Finding{Kind: kind, ID: "o.s.main", Summary: "sum " + kind, Headline: "head"}); err != nil {
t.Fatal(err)
}
e, _ := r.state.Get("module.o=2es=2emain." + kind)
var got testFinding
if e == nil || json.Unmarshal(e.Value, &got) != nil {
t.Fatalf("%s not kept: %v", kind, r.state.m)
}
sev := "warning"
if kind == views.TooLargeKind {
sev = "urgent"
}
if got.KindField != kind || got.ScopeField != "module" || got.IDField != "o=2es=2emain" || got.SeverityVal != sev || got.SourceVal != "views" ||
got.SubjectVal.Scope != "module" || got.SubjectVal.ID != "m" || got.SubjectVal.Machine != "n1" || got.SummaryVal != "sum "+kind || got.HeadlineVal != "head" || got.ObserverVal != "m" {
t.Errorf("%s laid out as %+v", kind, got)
}
}
for name, c := range map[string]struct {
f *testFinding
want string
}{
"scope": {&testFinding{IDField: "a", KindField: "k"}, "scope"},
"id": {&testFinding{ScopeField: "module", KindField: "k"}, "id"},
"kind": {&testFinding{ScopeField: "module", IDField: "a"}, "kind"},
"dotted": {&testFinding{ScopeField: "module", IDField: "a", KindField: "k.x"}, "kind"},
"owner": {&testFinding{ScopeField: "module", IDField: "a", KindField: "k", Owner: "Not A Form"}, "owner"},
"waiting-on": {&testFinding{ScopeField: "module", IDField: "a", KindField: "k", WaitingOn: "x y"}, "waiting-on"},
} {
if err := views.CheckFinding(c.f); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v", name, err)
}
if err := r.w.Write(context.Background(), c.f); err == nil {
t.Errorf("%s: written", name)
}
}
if err := views.CheckFinding(finding("a", "s")); err != nil {
t.Error(err)
}
}