Every one of the 48 core failures of research 031 was found by a person looking; the mesh's answers carried the fact for whoever asked and told nobody. - The condition store (to-be 45 §2): mesh-controller_conditions, one key per open condition, written by compare-and-set so a person's silence and the watchdogs never lose each other's word; every transition kept ninety days in mesh-controller_condition-history and said as the seat's events condition-raised / condition-changed / condition-cleared (the condition at the top level, with event, at, change, why, show), offered again while the bus is away. Raised and cleared by observation only; a clearing reopened within ten minutes is the same condition with its count up, its silence kept. Verbs: conditions, conditions show, conditions silence (a hand act, at most a week), conditions history. - ADR 0224's provider standing is the first kind, provider-failing, held by the provider's events; the provider_standing table is no longer read or written (left in place: dropping it is the operator's word). - status leads with the open conditions, urgent first, and says all well only with none open; conditions it cannot read are said and not well. - The signals table compiled in, one watchdog loop over it every 30s: S1 heartbeat (3 intervals, asleep machines excepted, control node urgent after 30 min), S2 report after a send, S3 plan tier, S4 event loop deaf, S5 merge not acted, S6 ask lost, S7 call hung, S8 provider silent, S9 advisories, S10 self-check silent, S11 node tools silent, S13 stale refusals; S12, S14, S15 deferred with their reasons. A row that cannot see raises probe-failed and clears nothing. A test generated from the table suppresses each signal inside and past its bound. - The bus's advisories (maximum deliveries, a mesh consumer deleted) and the controller's own slow consumer and refused subjects, said in the mesh's words. - doctor: the probe registry D1-D10 (D5 deferred) and DW, every five minutes, each in thirty seconds; a probe that cannot run is never a pass. D1 validates with mesh-host's own validator. Every run ends with the doctor-heartbeat event mesh-watcher listens for. - The controller is granted its new buckets, events, the two advisories and $SRV.INFO; the node tools their tools-alive heartbeat. The streams and consumers the controller asserts and the ones D6/D7 expect are one derivation.
119 lines
4.7 KiB
Go
119 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A provider that keeps failing a consumer is a problem `status` names (novox/hq ADR 0224), kept as
|
|
// the condition store's first kind (to-be 45 §2). On 2026-10-05 the identity provider refused every
|
|
// consumer for a day and status called the mesh well (04-ISSUES/179): this is that day, told to the
|
|
// controller the way the provider now tells it.
|
|
func TestAProviderFailingAConsumerBreaksAllWellUntilItRecovers(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "idp", Version: "1",
|
|
Receives: map[string]string{"oidc-client": "/var/lib/mesh/idp/mesh.json"}})
|
|
if _, err := assign(ctx, open, "anchor", "idp"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kept := standings{keeper: func() *conditions.Keeper { return conditionsFrom }}
|
|
since := time.Now().Add(-23 * time.Hour)
|
|
failing := link.Standing{Module: "idp", Failing: true, Provider: "oidc-client", ProviderNode: "anchor",
|
|
Consumer: "mesh_laptop_dashboard", Node: "laptop", Class: "credentials-rejected",
|
|
Error: `Keycloak token request failed: 401 {"error":"invalid_grant"}`, Since: since, Attempts: 31000}
|
|
if _, err := kept.Stood(ctx, failing); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
asked, err := theThreeQuestions(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asked.well() {
|
|
t.Fatal("a mesh whose identity provider fails a consumer reads as well")
|
|
}
|
|
said := printed(t, func() error { return printStatus(asked) })
|
|
for _, want := range []string{"1 open condition(s)", "provider.idp.anchor.mesh_laptop_dashboard.failing",
|
|
"idp on anchor keeps failing mesh_laptop_dashboard on laptop", "credentials-rejected", "31000 attempt(s)"} {
|
|
if !strings.Contains(said, want) {
|
|
t.Fatalf("status does not say %q:\n%s", want, said)
|
|
}
|
|
}
|
|
if strings.Contains(said, "all doing what they were told") {
|
|
t.Fatalf("status said all well beside a failing provider:\n%s", said)
|
|
}
|
|
if !strings.HasPrefix(said, "1 open condition(s)") {
|
|
t.Fatalf("status does not lead with what is open:\n%s", said)
|
|
}
|
|
body, err := statusAsJSON(asked)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc struct {
|
|
Conditions []conditions.Condition `json:"conditions"`
|
|
Failing []conditions.Condition `json:"failing"`
|
|
}
|
|
if err := json.Unmarshal(body, &doc); err != nil || len(doc.Failing) != 1 || len(doc.Conditions) != 1 ||
|
|
!strings.Contains(doc.Failing[0].Evidence[0].Said, "invalid_grant") {
|
|
t.Fatalf("the document does not carry it: %v\n%s", err, body)
|
|
}
|
|
// Both machines' `node show` name it: where the provider runs, and where the consumer is.
|
|
for _, node := range []string{"anchor", "laptop"} {
|
|
shown := printed(t, func() error { return showNode(ctx, open.inventory, node) })
|
|
if !strings.Contains(shown, "open condition(s) about this machine") || !strings.Contains(shown, "mesh_laptop_dashboard") {
|
|
t.Fatalf("node show %s does not name it:\n%s", node, shown)
|
|
}
|
|
}
|
|
|
|
// Recovered: gone, and the mesh may be well again as far as this is concerned.
|
|
failing.Failing = false
|
|
if cleared, err := kept.Stood(ctx, failing); err != nil || !cleared {
|
|
t.Fatalf("%v %v", cleared, err)
|
|
}
|
|
asked, err = theThreeQuestions(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked.conditions) != 0 {
|
|
t.Fatalf("a recovered consumer is still named: %+v", asked.conditions)
|
|
}
|
|
}
|
|
|
|
// A provider no longer assigned where it ran has nothing running to fail anybody: its last word is
|
|
// cleared on the next look, said as resolved by the assignment.
|
|
func TestAnUnassignedProvidersLastWordIsCleared(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
kept := standings{keeper: func() *conditions.Keeper { return conditionsFrom }}
|
|
if _, err := kept.Stood(ctx, link.Standing{Module: "gone", Failing: true,
|
|
ProviderNode: "anchor", Consumer: "x", Since: time.Now()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all, err := conditionsFrom.Open(ctx)
|
|
if err != nil || len(all) != 1 {
|
|
t.Fatalf("%+v %v", all, err)
|
|
}
|
|
if err := unassignedProviders(ctx, open.inventory, conditionsFrom, all); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if all, _ := conditionsFrom.Open(ctx); len(all) != 0 {
|
|
t.Fatalf("%+v", all)
|
|
}
|
|
}
|
|
|
|
// **The store away holds a recovery** (ADR 0224 §3): a standing that cannot be kept is asked again.
|
|
func TestAStandingTheStoreCannotKeepIsAskedAgain(t *testing.T) {
|
|
kept := standings{keeper: func() *conditions.Keeper { return nil }}
|
|
if _, err := kept.Stood(t.Context(), link.Standing{Module: "idp", ProviderNode: "anchor", Consumer: "x"}); err == nil ||
|
|
!strings.Contains(err.Error(), link.ErrTryAgain.Error()) {
|
|
t.Fatalf("answered %v", err)
|
|
}
|
|
}
|