Join 9: mesh-vault adopts the licence bindings' follower through a views reader, on its one writer (A-134)
The follower main added (issue 1409) used the removed views.FindingWriter: it is adopted by a views.Reader holding the vault's one finding writer, which the assignments view shares, and hears claude-licence-manager.binding.changed.<consumer> through the adoption. Builds: design 47 G A-16, A-28, A-134 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -16,7 +16,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
@@ -184,10 +183,11 @@ func licenceBindingsAnswer(raw []byte) (views.Answer, error) {
|
||||
// licenceBindingsView is the first view's follower; nil until main begins it.
|
||||
var licenceBindingsView *views.Follower
|
||||
|
||||
// newLicenceBindingsView makes the follower of the first view: asked until it answers, and each answer, at the start or
|
||||
// asked again by hand, written into the state.
|
||||
// newLicenceBindingsView makes the follower of the first view, adopted by a views.Reader holding the vault's writer (design
|
||||
// 47 G A-28, A-134), so the reader writes and clears its findings: asked until it answers, and each answer, at the start or
|
||||
// asked again by hand, written into the state; each binding.changed.<consumer> is lb.Heard.
|
||||
func newLicenceBindingsView(ask func(subject string, body any) (json.RawMessage, error), lb LicenceBindings,
|
||||
findings views.FindingWriter) *views.Follower {
|
||||
findings views.WireWriter) (*views.Follower, *views.Reader, error) {
|
||||
var mu sync.Mutex
|
||||
var taken time.Time
|
||||
var f *views.Follower
|
||||
@@ -199,13 +199,6 @@ func newLicenceBindingsView(ask func(subject string, body any) (json.RawMessage,
|
||||
}
|
||||
return licenceBindingsAnswer(raw)
|
||||
},
|
||||
Raise: func(s views.Status) {
|
||||
fd := views.UnansweredFinding(Seat, s, time.Now())
|
||||
if err := findings.Put(fd); err != nil {
|
||||
say("the finding %s is not written: %v", fd.ID, err)
|
||||
}
|
||||
},
|
||||
Clear: func(s views.Status) { _ = findings.Delete(views.UnansweredFinding(Seat, s, time.Now())) },
|
||||
// Each answer, once: its picture is the manager's bindings when it answered.
|
||||
Said: func(s views.Status) {
|
||||
mu.Lock()
|
||||
@@ -229,31 +222,32 @@ func newLicenceBindingsView(ask func(subject string, body any) (json.RawMessage,
|
||||
sort.Strings(consumers)
|
||||
say("the licence bindings of %d consumer(s) are kept from the first view: %v", len(consumers), consumers)
|
||||
}}
|
||||
return f
|
||||
r := &views.Reader{Module: Seat, Findings: findings, Logf: say}
|
||||
manager, event, _ := strings.Cut(BindingChangedEvent, ".")
|
||||
if err := r.Adopt(f, manager, event, func(_ context.Context, body []byte) error { return lb.Heard(body) }); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return f, r, nil
|
||||
}
|
||||
|
||||
// watchLicenceBindings begins the first view and hears `binding.changed`. It answers whether it began; one that did not
|
||||
// is begun again at the next minute pass.
|
||||
func watchLicenceBindings(lb LicenceBindings) bool {
|
||||
err := stdio.Subscribe(BindingChangedEvent, func(e stdio.Envelope) error {
|
||||
if err := lb.Heard(e.Body); err != nil {
|
||||
say("%v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if licenceBindingsView != nil {
|
||||
return true
|
||||
}
|
||||
w, err := vaultFindings()
|
||||
if err != nil {
|
||||
say("does not hear %s yet (%v); asked again at the next pass", BindingChangedEvent, err)
|
||||
say("the vault's finding writer does not start yet (%v); the licence bindings are begun at the next pass", err)
|
||||
return false
|
||||
}
|
||||
if licenceBindingsView == nil {
|
||||
licenceBindingsView = newLicenceBindingsView(stdio.Ask, lb,
|
||||
meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
|
||||
go func() {
|
||||
if err := licenceBindingsView.Start(context.Background()); err != nil {
|
||||
say("the first view of the licence bindings ended unanswered: %v", err)
|
||||
}
|
||||
}()
|
||||
f, r, err := newLicenceBindingsView(stdio.Ask, lb, w)
|
||||
if err != nil {
|
||||
say("the licence bindings are not followed: %v", err)
|
||||
return false
|
||||
}
|
||||
licenceBindingsView = f
|
||||
r.Start(context.Background())
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -184,3 +186,55 @@ func TestTheFirstViewToolsCoverTheLicenceBindings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The licence bindings' follower is adopted by a reader holding the vault's writer (design 47 G A-28, A-134): past its bound
|
||||
// the reader writes its unanswered finding through that writer, the answer clears it, and the follower is the reader's.
|
||||
func TestTheLicenceBindingsFollowerIsAdoptedAndItsFindingGoesThroughTheVaultsWriter(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
up := false
|
||||
ask := func(string, any) (json.RawMessage, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !up {
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
return json.RawMessage(`[]`), nil
|
||||
}
|
||||
written := make(chan string, 4)
|
||||
lb, _ := memLicenceBindings()
|
||||
f, r, err := newLicenceBindingsView(ask, lb, signalledFindings{written})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !r.Follows("claude-licence-manager.bindings") {
|
||||
t.Fatal("the reader does not follow the licence bindings")
|
||||
}
|
||||
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- f.Start(context.Background()) }()
|
||||
if got := <-written; got != "write first-view-unanswered" {
|
||||
t.Fatalf("past its bound the writer was told %q, want the unanswered finding written", got)
|
||||
}
|
||||
mu.Lock()
|
||||
up = true
|
||||
mu.Unlock()
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := <-written; got != "clear first-view-unanswered" {
|
||||
t.Errorf("the answer told the writer %q, want the finding cleared", got)
|
||||
}
|
||||
}
|
||||
|
||||
// signalledFindings says each write and clear of a finding, by its kind.
|
||||
type signalledFindings struct{ said chan<- string }
|
||||
|
||||
func (s signalledFindings) WriteWire(_ context.Context, f map[string]any) error {
|
||||
s.said <- fmt.Sprint("write ", f["kind"])
|
||||
return nil
|
||||
}
|
||||
func (s signalledFindings) Clear(_ context.Context, _, _, kind, _ string) error {
|
||||
s.said <- "clear " + kind
|
||||
return nil
|
||||
}
|
||||
func (s signalledFindings) ModuleNode() (string, string) { return Seat, "" }
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
@@ -120,16 +121,7 @@ func watchLeavers(env *Env) bool {
|
||||
return true
|
||||
}
|
||||
if assignmentsView == nil {
|
||||
// The vault's finding writer (design 47 G §689, A-16): it states the assignments view's findings and observes none of
|
||||
// its own, and names no secret.
|
||||
w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{
|
||||
Asked: meshconditions.SubjectFindingsAsked, Module: Seat, State: stdio.State(FindingsState),
|
||||
Observe: func(context.Context) (views.Observed[meshconditions.Finding], error) {
|
||||
return views.Observed[meshconditions.Finding]{}, nil
|
||||
}})
|
||||
if err == nil {
|
||||
err = w.Start(context.Background())
|
||||
}
|
||||
w, err := vaultFindings()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -144,6 +136,34 @@ func watchLeavers(env *Env) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// The vault's one finding writer (design 47 G §689, A-16, A-134): it states the findings of both its followers, the
|
||||
// assignments view's and the licence bindings', observes none of its own, and names no secret. Made and started once.
|
||||
var (
|
||||
findingsMu sync.Mutex
|
||||
findingsWriter views.WireWriter
|
||||
)
|
||||
|
||||
func vaultFindings() (views.WireWriter, error) {
|
||||
findingsMu.Lock()
|
||||
defer findingsMu.Unlock()
|
||||
if findingsWriter != nil {
|
||||
return findingsWriter, nil
|
||||
}
|
||||
w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{
|
||||
Asked: meshconditions.SubjectFindingsAsked, Module: Seat, State: stdio.State(FindingsState),
|
||||
Observe: func(context.Context) (views.Observed[meshconditions.Finding], error) {
|
||||
return views.Observed[meshconditions.Finding]{}, nil
|
||||
}})
|
||||
if err == nil {
|
||||
err = w.Start(context.Background())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
findingsWriter = w
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// assigned is the assignments mirror as the vault's watch keeps it.
|
||||
var assigned *WatchedAssignments
|
||||
|
||||
|
||||
@@ -187,7 +187,6 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
// consumer, never a secret (design 39 bus-and-switch §17, B25).
|
||||
if f == "licencebindings.go" {
|
||||
code = []byte(strings.Replace(string(code), "st := stdio.State(LicenceBindingsState)", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)}", "", 1))
|
||||
}
|
||||
// The signing keys' public halves, in signing.go alone: one read and one write of its own bucket (ADR 0550 §2).
|
||||
if f == "signing.go" {
|
||||
|
||||
Reference in New Issue
Block a user