diff --git a/health-and-repair/go/clients/meshconditionsclient/client.go b/health-and-repair/go/clients/meshconditionsclient/client.go index 5a28c7b..8e3a014 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client.go +++ b/health-and-repair/go/clients/meshconditionsclient/client.go @@ -11,6 +11,7 @@ import ( "time" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-sdk/go/views" ) // The two severities, how soon the operator is needed. @@ -170,3 +171,44 @@ func SilencedAt(c seat.ConditionsByKeyValue, now time.Time) bool { func Escalated(c seat.ConditionsByKeyValue) bool { return c.Resolver == ResolverOperator && len(c.Tried) > 0 } + +// FindingVersion is the layout version of a finding's value. +const FindingVersion = 1 + +// FindingState is a part's own state `findings`, where the condition keeper reads what a part saw: the runtime's kept +// state (mesh-sdk's KeptState) satisfies it. +type FindingState interface { + Put(key string, value any) (uint64, error) + Delete(key string) error +} + +// FindingWriter writes and clears a part's findings of a module in its own state `findings`, in this seat's layout; it +// is mesh-sdk's views.FindingWriter. A finding is a warning about the module its ID names, and is checked +// (CheckFinding) before it is written. +type FindingWriter struct{ State FindingState } + +var _ views.FindingWriter = FindingWriter{} + +// FindingFor is the value and key of the seat's finding for f. +func FindingFor(f views.Finding) seat.FindingsByKeyValue { + at := f.At.UTC().Format(time.RFC3339Nano) + return seat.FindingsByKeyValue{V: FindingVersion, Key: Key(ScopeModule, f.ID, f.Kind), Kind: f.Kind, + Subject: seat.Subject{Scope: ScopeModule, ID: f.ID}, Severity: Warning, Summary: f.Summary, Headline: f.Headline, + Explanation: f.Explanation, Evidence: []seat.Evidence{{At: at, Said: f.Summary}}, Source: f.Observer + "." + f.Kind, + Observer: f.Observer, At: at} +} + +// Put writes the finding. +func (w FindingWriter) Put(f views.Finding) error { + v := FindingFor(f) + if err := CheckFinding(v); err != nil { + return err + } + _, err := w.State.Put(v.Key, v) + return err +} + +// Delete clears the finding. +func (w FindingWriter) Delete(f views.Finding) error { + return w.State.Delete(Key(ScopeModule, f.ID, f.Kind)) +} diff --git a/health-and-repair/go/clients/meshconditionsclient/client_test.go b/health-and-repair/go/clients/meshconditionsclient/client_test.go index 7baef69..746368a 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client_test.go +++ b/health-and-repair/go/clients/meshconditionsclient/client_test.go @@ -6,6 +6,7 @@ import ( "time" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-sdk/go/views" ) // ADR 0564 clause 3: the key rule, the checks of a finding, the order and the silence a person reads conditions by. @@ -60,3 +61,41 @@ func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) { t.Errorf("the seat's addresses: %+v", a) } } + +type fakeFindings struct { + put map[string]any + deleted []string +} + +func (f *fakeFindings) Put(key string, value any) (uint64, error) { + if f.put == nil { + f.put = map[string]any{} + } + f.put[key] = value + return 1, nil +} +func (f *fakeFindings) Delete(key string) error { f.deleted = append(f.deleted, key); return nil } + +func TestTheFindingWriterWritesAndClearsAModulesFindingInTheSeatsLayout(t *testing.T) { + state := &fakeFindings{} + var w views.FindingWriter = FindingWriter{State: state} + f := views.Finding{Observer: "mesh-config", Kind: "unanswered", ID: "mesh-config.mesh-vault.secrets", Summary: "no first view", + Headline: "waits", Explanation: "look", At: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)} + if err := w.Put(f); err != nil { + t.Fatal(err) + } + key := "module.mesh-config.mesh-vault.secrets.unanswered" + v, ok := state.put[key].(seat.FindingsByKeyValue) + if !ok || v.Key != key || v.Severity != Warning || v.Source != "mesh-config.unanswered" || v.Subject.Scope != ScopeModule || len(v.Evidence) != 1 { + t.Fatalf("written %#v", state.put) + } + if err := CheckFinding(v); err != nil { + t.Error(err) + } + if err := w.Delete(f); err != nil || len(state.deleted) != 1 || state.deleted[0] != key { + t.Errorf("deleted %v: %v", state.deleted, err) + } + if err := w.Put(views.Finding{Observer: "x", Kind: "k", ID: "i"}); err == nil { + t.Error("a finding that says nothing was written") + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod index c29f214..f2409b8 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.mod +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -3,3 +3,5 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshcondit go 1.26.0 require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef diff --git a/health-and-repair/go/clients/meshhandactsclient/client.go b/health-and-repair/go/clients/meshhandactsclient/client.go index e9fbaa7..fd7fc97 100644 --- a/health-and-repair/go/clients/meshhandactsclient/client.go +++ b/health-and-repair/go/clients/meshhandactsclient/client.go @@ -10,6 +10,7 @@ import ( "strings" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" + "git.novox.be/novox/mesh-sdk/go/views" ) // The verbs an act is recorded under in the log, and the causes and kinds that are no repair. @@ -71,6 +72,15 @@ func (r Recorder) Record(q Request) (Taken, error) { return r.record(seat.VerbHandAct, body) } +// RecordHandAct records a part's act before it does it, as mesh-sdk's views.HandActs asks: the recorder is the +// interface's implementation, and an act the seat does not take is the error that says nothing was recorded. +func (r Recorder) RecordHandAct(a views.HandAct) error { + _, err := r.Record(Request{What: a.What, Why: a.Why, Cause: a.Cause, Verb: a.Verb}) + return err +} + +var _ views.HandActs = Recorder{} + // RecordWarrant records what the asking module did on the operator's warrant for its ask. func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) { return r.record(seat.VerbWarranted, map[string]any{"asker": asker, "ask": ask}) diff --git a/health-and-repair/go/clients/meshhandactsclient/client_test.go b/health-and-repair/go/clients/meshhandactsclient/client_test.go index a85870e..a4ac41d 100644 --- a/health-and-repair/go/clients/meshhandactsclient/client_test.go +++ b/health-and-repair/go/clients/meshhandactsclient/client_test.go @@ -3,11 +3,13 @@ package meshhandactsclient import ( "encoding/json" "errors" + "fmt" "reflect" "strings" "testing" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" + "git.novox.be/novox/mesh-sdk/go/views" ) // ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4, L5b): an act reads the controller's entries unchanged, and a @@ -71,3 +73,21 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { t.Error("an act with no why was recorded") } } + +func TestTheRecorderIsMeshSDKsHandActRecorder(t *testing.T) { + var asked []string + r := Recorder{Ask: func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key+" "+fmt.Sprint(body)) + return nil, nil + }} + var acts views.HandActs = r + if err := acts.RecordHandAct(views.HandAct{What: "asked a view again", Why: "it is stale", Cause: "stale"}); err != nil { + t.Fatal(err) + } + if len(asked) != 1 || !strings.HasPrefix(asked[0], "seat:mesh-hand-acts.hand-act ") || !strings.Contains(asked[0], "asked a view again") { + t.Errorf("asked %v", asked) + } + if err := acts.RecordHandAct(views.HandAct{What: "x"}); err == nil { + t.Error("an act with no why was taken") + } +} diff --git a/health-and-repair/go/clients/meshhandactsclient/go.mod b/health-and-repair/go/clients/meshhandactsclient/go.mod index 75a57a4..67224be 100644 --- a/health-and-repair/go/clients/meshhandactsclient/go.mod +++ b/health-and-repair/go/clients/meshhandactsclient/go.mod @@ -3,3 +3,5 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandac go 1.26.0 require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-20261105000000-000000000000 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef