From d7b53bf2f16ddeb7df4bb58552120aa0b368f4dc Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 22:46:17 +0100 Subject: [PATCH] D1 (watchman): the watchman names its own module from its credential's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its finding observer, its dead-letter consumer, its reader's module and its tools' first views take the credential's module (the tools: the module the runtime serves them as) instead of constants of its name; a guard test finds none in its code. Builds: design 47 G A-296, §428 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-watchman-tools/main.go | 2 +- .../integration_test.go | 20 +++++------ .../cmd/mesh-delivery-watchman/main.go | 4 +-- .../cmd/mesh-delivery-watchman/main_test.go | 12 +++---- .../mesh-delivery-watchman/ownmodule_test.go | 4 +++ .../cmd/mesh-delivery-watchman/signals.go | 4 ++- .../internal/deadletters/deadletters.go | 11 +++--- .../internal/watch/finding.go | 17 ++++------ .../internal/watch/findings_test.go | 10 +++--- .../internal/watch/names.go | 1 - .../internal/watch/watch.go | 3 +- .../mesh-delivery-watchman/manifest_test.go | 2 +- .../mesh-delivery-watchman/ownmodule_test.go | 4 +++ .../mesh-delivery-watchman/ownname_test.go | 34 +++++++++++++++++++ 14 files changed, 82 insertions(+), 46 deletions(-) create mode 100644 modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/ownmodule_test.go create mode 100644 modules/mesh-delivery-watchman/ownmodule_test.go create mode 100644 modules/mesh-delivery-watchman/ownname_test.go diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go index 7befa24c..a66d18d5 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman-tools/main.go @@ -209,7 +209,7 @@ func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, err Description: "Which part waits for which view, on which machine and since when (design 47 G §83): every open condition of the " + "kind view-waits the condition keeper holds, which each part writes when it has waited for a view past its bound. A view " + "a part holds is not listed. Ask the part's _first_view_again to ask a view again. Read-only."}, - }, views.StatusTools(watch.Observer, ToolFirstViews, ToolFirstViewAgain, stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) + }, views.StatusTools(os.Getenv(stdio.ServedAs), ToolFirstViews, ToolFirstViewAgain, stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) } // waitingView is one part waiting for one view, as watchman_views answers it. diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go index f63192ef..794afb0b 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go @@ -67,7 +67,7 @@ accounts { // families (DLV-M5, ADR 0615): the watchman (n1) follows both, never their buckets. func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) { t.Helper() - viewstest.Serve(t, ctx, js, nc, watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants", "n1_"+watch.WatchmanModule) + viewstest.Serve(t, ctx, js, nc, watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants", "n1_"+watchmanModule) } @@ -95,12 +95,12 @@ func sayAsk(t *testing.T, ctx context.Context, js jetstream.JetStream, event, id // own and firstViewsBucket are the beds' watchman's own buckets, named from the module its credential names (A-296). var ( - own = watch.BucketsOf(watch.WatchmanModule) - firstViewsBucket = watch.WatchmanModule + "_" + views.StatusState + own = watch.BucketsOf(watchmanModule) + firstViewsBucket = watchmanModule + "_" + views.StatusState ) // findingEventsSubject is the subject the stand-in seat mesh-broker's show answers for the watchman's finding-events. -const findingEventsSubject = "mesh.bus." + watch.WatchmanModule + ".finding-events" +const findingEventsSubject = "mesh.bus." + watchmanModule + ".finding-events" // findingsStream is the watchman's own stream of its findings' events, its requires entry finding-events (design 47 G // §383, §529), which its finding writer says into under the names a stand-in of the seat mesh-broker's show answers (A-274). @@ -172,7 +172,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 {Bucket: own.Stuck, History: 20}, {Bucket: own.Pulse, LimitMarkerTTL: time.Second}, {Bucket: watch.AssignerWantsBucket, History: 20}, - {Bucket: watch.FindingsBucket, History: 5}, + {Bucket: (watchmanModule + "_findings"), History: 5}, {Bucket: firstViewsBucket, History: 1}, } { kv, err := js.CreateKeyValue(ctx, c) @@ -315,7 +315,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 } // 1952/WM4: what stopped moving is the watchman's own finding: the ask no node ended, and node n1 silent. - finding := func(key string) bool { _, ok := has(watch.FindingsBucket, key); return ok } + finding := func(key string) bool { _, ok := has((watchmanModule + "_findings"), key); return ok } until("the stuck ask's finding", func() bool { return finding(watch.FindingKey(watch.ScopeDelivery, stuckOf(askStream, ask1), watch.KindStuck)) }) @@ -334,14 +334,14 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 // wait; the consumer removed, it makes its own and keeps a notice with its headers. waits := watch.FindingKey(watch.ScopeBus, "DEAD_LETTER_NOTICES.controller", watch.KindNoticesWaiting) until("the notices said waiting behind the controller's consumer", func() bool { return finding(waits) }) - if _, err := js.Consumer(ctx, buscompose.DeadLetterNoticesStream, deadletters.Consumer); err == nil { + if _, err := js.Consumer(ctx, buscompose.DeadLetterNoticesStream, watchmanModule); err == nil { t.Fatal("the watchman made its consumer while the controller's stands") } if err := js.DeleteConsumer(ctx, buscompose.DeadLetterNoticesStream, buscompose.ControllerName); err != nil { t.Fatal(err) } until("the watchman's own consumer on the notices", func() bool { - _, err := js.Consumer(ctx, buscompose.DeadLetterNoticesStream, deadletters.Consumer) + _, err := js.Consumer(ctx, buscompose.DeadLetterNoticesStream, watchmanModule) return err == nil }) until("the wait no longer said", func() bool { return !finding(waits) }) @@ -420,7 +420,7 @@ func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 {Bucket: own.Stuck, History: 20}, {Bucket: own.Pulse, LimitMarkerTTL: time.Second}, {Bucket: watch.AssignerWantsBucket, History: 20}, - {Bucket: watch.FindingsBucket, History: 5}, + {Bucket: (watchmanModule + "_findings"), History: 5}, {Bucket: firstViewsBucket, History: 1}, } { kv, err := js.CreateKeyValue(ctx, c) @@ -501,7 +501,7 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { {Bucket: own.Stuck, History: 20}, {Bucket: own.Pulse, LimitMarkerTTL: time.Second}, {Bucket: watch.AssignerWantsBucket, History: 20}, - {Bucket: watch.FindingsBucket, History: 5}, + {Bucket: (watchmanModule + "_findings"), History: 5}, {Bucket: firstViewsBucket, History: 1}, } { kv, err := js.CreateKeyValue(ctx, c) diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index f8fbeb4c..efc8e1bc 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -255,7 +255,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return fmt.Errorf("the watchman's finding writer: %w", err) } defer held.w.Close() - reader := &views.ProcessReader{Module: watch.WatchmanModule, Call: calls.Call, Logf: logger.Printf, Hub: hub, + reader := &views.ProcessReader{Module: cred.Module, Call: calls.Call, Logf: logger.Printf, Hub: hub, Findings: func() views.WireWriter { return held.w }} wants, err := reader.Follow(watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants") if err != nil { @@ -369,7 +369,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro }() }) } - dead := &deadKeeper{ctx: ctx, js: js, w: w, logf: logger.Printf} + dead := &deadKeeper{ctx: ctx, js: js, w: w, logf: logger.Printf, module: cred.Module} w.SetRecount(dead.count) dead.count() dead.ages() diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go index d6f6358a..1f684d63 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go @@ -54,7 +54,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 }) t.Run("DLV-W1: the consumed events reach the watchman by their subjects", func(t *testing.T) { out := &findingsOut{findings: map[string]bool{}} - w := watch.New(watch.WatchmanModule, out, time.Now) + w := watch.New(watchmanModule, out, time.Now) say := func(string, ...any) {} // The bus module's own stream is its history, never heard here: only the seat's subject (ruling 13:0x). consumed(w, "mesh.stream.nats.event.refused", []byte(`{"module":"m","local":"l","why":"no room"}`), say) @@ -77,7 +77,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 }) t.Run("design 47 G §500: the indexer's manifest-refused reaches the watchman by its subject", func(t *testing.T) { out := &findingsOut{findings: map[string]bool{}} - w := watch.New(watch.WatchmanModule, out, time.Now) + w := watch.New(watchmanModule, out, time.Now) consumed(w, "mesh.seat.mesh-catalog.event.manifest-refused", []byte(`{"module":"m","digest":"d","problems":["x"]}`), func(string, ...any) {}) if !out.findings[watch.FindingKey(watch.ScopeModule, "m", watch.KindManifestRefused)] { t.Fatalf("a refused manifest reached %v", out.findings) @@ -85,7 +85,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 }) t.Run("design 47 G §499, §541: a node-engine's wait-held and wait-ended reach the watchman by their subjects", func(t *testing.T) { out := &findingsOut{findings: map[string]bool{}} - w := watch.New(watch.WatchmanModule, out, time.Now) + w := watch.New(watchmanModule, out, time.Now) say := func(string, ...any) {} body := fmt.Sprintf(`{"step":"compose","reason":"entry-missing","since":%q}`, time.Now().Add(-2*time.Hour).UTC().Format(time.RFC3339)) consumed(w, "mesh.seat.node-engine.event.wait-held.ace.web", []byte(body), say) @@ -99,7 +99,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 }) t.Run("design 47 G A-192: a module's wait-held on its stream, naming a part, reaches the watchman", func(t *testing.T) { out := &findingsOut{findings: map[string]bool{}} - w := watch.New(watch.WatchmanModule, out, time.Now) + w := watch.New(watchmanModule, out, time.Now) body := fmt.Sprintf(`{"step":"building","part":"mesh-sdk@v0.1.14","module":"web","fingerprint":"src2:f","bound":"1h0m0s","since":%q}`, time.Now().Add(-2*time.Hour).UTC().Format(time.RFC3339)) consumed(w, "mesh.stream.mesh-delivery-builder.event.wait-held.web.src2:f", []byte(body), func(string, ...any) {}) if len(out.findings) != 1 { @@ -120,7 +120,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 "unreadable": func(string, any) (json.RawMessage, error) { return json.RawMessage(`{"waits":`), nil }, } { out := &findingsOut{findings: map[string]bool{}} - w := watch.New(watch.WatchmanModule, out, time.Now) + w := watch.New(watchmanModule, out, time.Now) const key = "m.00000000000000000042" old := watch.Finding{Scope: watch.ScopeDelivery, ID: key, Kind: watch.KindTakeInWaits, Severity: watch.Warning, Source: "take-in", Since: time.Now().Add(-3 * time.Hour), Summary: "held", Said: "held", Headline: "held", Explanation: "held", Needs: "a take-in"} @@ -146,7 +146,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 // The watchman's want wait starts at the entry's time (createdOr): through mesh-sdk's views.ProcessReader it is // the time the owner's bus wrote the key, never the watchman's start. written := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC) - r := &views.ProcessReader{Module: watch.WatchmanModule, Call: func(string, any) (json.RawMessage, error) { + r := &views.ProcessReader{Module: watchmanModule, Call: func(string, any) (json.RawMessage, error) { return json.Marshal(views.Answer{V: views.Version, Owner: watch.AssignerModule, State: wantsState, Revision: 3, Created: written, At: written.Add(time.Hour), Values: map[string]json.RawMessage{"ace.mail": json.RawMessage(`{}`)}, Written: map[string]time.Time{"ace.mail": written}}) }} diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/ownmodule_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/ownmodule_test.go new file mode 100644 index 00000000..b0c93439 --- /dev/null +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/ownmodule_test.go @@ -0,0 +1,4 @@ +package main + +// watchmanModule is the module the tests run the watchman as, as its credential would say it. +const watchmanModule = "mesh-delivery-watchman" diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/signals.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/signals.go index f8a3c0d7..a10b1182 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/signals.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/signals.go @@ -145,6 +145,8 @@ type deadKeeper struct { js jetstream.JetStream w *watch.Watchman logf func(string, ...any) + // module is the watchman's module, its credential's: the name of its own consumer on DEAD_LETTER_NOTICES. + module string mu sync.Mutex taking jetstream.ConsumeContext @@ -218,7 +220,7 @@ func (d *deadKeeper) check() { if !take || d.taking != nil { return } - cc, err := deadletters.TakeNotices(d.ctx, d.js, func(deadletters.DeadLetter) { go d.count() }, d.logf) + cc, err := deadletters.TakeNotices(d.ctx, d.js, d.module, func(deadletters.DeadLetter) { go d.count() }, d.logf) if err != nil { d.logf("the dead-letter notices cannot be taken yet: %v", err) return diff --git a/modules/mesh-delivery-watchman/internal/deadletters/deadletters.go b/modules/mesh-delivery-watchman/internal/deadletters/deadletters.go index c7c30150..03f281fe 100644 --- a/modules/mesh-delivery-watchman/internal/deadletters/deadletters.go +++ b/modules/mesh-delivery-watchman/internal/deadletters/deadletters.go @@ -3,7 +3,7 @@ // changed in what is kept, aged or withheld, on the jetstream API. // // DEAD_LETTER_NOTICES is a work queue that holds one consumer. The ended controller's consumer `controller` never -// expires, so the watchman makes its own (Consumer) only once `controller` is gone (Gate); while it stands the watchman says +// expires, so the watchman makes its own (its module's name) only once `controller` is gone (Gate); while it stands the watchman says // so as a finding. Removing `controller` is a hand act with the bus module's tool (novox/hq ADR 0590 clause 6). Delivering // and dropping wait for the hand-act log's home (issue 1953), so they are refused naming that. package deadletters @@ -23,9 +23,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/buscompose" ) -// Consumer is the watchman's own consumer on DEAD_LETTER_NOTICES, made only by Gate's leave. -const Consumer = "mesh-delivery-watchman" - // The headers a kept message carries beside its own (mesh-controller link.DeadHeader*: the same names, so the // controller's copies and the watchman's are read alike). const ( @@ -332,11 +329,11 @@ func ControllerStands(ctx context.Context, js jetstream.JetStream) (bool, error) // TakeNotices makes the watchman's own durable consumer on DEAD_LETTER_NOTICES (never touching `controller`) and keeps // each notice it takes; a notice not kept is handed back to be taken again. kept hears each one kept. -func TakeNotices(ctx context.Context, js jetstream.JetStream, kept func(DeadLetter), logf func(string, ...any)) (jetstream.ConsumeContext, error) { +func TakeNotices(ctx context.Context, js jetstream.JetStream, consumer string, kept func(DeadLetter), logf func(string, ...any)) (jetstream.ConsumeContext, error) { c, err := js.CreateOrUpdateConsumer(ctx, buscompose.DeadLetterNoticesStream, jetstream.ConsumerConfig{ - Durable: Consumer, AckPolicy: jetstream.AckExplicitPolicy, AckWait: time.Minute}) + Durable: consumer, AckPolicy: jetstream.AckExplicitPolicy, AckWait: time.Minute}) if err != nil { - return nil, fmt.Errorf("the consumer %s on %s cannot be made: %w", Consumer, buscompose.DeadLetterNoticesStream, err) + return nil, fmt.Errorf("the consumer %s on %s cannot be made: %w", consumer, buscompose.DeadLetterNoticesStream, err) } return c.Consume(func(m jetstream.Msg) { d, err := Keep(ctx, js, m.Data()) diff --git a/modules/mesh-delivery-watchman/internal/watch/finding.go b/modules/mesh-delivery-watchman/internal/watch/finding.go index 837e3db3..199e5120 100644 --- a/modules/mesh-delivery-watchman/internal/watch/finding.go +++ b/modules/mesh-delivery-watchman/internal/watch/finding.go @@ -16,12 +16,6 @@ import ( // through mesh-sdk's finding writer (design 47 G §689), only when what it says changes, and it deletes the key when it no longer observes it. The condition keeper (mesh-conditions) raises and // clears the conditions from them; the watchman writes no condition and reads none (Limits 11). -// FindingsBucket is the watchman's findings. -var FindingsBucket = Observer + "_findings" - -// Observer is the name a finding of the watchman carries. -const Observer = "mesh-delivery-watchman" - // The scopes and severities a finding of the watchman names, as the controller's conditions name them, so the keeper's // `compare` meets the same keys. const ( @@ -91,7 +85,7 @@ func (f Finding) Contract() condseat.Finding { id = id + "." + KeyID(ref) } v := condseat.Finding{Scope: f.Scope, ID: id, Kind: f.Kind, Ref: ref, Severity: f.Severity, Summary: f.Summary, - Headline: f.Headline, Explanation: f.Explanation, Needs: f.Needs, Source: f.Source, Observer: Observer, + Headline: f.Headline, Explanation: f.Explanation, Needs: f.Needs, Source: f.Source, Observer: f.Observer, At: f.Since.UTC(), Since: f.Since.UTC(), Subject: condseat.Subject{Scope: f.Scope, ID: id, Machine: f.Machine, Also: f.Also}} if f.Said != "" { @@ -124,6 +118,7 @@ func DecodeFinding(data []byte) (Finding, error) { // find writes a finding when what it says differs from what the bucket holds (ADR 0289 §1). func (w *Watchman) find(f Finding) { + f.Observer = w.module // the watchman of the module its credential names value, err := EncodeFinding(f) if err != nil { w.say("no finding: %v", err) @@ -196,10 +191,10 @@ func (w *Watchman) Rewrite() int { func (w *Watchman) NotHeard(family, consequence string, why error) { w.mu.Lock() defer w.mu.Unlock() - summary, headline, explanation := heard.Words(Observer, family, consequence, why) + summary, headline, explanation := heard.Words(w.module, family, consequence, why) w.say("%s", summary) - w.find(Finding{Scope: ScopeModule, ID: heard.ID(Observer, family), Token: heard.Kind, Kind: heard.Kind, Severity: Warning, - Source: Observer + "." + heard.Kind, Since: w.now().UTC().Truncate(time.Second), Summary: summary, Headline: headline, + w.find(Finding{Scope: ScopeModule, ID: heard.ID(w.module, family), Token: heard.Kind, Kind: heard.Kind, Severity: Warning, + Source: w.module + "." + heard.Kind, Since: w.now().UTC().Truncate(time.Second), Summary: summary, Headline: headline, Explanation: explanation}) } @@ -207,5 +202,5 @@ func (w *Watchman) NotHeard(family, consequence string, why error) { func (w *Watchman) Heard(family string) { w.mu.Lock() defer w.mu.Unlock() - w.unfind(heard.Key(Observer, family)) + w.unfind(heard.Key(w.module, family)) } diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 6e3a2778..9e292509 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -95,7 +95,7 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{FindingKey(ScopeDelivery, revKey, KindTakeInWaits): Warning}}, {"1966/AC195-5: a seed ends the finding and the held wait of a key indexer_pending no longer names, and keeps one it names with its own since", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { // A previous run's finding for revKey stands in the bucket (read at the start); a wait of staleKey is held from an event. - old := Finding{Scope: ScopeDelivery, ID: revKey, Kind: KindTakeInWaits, Severity: Warning, Source: "take-in", Since: t0.Add(-3 * time.Hour), Observer: Observer, Summary: "held", Said: "held", Headline: "held", Explanation: "held", Needs: "a take-in"} + old := Finding{Scope: ScopeDelivery, ID: revKey, Kind: KindTakeInWaits, Severity: Warning, Source: "take-in", Since: t0.Add(-3 * time.Hour), Observer: testModule, Summary: "held", Said: "held", Headline: "held", Explanation: "held", Needs: "a take-in"} value, err := EncodeFinding(old) if err != nil { t.Fatal(err) @@ -262,7 +262,7 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{refusedKey("mesh-knowledge", "gone"): Urgent}}, {"ADR 0615 §5 (review round 1): a consumer the watchman cannot bind is family-not-heard naming its stream, until it binds", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.NotHeard("EVENTS", "what it carries is heard once the grants part has made the consumer n1_mesh-delivery-watchman", errors.New("consumer not found")) - f := o.findings[heard.Key(Observer, "EVENTS")] + f := o.findings[heard.Key(testModule, "EVENTS")] if f.Kind != heard.Kind || f.Severity != Warning || !strings.Contains(f.Summary, "does not hear EVENTS") || !strings.Contains(f.Summary, "consumer not found") { t.Fatalf("findings = %v", o.findings) } @@ -475,8 +475,8 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{}}, {"WM4: a finding read at the start and not observed again is deleted; a signal's stays", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { for _, f := range []Finding{ - {Scope: ScopeDelivery, ID: "x.y", Kind: KindStuck, Severity: Warning, Summary: "old", Source: "stuck"}, - {Scope: ScopeMachine, ID: "ace", Kind: KindToolsSilent, Severity: Warning, Summary: "silent", Source: "S11"}, + {Scope: ScopeDelivery, ID: "x.y", Kind: KindStuck, Severity: Warning, Summary: "old", Source: "stuck", Observer: testModule}, + {Scope: ScopeMachine, ID: "ace", Kind: KindToolsSilent, Severity: Warning, Summary: "silent", Source: "S11", Observer: testModule}, } { v, _ := EncodeFinding(f) o.findings[f.Key()], _ = DecodeFinding(v) @@ -535,7 +535,7 @@ func TestTheWatchmansFindings(t *testing.T) { got := map[string]string{} for k, f := range o.findings { got[k] = f.Severity - if f.Key() != k || f.Observer != Observer { + if f.Key() != k || f.Observer != testModule { t.Errorf("finding %s = %+v", k, f) } } diff --git a/modules/mesh-delivery-watchman/internal/watch/names.go b/modules/mesh-delivery-watchman/internal/watch/names.go index c891be91..d26efc7c 100644 --- a/modules/mesh-delivery-watchman/internal/watch/names.go +++ b/modules/mesh-delivery-watchman/internal/watch/names.go @@ -8,7 +8,6 @@ import ( // states and the tools it invokes) and the seats' state names; a unit row checks them against it (TestManifestStateAndReads). // No contract names a holder (novox/hq ADR 0625 §4, ADR 0634 §1). const ( - WatchmanModule = "mesh-delivery-watchman" AssignerModule = "mesh-delivery-assigner" // ProvisionsBucket is the seat mesh-provisioner's state provisions (reads: mesh-provisioner.provisions). ProvisionsBucket = "mesh-provisioner_provisions" diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index d17e328e..08aab116 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -56,6 +56,7 @@ type pulse struct { // safe to call from several watches at once. type Watchman struct { mu sync.Mutex + module string // the module it runs as, its credential's (A-296) own OwnBuckets // its own buckets, named from its credential's module (A-296) out Out now func() time.Time @@ -123,7 +124,7 @@ type Watchman struct { // New is the watchman of module (its credential's), writing to out, reading the time from now. func New(module string, out Out, now func() time.Time) *Watchman { - return &Watchman{own: BucketsOf(module), out: out, now: now, counts: map[string]int{}, presence: map[string]presence{}, + return &Watchman{module: module, own: BucketsOf(module), out: out, now: now, counts: map[string]int{}, presence: map[string]presence{}, pulses: map[string]*pulse{}, stuck: map[string]meshwatchdogsclient.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, held: map[string]bool{}, starts: map[string]startMark{}, bound: meshwatchdogsclient.PulseMoveBound, stepBound: StepBound, progressBound: ProgressBound, findings: map[string][]byte{}, seededFindings: map[string]bool{}, signals: map[string]signalDue{}, busCreated: map[string]bool{}, takeInHeld: map[string]Finding{}, pendingBranch: map[string]string{}, takenEntries: map[string][]string{}, recordMilestone: map[string]string{}, away: map[string]bool{}, advised: map[string]int{}, advisedSince: map[string]time.Time{}, diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index 1bbbfc0b..8abe5414 100644 --- a/modules/mesh-delivery-watchman/manifest_test.go +++ b/modules/mesh-delivery-watchman/manifest_test.go @@ -168,7 +168,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 return e } for name, row := range map[string]struct{ got, want string }{ - "module": {watch.WatchmanModule, m.Module}, + "module": {watchmanModule, m.Module}, // A-296: its own buckets are named from its credential's module, each a state its manifest keeps. "presence": {watch.BucketsOf("a-watchman").Presence, "a-watchman_" + m.State[0].Name}, "pulse": {watch.BucketsOf("a-watchman").Pulse, "a-watchman_" + m.State[1].Name}, diff --git a/modules/mesh-delivery-watchman/ownmodule_test.go b/modules/mesh-delivery-watchman/ownmodule_test.go new file mode 100644 index 00000000..83cb8f0a --- /dev/null +++ b/modules/mesh-delivery-watchman/ownmodule_test.go @@ -0,0 +1,4 @@ +package manifest + +// watchmanModule is the module the manifest names, which the tests run the watchman as (its credential would say it). +const watchmanModule = "mesh-delivery-watchman" diff --git a/modules/mesh-delivery-watchman/ownname_test.go b/modules/mesh-delivery-watchman/ownname_test.go new file mode 100644 index 00000000..0fb3ae70 --- /dev/null +++ b/modules/mesh-delivery-watchman/ownname_test.go @@ -0,0 +1,34 @@ +package manifest + +import ( + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// No code of the watchman names its own module: its buckets, its consumer, its finding observer and its tools' names take the +// module its credential (or the runtime that serves its tools) names (design 47 G A-296, §428). The manifest alone says it. +func TestNoCodeNamesTheWatchmansOwnModule(t *testing.T) { + name := regexp.MustCompile(`"` + regexp.QuoteMeta(watchmanModule) + `(_[^"]*)?"`) + err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return err + } + src, err := os.ReadFile(path) + if err != nil { + return err + } + for i, line := range strings.Split(string(src), "\n") { + if !strings.HasPrefix(strings.TrimSpace(line), "//") && name.MatchString(line) { + t.Errorf("%s:%d names the watchman's own module: %s", path, i+1, strings.TrimSpace(line)) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } +}