diff --git a/health-and-repair/go/clients/meshconditionsclient/callunanswered.go b/health-and-repair/go/clients/meshconditionsclient/callunanswered.go new file mode 100644 index 0000000..e3520c1 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/callunanswered.go @@ -0,0 +1,11 @@ +package meshconditionsclient + +import "time" + +// KindCallUnanswered is the kind of a kept call still unanswered past CallUnansweredAfter (novox/hq ADR 0668 §3, issue +// 1966): a call whose answer the caller keeps, asked again through every transport fault, that no answer has ended. The +// caller writes it into its own findings, keeps the wait, and clears it when the call is answered. +const KindCallUnanswered = "call-unanswered" + +// CallUnansweredAfter is how long a kept call stays unanswered before it is a finding (ADR 0668 §3; ADR 0564 §2's bound). +const CallUnansweredAfter = 5 * time.Minute diff --git a/health-and-repair/go/clients/meshconditionsclient/client.go b/health-and-repair/go/clients/meshconditionsclient/client.go index 2931dc8..c2fbc08 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client.go +++ b/health-and-repair/go/clients/meshconditionsclient/client.go @@ -1,17 +1,15 @@ // Package meshconditionsclient is the logic every observer, healer and reader of conditions shares over the seat -// mesh-conditions (novox/hq ADR 0564, ADR 0634 §4): how a finding's key is made and checked, the scopes and the bounds, +// mesh-conditions (novox/hq ADR 0564, ADR 0634 §4): how a finding's key is made, the scopes and the bounds, // the order a person reads conditions in, and the answers a notification offers. The layouts are the seat's contract's. package meshconditionsclient import ( - "fmt" "regexp" "sort" "strings" "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. @@ -111,39 +109,6 @@ func SilenceAction(key string) seat.Action { Arguments: map[string]string{"key": key, "for": "7d", "why": "", "cause": CauseOperatorAnswer}} } -// CheckFinding refuses a finding the keeper could not make a condition of, naming why. -func CheckFinding(f seat.FindingsByKeyValue) error { - known := false - for _, s := range Scopes { - known = known || s == f.Subject.Scope - } - token := f.Ref - if token == "" { - token = f.Kind - } - switch { - case !known: - return fmt.Errorf("health: a finding's scope is one of %s, not %q", strings.Join(Scopes, ", "), f.Subject.Scope) - case strings.TrimSpace(f.Subject.ID) == "": - return fmt.Errorf("health: a %s finding names what it is about", f.Subject.Scope) - case strings.TrimSpace(f.Kind) == "": - return fmt.Errorf("health: the finding %s has no kind", f.Key) - case f.Key != Key(f.Subject.Scope, f.Subject.ID, token): - return fmt.Errorf("health: the finding's key %q is not %q, its scope, id and kind", f.Key, Key(f.Subject.Scope, f.Subject.ID, token)) - case f.Severity != Urgent && f.Severity != Warning: - return fmt.Errorf("health: the finding %s is urgent or a warning, not %q", f.Key, f.Severity) - case strings.TrimSpace(f.Summary) == "": - return fmt.Errorf("health: the finding %s says nothing", f.Key) - case strings.TrimSpace(f.Source) == "": - return fmt.Errorf("health: the finding %s does not say what saw it", f.Key) - case strings.TrimSpace(f.Observer) == "": - return fmt.Errorf("health: the finding %s does not name its observer", f.Key) - case len(f.Evidence) > KeptEvidence: - return fmt.Errorf("health: the finding %s keeps %d observations, at most %d", f.Key, len(f.Evidence), KeptEvidence) - } - return nil -} - // Order sorts conditions as a person reads them: urgent first, then oldest raised, then by key. func Order(list []seat.ConditionsByKeyValue) { raised := func(c seat.ConditionsByKeyValue) time.Time { return c.Raised } @@ -171,44 +136,3 @@ 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() - 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 f351fd2..f377f6d 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client_test.go +++ b/health-and-repair/go/clients/meshconditionsclient/client_test.go @@ -1,14 +1,10 @@ package meshconditionsclient import ( - "os" - "reflect" - "strings" "testing" "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. @@ -19,33 +15,8 @@ func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) { if k := Key("call", "a b", ""); k != "call.a_b._" { t.Errorf("an unsafe part and an empty token: %s", k) } - // Issue 1966, ADR 0668 §3: a kept call unanswered is a finding the keeper can make a condition of, keyed by what waits. - o := CallUnanswered("ace", "seat:node-engine.manifest-reads", "ace", "no responders", "enginereads", time.Unix(0, 0)) - if f := o.Finding("mesh-delivery-assigner", "r1", time.Unix(300, 0)); o.Key() != "call.ace.call-unanswered" || CheckFinding(f) != nil { - t.Errorf("a call unanswered: key %s, %v", o.Key(), CheckFinding(f)) - } - good := seat.FindingsByKeyValue{Key: "machine.g14.silent", Kind: "silent", Severity: Warning, Summary: "x", Source: "S1", - Observer: "mesh-delivery-watchman", Subject: seat.Subject{Scope: ScopeMachine, ID: "g14"}} - for _, c := range []struct { - name string - change func(*seat.FindingsByKeyValue) - refuse string - }{ - {"a good finding", func(*seat.FindingsByKeyValue) {}, ""}, - {"an unknown scope", func(f *seat.FindingsByKeyValue) { f.Subject.Scope = "moon" }, "scope is one of"}, - {"a key that is not its subject's", func(f *seat.FindingsByKeyValue) { f.Key = "machine.g14.other" }, "is not"}, - {"a reference as the key's last part", func(f *seat.FindingsByKeyValue) { f.Ref = "gone"; f.Key = "machine.g14.gone" }, ""}, - {"no summary", func(f *seat.FindingsByKeyValue) { f.Summary = " " }, "says nothing"}, - {"no observer", func(f *seat.FindingsByKeyValue) { f.Observer = "" }, "observer"}, - {"a severity of neither kind", func(f *seat.FindingsByKeyValue) { f.Severity = "info" }, "urgent or a warning"}, - {"too much evidence", func(f *seat.FindingsByKeyValue) { f.Evidence = make([]seat.Evidence, KeptEvidence+1) }, "keeps 11"}, - } { - f := good - c.change(&f) - err := CheckFinding(f) - if (c.refuse == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.refuse)) { - t.Errorf("%s: %v", c.name, err) - } + if KindCallUnanswered != "call-unanswered" || CallUnansweredAfter != 5*time.Minute { + t.Error("the kept call's finding kind and bound") } list := []seat.ConditionsByKeyValue{ {Key: "b", Severity: Warning, Raised: when("2026-11-01T10:00:00Z")}, @@ -69,44 +40,6 @@ func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) { } } -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") - } -} - func when(s string) time.Time { t, err := time.Parse(time.RFC3339Nano, s) if err != nil { @@ -116,85 +49,3 @@ func when(s string) time.Time { } func ptr(t time.Time) *time.Time { return &t } - -type fakeBucket struct { - kept map[string][]byte - puts, dels int -} - -func (b *fakeBucket) Get(key string) ([]byte, bool, error) { v, ok := b.kept[key]; return v, ok, nil } -func (b *fakeBucket) Put(key string, v []byte) error { - if b.kept == nil { - b.kept = map[string][]byte{} - } - b.kept[key] = v - b.puts++ - return nil -} -func (b *fakeBucket) Delete(key string) error { delete(b.kept, key); b.dels++; return nil } - -// The goldens are what mesh-sdk's go/health EncodeFinding wrote for the same findings: the helpers write the same -// values, read by the seat's strict decoder. -func TestAnObservationIsWrittenAsMeshSDKsHealthWroteItsFinding(t *testing.T) { - at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) - for _, c := range []struct { - golden string - o Observation - run string - }{ - {"finding_full.json", Observation{Scope: ScopeMachine, ID: "g14", Kind: "silent-kind", Ref: "silent", Machine: "g14", Also: []string{"ace"}, - Severity: Urgent, Summary: "g14 is silent", Said: "no pulse", Headline: "g14 is silent", Explanation: "It said nothing.", - Resolved: "It speaks.", Needs: "look", Source: "S1", Resolver: "operator"}, "r1"}, - {"finding_minimal.json", Observation{Scope: ScopeModule, ID: "nats.g14", Kind: "build-failed", Summary: "failed", - Source: "builder.build-failed"}, ""}, - } { - raw, err := os.ReadFile("testdata/" + c.golden) - if err != nil { - t.Fatal(err) - } - want, err := seat.DecodeFindingsByKeyValue(raw) - if err != nil { - t.Fatalf("%s: %v", c.golden, err) - } - observer := want.Observer - got, err := EncodeFinding(c.o.Finding(observer, c.run, at)) - if err != nil { - t.Fatalf("%s: %v", c.golden, err) - } - have, err := seat.DecodeFindingsByKeyValue(got) - if err != nil || !reflect.DeepEqual(have, want) { - t.Errorf("%s: wrote %s, want %s (%v)", c.golden, got, raw, err) - } - if back, err := DecodeFinding(raw); err != nil || !reflect.DeepEqual(back, want) { - t.Errorf("%s: read back %v", c.golden, err) - } - } -} - -func TestRaiseWritesWhenTheWordsChangeAndClearDeletesWhatIsThere(t *testing.T) { - b := &fakeBucket{} - o := Observation{Scope: ScopeModule, ID: "m", Kind: "k", Summary: "one", Source: "x.k"} - at := when("2026-11-05T12:00:00Z") - if w, err := Raise(b, "obs", "", o, at); err != nil || !w { - t.Fatalf("first: %v %v", w, err) - } - if w, err := Raise(b, "obs", "r2", o, at.Add(time.Hour)); err != nil || w || b.puts != 1 { - t.Fatalf("the same words later: wrote %v, %d puts, %v", w, b.puts, err) - } - o.Summary = "two" - if w, err := Raise(b, "obs", "", o, at); err != nil || !w || b.puts != 2 { - t.Fatalf("changed: %v %d %v", w, b.puts, err) - } - if _, err := Raise(b, "obs", "", Observation{Scope: "moon", ID: "m", Kind: "k"}, at); err == nil || b.puts != 2 { - t.Errorf("a finding the keeper cannot use was written: %v", err) - } - if c, err := Clear(b, ScopeModule, "m", "k"); err != nil || !c || b.dels != 1 { - t.Fatalf("clear: %v %v %d", c, err, b.dels) - } - if c, err := Clear(b, ScopeModule, "m", "k"); err != nil || c || b.dels != 1 { - t.Errorf("clearing what is not there: %v %v %d", c, err, b.dels) - } - if FindingsBucket("obs") != "obs_findings" { - t.Error(FindingsBucket("obs")) - } -} diff --git a/health-and-repair/go/clients/meshconditionsclient/heard/heard.go b/health-and-repair/go/clients/meshconditionsclient/heard/heard.go index e00d535..da9fbca 100644 --- a/health-and-repair/go/clients/meshconditionsclient/heard/heard.go +++ b/health-and-repair/go/clients/meshconditionsclient/heard/heard.go @@ -13,7 +13,7 @@ import ( "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" - "git.novox.be/novox/mesh-sdk/go/views" + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" ) // Kind is the finding's kind. @@ -43,16 +43,18 @@ 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, in the layout of the seat mesh-conditions' state findings. +// Finding is observer's finding that it does not hear family, as the seat mesh-conditions' generated type +// (design 47 G 689): its key and its value. The check of its rules is mesh-sdk's writer's, which refuses a finding +// breaking them before it writes. func Finding(observer, family, consequence string, why error, at time.Time) (string, []byte, error) { summary, headline, explanation := Words(observer, family, consequence, why) - 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 - } + at = at.UTC() + v := seat.Finding{Scope: meshconditionsclient.ScopeModule, ID: ID(observer, family), Kind: Kind, + Subject: seat.Subject{Scope: meshconditionsclient.ScopeModule, ID: ID(observer, family)}, + Severity: meshconditionsclient.Warning, Summary: summary, Headline: headline, Explanation: explanation, + Evidence: []seat.Evidence{{At: at, Said: summary}}, Source: observer + "." + Kind, Observer: observer, At: at, Since: at} value, err := json.Marshal(v) - return v.Key, value, err + return Key(observer, family), value, err } // Writer writes and deletes the finding in the observer's findings state. Put or Delete nil writes nothing, said in the diff --git a/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go b/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go index cc9b69a..9beb0e9 100644 --- a/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go +++ b/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" "time" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" ) // A consumer that does not bind writes the finding, with why, under the observer's findings key; the one that binds @@ -22,6 +24,11 @@ func TestBindWritesTheFindingUntilTheConsumerBinds(t *testing.T) { if err := w.Bind("placement", "the first view is the only picture", func() error { return errors.New("no consumer") }); err == nil { t.Fatal("a failed bind was not answered") } + // Design 47 G 689: the value is the seat's generated finding, read by its strict decoder, key and subject by the rule. + if f, err := seat.DecodeFindingsByScopeIDKindValue(puts[key]); err != nil || f.Observer != "mesh-x" || f.Kind != Kind || + f.Scope != "module" || f.ID != "mesh-x.placement" || f.Subject.ID != f.ID || f.Severity != "warning" || f.Source == "" { + t.Errorf("the finding is not the seat's generated one: %+v (%v)", f, err) + } if v := string(puts[key]); !strings.Contains(v, "no consumer") || !strings.Contains(v, "mesh-x does not hear placement") { t.Errorf("the finding is %q", v) } diff --git a/health-and-repair/go/clients/meshconditionsclient/observation.go b/health-and-repair/go/clients/meshconditionsclient/observation.go deleted file mode 100644 index 9af47d6..0000000 --- a/health-and-repair/go/clients/meshconditionsclient/observation.go +++ /dev/null @@ -1,168 +0,0 @@ -package meshconditionsclient - -import ( - "encoding/json" - "fmt" - "time" - - seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" -) - -// What an observer says it sees wrong, raised and cleared as a finding in its own state `findings` (novox/hq ADR 0564 -// clause 2, ADR 0634 §4): one write shared by every observer (the self-check, the healers, the watchman, the builder, -// the indexer, the node-engine), in this seat's layout. The condition keeper raises and clears the conditions from them. - -// FindingsState is the name of an observer's state of findings. -const FindingsState = "findings" - -// FindingsBucket is the bucket of an observer's findings, `_findings`. -func FindingsBucket(observer string) string { return observer + "_" + FindingsState } - -// Observation is one thing an observer sees wrong now: what it is about (Scope, ID, Machine, Also), which kind of fault -// (Kind; Ref when the key's last part is not the kind), and its words. -type Observation struct { - Scope, ID, Kind, Ref, Machine string - Also []string - // Severity is Urgent or Warning; Warning when empty. - Severity string - // Summary is one line; Said the one observation kept as evidence (the summary when empty). - Summary, Said string - Headline, Explanation, Resolved, Needs string - // Source is the row, probe or event that saw it. - Source string - // Resolver is who resolves it, self when empty. - Resolver string - Actions []seat.Action - // Since is when the finding is stamped; the zero value leaves it to the caller's moment. - Since time.Time -} - -// Key is the finding's key: `..`, the token Ref or else Kind. -func (o Observation) Key() string { - token := o.Ref - if token == "" { - token = o.Kind - } - return Key(o.Scope, o.ID, token) -} - -// Finding is the observation as the seat's finding written by observer in run, at the moment at. -func (o Observation) Finding(observer, run string, at time.Time) seat.FindingsByKeyValue { - at = at.UTC() - said := o.Said - if said == "" { - said = o.Summary - } - sev := o.Severity - if sev == "" { - sev = Warning - } - f := seat.FindingsByKeyValue{V: FindingVersion, Key: o.Key(), Kind: o.Kind, - Subject: seat.Subject{Scope: o.Scope, ID: o.ID, Machine: o.Machine, Also: o.Also}, - Severity: sev, Summary: o.Summary, Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, - Needs: o.Needs, Actions: o.Actions, Evidence: []seat.Evidence{{At: at, Said: said}}, Source: o.Source, - Resolver: o.Resolver, Observer: observer, Run: run, At: at} - if o.Ref != "" && o.Ref != o.Kind { - f.Ref = o.Ref - } - return f -} - -// EncodeFinding is f as JSON at FindingVersion; it refuses what CheckFinding refuses. -func EncodeFinding(f seat.FindingsByKeyValue) ([]byte, error) { - f.V = FindingVersion - if f.Evidence == nil { - f.Evidence = []seat.Evidence{} - } - if err := CheckFinding(f); err != nil { - return nil, err - } - return json.Marshal(f) -} - -// DecodeFinding reads a finding's value; it refuses another version and what CheckFinding refuses. A finding written -// before the field was named `ref` (novox/hq issue 1952) is read by its old name, `token`. -func DecodeFinding(data []byte) (seat.FindingsByKeyValue, error) { - f, err := seat.DecodeFindingsByKeyValue(data) - if err != nil { - return seat.FindingsByKeyValue{}, fmt.Errorf("health: a finding is not readable: %w", err) - } - if f.V != FindingVersion { - return seat.FindingsByKeyValue{}, fmt.Errorf("health: a finding of version %d, and this reads %d", f.V, FindingVersion) - } - if err := CheckFinding(f); err != nil { - return seat.FindingsByKeyValue{}, err - } - return f, nil -} - -// Says is what a finding says, the moments and the run that wrote it aside: two findings that say the same are one -// write (novox/hq ADR 0289 §1), so the condition moves only when the words do. -func Says(f seat.FindingsByKeyValue) string { - f.At, f.Run, f.V = time.Time{}, "", 0 - ev := make([]seat.Evidence, len(f.Evidence)) - for i, e := range f.Evidence { - ev[i] = seat.Evidence{Said: e.Said} - } - f.Evidence = ev - raw, _ := json.Marshal(f) - return string(raw) -} - -// FindingBucket is the part of an observer's state `findings` that Raise and Clear use. -type FindingBucket interface { - // Get answers the value at key; found is false when there is none. - Get(key string) (value []byte, found bool, err error) - Put(key string, value []byte) error - Delete(key string) error -} - -// Raise writes the observation as observer's finding in run at at, only when what it says differs from the finding kept -// (so the keeper's condition moves only then); wrote says whether it wrote. A finding the keeper could not make a -// condition of is refused, nothing written. -func Raise(b FindingBucket, observer, run string, o Observation, at time.Time) (wrote bool, err error) { - f := o.Finding(observer, run, at) - value, err := EncodeFinding(f) - if err != nil { - return false, err - } - if kept, found, err := b.Get(f.Key); err != nil { - return false, err - } else if found { - if old, err := DecodeFinding(kept); err == nil && Says(old) == Says(f) { - return false, nil - } - } - return true, b.Put(f.Key, value) -} - -// Clear deletes the finding about scope and id of kind (its token); one not there is nothing to clear, and cleared says -// whether one was. -func Clear(b FindingBucket, scope, id, token string) (cleared bool, err error) { - key := Key(scope, id, token) - if _, found, err := b.Get(key); err != nil || !found { - return false, err - } - return true, b.Delete(key) -} - -// KindCallUnanswered is the kind of a kept call still unanswered past CallUnansweredAfter (novox/hq ADR 0668 §3, issue -// 1966): a call whose answer the caller keeps, asked again through every transport fault, that no answer has ended. The -// caller writes it into its own findings, keeps the wait, and clears it when the call is answered. -const KindCallUnanswered = "call-unanswered" - -// CallUnansweredAfter is how long a kept call stays unanswered before it is a finding (ADR 0668 §3; ADR 0564 §2's bound). -const CallUnansweredAfter = 5 * time.Minute - -// CallUnanswered is the observation of a kept call to callee (a seat verb's or a tool's key, as process.Conn.Call names -// it) about id (what the caller waits on: a node, a merge, a key), unanswered since since, its last fault said. Its key -// is `call..call-unanswered`, cleared by Clear(b, ScopeCall, id, KindCallUnanswered). -func CallUnanswered(id, callee, machine, fault, source string, since time.Time) Observation { - return Observation{Scope: ScopeCall, ID: id, Kind: KindCallUnanswered, Machine: machine, Severity: Warning, - Summary: fmt.Sprintf("the call %s about %s is unanswered since %s", callee, id, since.UTC().Format(time.RFC3339)), - Said: fault, - Headline: "A call whose answer is kept is not answered", - Explanation: fmt.Sprintf("The call %s is made again on every transport fault and on the callee's event; it has "+ - "not been answered since %s. Its last fault: %s.", callee, since.UTC().Format(time.RFC3339), fault), - Resolved: "the call is answered", Source: source} -} diff --git a/health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json b/health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json deleted file mode 100644 index 54bf2f8..0000000 --- a/health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json +++ /dev/null @@ -1 +0,0 @@ -{"v":1,"key":"machine.g14.silent","kind":"silent-kind","ref":"silent","subject":{"scope":"machine","id":"g14","machine":"g14","also":["ace"]},"severity":"urgent","summary":"g14 is silent","headline":"g14 is silent","explanation":"It said nothing.","resolved":"It speaks.","needs":"look","evidence":[{"at":"2026-11-05T12:00:00Z","said":"no pulse"}],"source":"S1","resolver":"operator","observer":"mesh-delivery-watchman","run":"r1","at":"2026-11-05T12:00:00Z"} diff --git a/health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json b/health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json deleted file mode 100644 index e0fef12..0000000 --- a/health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json +++ /dev/null @@ -1 +0,0 @@ -{"v":1,"key":"module.nats.g14.build-failed","kind":"build-failed","subject":{"scope":"module","id":"nats.g14"},"severity":"warning","summary":"failed","headline":"","explanation":"","resolved":"","evidence":[{"at":"2026-11-05T12:00:00Z","said":"failed"}],"source":"builder.build-failed","observer":"mesh-delivery-builder","at":"2026-11-05T12:00:00Z"}