Merge pull request 'licence manager: say what moved a binding, why, and who asked (issue 625)' (#270) from fix/625-a-licence-move-says-what-why-and-who into main
This commit was merged in pull request #270.
This commit is contained in:
@@ -285,6 +285,25 @@ func TestANewerGenerationFetchesTheTokenOnceByTheSeatsVerb(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The node's line names the generation it applied and why the manager moved it, and who asked (novox/hq issue 625).
|
||||
func TestAnAppliedBindingSaysWhyAndWho(t *testing.T) {
|
||||
p, w := node(t, "laptop")
|
||||
var asked []string
|
||||
done, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 7,
|
||||
Why: "the switch verb", By: "g14.node-tools"}, seat(t, "personal", "at-7", 7, &asked), writer(w))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{`"generation":7`, `"why":"the switch verb"`, `"by":"g14.node-tools"`} {
|
||||
if !strings.Contains(done, want) {
|
||||
t.Fatalf("the line %s lacks %s", done, want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(done, "at-7") {
|
||||
t.Fatal("the line carries the token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheTokenANodeIsHandedReplacesALoginsGrantAndLeavesNoRefreshToken(t *testing.T) {
|
||||
p, w := node(t, "laptop")
|
||||
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-old","refreshToken":"rt-spent","expiresAt":`+
|
||||
|
||||
@@ -298,6 +298,9 @@ type BindingState struct {
|
||||
Licence string `json:"licence"`
|
||||
Kind string `json:"kind"`
|
||||
Generation int64 `json:"generation"`
|
||||
// Why and By are what moved the binding and who asked, as the manager recorded it (novox/hq issue 625).
|
||||
Why string `json:"why,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
// Current is what the seat answers to `current`: the licence this node is bound to and its token, sealed.
|
||||
@@ -433,6 +436,15 @@ func OnBinding(p Paths, b *BindingState, ask Ask, write WriteManaged) (string, e
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// The line says which binding this was and why the manager moved it, so a node's journal alone answers
|
||||
// what changed here and who asked (novox/hq issue 625).
|
||||
out["generation"] = b.Generation
|
||||
if b.Why != "" {
|
||||
out["why"] = b.Why
|
||||
}
|
||||
if b.By != "" {
|
||||
out["by"] = b.By
|
||||
}
|
||||
raw, _ := json.Marshal(out)
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ func text(a map[string]any, k string) (string, error) {
|
||||
}
|
||||
|
||||
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's subject.
|
||||
// What it changes is recorded as asked by its caller, as the runtime proved it (novox/hq issues 365 and 625).
|
||||
func verb(name, description string, input map[string]any, run func(ctx context.Context, m *Manager, a map[string]any) (any, error)) stdio.Tool {
|
||||
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input,
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
@@ -260,7 +261,8 @@ func verb(name, description string, input map[string]any, run func(ctx context.C
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return run(context.Background(), m, a)
|
||||
ctx := WithCause(context.Background(), Cause{Why: "the " + name + " verb", By: stdio.CallerOf(a)})
|
||||
return run(ctx, m, a)
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -288,7 +290,7 @@ func tools() []stdio.Tool {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Bind(ctx, c, l, "bind")
|
||||
return m.Bind(ctx, c, l)
|
||||
}),
|
||||
verb("switch", "Move a consumer to another licence. Its node fetches the new licence's token at once and points the agent's account at it. A login on a node does this by itself (ADR 0209); this is for moving one without a login, or back.",
|
||||
map[string]any{"consumer": consumer, "licence": str("the licence to move to")},
|
||||
@@ -297,7 +299,7 @@ func tools() []stdio.Tool {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Bind(ctx, c, l, "switch")
|
||||
return m.Bind(ctx, c, l)
|
||||
}),
|
||||
verb("release", "Unbind a consumer. Its node keeps its last token, which expires within hours.",
|
||||
map[string]any{"consumer": consumer},
|
||||
@@ -306,7 +308,7 @@ func tools() []stdio.Tool {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Release(ctx, c, "release")
|
||||
return m.Release(ctx, c)
|
||||
}),
|
||||
verb("refresh", "Refresh a licence now, or every due licence when none is named; under each licence's lease, so it never races the daemon. Answers the outcome, never a token.",
|
||||
map[string]any{"licence": str("a licence; absent for every due one")},
|
||||
|
||||
@@ -59,6 +59,54 @@ type BindingState struct {
|
||||
Licence string `json:"licence"`
|
||||
Kind string `json:"kind"`
|
||||
Generation int64 `json:"generation"`
|
||||
// Why and By are what moved it and who asked (novox/hq issue 625), so the node can say so when it applies it.
|
||||
Why string `json:"why,omitempty"`
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
// Cause is why a consumer's binding changed and who or what asked for it (novox/hq issue 625): Why is the verb
|
||||
// called or the rule that fired, By the caller the runtime proved (stdio.CallerOf) or this module itself.
|
||||
type Cause struct {
|
||||
Why string
|
||||
By string
|
||||
}
|
||||
|
||||
// Itself names this module where it moved a binding by a rule of its own, with no caller.
|
||||
const Itself = "claude-licence-manager"
|
||||
|
||||
type causeKey struct{}
|
||||
|
||||
// WithCause is ctx carrying why what runs in it changes a binding, and who asked.
|
||||
func WithCause(ctx context.Context, c Cause) context.Context {
|
||||
return context.WithValue(ctx, causeKey{}, c)
|
||||
}
|
||||
|
||||
// CauseOf is the cause ctx carries, or "a rule of this module's own" when none: never empty, so a record always
|
||||
// says something, and a change nobody named a cause for reads as this module's own.
|
||||
func CauseOf(ctx context.Context) Cause {
|
||||
if c, ok := ctx.Value(causeKey{}).(Cause); ok && c.By != "" {
|
||||
return c
|
||||
}
|
||||
return Cause{Why: "a rule of this module's own", By: Itself}
|
||||
}
|
||||
|
||||
// record says one change of what a consumer holds, the same fields in three places (novox/hq issue 625): a line
|
||||
// in the journal, a row in the audit table, and the event `licence.<what>` on the bus. Never a token.
|
||||
func (m *Manager) record(ctx context.Context, what, line string, detail map[string]any) {
|
||||
_ = m.Store.Audit(ctx, what, detail)
|
||||
m.Log("%s", line)
|
||||
_ = m.Emit("licence."+what, detail)
|
||||
}
|
||||
|
||||
// moved records a consumer bound or switched: from which licence to which, at which generation, why and by whom.
|
||||
func (m *Manager) moved(ctx context.Context, consumer, from, licence string, generation int64, c Cause) {
|
||||
what, line := "bound", fmt.Sprintf("bound %s to %s", consumer, licence)
|
||||
if from != "" {
|
||||
what, line = "switched", fmt.Sprintf("switched %s from %s to %s", consumer, from, licence)
|
||||
}
|
||||
m.record(ctx, what, fmt.Sprintf("%s (generation %d): %s; asked by %s", line, generation, c.Why, c.By),
|
||||
map[string]any{"consumer": consumer, "licence": licence, "from": from, "generation": generation,
|
||||
"why": c.Why, "by": c.By})
|
||||
}
|
||||
|
||||
// Settings are the manager's own, declared with defaults (design 39 §8).
|
||||
@@ -267,11 +315,12 @@ func (m *Manager) bindReporters(ctx context.Context, reports []Holdings) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = m.Store.Audit(ctx, "bound", map[string]any{"consumer": rep.Node, "licence": l.Name, "by": "its account's report"})
|
||||
if err := m.PutBinding(ctx, rep.Node, BindingState{Licence: l.Name, Kind: l.Kind, Generation: b.Generation}); err != nil {
|
||||
c := Cause{Why: rep.Node + " reported the account of " + l.Name + " and was bound to nothing", By: Itself}
|
||||
m.moved(ctx, rep.Node, "", l.Name, b.Generation, c)
|
||||
if err := m.PutBinding(ctx, rep.Node, BindingState{Licence: l.Name, Kind: l.Kind, Generation: b.Generation,
|
||||
Why: c.Why, By: c.By}); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Log("bound %s to %s, the licence its account already has", rep.Node, l.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -351,21 +400,21 @@ func (m *Manager) adoptOne(ctx context.Context, c Candidate, reports []Holdings)
|
||||
|
||||
// A login moves its node (ADR 0209): the node this login came from is bound to its licence —
|
||||
// switched, if it was bound to another. Then every node reporting this account and bound to nothing.
|
||||
// Who asked is the person who logged in there; the mesh knows them only as that login (novox/hq issue 625).
|
||||
cause := Cause{Why: "a login to " + l.Name + " on " + c.Node + " (ADR 0209)", By: Itself}
|
||||
ctx = WithCause(ctx, cause)
|
||||
if b, err := m.Store.Binding(ctx, c.Node); err != nil {
|
||||
return "", err
|
||||
} else if b == nil || b.Licence != l.Name {
|
||||
if _, err := m.Store.Bind(ctx, c.Node, l.Name); err != nil {
|
||||
moved, err := m.Store.Bind(ctx, c.Node, l.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
from := ""
|
||||
if b != nil {
|
||||
from = b.Licence
|
||||
}
|
||||
_ = m.Store.Audit(ctx, map[bool]string{true: "switched", false: "bound"}[b != nil],
|
||||
map[string]any{"consumer": c.Node, "licence": l.Name, "from": from, "by": "a login there"})
|
||||
if b != nil {
|
||||
m.Log("%s moved from %s to %s: a login there", c.Node, from, l.Name)
|
||||
}
|
||||
m.moved(ctx, c.Node, from, l.Name, moved.Generation, cause)
|
||||
}
|
||||
for _, rep := range reports {
|
||||
if rep.Identity == nil || rep.Identity.AccountUUID != c.Identity.AccountUUID {
|
||||
@@ -374,8 +423,9 @@ func (m *Manager) adoptOne(ctx context.Context, c Candidate, reports []Holdings)
|
||||
if b, err := m.Store.Binding(ctx, rep.Node); err != nil || b != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := m.Store.Bind(ctx, rep.Node, l.Name); err == nil {
|
||||
_ = m.Store.Audit(ctx, "bound", map[string]any{"consumer": rep.Node, "licence": l.Name, "by": "its login"})
|
||||
if b, err := m.Store.Bind(ctx, rep.Node, l.Name); err == nil {
|
||||
m.moved(ctx, rep.Node, "", l.Name, b.Generation,
|
||||
Cause{Why: rep.Node + " reports the account of " + l.Name + ", logged in on " + c.Node + ", and was bound to nothing", By: Itself})
|
||||
}
|
||||
}
|
||||
if err := m.publishAdvance(ctx, l); err != nil {
|
||||
@@ -399,14 +449,17 @@ func (m *Manager) keep(l *Licence, g FullGrant) {
|
||||
l.RotatedAt = m.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// publishAdvance gives every consumer of a licence a new generation, and tells each in the state.
|
||||
// publishAdvance gives every consumer of a licence a new generation, and tells each in the state with the
|
||||
// cause ctx carries, so the node says why it fetched again (novox/hq issue 625).
|
||||
func (m *Manager) publishAdvance(ctx context.Context, l Licence) error {
|
||||
bindings, err := m.Store.Advance(ctx, l.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := CauseOf(ctx)
|
||||
for _, b := range bindings {
|
||||
if err := m.PutBinding(ctx, b.Consumer, BindingState{Licence: b.Licence, Kind: l.Kind, Generation: b.Generation}); err != nil {
|
||||
if err := m.PutBinding(ctx, b.Consumer, BindingState{Licence: b.Licence, Kind: l.Kind, Generation: b.Generation,
|
||||
Why: c.Why, By: c.By}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -482,7 +535,19 @@ func (m *Manager) Rotate(ctx context.Context, name string, force bool) (map[stri
|
||||
if err := m.Store.SaveLicence(ctx, *l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = m.Store.Audit(ctx, "rotated", map[string]any{"licence": name, "expiresAt": l.AccessExpiresAt})
|
||||
c := CauseOf(ctx)
|
||||
if force {
|
||||
c.Why = "a refresh asked for: " + c.Why
|
||||
} else {
|
||||
c = Cause{Why: "due: older than its cadence or near its expiry", By: Itself}
|
||||
if by := CauseOf(ctx); by.By != Itself {
|
||||
c.By = by.By
|
||||
}
|
||||
}
|
||||
ctx = WithCause(ctx, Cause{Why: "a rotation of " + name + " (" + c.Why + ")", By: c.By})
|
||||
m.record(ctx, "rotated", fmt.Sprintf("rotated %s, its token now expiring %s: %s; asked by %s", name,
|
||||
time.UnixMilli(l.AccessExpiresAt).UTC().Format(time.RFC3339), c.Why, c.By),
|
||||
map[string]any{"licence": name, "expiresAt": l.AccessExpiresAt, "why": c.Why, "by": c.By})
|
||||
if err := m.publishAdvance(ctx, *l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -594,8 +659,9 @@ func (m *Manager) Current(ctx context.Context, consumer, publicKey string) (map[
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Bind binds or switches a consumer: a person's act (ADR 0183), told to the consumer as a new generation.
|
||||
func (m *Manager) Bind(ctx context.Context, consumer, licence, by string) (map[string]any, error) {
|
||||
// Bind binds or switches a consumer: a person's act (ADR 0183), told to the consumer as a new generation, and
|
||||
// recorded with the cause ctx carries — the verb and its proven caller (novox/hq issue 625).
|
||||
func (m *Manager) Bind(ctx context.Context, consumer, licence string) (map[string]any, error) {
|
||||
l, err := m.Store.Licence(ctx, licence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -611,19 +677,21 @@ func (m *Manager) Bind(ctx context.Context, consumer, licence, by string) (map[s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from, what := "", "bound"
|
||||
from := ""
|
||||
if before != nil {
|
||||
from, what = before.Licence, "switched"
|
||||
from = before.Licence
|
||||
}
|
||||
_ = m.Store.Audit(ctx, what, map[string]any{"consumer": consumer, "licence": licence, "from": from, "by": by})
|
||||
if err := m.PutBinding(ctx, consumer, BindingState{Licence: licence, Kind: l.Kind, Generation: b.Generation}); err != nil {
|
||||
c := CauseOf(ctx)
|
||||
m.moved(ctx, consumer, from, licence, b.Generation, c)
|
||||
if err := m.PutBinding(ctx, consumer, BindingState{Licence: licence, Kind: l.Kind, Generation: b.Generation,
|
||||
Why: c.Why, By: c.By}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"consumer": consumer, "licence": licence, "generation": b.Generation, "from": from}, nil
|
||||
}
|
||||
|
||||
// Release unbinds a consumer; its node keeps its last token, which expires within hours.
|
||||
func (m *Manager) Release(ctx context.Context, consumer, by string) (map[string]any, error) {
|
||||
func (m *Manager) Release(ctx context.Context, consumer string) (map[string]any, error) {
|
||||
was, err := m.Store.Binding(ctx, consumer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -634,7 +702,9 @@ func (m *Manager) Release(ctx context.Context, consumer, by string) (map[string]
|
||||
if err := m.DeleteBinding(ctx, consumer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = m.Store.Audit(ctx, "released", map[string]any{"consumer": consumer, "licence": was.Licence, "by": by})
|
||||
c := CauseOf(ctx)
|
||||
m.record(ctx, "released", fmt.Sprintf("released %s from %s: %s; asked by %s", consumer, was.Licence, c.Why, c.By),
|
||||
map[string]any{"consumer": consumer, "licence": was.Licence, "generation": was.Generation, "why": c.Why, "by": c.By})
|
||||
return map[string]any{"consumer": consumer, "released": true, "was": was.Licence}, nil
|
||||
}
|
||||
|
||||
@@ -685,7 +755,10 @@ func (m *Manager) adoptKey(ctx context.Context, name, key, from string) (map[str
|
||||
if err := m.Store.SaveLicence(ctx, l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = m.Store.Audit(ctx, "adopted", map[string]any{"licence": name, "kind": "api-key", "from": from})
|
||||
c := CauseOf(ctx)
|
||||
_ = m.Store.Audit(ctx, "adopted", map[string]any{"licence": name, "kind": "api-key", "from": from, "why": c.Why, "by": c.By})
|
||||
m.Log("adopted the API key %s from %s; asked by %s", name, from, c.By)
|
||||
ctx = WithCause(ctx, Cause{Why: "the API key " + name + " adopted from " + from, By: c.By})
|
||||
if err := m.publishAdvance(ctx, l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -132,8 +132,13 @@ func TestAManagerWithNoLicenceAdoptsTheNewestLoginThatRefreshesAndNeverExchanges
|
||||
if len(bs) != 2 || mm.state["laptop"].Licence != licence1 || mm.state["server"].Licence != licence1 {
|
||||
t.Fatalf("bindings %v, state %v", bs, mm.state)
|
||||
}
|
||||
if !strings.HasPrefix(strings.Join(mm.events, "|"), "licence.adopted") {
|
||||
t.Fatalf("events %v", mm.events)
|
||||
// The adoption is said, and so is each binding it made (novox/hq issue 625).
|
||||
events := strings.Join(mm.events, "|")
|
||||
for _, want := range []string{"licence.adopted ", `licence.bound {"by":"claude-licence-manager","consumer":"laptop"`,
|
||||
`licence.bound {"by":"claude-licence-manager","consumer":"server"`} {
|
||||
if !strings.Contains(events, want) {
|
||||
t.Fatalf("events lack %s: %v", want, mm.events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,13 +239,13 @@ func TestARotationAndASwitchEachGiveANewerGeneration(t *testing.T) {
|
||||
if _, err := mm.m.AdoptKey(ctx, "api", file); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := mm.m.Bind(ctx, "laptop", "api", "test"); err != nil {
|
||||
if _, err := mm.m.Bind(ctx, "laptop", "api"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mm.state["laptop"].Licence != "api" || mm.state["laptop"].Generation <= g1 {
|
||||
t.Fatalf("a switch to a licence rotated less often went backwards: %v", mm.state["laptop"])
|
||||
}
|
||||
if _, err := mm.m.Release(ctx, "laptop", "test"); err != nil {
|
||||
if _, err := mm.m.Release(ctx, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := mm.state["laptop"]; ok {
|
||||
@@ -454,3 +459,73 @@ func TestWhatIsNotAnAPIKeyIsRefused(t *testing.T) {
|
||||
t.Fatal("nothing was kept")
|
||||
}
|
||||
}
|
||||
|
||||
// A change of what a consumer holds says what, why and who in the journal, the audit and an event (novox/hq
|
||||
// issue 625): a login, a switch through the verb by a proven caller, a rotation and a release.
|
||||
func TestEveryMoveSaysWhatWhyAndWho(t *testing.T) {
|
||||
mm := newMesh(t)
|
||||
var lines []string
|
||||
mm.m.Log = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) }
|
||||
ctx := context.Background()
|
||||
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
|
||||
if s := mm.state["laptop"]; !strings.Contains(s.Why, "a login to "+licence1+" on laptop") || s.By != Itself {
|
||||
t.Fatalf("a login's binding said %+v", s)
|
||||
}
|
||||
file := filepath.Join(t.TempDir(), "key")
|
||||
_ = os.WriteFile(file, []byte("sk-ant-api03-key-0123456789abcdefghij\n"), 0o600)
|
||||
if _, err := mm.m.AdoptKey(ctx, "api", file); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
asked := WithCause(ctx, Cause{Why: "the switch verb", By: "g14.node-tools"})
|
||||
if _, err := mm.m.Bind(asked, "laptop", "api"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s := mm.state["laptop"]; s.Why != "the switch verb" || s.By != "g14.node-tools" {
|
||||
t.Fatalf("the node is told %+v", s)
|
||||
}
|
||||
said := strings.Join(lines, "\n")
|
||||
if !strings.Contains(said, "switched laptop from "+licence1+" to api (generation ") ||
|
||||
!strings.Contains(said, "the switch verb; asked by g14.node-tools") {
|
||||
t.Fatalf("the journal said:\n%s", said)
|
||||
}
|
||||
var switched string
|
||||
for _, e := range mm.events {
|
||||
if strings.HasPrefix(e, "licence.switched ") {
|
||||
switched = e
|
||||
}
|
||||
}
|
||||
for _, want := range []string{`"by":"g14.node-tools"`, `"from":"` + licence1 + `"`, `"licence":"api"`, `"consumer":"laptop"`, `"generation":`} {
|
||||
if !strings.Contains(switched, want) {
|
||||
t.Fatalf("the event %q lacks %s; events %v", switched, want, mm.events)
|
||||
}
|
||||
}
|
||||
last := mm.store.Audits[len(mm.store.Audits)-1]
|
||||
if raw, _ := json.Marshal(last); !strings.Contains(string(raw), `"by":"g14.node-tools"`) || !strings.Contains(string(raw), "switched") {
|
||||
t.Fatalf("the audit kept %s", raw)
|
||||
}
|
||||
|
||||
_, _ = mm.m.Rotate(WithCause(ctx, Cause{Why: "the refresh verb", By: "novox.node-tools"}), licence1, true)
|
||||
if !strings.Contains(strings.Join(mm.events, "|"), `licence.rotated {"by":"novox.node-tools"`) {
|
||||
t.Fatalf("a rotation asked for said no caller: %v", mm.events)
|
||||
}
|
||||
|
||||
if _, err := mm.m.Release(WithCause(ctx, Cause{Why: "the release verb", By: "person.operator"}), "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if said := strings.Join(lines, "\n"); !strings.Contains(said, "released laptop from api: the release verb; asked by person.operator") {
|
||||
t.Fatalf("the journal said:\n%s", said)
|
||||
}
|
||||
for _, e := range mm.events {
|
||||
if strings.Contains(e, "rt-a") || strings.Contains(e, "sk-ant-") || strings.Contains(e, "local-laptop") {
|
||||
t.Fatalf("an event carries a secret: %s", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A call with no cause is said as this module's own, never as nobody.
|
||||
func TestNoCauseReadsAsTheModulesOwn(t *testing.T) {
|
||||
if c := CauseOf(context.Background()); c.By != Itself || c.Why == "" {
|
||||
t.Fatalf("no cause read as %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module claude-licence-manager
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.7
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac h1:jnZ/n+fFMo7VQPNNXK1gjStHIJPw3SPbzPxsf/PpZqw=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261012082113-9747f3e1b1ac/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
],
|
||||
"contributes": {
|
||||
"postgres-database": {
|
||||
"name": "claude_licences"
|
||||
"name": "claude_licences",
|
||||
"readable": [
|
||||
"audit",
|
||||
"binding"
|
||||
]
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
@@ -59,6 +63,10 @@
|
||||
"emits": [
|
||||
"licence.adopted",
|
||||
"licence.refused",
|
||||
"licence.bound",
|
||||
"licence.switched",
|
||||
"licence.released",
|
||||
"licence.rotated",
|
||||
"licence.failing",
|
||||
"usage.read"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user