diff --git a/modules/claude-code/cmd/claude-code/claude_code_test.go b/modules/claude-code/cmd/claude-code/claude_code_test.go index 2f75f5e0b..f514945e3 100644 --- a/modules/claude-code/cmd/claude-code/claude_code_test.go +++ b/modules/claude-code/cmd/claude-code/claude_code_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "os" "path/filepath" @@ -9,6 +10,8 @@ import ( "strings" "testing" "time" + + "git.novox.be/novox/mesh-sdk/go/views" ) var now = time.Now().UnixMilli() @@ -595,6 +598,20 @@ func TestTheFirstViewOfTheBindingIsApplied(t *testing.T) { if _, err := FirstBinding(p, ask, writer(w)); err != nil || len(asked) != 6 || asked[5] != VaultGet { t.Fatalf("a start asked %v", asked) } + // A-134: the first view is the follower the module's views reader adopts on binding.changed.; its answer + // applies the binding as FirstBinding does. + r := &views.Reader{Module: Module} + f := bindingsFollower(p, ask, writer(w), func(done string, err error) { + if err != nil || !strings.Contains(done, `"generation":9`) { + t.Errorf("the adopted first view applied %q, %v", done, err) + } + }) + if err := r.Adopt(f, Manager, BindingChangedEvent, func(context.Context, []byte) error { return nil }); err != nil || !r.Follows(Manager+".bindings") { + t.Fatalf("the bindings follower is not adopted: %v", err) + } + if err := f.Start(context.Background()); err != nil || len(asked) != 9 { + t.Fatalf("the adopted first view: %v, asked %v", err, asked) + } raw, _ := os.ReadFile("../../module.json") var m struct { Consumes, Invokes []string diff --git a/modules/claude-code/cmd/claude-code/main.go b/modules/claude-code/cmd/claude-code/main.go index ab7cf6b0a..6243323be 100644 --- a/modules/claude-code/cmd/claude-code/main.go +++ b/modules/claude-code/cmd/claude-code/main.go @@ -668,22 +668,15 @@ func run(p Paths, view *ServerView, configView *ConfigView) { } go report() } - go persist("hearing this node's licence binding", func() error { - err := stdio.Subscribe(BindingChanged, func(e stdio.Envelope) error { - applied(OnBindingChanged(p, e.Body, ask, writeManaged)) + // Adopted by this module's views reader (design 47 G A-134): the reader hears binding.changed. and asks the + // first view once its subscription is taken, and raises and clears that view's findings through the module's writer. + if err := meshViews.Adopt(bindingsFollower(p, ask, writeManaged, applied), Manager, BindingChangedEvent, + func(_ context.Context, body []byte) error { + applied(OnBindingChanged(p, body, ask, writeManaged)) return nil - }) - if err == nil { - go persist("asking this node's licence binding", func() error { - done, err := FirstBinding(p, ask, writeManaged) - if err == nil { - applied(done, nil) - } - return err - }, func(int) {}) - } - return err - }, func(n int) { say("hearing this node's licence binding%s", refusals(n)) }) + }); err != nil { + say("this node's licence binding is not followed: %v", err) + } // The token of the licence this node holds, asked again when the vault says its access secret changed (§19). go persist("hearing the vault's changes of this node's licence token", func() error { return stdio.Subscribe(SecretChanged, func(e stdio.Envelope) error { diff --git a/modules/claude-code/cmd/claude-code/node.go b/modules/claude-code/cmd/claude-code/node.go index 6c2117ea5..011ebb8c8 100644 --- a/modules/claude-code/cmd/claude-code/node.go +++ b/modules/claude-code/cmd/claude-code/node.go @@ -18,6 +18,7 @@ package main // every node, `.` for one — which every node watches. import ( + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -30,6 +31,8 @@ import ( "strings" "sync" "time" + + "git.novox.be/novox/mesh-sdk/go/views" ) // Seat is the licence manager's role, and Manager the module whose events this one consumes. @@ -481,7 +484,25 @@ func fetched(p Paths, b *BindingState, ask Ask, write WriteManaged) (string, err // BindingChanged is the manager's event of a binding written or deleted (design 39 bus-and-switch §21), which this // module consumes (§22). -const BindingChanged = Manager + ".binding.changed" +const BindingChanged = Manager + "." + BindingChangedEvent + +// BindingChangedEvent is that event's name as the manager says it, with the consumer as its further part +// (`binding.changed.`, design 47 G A-134). +const BindingChangedEvent = "binding.changed" + +// bindingsFollower is this node's first view of its licence binding, adopted by the module's views reader (A-134): its +// ask is FirstBinding, whose outcome goes to applied; the picture keeps no binding, which the node's own files hold. +func bindingsFollower(p Paths, ask Ask, write WriteManaged, applied func(string, error)) *views.Follower { + return &views.Follower{Owner: Manager, Tool: SeatVerb("bindings"), State: "bindings", + Ask: func(context.Context, string) (views.Answer, error) { + done, err := FirstBinding(p, ask, write) + if err != nil { + return views.Answer{}, err + } + applied(done, nil) + return views.Answer{Owner: Manager, State: "bindings", Values: map[string]json.RawMessage{}, At: time.Now()}, nil + }} +} // bindingOf is a consumer's binding as the manager says it, in binding.changed and in the verb bindings' answer: an // empty licence is a release (B22: decoded from a copy of the writer's form, testdata/binding-changed.json).