Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
831f34e59a | ||
|
|
86be014907 | ||
|
|
06358f94bd | ||
|
|
c52afdf88b | ||
|
|
019614aedc | ||
|
|
fe7c5ccb64 | ||
|
|
fa6dc3a48b | ||
|
|
7c5df1d7aa | ||
|
|
7f8701637f | ||
|
|
067f2f4d61 | ||
|
|
0ef81d85b3 | ||
|
|
04379702e5 | ||
|
|
9a3ead8fcd | ||
|
|
fda81c129e | ||
|
|
ecc7621274 | ||
|
|
3ed3266dde | ||
|
|
9e9877e6ec | ||
|
|
0f0ec66e9a | ||
|
|
344fc6f6bf | ||
|
|
70396caeab | ||
|
|
4aa24ba013 | ||
|
|
067117b8b8 | ||
|
|
e7b3fd470b | ||
|
|
f8e4c1b0ae | ||
|
|
a73b02ea30 | ||
|
|
f83e6e4713 | ||
|
|
f723d4372f | ||
|
|
b44e600e87 | ||
|
|
ab610ffed4 | ||
|
|
c4a6452f21 | ||
|
|
e009088150 | ||
|
|
40c4fabff6 | ||
|
|
0bfc72d230 | ||
|
|
32c4c52261 | ||
|
|
d2de18fd6b | ||
|
|
63de1f7e0d | ||
|
|
9523050a98 | ||
|
|
326897d877 | ||
|
|
a3e9f2cc0f | ||
|
|
5cdf8295f3 | ||
|
|
0e6c358623 | ||
|
|
fe7d4504bf | ||
|
|
db104bdb08 | ||
|
|
c84dfb5e0b | ||
|
|
67e4d4b062 | ||
|
|
f41a5a8df1 | ||
|
|
09824b4839 | ||
|
|
272eff8b39 | ||
|
|
9249ca7b57 | ||
|
|
049bd176c3 | ||
|
|
98e08c0dd0 | ||
|
|
2c5e9c9a98 | ||
|
|
e4382d016e | ||
|
|
2877b87f29 | ||
|
|
a8c1c275dd | ||
|
|
204a02240c | ||
|
|
432de55186 | ||
|
|
2f52f91b5c | ||
|
|
c1a32b4385 | ||
|
|
4497f26257 | ||
|
|
fc5b849d85 | ||
|
|
ce3bedd584 | ||
|
|
90f7f87c2a | ||
|
|
68b5febee7 | ||
|
|
3483f603ae | ||
|
|
5ccce4967a | ||
|
|
b3fcf408f2 | ||
|
|
3d1a450406 | ||
|
|
7375d613b4 | ||
|
|
0471b2e105 | ||
|
|
2f8cb338ff | ||
|
|
a6c2f7b31a | ||
|
|
2b495c4818 | ||
|
|
37613ec6a9 | ||
|
|
cff7588797 | ||
|
|
f06335cf7e | ||
|
|
c414d27c42 | ||
|
|
fc300e3d72 | ||
|
|
2002b08505 | ||
|
|
6de5c6680e | ||
|
|
4a6395feae | ||
|
|
1db0181920 | ||
|
|
8c07d2ef67 | ||
|
|
a673fc52a7 | ||
|
|
b0dc7e08de | ||
|
|
aa4a4253ad | ||
|
|
cd7582a0f2 | ||
|
|
1d3e2b8aca | ||
|
|
462bcfe451 | ||
|
|
432326264f | ||
|
|
3e08301b27 |
@@ -2,6 +2,7 @@ node_modules/
|
||||
dist/
|
||||
|
||||
# Go tool bundles built in place (go build in a module's cmd/<name>-tools) are build output.
|
||||
modules/pacman/pacman-tools
|
||||
modules/slack/cmd/slack-tools/slack-tools
|
||||
modules/jetbrains-toolbox/cmd/toolbox-tools/toolbox-tools
|
||||
modules/messenger/cmd/messenger/messenger
|
||||
@@ -41,3 +42,5 @@ modules/mesh-config/cmd/mesh-config/mesh-config
|
||||
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
||||
modules/networkmanager/uplink-tools
|
||||
checks/words/words
|
||||
modules/mesh-cli/mesh-cli
|
||||
modules/mesh-cli/cmd/mesh-cli/mesh-cli
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// Package controllerdeps is the catalogue's dependence check on the controller (novox/hq ADR 0590 §5, issue 1126): the
|
||||
// controller is not placed, so no module of this tree reads, requires, invokes, consumes, waits on, uses or otherwise
|
||||
// names it. Nothing is kept: not the lease, not the entries once kept to its write (ADR 0571 §13, superseded with ADR
|
||||
// 0582). A later decision that places a controller again must first lift this check.
|
||||
//
|
||||
// It refuses, over the tree at root:
|
||||
//
|
||||
// - a directory modules/mesh-controller: the controller is no module of the catalogue;
|
||||
// - a manifest string naming the controller's seat, a state, verb or event of it, or one of its buckets
|
||||
// (`mesh-controller.<name>`, `seat:mesh-controller.<verb>`, `mesh-controller_<bucket>`), anywhere in the manifest;
|
||||
// - a manifest string that is the bare module name `mesh-controller` outside the module's own `config`, where a value
|
||||
// is the module's data (an issue owner naming the controller's repository is no dependency), not an address;
|
||||
// - a line of any module's file naming one of the controller's buckets (`mesh-controller_`): its buckets are removed
|
||||
// after the controller's end (ADR 0590 §6), so a reader of one reads nothing. A test (`_test.go`, `.test.ts`) reads
|
||||
// no live bucket, its names are its own data, and a `testdata` directory is a copy of text from elsewhere (a design,
|
||||
// a backlog): both are skipped.
|
||||
//
|
||||
// The rule is said once, here, for the catalogue: decoding stays the manifests check's (a manifest that is no JSON is
|
||||
// an error here, never judged).
|
||||
package controllerdeps
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Module is the controller's module name, and Bucket the prefix every bucket of its carries.
|
||||
const (
|
||||
Module = "mesh-controller"
|
||||
Bucket = Module + "_"
|
||||
)
|
||||
|
||||
// address is a string naming the controller's seat, one of its states, verbs or events, or one of its buckets.
|
||||
var address = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller([._][A-Za-z0-9])`)
|
||||
|
||||
// Refusals is each reason the check gives over the tree at root, sorted; none when it passes.
|
||||
func Refusals(root string) ([]string, error) {
|
||||
var out []string
|
||||
if _, err := os.Lstat(filepath.Join(root, "modules", Module)); err == nil {
|
||||
out = append(out, Module+": the controller is no module of the catalogue: it is not placed and nothing may depend on it (novox/hq ADR 0590 §5)")
|
||||
}
|
||||
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range files {
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc any
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", f, err)
|
||||
}
|
||||
module := filepath.Base(filepath.Dir(f))
|
||||
for _, s := range names(doc, false, nil) {
|
||||
out = append(out, fmt.Sprintf("%s: names %q: the controller is not placed, so no manifest reads, requires, invokes, "+
|
||||
"consumes, waits on or uses it; name the seat that holds the job now (novox/hq ADR 0590 §5)", module, s))
|
||||
}
|
||||
}
|
||||
buckets, err := bucketLines(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, buckets...)
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// names collects the strings of a decoded manifest that name the controller; inConfig is under the module's `config`.
|
||||
func names(v any, inConfig bool, into []string) []string {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
if address.MatchString(v) || (!inConfig && strings.TrimSpace(v) == Module) {
|
||||
into = append(into, v)
|
||||
}
|
||||
case []any:
|
||||
for _, e := range v {
|
||||
into = names(e, inConfig, into)
|
||||
}
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
into = names(v[k], inConfig || k == "config", into)
|
||||
}
|
||||
}
|
||||
return into
|
||||
}
|
||||
|
||||
// bucketLines is every line of a module's file, outside tests and testdata, that names one of the controller's buckets.
|
||||
func bucketLines(root string) ([]string, error) {
|
||||
var out []string
|
||||
modules := filepath.Join(root, "modules")
|
||||
err := filepath.WalkDir(modules, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) && path == modules {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if n := d.Name(); n == "testdata" || n == "node_modules" || n == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !d.Type().IsRegular() || strings.HasSuffix(d.Name(), "_test.go") || strings.HasSuffix(d.Name(), ".test.ts") {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
for n := 1; sc.Scan(); n++ {
|
||||
if strings.Contains(sc.Text(), Bucket) {
|
||||
out = append(out, fmt.Sprintf("%s:%d: names the controller's bucket (%s…): its buckets are removed after its "+
|
||||
"end (novox/hq ADR 0590 §6); read the seat that holds the job now", filepath.ToSlash(rel), n, Bucket))
|
||||
}
|
||||
}
|
||||
return sc.Err()
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package controllerdeps
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ADR 0590 §5 (issue 1126 B10): no manifest of the catalogue at this head reads, invokes, consumes, waits on, uses or
|
||||
// otherwise names the controller's seat, states or buckets, and no module's code names a bucket of the controller's.
|
||||
func TestNothingInTheCatalogueNamesTheController(t *testing.T) {
|
||||
got, err := Refusals("../..")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range got {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
||||
|
||||
// The check refuses every name of the controller, the lease and the entries once kept to its write among them, on a
|
||||
// tree of its own; a repository named mesh-controller (an issue's owner) and a word in prose are no dependency.
|
||||
func TestRefusalsJudgeATree(t *testing.T) {
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
want string // empty: passes
|
||||
}{
|
||||
{"the lease is refused like any read", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.lease"]}`}, `a: names "mesh-controller.lease"`},
|
||||
{"a verb once kept to the write", map[string]string{"modules/b/module.json": `{"module":"b","invokes":["seat:mesh-controller.collect"]}`}, `b: names "seat:mesh-controller.collect"`},
|
||||
{"an event consumed", map[string]string{"modules/c/module.json": `{"module":"c","consumes":[{"event":"mesh-controller.artifacts-gone"}]}`}, "mesh-controller.artifacts-gone"},
|
||||
{"a requirement", map[string]string{"modules/d/module.json": `{"module":"d","requires":[{"name":"x","from":"mesh-controller"}]}`}, `d: names "mesh-controller"`},
|
||||
{"the controller's own module", map[string]string{"modules/mesh-controller/module.json": `{"module":"mesh-controller"}`}, "mesh-controller: the controller is no module of the catalogue"},
|
||||
{"a bucket in code", map[string]string{"modules/e/cmd/e/main.go": "package main\nconst b = \"mesh-controller_assignments\"\n"}, "modules/e/cmd/e/main.go:2: names the controller's bucket"},
|
||||
{"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""},
|
||||
{"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""},
|
||||
{"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""},
|
||||
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-placement.placement"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for path, text := range row.files {
|
||||
p := filepath.Join(root, path)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, err := Refusals(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if joined := strings.Join(got, "\n"); (row.want == "") != (joined == "") || !strings.Contains(joined, row.want) {
|
||||
t.Errorf("refusals %q, want %q", joined, row.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module checks/controllerdeps
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,3 @@
|
||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
||||
# every module's directory: its manifest, for a name of the controller, and its files, for one of the controller's buckets.
|
||||
modules
|
||||
@@ -2,4 +2,4 @@ module checks/credentialform
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# The calls of the controller's hand-act log a manifest of this catalogue may still invoke, each `<module> <verb> <why>`:
|
||||
# the fallback of a recorder (mesh-sdk handacts.Recorder) while the seat mesh-hand-acts has no holder that answers, until
|
||||
# the controller's lease is written to a holder that is no controller. The check of this directory (handacts) refuses any
|
||||
# other, and an entry whose module no longer invokes it: the list only shrinks, and it is empty before the lease write
|
||||
# (novox/hq ADR 0565 §11; issue 1953, issue 774). It is empty: no manifest invokes the controller's log (issue 1953, 1955).
|
||||
@@ -1,3 +0,0 @@
|
||||
module git.novox.be/novox/mesh-catalog/checks/handacts
|
||||
|
||||
go 1.22
|
||||
@@ -1,109 +0,0 @@
|
||||
// Command handacts refuses a manifest that records into the controller's hand-act log (novox/hq ADR 0565 §11): every
|
||||
// recorder records through the seat mesh-hand-acts from its delivery, so no manifest invokes
|
||||
// `seat:mesh-controller.hand-act`, `.warranted` or `.drill`. The recorders' fallbacks to the controller's log while the
|
||||
// seat has no holder are named in checks/handacts/fallbacks, each module and verb with why; that list only shrinks, and
|
||||
// an entry whose module no longer invokes the verb is refused too.
|
||||
//
|
||||
// go run . <catalog root>
|
||||
//
|
||||
// Exits 1 on a finding.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Verbs are the controller's hand-act verbs, as a manifest invokes them, and Allowed the list of named fallbacks.
|
||||
var Verbs = []string{"hand-act", "warranted", "drill"}
|
||||
|
||||
const (
|
||||
invokePrefix = "seat:mesh-controller."
|
||||
Allowed = "checks/handacts/fallbacks"
|
||||
)
|
||||
|
||||
// Findings is every refusal in the catalog at root, sorted.
|
||||
func Findings(root string) ([]string, error) {
|
||||
allowed := map[string]bool{}
|
||||
raw, err := os.ReadFile(filepath.Join(root, Allowed))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if f := strings.Fields(line); len(f) >= 3 && !strings.HasPrefix(f[0], "#") {
|
||||
allowed[f[0]+" "+f[1]] = true
|
||||
} else if len(f) > 0 && !strings.HasPrefix(f[0], "#") {
|
||||
return nil, fmt.Errorf("%s: %q is not `<module> <verb> <why>`", Allowed, line)
|
||||
}
|
||||
}
|
||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
invoked := map[string]bool{}
|
||||
for _, path := range manifests {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m struct {
|
||||
Module string `json:"module"`
|
||||
Invokes []string `json:"invokes"`
|
||||
}
|
||||
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
|
||||
continue
|
||||
}
|
||||
for _, inv := range m.Invokes {
|
||||
verb, ok := strings.CutPrefix(strings.TrimSpace(inv), invokePrefix)
|
||||
if !ok || !contains(Verbs, verb) {
|
||||
continue
|
||||
}
|
||||
invoked[m.Module+" "+verb] = true
|
||||
if !allowed[m.Module+" "+verb] {
|
||||
out = append(out, fmt.Sprintf("%s invokes %s%s: record through seat:mesh-hand-acts.%s (novox/hq ADR 0565 §11), "+
|
||||
"or name the fallback in %s", m.Module, invokePrefix, verb, verb, Allowed))
|
||||
}
|
||||
}
|
||||
}
|
||||
for entry := range allowed {
|
||||
if !invoked[entry] {
|
||||
out = append(out, fmt.Sprintf("%s names %q, which is no longer invoked: take it off the list, which only shrinks",
|
||||
Allowed, entry))
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: handacts <catalog root>")
|
||||
os.Exit(2)
|
||||
}
|
||||
found, err := Findings(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, f := range found {
|
||||
fmt.Println(f)
|
||||
}
|
||||
if len(found) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("no manifest records into the controller's hand-act log but the fallbacks named")
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// No manifest invokes the controller's hand-act verbs but a named fallback; a named fallback no longer invoked is
|
||||
// refused (novox/hq ADR 0565 §11's catalog test).
|
||||
func TestFindings(t *testing.T) {
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
want string // empty: clean
|
||||
}{
|
||||
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
|
||||
{"an unnamed call", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.warranted"]}`},
|
||||
"a invokes seat:mesh-controller.warranted"},
|
||||
{"a named fallback, and the controller's own", map[string]string{
|
||||
"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.drill"]}`,
|
||||
"modules/mesh-controller/module.json": `{"module":"mesh-controller","invokes":["seat:mesh-controller.hand-act"]}`,
|
||||
Allowed: "# why\na drill fallback until the lease write\n"}, ""},
|
||||
{"a fallback no longer invoked", map[string]string{Allowed: "gone hand-act fallback\n"}, `names "gone hand-act"`},
|
||||
{"another verb of the controller", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.conditions"]}`}, ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for path, text := range row.files {
|
||||
p := filepath.Join(root, path)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, err := Findings(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
|
||||
t.Fatalf("findings = %q, want one containing %q", all, row.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The run of this check over the real tree.
|
||||
func TestTheCatalogRecordsOnlyThroughTheSeat(t *testing.T) {
|
||||
got, err := Findings(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range got {
|
||||
t.Error(f)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
||||
# every manifest, for what it invokes.
|
||||
modules/*/module.json
|
||||
@@ -2,4 +2,4 @@ module checks/health
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
|
||||
@@ -2,4 +2,4 @@ module checks/manifests
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# The modules of this catalogue that still read the controller's assignments (mesh-controller.assignments), each with
|
||||
# why. The check of this directory (noplacement) refuses any other module that reads them, and an entry here whose module no longer
|
||||
# does: the list only shrinks, and it is empty before the controller's lease is written to a holder that is no
|
||||
# controller (novox/hq ADR 0549 §3, §6; issue 774).
|
||||
@@ -1,7 +1,3 @@
|
||||
module git.novox.be/novox/mesh-catalog/checks/noplacement
|
||||
|
||||
go 1.22
|
||||
|
||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
||||
|
||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
|
||||
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`;
|
||||
//
|
||||
// 4. a manifest, other than mesh-controller's, that reads the controller's assignments (`mesh-controller.assignments`):
|
||||
// every reader takes placement from the seat mesh-placement (novox/hq ADR 0549 §3, ADR 0567), and the lease write is
|
||||
// refused while one still reads them (ADR 0549 §6; issue 774). The modules that still read them for what the
|
||||
// placement does not carry are named in checks/noplacement/assignments-readers, each with why; that list only
|
||||
// shrinks: an entry whose module no longer reads them is refused too.
|
||||
// The controller's assignments, which item 4 once listed readers of, are refused with every other name of the
|
||||
// controller by checks/controllerdeps (novox/hq ADR 0590 §5).
|
||||
//
|
||||
// go run . <catalogue root>
|
||||
//
|
||||
@@ -61,74 +58,10 @@ func Findings(root string) ([]string, error) {
|
||||
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
|
||||
}
|
||||
}
|
||||
readers, err := assignmentsReaders(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, readers...)
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AssignmentsRead is the controller's assignments as a manifest reads them, and Allowed the file, relative to the
|
||||
// catalogue's root, that names the modules still reading them: `<module> <why>` per line, `#` a comment.
|
||||
const (
|
||||
AssignmentsRead = "mesh-controller.assignments"
|
||||
Allowed = "checks/noplacement/assignments-readers"
|
||||
)
|
||||
|
||||
// assignmentsReaders is every finding on the reads of the controller's assignments: a module other than the controller
|
||||
// reading them that Allowed does not name, and a module Allowed names that no longer reads them (issue 774).
|
||||
func assignmentsReaders(root string) ([]string, error) {
|
||||
allowed := map[string]bool{}
|
||||
raw, err := os.ReadFile(filepath.Join(root, Allowed))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
|
||||
allowed[f[0]] = true
|
||||
}
|
||||
}
|
||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var found []string
|
||||
reads := map[string]bool{}
|
||||
for _, path := range manifests {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m struct {
|
||||
Module string `json:"module"`
|
||||
Reads []string `json:"reads"`
|
||||
}
|
||||
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
|
||||
continue // a manifest that does not read is the manifest checks' to refuse
|
||||
}
|
||||
for _, r := range m.Reads {
|
||||
if strings.TrimSpace(r) != AssignmentsRead {
|
||||
continue
|
||||
}
|
||||
reads[m.Module] = true
|
||||
if !allowed[m.Module] {
|
||||
found = append(found, fmt.Sprintf("%s reads %s: read where modules run from mesh-delivery-assigner.placement, "+
|
||||
"the seat mesh-placement's (novox/hq ADR 0549 §3, ADR 0567); the lease write is refused while a module reads "+
|
||||
"the assignments (ADR 0549 §6)", m.Module, AssignmentsRead))
|
||||
}
|
||||
}
|
||||
}
|
||||
for module := range allowed {
|
||||
if !reads[module] {
|
||||
found = append(found, fmt.Sprintf("%s names %s, which no longer reads %s: take it off the list, which only shrinks",
|
||||
Allowed, module, AssignmentsRead))
|
||||
}
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// named collects the strings of a decoded manifest that start with one of the Prefixes.
|
||||
func named(v any, into []string) []string {
|
||||
switch v := v.(type) {
|
||||
@@ -166,5 +99,5 @@ func main() {
|
||||
if len(found) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("no placement directory, no manifest reads the indexer's placement, and only the modules listed read the controller's assignments")
|
||||
fmt.Println("no placement directory, and no manifest reads the indexer's placement")
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
||||
)
|
||||
|
||||
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
|
||||
@@ -21,12 +19,6 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
|
||||
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
|
||||
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
|
||||
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
|
||||
// Issue 774 (ADR 0549 §3, §6): a reader of the controller's assignments is refused unless the list names it, the
|
||||
// controller's own manifest is not judged, and a name on the list whose module no longer reads them is refused.
|
||||
{"an unlisted reader of the assignments", map[string]string{"modules/r/module.json": `{"module":"r","reads":["mesh-controller.assignments"]}`}, "r reads mesh-controller.assignments"},
|
||||
{"a listed reader and the controller", map[string]string{"modules/l/module.json": `{"module":"l","reads":["mesh-controller.assignments"]}`,
|
||||
"modules/mesh-controller/module.json": `{"module":"mesh-controller","reads":["mesh-controller.assignments"]}`, Allowed: "# why\nl its plan\n"}, ""},
|
||||
{"a listed module that reads them no more", map[string]string{Allowed: "gone no longer reads\n"}, "names gone, which no longer reads"},
|
||||
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
@@ -60,29 +52,4 @@ func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
|
||||
for _, f := range got {
|
||||
t.Error(f)
|
||||
}
|
||||
// Issue 774 (ADR 0549 §3, §6; the comparison of novox/hq ADR 0529 clause 2): the list of modules still reading the
|
||||
// controller's assignments gains none the trunk's copy at the merge base does not hold. Its first copy gains nothing.
|
||||
path, ok := trunk.Copy(t, Allowed)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
names := func(path string) map[string]bool {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
|
||||
out[f[0]] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
base := names(path)
|
||||
for module := range names(filepath.Join("..", "..", Allowed)) {
|
||||
if !base[module] {
|
||||
t.Errorf("%s gains %s, which the trunk's copy does not hold: the list only shrinks", Allowed, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ module checks/readable
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
|
||||
+37
-1
@@ -4,7 +4,9 @@
|
||||
// own code that is not a test (a tool's description, the notes and errors it answers with) and every
|
||||
// description in its manifest. Comments are not read: an agent never sees them. And no daemon's line
|
||||
// (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to
|
||||
// type of the node-engine's or the controller's binary, where the operator's command is `nox`.
|
||||
// type of the node-engine's or the controller's binary, where the operator's command is `nox`. And no address of the
|
||||
// controller (novox/hq ADR 0590 §5): it is not placed, so a text naming one of its verbs, states or buckets sends its
|
||||
// reader nowhere.
|
||||
//
|
||||
// A word is matched whole and in any case, a space in it matching any run of white space, and its plural
|
||||
// (`s` or `es` on its last part) is matched as the word. A vendor word is
|
||||
@@ -89,11 +91,45 @@ func run(root string) int {
|
||||
len(findings))
|
||||
return 1
|
||||
}
|
||||
// And no address of the controller (novox/hq ADR 0590 §5): it is not placed, so a text that sends an agent or the
|
||||
// operator to one of its verbs, states or buckets sends them nowhere.
|
||||
if findings := checkAddresses(texts); len(findings) > 0 {
|
||||
for _, f := range findings {
|
||||
fmt.Println(f)
|
||||
}
|
||||
fmt.Printf("words: %d address(es) of the controller in what a module shows — name the seat that holds the job now (novox/hq ADR 0590)\n",
|
||||
len(findings))
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
|
||||
len(texts), files)
|
||||
return 0
|
||||
}
|
||||
|
||||
// controllerAddress is an address of the controller: its seat's verb or state (`mesh-controller.<name>`, with or without
|
||||
// `seat:`) or one of its buckets (`mesh-controller_<bucket>`). The module's name in prose is no address.
|
||||
var controllerAddress = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller[._][A-Za-z0-9<]`)
|
||||
|
||||
// checkAddresses is every text that names an address of the controller (novox/hq ADR 0590 §5).
|
||||
func checkAddresses(texts []text) []string {
|
||||
var out []string
|
||||
for _, t := range texts {
|
||||
if !strings.Contains(t.s, "mesh-controller") {
|
||||
continue
|
||||
}
|
||||
if m := controllerAddress.FindStringIndex(t.s); m != nil {
|
||||
line := t.line
|
||||
if line > 0 {
|
||||
line += strings.Count(t.s[:m[0]], "\n")
|
||||
}
|
||||
out = append(out, fmt.Sprintf("%s:%d: names an address of the controller, which is not placed — in %q", t.file, line,
|
||||
excerpt(t.s, m[0], m[1])))
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of
|
||||
// its manifests.
|
||||
func collect(root string) ([]text, int, error) {
|
||||
|
||||
@@ -95,3 +95,25 @@ func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) {
|
||||
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0590 §5 (issue 1126 B10): no text a module shows names an address of the controller, which is not placed; prose
|
||||
// naming it passes.
|
||||
func TestCheckAddressesRefusesTheControllersAddresses(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
s string
|
||||
want bool
|
||||
}{
|
||||
{"push this machine (mesh-controller.push)", true},
|
||||
{"ask seat:mesh-controller.nodes", true},
|
||||
{"the bucket mesh-controller_assignments", true},
|
||||
{"`mesh-controller.<verb>`", true},
|
||||
{"the mesh-controller module is gone.", false},
|
||||
{"located in mesh-controller#246", false},
|
||||
{"ask mesh-placement.placement", false},
|
||||
} {
|
||||
got := checkAddresses([]text{{file: "m/a.go", line: 1, s: c.s}})
|
||||
if (len(got) == 1) != c.want {
|
||||
t.Errorf("checkAddresses(%q) = %q, want a finding: %v", c.s, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ module adwaita
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestOlderNamesNotReadAreSaidFirst(t *testing.T) {
|
||||
func references(t *testing.T, a *asked) func(map[string]any) (any, error) {
|
||||
f := world()
|
||||
s := &Store{Container: "mesh-registry", Run: f.run(t)}
|
||||
for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
|
||||
for _, tl := range Tools(s, recordsAsked{a}, &Catalogue{Ask: a.ask}) {
|
||||
if tl.Name == "artifact_store_references" {
|
||||
return tl.Run
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (runtimeBus) Publish(subject string, body any) error {
|
||||
return stdio.SeatPublish(subject, body, "")
|
||||
}
|
||||
|
||||
// follow watches the builder's done keys and the wants: a build's end sweeps (once the lease names no controller) and
|
||||
// follow watches the builder's done keys and the wants: a build's end sweeps and
|
||||
// observes again; a want changed observes again. Event-driven: nothing reads them on a timer.
|
||||
func follow(h *Holder) {
|
||||
<-stdio.Served()
|
||||
|
||||
@@ -9,8 +9,8 @@ package main
|
||||
// At each build's end (a done key written) it sweeps as the controller's take-in did: every kept archive held first
|
||||
// (ADR 0251 §8), then what is eligible let go of, oldest first, within the same bound per sweep, an index left for a
|
||||
// `collect` a person confirms (ADR 0251 §7), each deletion recorded in the hand-act log (seat mesh-hand-acts) and said
|
||||
// as artifacts-gone. **Nothing is let go until the controller's lease names a holder that is no controller**: until
|
||||
// then the controller's take-in sweeps, and two sweeps never run at once. It also observes S25, image-missing: a want's
|
||||
// as artifacts-gone. **No lease turns it** (issue 1126): the controller ended by its unassignment, so its take-in sweeps
|
||||
// nothing and this seat is the one sweep. It also observes S25, image-missing: a want's
|
||||
// image the store does not hold, a finding in its own state `findings` the condition keeper reads.
|
||||
|
||||
import (
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
@@ -40,7 +39,6 @@ const (
|
||||
HavesRead = "node-engine.have"
|
||||
// ManifestsRead is the indexer's manifests: the images a manifest pins by itself.
|
||||
ManifestsRead = delivery.IndexerModule + ".manifests"
|
||||
LeaseRead = buscompose.ControllerLease
|
||||
// FindingsState is the holder's own findings (health.FindingsState): image-missing.
|
||||
FindingsState = health.FindingsState
|
||||
// Module is this module, the observer its findings name.
|
||||
@@ -58,7 +56,7 @@ const (
|
||||
ImageMissing = "image-missing"
|
||||
)
|
||||
|
||||
// Bus is what the holder reads and writes on the bus: its read states (wants, builds, haves, the lease), its own
|
||||
// Bus is what the holder reads and writes on the bus: its read states (wants, builds, haves), its own
|
||||
// findings, and its seat's event. The runtime in main; a test's in memory.
|
||||
type Bus interface {
|
||||
Keys(state string) ([]string, error)
|
||||
@@ -106,13 +104,6 @@ type Build struct {
|
||||
Manifest string
|
||||
}
|
||||
|
||||
// LeaseAway says the controller's lease names a holder that is no controller: then the seat sweeps.
|
||||
func (h *Holder) LeaseAway() bool {
|
||||
raw, found, err := h.Bus.Get(LeaseRead, buscompose.ControllerLeaseKey)
|
||||
away, _ := buscompose.LeaseHandedAway(raw, found, err)
|
||||
return away
|
||||
}
|
||||
|
||||
// Builds is every done build the builder's keys hold, each with its end.
|
||||
func (h *Holder) Builds() ([]Build, error) {
|
||||
keys, err := h.Bus.Keys(BuildsRead)
|
||||
@@ -495,10 +486,6 @@ func (h *Holder) Collect(why string, confirm bool, most int) (*CollectAnswer, er
|
||||
if most > MostAtAll {
|
||||
return nil, fmt.Errorf("collect lets go of at most %d at once, not %d", MostAtAll, most)
|
||||
}
|
||||
if confirm && !h.LeaseAway() {
|
||||
return nil, errors.New("the controller holds its lease, and its take-in sweeps the store until it hands it away: " +
|
||||
"this seat lets nothing go before then (novox/hq ADR 0571 clause 2); ask the controller's collect")
|
||||
}
|
||||
return h.Sweep(context.Background(), Sweeping{Confirmed: confirm, DryRun: !confirm, Most: most, Why: why, By: "collect"})
|
||||
}
|
||||
|
||||
@@ -665,11 +652,8 @@ func (h *Holder) sayGone(j *judged, refs []string, how string) {
|
||||
}
|
||||
}
|
||||
|
||||
// AfterBuild is the sweep at a build's end: only once the lease names no controller.
|
||||
// AfterBuild is the sweep at a build's end.
|
||||
func (h *Holder) AfterBuild(key string) {
|
||||
if !h.LeaseAway() {
|
||||
return
|
||||
}
|
||||
a, err := h.Sweep(context.Background(), Sweeping{Most: MostPerSweep, By: "after the build " + key})
|
||||
if err != nil {
|
||||
h.logf("the sweep after %s: %v", key, err)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
package main
|
||||
|
||||
// The seat mesh-artifact-sweep's holder whole against a real bus (novox/hq ADR 0571 clause 2, issue 1955): the builder's done
|
||||
// keys, the wants and the controller's lease in a real nats-server's key-value buckets, a build's end heard by a watch
|
||||
// keys and the wants in a real nats-server's key-value buckets, no lease key anywhere (issue 1126), a build's end heard by a watch
|
||||
// of the builds as the runtime hands it over, each deletion recorded by a request to the seat mesh-hand-acts on the bus,
|
||||
// artifacts-gone said on the seat's subject, and S25's finding written to the holder's own bucket. Only the store's
|
||||
// door is in memory: its HTTP is the copied registry package's, tested there.
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
@@ -117,7 +116,7 @@ func (b realBus) Publish(subject string, body any) error {
|
||||
return b.nc.Publish(subject, raw)
|
||||
}
|
||||
|
||||
func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
|
||||
func TestTheSeatSweepsAtABuildsEndOnARealBusWithNoLease(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
nc, err := nats.Connect(testbus.URL(t))
|
||||
@@ -129,7 +128,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range []string{BuildsRead, WantsRead, HavesRead, LeaseRead, ManifestsRead, "artifact-store-tools." + FindingsState} {
|
||||
for _, s := range []string{BuildsRead, WantsRead, HavesRead, ManifestsRead, "artifact-store-tools." + FindingsState} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(s), History: 5}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -137,7 +136,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
|
||||
bus := realBus{ctx: ctx, nc: nc, js: js}
|
||||
|
||||
// The mesh as aMesh lays it, copied onto the real buckets: letta's seven builds, a want, the store's listing.
|
||||
mem, memb, door, _ := aMesh(t, &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3})
|
||||
mem, memb, door, _ := aMesh(t)
|
||||
// The builder's log as written today: aMesh dates it from 2026-10-01 for its fixed clock, and this holder runs on the
|
||||
// real clock, so read as dated the entries fall out of the log's 30 days and every copy is kept as unrecorded.
|
||||
shift := time.Since(memb.log[len(memb.log)-1].At)
|
||||
@@ -145,7 +144,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
|
||||
e.At = e.At.Add(shift)
|
||||
bus.log = append(bus.log, e)
|
||||
}
|
||||
for _, s := range []string{BuildsRead, WantsRead, LeaseRead, ManifestsRead} {
|
||||
for _, s := range []string{BuildsRead, WantsRead, ManifestsRead} {
|
||||
keys, _ := memb.Keys(s)
|
||||
for _, k := range keys[:min(len(keys), 6)] { // the seventh build ends later, on the bus
|
||||
raw, _, _ := memb.Get(s, k)
|
||||
@@ -175,7 +174,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &Holder{Bus: findingsAt{realBus: bus}, Door: mem.Door, Listed: mem.Listed, Now: time.Now, Logf: t.Logf}
|
||||
h.Acts = handacts.Recorder{LeaseAway: h.LeaseAway, Ask: func(key string, body any) (json.RawMessage, error) {
|
||||
h.Acts = handacts.Recorder{Ask: func(key string, body any) (json.RawMessage, error) {
|
||||
raw, _ := json.Marshal(body)
|
||||
m, err := nc.Request("mesh.seat."+strings.TrimPrefix(strings.Replace(key, ".", ".tool.", 1), "seat:"), raw, 5*time.Second)
|
||||
if err != nil {
|
||||
@@ -204,18 +203,10 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// While the lease names the controller, a build's end lets nothing go.
|
||||
// No lease key: the build's end holds the kept archives and lets go of letta's 2nd, which the 7th moved past the five
|
||||
// newest and no want names (the want keeps the 1st), recording each deletion.
|
||||
ended()
|
||||
if len(door.asked) != 0 {
|
||||
t.Fatalf("before the lease write the store was asked %v", door.asked)
|
||||
}
|
||||
// The lease handed away: the next build's end holds the kept archives and lets go of letta's 2nd, which the 7th moved
|
||||
// past the five newest and no want names (the want keeps the 1st), recording each deletion.
|
||||
if err := bus.Put(LeaseRead, buscompose.ControllerLeaseKey, buscompose.LeaseHolder{HandedTo: "mesh-grants", Why: "the lease write"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ended()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for {
|
||||
mu.Lock()
|
||||
n := len(gone)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
@@ -139,8 +138,8 @@ func archiveRef(module string, n int) string {
|
||||
}
|
||||
|
||||
// A mesh with seven done builds of letta, the n-th ending n hours after a day began, each with an image and an archive;
|
||||
// a want names the first on ace; the store lists them all and one image no build names; the lease as given.
|
||||
func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memDoor, *[]string) {
|
||||
// a want names the first on ace; the store lists them all and one image no build names; no lease key (issue 1126).
|
||||
func aMesh(t *testing.T) (*Holder, *memBus, *memDoor, *[]string) {
|
||||
t.Helper()
|
||||
day := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||
b := &memBus{states: map[string]map[string][]byte{}, history: map[string][]stdio.StateRevision{}}
|
||||
@@ -194,9 +193,6 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD
|
||||
_ = b.Put(ManifestsRead, mk, json.RawMessage(manifest))
|
||||
want, _ := json.Marshal(delivery.Want{V: delivery.Version, FP: fp(1), Self: true})
|
||||
_ = b.Put(WantsRead, "want.ace.letta", json.RawMessage(want))
|
||||
if lease != nil {
|
||||
_ = b.Put(LeaseRead, buscompose.ControllerLeaseKey, lease)
|
||||
}
|
||||
var acts []string
|
||||
h := &Holder{Bus: b, Door: func() (Door, error) { return door, nil },
|
||||
Listed: func(context.Context) (*View, error) { return &View{L: listing}, nil },
|
||||
@@ -206,18 +202,14 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD
|
||||
acts = append(acts, key+" "+string(raw))
|
||||
return json.RawMessage(`{}`), nil
|
||||
}}}
|
||||
h.Acts.LeaseAway = h.LeaseAway
|
||||
return h, b, door, &acts
|
||||
}
|
||||
|
||||
// ADR 0571 clause 2: the keep set from the store's listing, the wants and the five newest builds; nothing let go of while
|
||||
// the lease names a controller; once it names another holder, a build's end lets go of the eligible within the bound,
|
||||
// leaves an index for a confirmed collect, records each deletion and says artifacts-gone.
|
||||
func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t *testing.T) {
|
||||
controller := &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3}
|
||||
handed := &buscompose.LeaseHolder{HandedTo: "mesh-grants", HandedBy: "person.jochen", Why: "the lease write"}
|
||||
|
||||
h, _, _, _ := aMesh(t, controller)
|
||||
// ADR 0571 clause 2, 1126/A4/AC2: the keep set from the store's listing, the wants and the five newest builds; with no
|
||||
// lease key, a build's end and a confirmed collect let go of the eligible within the bound, leave an index for a
|
||||
// confirmed collect, record each deletion as a hand act and say artifacts-gone.
|
||||
func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsWithNoLease(t *testing.T) {
|
||||
h, _, _, _ := aMesh(t)
|
||||
r, err := h.Artifacts("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -244,9 +236,10 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
|
||||
t.Errorf("the references are %d, counted %v, at %q", len(r.References), r.Counts, r.Store)
|
||||
}
|
||||
|
||||
all := []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
lease *buscompose.LeaseHolder
|
||||
how string // "build" (a build's end), "collect" (a confirmed collect), or a sweep asked with most
|
||||
index bool
|
||||
most int
|
||||
letGo []string
|
||||
@@ -254,45 +247,58 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
|
||||
skipped int
|
||||
unlog int
|
||||
}{
|
||||
{name: "the lease names a controller", lease: controller},
|
||||
{name: "no lease key", lease: nil},
|
||||
{name: "handed away", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}},
|
||||
{name: "an index waits", lease: handed, index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1},
|
||||
{name: "bounded", lease: handed, most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2},
|
||||
{name: "a build's end", how: "build", letGo: all},
|
||||
{name: "a confirmed collect", how: "collect", letGo: all},
|
||||
{name: "a sweep", most: MostPerSweep, letGo: all},
|
||||
{name: "an index waits", index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1},
|
||||
{name: "bounded", most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2},
|
||||
// A kept build the log no longer holds: what it stood on is unknown, so no copy goes.
|
||||
{name: "a kept build's bases unknown", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3},
|
||||
{name: "a kept build's bases unknown", most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h, b, door, acts := aMesh(t, c.lease)
|
||||
h, b, door, acts := aMesh(t)
|
||||
door.indexes[imageRef("letta", 2)] = c.index
|
||||
if c.unlog > 0 {
|
||||
b.log = append(b.log[:c.unlog-1:c.unlog-1], b.log[c.unlog:]...)
|
||||
}
|
||||
if c.most == 0 {
|
||||
switch c.how {
|
||||
case "build":
|
||||
h.AfterBuild("letta.x")
|
||||
if _, err := h.Collect("tidy", true, 0); err == nil {
|
||||
t.Error("a confirmed collect was let through while the controller sweeps")
|
||||
var gone []string
|
||||
for _, x := range door.asked {
|
||||
if r, ok := strings.CutPrefix(x, "let go "); ok {
|
||||
gone = append(gone, r)
|
||||
}
|
||||
}
|
||||
if len(door.asked) != 0 || len(*acts) != 0 || len(b.published) != 0 {
|
||||
t.Fatalf("before the lease write the store was asked %v, acts %v, said %v", door.asked, *acts, b.published)
|
||||
if fmt.Sprint(gone) != fmt.Sprint(c.letGo) {
|
||||
t.Fatalf("a build's end let go of %v, want %v", gone, c.letGo)
|
||||
}
|
||||
default:
|
||||
var a *CollectAnswer
|
||||
var err error
|
||||
if c.how == "collect" {
|
||||
a, err = h.Collect("tidy", true, 0)
|
||||
} else {
|
||||
a, err = h.Sweep(context.Background(), Sweeping{Most: c.most, By: "after the build letta.x"})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(a.LetGo) != fmt.Sprint(c.letGo) || a.Left != c.left || a.Skipped != c.skipped || a.Held != 6 || a.Missing != 0 {
|
||||
t.Fatalf("let go of %v (left %d, skipped %d, held %d, missing %d), want %v", a.LetGo, a.Left, a.Skipped, a.Held, a.Missing, c.letGo)
|
||||
}
|
||||
return
|
||||
}
|
||||
a, err := h.Sweep(context.Background(), Sweeping{Most: c.most, By: "after the build letta.x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(a.LetGo) != fmt.Sprint(c.letGo) || a.Left != c.left || a.Skipped != c.skipped || a.Held != 6 || a.Missing != 0 {
|
||||
t.Fatalf("let go of %v (left %d, skipped %d, held %d, missing %d), want %v", a.LetGo, a.Left, a.Skipped, a.Held, a.Missing, c.letGo)
|
||||
}
|
||||
// Every kept archive held before the first deletion.
|
||||
first := -1
|
||||
for i, x := range door.asked {
|
||||
if strings.HasPrefix(x, "let go ") && first < 0 {
|
||||
first = i
|
||||
held := 0
|
||||
for _, x := range door.asked {
|
||||
if strings.HasPrefix(x, "let go ") {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(x, "hold ") {
|
||||
held++
|
||||
}
|
||||
}
|
||||
if first != 6 {
|
||||
if held != 6 {
|
||||
t.Errorf("the store was asked %v: the six kept archives are held first", door.asked)
|
||||
}
|
||||
if len(*acts) != len(c.letGo) || !strings.HasPrefix((*acts)[0], "seat:mesh-hand-acts.hand-act ") ||
|
||||
@@ -310,7 +316,7 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
|
||||
// S25 (image-missing): a want whose build names an image the store does not hold is a finding in the seat's own state,
|
||||
// written once and deleted once the store holds it again; images names what a machine's wants and haves name.
|
||||
func TestAWantedImageTheStoreLacksIsAFindingAndImagesNamesWantsAndHaves(t *testing.T) {
|
||||
h, b, _, _ := aMesh(t, nil)
|
||||
h, b, _, _ := aMesh(t)
|
||||
view, _ := h.Listed(context.Background())
|
||||
repo, digest, _ := splitReference(imageRef("letta", 1))
|
||||
delete(view.L.Repos[repo].Revisions, digest)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The artifact-store-tools module's Go bundle (novox/hq ADR 0251 §1–3, ADR 0571 clause 2, to-be 51): a process the
|
||||
// node's runtime launches and speaks MCP over stdio to. It says what the artifact store holds — its repositories, its
|
||||
// size, and what the mesh keeps of each manifest — read from the store's own files through the store's own container,
|
||||
// and it holds the seat mesh-artifact-sweep: the keep set, the sweep at each build's end once the controller's lease names
|
||||
// no controller, and S25's image-missing.
|
||||
// and it holds the seat mesh-artifact-sweep: the keep set, the sweep at each build's end, and S25's image-missing.
|
||||
//
|
||||
// **A module beside the store, not the store's own.** A module that provides the artifact store cannot also build an
|
||||
// artifact: building publishes to the store, so the store would be needed to create itself. So these tools are a
|
||||
@@ -29,7 +28,7 @@ func main() {
|
||||
}
|
||||
h := &Holder{Bus: runtimeBus{}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
|
||||
Now: time.Now, Logf: logf}
|
||||
h.Acts = handacts.Recorder{Ask: stdio.Ask, LeaseAway: h.LeaseAway}
|
||||
h.Acts = handacts.Recorder{Ask: stdio.Ask}
|
||||
go follow(h)
|
||||
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): artifact-store-tools.
|
||||
if err := stdio.Serve("", append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...)); err != nil {
|
||||
|
||||
@@ -2,22 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// What the records say, asked of the controller and never copied (novox/hq ADR 0251 §2). The
|
||||
// controller decides what the mesh keeps; this bundle only sets that beside what the store holds.
|
||||
// What the records say, in the shape the seat's holder answers them (Holder; novox/hq ADR 0251 §2, ADR 0571 clause 2):
|
||||
// the holder decides what the mesh keeps; the tools only set that beside what the store holds.
|
||||
|
||||
// Asking is how a tool on the mesh is asked: stdio.Ask, or a test's.
|
||||
type Asking func(key string, body any) (json.RawMessage, error)
|
||||
|
||||
// ControllerSeat is the seat whose verbs the controller serves.
|
||||
const ControllerSeat = "mesh-controller"
|
||||
|
||||
// Record is one reference the mesh recorded making, as the controller's `artifacts` answers it.
|
||||
// Record is one reference the mesh recorded making, as the seat's `artifacts` answers it.
|
||||
type Record struct {
|
||||
Reference string `json:"reference"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
@@ -27,7 +21,7 @@ type Record struct {
|
||||
Why []string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// Records is the controller's `artifacts` answer.
|
||||
// Records is the seat's `artifacts` answer.
|
||||
type Records struct {
|
||||
Store string `json:"store"`
|
||||
KeptBuilds int `json:"kept_builds"`
|
||||
@@ -39,7 +33,7 @@ type Records struct {
|
||||
Note string `json:"-"`
|
||||
}
|
||||
|
||||
// CollectAnswer is the controller's `collect` answer.
|
||||
// CollectAnswer is the seat's `collect` answer.
|
||||
type CollectAnswer struct {
|
||||
DryRun bool `json:"dry_run"`
|
||||
Store string `json:"store"`
|
||||
@@ -53,15 +47,12 @@ type CollectAnswer struct {
|
||||
Skipped int `json:"skipped"`
|
||||
Left int `json:"left"`
|
||||
Stopped string `json:"stopped"`
|
||||
// Rest is whatever else the controller said, passed on as it said it.
|
||||
// Rest is whatever else the sweep said, passed on as it said it.
|
||||
Rest map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// Controller asks the controller's seat through the runtime.
|
||||
type Controller struct{ Ask Asking }
|
||||
|
||||
// answerOf reads a verb's answer whatever wraps it: the controller's {output, ok, answer}, a text the
|
||||
// runtime handed over, or the protocol's content list.
|
||||
// answerOf reads a verb's answer whatever wraps it: {output, ok, answer}, a text the runtime handed over, or the
|
||||
// protocol's content list.
|
||||
func answerOf(raw json.RawMessage) (json.RawMessage, string, bool) {
|
||||
var s string
|
||||
if json.Unmarshal(raw, &s) == nil {
|
||||
@@ -95,89 +86,11 @@ func answerOf(raw json.RawMessage) (json.RawMessage, string, bool) {
|
||||
return raw, string(raw), true
|
||||
}
|
||||
|
||||
func (c Controller) verb(name string, args map[string]any) (json.RawMessage, error) {
|
||||
if c.Ask == nil {
|
||||
return nil, errors.New("this bundle has no way to ask the controller")
|
||||
}
|
||||
raw, err := c.Ask("seat:"+ControllerSeat+"."+name, args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the controller's %s could not be asked: %w", name, err)
|
||||
}
|
||||
answer, output, ok := answerOf(raw)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the controller refused %s: %s", name, lastLine(output))
|
||||
}
|
||||
if len(answer) == 0 || string(answer) == "null" {
|
||||
return nil, fmt.Errorf("the controller's %s answered no data: %s", name, lastLine(output))
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func lastLine(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
return strings.TrimSpace(lines[len(lines)-1])
|
||||
}
|
||||
|
||||
// Artifacts asks what the records say, with the references already let go of when the answer can carry
|
||||
// them; when it cannot, without, and says so.
|
||||
func (c Controller) Artifacts(repository string) (*Records, error) {
|
||||
args := map[string]any{"collected": "true"}
|
||||
if repository != "" {
|
||||
args["repository"] = repository
|
||||
}
|
||||
raw, err := c.verb("artifacts", args)
|
||||
collected := true
|
||||
note := ""
|
||||
if err != nil {
|
||||
delete(args, "collected")
|
||||
var again error
|
||||
raw, again = c.verb("artifacts", args)
|
||||
if again != nil {
|
||||
return nil, again
|
||||
}
|
||||
collected = false
|
||||
note = "the references the mesh already let go of could not be asked for (" + err.Error() +
|
||||
"), so a manifest the mesh let go of that the store still holds is counted as unrecorded"
|
||||
}
|
||||
var r Records
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return nil, fmt.Errorf("the controller's artifacts answer is not readable: %v", err)
|
||||
}
|
||||
for i := range r.References {
|
||||
fill(&r.References[i])
|
||||
}
|
||||
r.CollectedAsked, r.Note = collected, note
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// Collect asks the controller's sweep: a dry run unless confirm, which needs why.
|
||||
func (c Controller) Collect(why string, confirm bool, most int) (*CollectAnswer, error) {
|
||||
args := map[string]any{}
|
||||
if confirm {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("a real collection needs why: nothing was asked")
|
||||
}
|
||||
args["why"] = why
|
||||
args["confirm"] = "true"
|
||||
}
|
||||
if most > 0 {
|
||||
args["most"] = strconv.Itoa(most)
|
||||
}
|
||||
raw, err := c.verb("collect", args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a CollectAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the controller's collect answer is not readable: %v", err)
|
||||
}
|
||||
_ = json.Unmarshal(raw, &a.Rest)
|
||||
if !confirm && !a.DryRun && len(a.LetGo) > 0 {
|
||||
return nil, fmt.Errorf("the controller let go of %d artifacts when only a dry run was asked", len(a.LetGo))
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// fill reads the repository, digest and kind out of a reference when the answer left them out.
|
||||
func fill(r *Record) {
|
||||
repo, digest, kind := splitReference(r.Reference)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -280,7 +281,7 @@ func TestOnlyTheirBytesLeaveOutWhatAnotherStateMarks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// asked records every ask, and answers as the controller would.
|
||||
// asked records every ask, and answers as the seat and the catalogue would.
|
||||
type asked struct {
|
||||
mu sync.Mutex
|
||||
calls []string
|
||||
@@ -293,6 +294,51 @@ type asked struct {
|
||||
noCatalogue bool
|
||||
}
|
||||
|
||||
// recordsAsked is the tools' Recorded in a test: each call asked of asked, as `artifacts` or `collect`, with the
|
||||
// arguments a real run gives the seat's sweep.
|
||||
type recordsAsked struct{ a *asked }
|
||||
|
||||
func (r recordsAsked) Artifacts(repository string) (*Records, error) {
|
||||
args := map[string]any{"collected": "true"}
|
||||
if repository != "" {
|
||||
args["repository"] = repository
|
||||
}
|
||||
raw, err := r.a.ask("artifacts", args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var recs Records
|
||||
answer, _, _ := answerOf(raw)
|
||||
if err := json.Unmarshal(answer, &recs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range recs.References {
|
||||
fill(&recs.References[i])
|
||||
}
|
||||
recs.CollectedAsked = true
|
||||
return &recs, nil
|
||||
}
|
||||
|
||||
func (r recordsAsked) Collect(why string, confirm bool, most int) (*CollectAnswer, error) {
|
||||
args := map[string]any{}
|
||||
if confirm {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, fmt.Errorf("a real collection needs why: nothing was asked")
|
||||
}
|
||||
args["why"], args["confirm"] = why, "true"
|
||||
}
|
||||
if most > 0 {
|
||||
args["most"] = strconv.Itoa(most)
|
||||
}
|
||||
raw, err := r.a.ask("collect", args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c CollectAnswer
|
||||
answer, _, _ := answerOf(raw)
|
||||
return &c, json.Unmarshal(answer, &c)
|
||||
}
|
||||
|
||||
func (a *asked) ask(key string, body any) (json.RawMessage, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
@@ -304,7 +350,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
|
||||
}
|
||||
var answer any
|
||||
switch key {
|
||||
case "seat:mesh-controller.artifacts":
|
||||
case "artifacts":
|
||||
answer = records()
|
||||
case CatalogueModules:
|
||||
if a.noCatalogue {
|
||||
@@ -331,7 +377,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
|
||||
// The catalogue's tool answers through the runtime as the protocol's content list.
|
||||
raw, _ := json.Marshal(map[string]any{"module": map[string]any{"module": name, "manifest": json.RawMessage(manifest)}})
|
||||
return json.Marshal(map[string]any{"content": []map[string]any{{"type": "text", "text": string(raw)}}})
|
||||
case "seat:mesh-controller.collect":
|
||||
case "collect":
|
||||
if args["confirm"] == "true" {
|
||||
answer = CollectAnswer{LetGo: []string{"artifact-store://app/server@" + d("m-old")}}
|
||||
} else {
|
||||
@@ -347,7 +393,7 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
|
||||
func tool(t *testing.T, a *asked, name string) func(map[string]any) (any, error) {
|
||||
f := world()
|
||||
s := &Store{Container: "mesh-registry", Run: f.run(t)}
|
||||
for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
|
||||
for _, tl := range Tools(s, recordsAsked{a}, &Catalogue{Ask: a.ask}) {
|
||||
if tl.Name == name {
|
||||
return tl.Run
|
||||
}
|
||||
@@ -390,21 +436,6 @@ func TestARealRunWithoutWhyIsRefusedAndAsksNothing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferencesFallBackWhenCollectedCannotBeCarried(t *testing.T) {
|
||||
a := &asked{fail: map[string]bool{"collected": true}}
|
||||
out, err := tool(t, a, "artifact_store_references")(map[string]any{"repository": "app/server"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := out.(map[string]any)
|
||||
if m["note"] == nil || m["manifests"] == nil {
|
||||
t.Errorf("answer %v", m)
|
||||
}
|
||||
if _, err := tool(t, a, "artifact_store_references")(map[string]any{"repository": "nope"}); err == nil {
|
||||
t.Error("an unknown repository was answered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
@@ -423,7 +454,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var served []string
|
||||
for _, tl := range Tools(&Store{}, Controller{}, &Catalogue{}) {
|
||||
for _, tl := range Tools(&Store{}, recordsAsked{&asked{}}, &Catalogue{}) {
|
||||
if !strings.HasPrefix(tl.Name, "artifact_store_") || tl.Description == "" || tl.Run == nil {
|
||||
t.Errorf("tool %q", tl.Name)
|
||||
}
|
||||
@@ -436,7 +467,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
t.Fatalf("served %v, manifest %v", served, listed)
|
||||
}
|
||||
// ADR 0571 clause 2: it holds the seat mesh-artifact-sweep and serves its verbs, and calls no verb of the controller's;
|
||||
// the lease is the one controller state it reads (clause 10's exemption).
|
||||
// it reads no controller state, the lease neither (issue 1126).
|
||||
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule}
|
||||
if !reflect.DeepEqual(m.Invokes, want) {
|
||||
t.Errorf("invokes %v", m.Invokes)
|
||||
@@ -455,7 +486,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
||||
t.Errorf("claims %+v, serves %v, the seat's verbs %v", m.Claims, verbs, contract)
|
||||
}
|
||||
for _, r := range m.Reads {
|
||||
if strings.HasPrefix(r, "mesh-controller.") && r != LeaseRead {
|
||||
if strings.HasPrefix(r, "mesh-controller.") {
|
||||
t.Errorf("reads the controller's %s", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// Recorded is what says which references the mesh keeps and lets go of them: the seat's holder (Holder, novox/hq ADR
|
||||
// 0571 clause 2), or before it the controller (Controller), each in the controller's answer's shape.
|
||||
// 0571 clause 2), or a test's stand-in.
|
||||
type Recorded interface {
|
||||
Artifacts(repository string) (*Records, error)
|
||||
Collect(why string, confirm bool, most int) (*CollectAnswer, error)
|
||||
|
||||
@@ -3,7 +3,7 @@ module artifactstoretools
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-builder.build-log",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"node-engine.have",
|
||||
"mesh-controller.lease"
|
||||
"node-engine.have"
|
||||
],
|
||||
"state": [
|
||||
"findings"
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestTheSwitcherDoesNotActOnAnotherModel(t *testing.T) {
|
||||
go func() { sw.Run(context.Background()); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatal("the switcher ran on another model")
|
||||
}
|
||||
if r := sw.Report(); r.Running || r.Disabled == "" || len(f.calls) != 0 {
|
||||
|
||||
@@ -2,7 +2,7 @@ module asuszephyrusg14
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module avahi
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module blueman
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module bluetooth
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -181,7 +181,7 @@ func standInCapture(release <-chan struct{}, fail error) func(ctx context.Contex
|
||||
// waitFor polls until the condition holds.
|
||||
func waitFor(t *testing.T, what string, cond func() bool) {
|
||||
t.Helper()
|
||||
for i := 0; i < 400; i++ {
|
||||
for i := 0; i < 3000; i++ {
|
||||
if cond() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestAFinishedGlassDocumentIsNotFiledAgainByItsExpiry(t *testing.T) {
|
||||
if _, err := r.call(t, "scanner.scan", docArgs(id, true)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
waitFor(t, "the expired document to be closed and filed", func() bool { e, _ := os.ReadDir(folder); return len(e) >= 1 })
|
||||
if e, _ := os.ReadDir(folder); len(e) != 1 {
|
||||
t.Errorf("%d files, want 1", len(e))
|
||||
}
|
||||
|
||||
@@ -311,8 +311,13 @@ func TestASlowUploadAndPrintAnswersStartedNeverATimeout(t *testing.T) {
|
||||
}
|
||||
got, _ = r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "g14"})
|
||||
id2, _ := got.(map[string]any)["ask"].(string)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
if _, err := r.call(t, "printer.print", map[string]any{"ask": id2}); err == nil || !strings.Contains(err.Error(), "refused by name") {
|
||||
t.Errorf("the failed upload's outcome: %v", err)
|
||||
var outcome error
|
||||
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(10 * time.Millisecond) {
|
||||
if _, outcome = r.call(t, "printer.print", map[string]any{"ask": id2}); outcome != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if outcome == nil || !strings.Contains(outcome.Error(), "refused by name") {
|
||||
t.Errorf("the failed upload's outcome: %v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module brother
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -120,8 +120,10 @@ nothing else of the home. A project is one entry of the projects directory and a
|
||||
The agent kept reaching for `ssh <machine> journalctl` while the service manager's `journal` verb existed. So:
|
||||
|
||||
- **The "instead of" table.** Every seat verb and module tool may say which shell commands it replaces
|
||||
(`replaces`, in the seat's definition or the module's manifest). The module asks the controller — `tools`,
|
||||
`modules`, `nodes`, `node` — at start, every ten minutes and on `claude_code_render`, keeps the answer in its
|
||||
(`replaces`, in the seat's definition or the module's manifest). The module reads the bus — the seats' verbs from the
|
||||
seats' contract, the placed modules' manifests from `mesh-delivery-indexer.modules` and `.manifests`, the
|
||||
machines and where each module runs from `mesh-delivery-assigner.placement` (issue 1126: the controller that
|
||||
answered this is not placed) — at start, every ten minutes and on `claude_code_render`, keeps it in its
|
||||
state (`mesh-tools.json`), and renders it into `CLAUDE.md` as one row per seat or module: `<node>/<seat>.` and
|
||||
each verb with the commands it replaces. Generated, never written by hand: a verb that gains `replaces` is in
|
||||
the next render. Ordered by what the guard here refused most, then the machines' seats, the modules, the
|
||||
|
||||
@@ -505,7 +505,7 @@ func TestTheHelperReadsOnlyARegularFile(t *testing.T) {
|
||||
if a.Err == "" || len(a.Data) != 0 {
|
||||
t.Fatalf("%s was read: %q", path, a.Data)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatalf("reading %s held the helper", path)
|
||||
}
|
||||
}
|
||||
@@ -567,7 +567,7 @@ func TestAHelperThatDoesNotAnswerFailsTheRequestInTime(t *testing.T) {
|
||||
if err == nil || !errors.Is(err, errHelperSlow) {
|
||||
t.Fatalf("a silent helper answered: %v", err)
|
||||
}
|
||||
if time.Since(start) > 2*time.Second {
|
||||
if time.Since(start) > 10*time.Second {
|
||||
t.Fatalf("the request waited %s", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,22 +8,26 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// meshForTheGuard is a mesh of two machines and the verbs that replace what an agent runs over ssh, as the
|
||||
// controller's records say them.
|
||||
// seats' contract and the modules' manifests say them.
|
||||
func meshForTheGuard() GuardData {
|
||||
tools := json.RawMessage(`{"seats":[
|
||||
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do.","replaces":["hostnamectl","uptime"]}]},
|
||||
{"seat":"node-service-manager","scope":"node","tools":[
|
||||
{"name":"status","description":"One unit as the service manager sees it now: its states.","replaces":["systemctl status","systemctl is-active"]},
|
||||
{"name":"restart","description":"Restart one unit.","replaces":["systemctl restart"]},
|
||||
{"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]},
|
||||
{"seat":"node-hostname","scope":"node","tools":[
|
||||
{"name":"add","description":"Add one address and its names to the operator's lines of /etc/hosts.","replaces":["edit /etc/hosts","HOSTALIASES"]}]}]}`)
|
||||
modules := json.RawMessage(`[{"module":"docker","on":["anchor"],"replaces":{"docker_logs":["docker logs"],"docker_list":["docker ps"]}},
|
||||
{"module":"systemd","on":["anchor","laptop"]}]`)
|
||||
return GuardData{Node: "laptop", Replacements: ReplacementsOf(tools, modules), Machines: []Machine{
|
||||
seats := []catalogue.Seat{
|
||||
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
|
||||
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
|
||||
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
|
||||
{Name: "status", Description: "One unit as the service manager sees it now: its states.", Replaces: []string{"systemctl status", "systemctl is-active"}},
|
||||
{Name: "restart", Description: "Restart one unit.", Replaces: []string{"systemctl restart"}},
|
||||
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
|
||||
{Name: "node-hostname", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
|
||||
{Name: "add", Description: "Add one address and its names to the operator's lines of /etc/hosts.", Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}}},
|
||||
}
|
||||
modules := []ModuleTools{{Module: "docker", On: []string{"anchor"}, Replaces: map[string][]string{"docker_logs": {"docker logs"}, "docker_list": {"docker ps"}}},
|
||||
{Module: "systemd", On: []string{"anchor", "laptop"}}}
|
||||
return GuardData{Node: "laptop", Replacements: ReplacementsOf(seats, modules), Machines: []Machine{
|
||||
{Name: "anchor", Domains: []string{"anchor" + InternalSuffix, "anchor.example"}, Addresses: []string{"192.0.2.10", "10.10.0.1"}},
|
||||
{Name: "laptop", Domains: []string{"laptop" + InternalSuffix}, Addresses: []string{"10.10.0.4"}},
|
||||
}}
|
||||
@@ -69,21 +73,21 @@ func bash(command string) HookInput {
|
||||
func TestSSHToAMeshMachineIsRefusedNamingTheTool(t *testing.T) {
|
||||
g := guard()
|
||||
for command, want := range map[string]string{
|
||||
"ssh anchor journalctl -u mesh-controller -n 50": "anchor/node-service-manager.journal",
|
||||
"ssh anchor journalctl -u mesh-config -n 50": "anchor/node-service-manager.journal",
|
||||
"ssh anchor 'sudo journalctl -fu sshd'": "anchor/node-service-manager.journal",
|
||||
"ssh -p 22 root@anchor.example systemctl status nats": "anchor/node-service-manager.status",
|
||||
"ssh anchor.internal systemctl restart nats": "anchor/node-service-manager.restart",
|
||||
"ssh -o StrictHostKeyChecking=no 10.10.0.1 docker ps -a": "anchor/docker.docker_list",
|
||||
"ssh anchor docker logs --tail 100 nats": "anchor/docker.docker_logs",
|
||||
"ssh laptop docker logs x": "anchor/docker.docker_logs",
|
||||
"ssh a uptime": "mesh-controller.node",
|
||||
"ssh a uptime": "mesh-placement.placement",
|
||||
"cd /tmp && ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"sudo -u root ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"timeout 10 ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"env LANG=C ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"bash -c 'ssh anchor journalctl -u x'": "anchor/node-service-manager.journal",
|
||||
`echo "$(ssh anchor journalctl -n 5)" | tail`: "anchor/node-service-manager.journal",
|
||||
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-controller.node",
|
||||
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-placement.placement",
|
||||
"ssh -J anchor 203.0.113.9 journalctl": "anchor/node-service-manager.journal",
|
||||
"ssh via-anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"ssh nas.lan journalctl": "anchor/node-service-manager.journal",
|
||||
|
||||
@@ -1,43 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// askingAController answers the controller's verbs as the seat does: `tools` in-process, the rest as the
|
||||
// command's output and its JSON.
|
||||
func askingAController(t *testing.T) Ask {
|
||||
// aBus is the states claude-code reads, as a test holds them: by read, key and value.
|
||||
type aBus map[string]map[string][]byte
|
||||
|
||||
func (b aBus) Keys(read string) ([]string, error) {
|
||||
if b[read] == nil {
|
||||
return nil, errors.New("no such state")
|
||||
}
|
||||
var keys []string
|
||||
for k := range b[read] {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (b aBus) Get(read, key string) ([]byte, bool, error) {
|
||||
v, ok := b[read][key]
|
||||
return v, ok, nil
|
||||
}
|
||||
|
||||
// someSeats are two seats as the seats' contract says them: one held once for the mesh, one on every machine.
|
||||
func someSeats() []catalogue.Seat {
|
||||
return []catalogue.Seat{
|
||||
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
|
||||
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}},
|
||||
{Name: "propose", Description: "Propose a change."}}},
|
||||
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
|
||||
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot.", Replaces: []string{"systemctl status", "systemctl is-active"}},
|
||||
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
|
||||
}
|
||||
}
|
||||
|
||||
// aMesh is two machines, anchor (with a public domain) and laptop, docker and zsh placed on anchor, docker's manifest
|
||||
// saying what its tool replaces, as the seat mesh-placement and the indexer keep them on the bus.
|
||||
func aMesh(t *testing.T) aBus {
|
||||
t.Helper()
|
||||
wrap := func(output string, answer any) json.RawMessage {
|
||||
raw, _ := json.Marshal(map[string]any{"ok": true, "output": output, "answer": answer})
|
||||
return raw
|
||||
}
|
||||
return func(address string, args any) (json.RawMessage, error) {
|
||||
switch address {
|
||||
case "seat:mesh-controller.tools":
|
||||
return json.RawMessage(`{"seats":[
|
||||
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do, what it is assigned, and why.","replaces":["hostnamectl","uptime"]},{"name":"nodes","description":"Every machine."}]},
|
||||
{"seat":"node-service-manager","scope":"node","tools":[
|
||||
{"name":"status","description":"One unit as the service manager sees it now: its states, whether it starts at boot.","replaces":["systemctl status","systemctl is-active"]},
|
||||
{"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]}]}`), nil
|
||||
case "seat:mesh-controller.modules":
|
||||
return wrap("[...]", []any{map[string]any{"module": "docker", "on": []string{"anchor"},
|
||||
"replaces": map[string][]string{"docker_logs": {"docker logs"}}}, map[string]any{"module": "zsh", "on": []string{"anchor"}}}), nil
|
||||
case "seat:mesh-controller.nodes":
|
||||
return wrap("", []any{map[string]any{"name": "anchor"}, map[string]any{"name": "laptop"}}), nil
|
||||
case "seat:mesh-controller.node":
|
||||
node, _ := args.(map[string]any)["node"].(string)
|
||||
if node == "anchor" {
|
||||
return wrap("anchor\n mode converged\n public domain Anchor.Example\n\n what it runs\n", nil), nil
|
||||
}
|
||||
return wrap("laptop\n mode converged\n", nil), nil
|
||||
b := aBus{PlacementRead: {}, ModulesRead: {}, ManifestsRead: {}}
|
||||
for _, p := range []delivery.Placement{
|
||||
{V: 1, Node: "anchor", Decided: delivery.NodeDecided{PublicDomain: "Anchor.Example"},
|
||||
Modules: []delivery.PlacedModule{{Module: "docker"}, {Module: "zsh"}}},
|
||||
{V: 1, Node: "laptop"},
|
||||
} {
|
||||
values, err := delivery.PlacementValues(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, v := range values {
|
||||
b[PlacementRead][k] = v
|
||||
}
|
||||
return nil, errors.New("no such verb")
|
||||
}
|
||||
for module, manifest := range map[string]string{
|
||||
"docker": `{"module":"docker","replaces":{"docker_logs":["docker logs"]}}`,
|
||||
"zsh": `{"module":"zsh"}`,
|
||||
} {
|
||||
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(manifest)))
|
||||
mkey, _ := delivery.ManifestsKey(digest)
|
||||
raw, err := json.Marshal(delivery.Manifest{V: 1, Module: module, JSON: manifest})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b[ManifestsRead][mkey] = raw
|
||||
key, _ := delivery.ModulesKey(module)
|
||||
b[ModulesRead][key], _ = json.Marshal(delivery.Module{V: 1, None: "nothing is built of it", Manifest: digest, Repository: "novox/mesh-catalog", Commit: "c"})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func resolving(name string) []string {
|
||||
@@ -45,10 +84,11 @@ func resolving(name string) []string {
|
||||
"laptop": {"127.0.0.1", "10.10.0.4"}}[name]
|
||||
}
|
||||
|
||||
// What the controller says of its tools and machines is read whole: the seats' verbs, the modules' own tools,
|
||||
// every machine with its domains and addresses — a loopback address never one of them.
|
||||
func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
|
||||
m, err := AskMeshTools(askingAController(t), resolving)
|
||||
// Issue 1126 B13: what the mesh says of its tools and machines is read from the bus whole, never asked of the
|
||||
// controller: the seats' verbs, the placed modules' own tools, every machine with its domains and addresses — a
|
||||
// loopback address never one of them.
|
||||
func TestTheMeshsToolsAreReadFromTheBus(t *testing.T) {
|
||||
m, err := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -56,7 +96,7 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
|
||||
for _, r := range m.Replacements {
|
||||
addresses = append(addresses, r.Address)
|
||||
}
|
||||
if got := strings.Join(addresses, ","); got != "mesh-controller.node,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
|
||||
if got := strings.Join(addresses, ","); got != "mesh-placement.placement,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
|
||||
t.Errorf("replacements: %s", got)
|
||||
}
|
||||
if len(m.Machines) != 2 || strings.Join(m.Machines[0].Domains, ",") != "anchor.internal,anchor.example" ||
|
||||
@@ -71,12 +111,12 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
|
||||
// **The table is generated, not written** (novox/hq ADR 0245): what the records say a verb replaces is a row,
|
||||
// the machines' seats first, and nothing is rendered where the records say nothing.
|
||||
func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
|
||||
m, _ := AskMeshTools(askingAController(t), resolving)
|
||||
m, _ := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
|
||||
table := InsteadOf(&m)
|
||||
for _, want := range []string{
|
||||
"| `<node>/node-service-manager.` | `status` — `systemctl status`, `systemctl is-active` · `journal` — `journalctl` |",
|
||||
"| `<node>/docker.` | `docker_logs` — `docker logs` |",
|
||||
"| `mesh-controller.` | `node` — `hostnamectl`, `uptime` |",
|
||||
"| `mesh-placement.` | `placement` — `hostnamectl`, `uptime` |",
|
||||
"Never `ssh` to a mesh machine",
|
||||
} {
|
||||
if !strings.Contains(table, want) {
|
||||
@@ -84,14 +124,14 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if strings.Index(table, "node-service-manager.") > strings.Index(table, "docker.") ||
|
||||
strings.Index(table, "docker.") > strings.Index(table, "`mesh-controller.`") {
|
||||
strings.Index(table, "docker.") > strings.Index(table, "`mesh-placement.`") {
|
||||
t.Errorf("not the machines' seats, then modules, then the mesh's:\n%s", table)
|
||||
}
|
||||
// What the guard here named most comes first: its row, and in its row its verb.
|
||||
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-controller.node": 1}
|
||||
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-placement.placement": 1}
|
||||
ranked := InsteadOf(&m)
|
||||
if !strings.Contains(ranked, "| `<node>/node-service-manager.` | `journal` — `journalctl` · `status`") ||
|
||||
strings.Index(ranked, "`mesh-controller.`") > strings.Index(ranked, "`<node>/docker.`") {
|
||||
strings.Index(ranked, "`mesh-placement.`") > strings.Index(ranked, "`<node>/docker.`") {
|
||||
t.Errorf("not ranked by the guard's record:\n%s", ranked)
|
||||
}
|
||||
m.Refused = nil
|
||||
@@ -121,7 +161,7 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
|
||||
// Asked again with the same answer, nothing is written and nothing renders.
|
||||
func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
|
||||
p := Paths{State: t.TempDir()}
|
||||
m, _ := AskMeshTools(askingAController(t), resolving)
|
||||
m, _ := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
|
||||
if !KeepMeshTools(p, m) {
|
||||
t.Fatal("the first answer was not kept")
|
||||
}
|
||||
@@ -138,18 +178,32 @@ func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Where claude-code runs is read from the controller's `modules` answer as it comes — JSON, from `module list
|
||||
// --json` — and from the printed list too.
|
||||
func TestTheMachinesRunningTheModuleAreReadFromTheControllersAnswer(t *testing.T) {
|
||||
asJSON := json.RawMessage(`{"ok":true,"output":"[...]","answer":[{"module":"zsh","on":["a"]},{"module":"claude-code","on":["a","b"]}]}`)
|
||||
if got := strings.Join(NodesRunning(asJSON, "claude-code"), ","); got != "a,b" {
|
||||
t.Errorf("from JSON: %q", got)
|
||||
// Where a module runs is read from the seat mesh-placement's placement (issue 1126 B13), never the controller's answer.
|
||||
func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
|
||||
b := aMesh(t)
|
||||
if got, err := NodesRunning(b, "zsh"); err != nil || strings.Join(got, ",") != "anchor" {
|
||||
t.Errorf("zsh runs on %v (%v)", got, err)
|
||||
}
|
||||
printed := json.RawMessage(`{"ok":true,"output":"zsh 1 built x on a\nclaude-code 1 built y on a, b\n"}`)
|
||||
if got := strings.Join(NodesRunning(printed, "claude-code"), ","); got != "a,b" {
|
||||
t.Errorf("from the printed list: %q", got)
|
||||
if got, err := NodesRunning(b, "claude-code"); err != nil || got != nil {
|
||||
t.Errorf("a module that runs nowhere: %v (%v)", got, err)
|
||||
}
|
||||
if got := NodesRunning(json.RawMessage(`{"ok":true,"answer":[{"module":"zsh","on":["a"]}]}`), "claude-code"); got != nil {
|
||||
t.Errorf("a module that runs nowhere: %v", got)
|
||||
if _, err := NodesRunning(aBus{}, "zsh"); err == nil {
|
||||
t.Error("an unreadable placement was read as no machine")
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1126 B13 AC1 (ADR 0590): over the seats the mesh defines, the rendered instructions, their table among them,
|
||||
// name no address of the controller, which is not placed: each job is named by the seat that owns it now.
|
||||
func TestTheRenderedInstructionsNameNoControllerAddress(t *testing.T) {
|
||||
m, err := ReadMeshToolsFrom(aMesh(t), catalogue.Seats(), resolving)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
md := RenderWithMesh(Facts{Node: "w", Console: "x"}, Settings{}, nil, "/h", nil, Config{}, &m)["CLAUDE.md"]
|
||||
if !strings.Contains(md, "## Instead of a shell command") {
|
||||
t.Fatalf("no table rendered:\n%s", md)
|
||||
}
|
||||
if strings.Contains(md, "mesh-controller") {
|
||||
t.Errorf("the instructions name the controller:\n%s", md)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package main
|
||||
|
||||
// instructionsText is the managed instruction file, generated from the TypeScript renderer it replaced so
|
||||
// the file under the agent's managed directory did not change by a byte when the module moved to Go;
|
||||
// a test holds it to that renderer's own output (testdata/rendered-by-typescript.json).
|
||||
// a test holds it to that renderer's output (testdata/rendered-by-typescript.json), changed since only
|
||||
// where the controller's verbs were named (issue 1126 B13: the controller is not placed, ADR 0590).
|
||||
func instructionsText(node, role string) string {
|
||||
return "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `" +
|
||||
node +
|
||||
"`\n- **Role:** " +
|
||||
role +
|
||||
"\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
"\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestAJudgementWaitsItsTurnWithinTheBound(t *testing.T) {
|
||||
if err == nil || !strings.Contains(err.Error(), "another judgement") {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
if time.Since(start) > 2*time.Second {
|
||||
if time.Since(start) > 10*time.Second {
|
||||
t.Fatal("waited past the bound")
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
|
||||
j := testJudge(true, true, func(ctx context.Context, _ []string, _ []byte) ([]byte, error) {
|
||||
deadline, _ = ctx.Deadline()
|
||||
select {
|
||||
case <-time.After(400 * time.Millisecond):
|
||||
case <-time.After(1500 * time.Millisecond): // well past Wait, however loaded the machine
|
||||
return []byte(`{"structured_output":{"verdict":"none"}}`), nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
@@ -191,7 +191,7 @@ func TestALongJudgementIsCollectedByItsTicket(t *testing.T) {
|
||||
t.Fatalf("not a ticket: %+v", out)
|
||||
}
|
||||
var got *Judged
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := 0; i < 600; i++ {
|
||||
got, err = j.Collect(out.Ticket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -223,12 +223,12 @@ func TestALongJudgementIsBoundedAndSaysWhyItFailed(t *testing.T) {
|
||||
t.Fatalf("a bound past the most taken: %v", err)
|
||||
}
|
||||
j.Wait = 20 * time.Millisecond
|
||||
j.Bound = 30 * time.Millisecond // the test's stand-in for JudgeBound: within is past it, so a ticket
|
||||
out, err := j.Judge(JudgeAsk{Question: "q", Schema: verdictSchema, Within: 200 * time.Millisecond})
|
||||
j.Bound = time.Second // the test's stand-in for JudgeBound: within is past it, so a ticket; far past Wait on a loaded machine
|
||||
out, err := j.Judge(JudgeAsk{Question: "q", Schema: verdictSchema, Within: 2 * time.Second})
|
||||
if err != nil || !out.Pending {
|
||||
t.Fatalf("%+v %v", out, err)
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := 0; i < 3000; i++ {
|
||||
got, err := j.Collect(out.Ticket)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "did not answer within") {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
func say(format string, args ...any) {
|
||||
@@ -204,54 +205,21 @@ func (s stateOf) Get(key string) (json.RawMessage, bool, error) {
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// nodesRunningMe is the nodes claude-code runs on, from the controller's list of modules — for the register
|
||||
// nodesRunningMe is the nodes claude-code runs on, from the seat mesh-placement's placement — for the register
|
||||
// tool's question.
|
||||
func nodesRunningMe() ([]string, error) {
|
||||
raw, err := ask("seat:mesh-controller.modules", map[string]any{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NodesRunning(raw, "claude-code"), nil
|
||||
}
|
||||
func nodesRunningMe() ([]string, error) { return NodesRunning(busStates{}, "claude-code") }
|
||||
|
||||
// NodesRunning reads where a module runs from the controller's `modules` answer. The verb runs `module list
|
||||
// --json`, so the answer is a JSON list of {module, on}: the lines of the printed list it was once read as
|
||||
// never came, and every "also on" hint and every `nodes: "all"` named no machine. The printed form, `<module>
|
||||
// … on a, b`, is still read when that is what came.
|
||||
func NodesRunning(raw json.RawMessage, module string) []string {
|
||||
var listed []struct {
|
||||
Module string `json:"module"`
|
||||
On []string `json:"on"`
|
||||
// busStates reads the states the manifest declares in its reads through the runtime.
|
||||
type busStates struct{}
|
||||
|
||||
func (busStates) Keys(read string) ([]string, error) { return stdio.State(read).Keys() }
|
||||
|
||||
func (busStates) Get(read, key string) ([]byte, bool, error) {
|
||||
e, err := stdio.State(read).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if json.Unmarshal(verbAnswer(raw), &listed) == nil {
|
||||
for _, m := range listed {
|
||||
if m.Module == module {
|
||||
return m.On
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
text := outputOf(raw)
|
||||
if text == "" {
|
||||
text = string(raw)
|
||||
}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
if !strings.HasPrefix(line, module+" ") {
|
||||
continue
|
||||
}
|
||||
_, on, ok := strings.Cut(line, " on ")
|
||||
if !ok || strings.TrimSpace(on) == "nothing" {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, n := range strings.Split(on, ",") {
|
||||
if n = strings.TrimSpace(n); n != "" {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
func fingerprintOfFile(path string) any {
|
||||
@@ -320,11 +288,11 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
|
||||
Description: "Claude Code on this machine as the mesh configured it: the licence it holds and when its token expires, what it reports holding, the managed files, the MCP servers registered here. Fingerprints only, never a token.",
|
||||
Run: func(map[string]any) (any, error) { return status(p), nil }},
|
||||
{Name: "claude_code_render",
|
||||
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after asking the controller again what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
|
||||
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after reading the bus again for what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
answer := map[string]any{}
|
||||
if m, err := AskMeshTools(ask, lookup); err != nil {
|
||||
answer["asked"] = "the controller did not answer in full (" + err.Error() + "); what was kept is rendered"
|
||||
if m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup); err != nil {
|
||||
answer["asked"] = "the bus could not be read in full (" + err.Error() + "); what was kept is rendered"
|
||||
} else {
|
||||
KeepMeshTools(p, m)
|
||||
}
|
||||
|
||||
@@ -950,11 +950,11 @@ func TestTheWatchHandlerReturnsAtOnceAndDrainAppliesWhatItNoted(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatalf("the watch handler waited for the look or the state (%d of 400 returned)", i)
|
||||
}
|
||||
}
|
||||
if took := time.Since(start); took > time.Second {
|
||||
if took := time.Since(start); took > 30*time.Second {
|
||||
t.Fatalf("400 values took %s to hand over", took)
|
||||
}
|
||||
shanks.mu.Unlock()
|
||||
|
||||
@@ -4,21 +4,27 @@ package main
|
||||
// shell need it (novox/hq ADR 0245): every seat verb and module tool that says what shell command it
|
||||
// replaces, and the mesh's machines by every name and address a session could reach one by.
|
||||
//
|
||||
// Asked of the controller — `tools` for the seats' verbs, `modules` for the modules' own tools, `nodes`
|
||||
// and `node` for the machines — at start and every few minutes, kept in the module's state so a render
|
||||
// never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb that
|
||||
// gains a `replaces` in the records is in the next render's table and the guard's next refusal.
|
||||
// Read from the bus at start and every few minutes (issue 1126 B13; the controller that answered this before is
|
||||
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-sdk catalogue), the modules'
|
||||
// own tools from the manifests the indexer keeps (`mesh-delivery-indexer.modules` and `.manifests`), and the
|
||||
// machines and where each module runs from the seat mesh-placement's `placement`. Kept in the module's state so
|
||||
// a render never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb
|
||||
// that gains a `replaces` is in the next render's table and the guard's next refusal.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Replacement is one verb or tool and the shell commands it is the mesh's way to do.
|
||||
@@ -41,11 +47,11 @@ type Machine struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
}
|
||||
|
||||
// MeshTools is what the controller said, as kept in the module's state.
|
||||
// MeshTools is what the bus said, as kept in the module's state.
|
||||
type MeshTools struct {
|
||||
Replacements []Replacement `json:"replacements"`
|
||||
Machines []Machine `json:"machines"`
|
||||
// Asked is when the controller last answered.
|
||||
// Asked is when the bus was last read whole.
|
||||
Asked string `json:"asked,omitempty"`
|
||||
// Refused is how often the guard here named each tool instead of a work-around, from its record: the
|
||||
// table's order. Never kept — read from the record at each render.
|
||||
@@ -78,42 +84,120 @@ func ReadMeshTools(p Paths) *MeshTools {
|
||||
return &m
|
||||
}
|
||||
|
||||
// verbAnswer is a controller verb's answer: the command's printed output, and its JSON when it printed one.
|
||||
func verbAnswer(raw json.RawMessage) json.RawMessage {
|
||||
var r struct {
|
||||
Output string `json:"output"`
|
||||
Answer json.RawMessage `json:"answer"`
|
||||
// The states read, each declared in the manifest's reads.
|
||||
const (
|
||||
PlacementRead = delivery.PlacementRead
|
||||
ModulesRead = delivery.IndexerModule + ".modules"
|
||||
ManifestsRead = delivery.IndexerModule + ".manifests"
|
||||
)
|
||||
|
||||
// States reads the module states the manifest declares in its reads: a state's keys, and one key's value.
|
||||
type States interface {
|
||||
Keys(read string) ([]string, error)
|
||||
Get(read, key string) ([]byte, bool, error)
|
||||
}
|
||||
|
||||
// ModuleTools is one module's own tools that replace a command, as its manifest says (`replaces`), and where it runs.
|
||||
type ModuleTools struct {
|
||||
Module string
|
||||
On []string
|
||||
Replaces map[string][]string
|
||||
}
|
||||
|
||||
// placements is every node's placement, from the seat mesh-placement's state.
|
||||
func placements(st States) (map[string]delivery.Placement, error) {
|
||||
keys, err := st.Keys(PlacementRead)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", PlacementRead, err)
|
||||
}
|
||||
if json.Unmarshal(raw, &r) != nil {
|
||||
return raw
|
||||
}
|
||||
if len(r.Answer) > 0 && string(r.Answer) != "null" {
|
||||
return r.Answer
|
||||
}
|
||||
if r.Output != "" {
|
||||
t := strings.TrimSpace(r.Output)
|
||||
for _, open := range []string{"[", "{"} {
|
||||
if strings.HasPrefix(t, open) {
|
||||
return json.RawMessage(t)
|
||||
}
|
||||
if i := strings.Index(t, "\n"+open); i >= 0 {
|
||||
return json.RawMessage(strings.TrimSpace(t[i+1:]))
|
||||
}
|
||||
read := delivery.NewPlacementKeys()
|
||||
for _, k := range keys {
|
||||
v, found, err := st.Get(PlacementRead, k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %w", PlacementRead, k, err)
|
||||
}
|
||||
if found {
|
||||
_, _ = read.Put(k, v, 0, false) // a key that does not read places nothing
|
||||
}
|
||||
return json.RawMessage(nil)
|
||||
}
|
||||
return raw
|
||||
return read.All(), nil
|
||||
}
|
||||
|
||||
func outputOf(raw json.RawMessage) string {
|
||||
var r struct {
|
||||
Output string `json:"output"`
|
||||
// runsOn is, by module, the nodes placement puts it on, sorted.
|
||||
func runsOn(placed map[string]delivery.Placement) map[string][]string {
|
||||
out := map[string][]string{}
|
||||
for node, p := range placed {
|
||||
for _, m := range p.Modules {
|
||||
out[m.Module] = append(out[m.Module], node)
|
||||
}
|
||||
}
|
||||
_ = json.Unmarshal(raw, &r)
|
||||
return r.Output
|
||||
for _, nodes := range out {
|
||||
sort.Strings(nodes)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var publicDomain = regexp.MustCompile(`(?m)^\s*public domain\s+(\S+)\s*$`)
|
||||
// NodesRunning is the nodes placement puts module on.
|
||||
func NodesRunning(st States, module string) ([]string, error) {
|
||||
placed, err := placements(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runsOn(placed)[module], nil
|
||||
}
|
||||
|
||||
// modulesTools is every placed module's own tools that replace a command, by module, from the manifest the indexer
|
||||
// keeps of it. A module whose manifest cannot be read is left out and said in the error.
|
||||
func modulesTools(st States, on map[string][]string) ([]ModuleTools, error) {
|
||||
var out []ModuleTools
|
||||
var problems []error
|
||||
modules := make([]string, 0, len(on))
|
||||
for m := range on {
|
||||
modules = append(modules, m)
|
||||
}
|
||||
sort.Strings(modules)
|
||||
for _, module := range modules {
|
||||
key, err := delivery.ModulesKey(module)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
raw, found, err := st.Get(ModulesRead, key)
|
||||
if err != nil || !found {
|
||||
if err != nil {
|
||||
problems = append(problems, fmt.Errorf("%s %s: %w", ModulesRead, module, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
m, err := delivery.DecodeModule(raw)
|
||||
if err != nil || m.Manifest == "" {
|
||||
continue
|
||||
}
|
||||
mkey, err := delivery.ManifestsKey(m.Manifest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
raw, found, err = st.Get(ManifestsRead, mkey)
|
||||
if err != nil || !found {
|
||||
if err != nil {
|
||||
problems = append(problems, fmt.Errorf("%s of %s: %w", ManifestsRead, module, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept, err := delivery.DecodeManifest(raw)
|
||||
if err != nil {
|
||||
problems = append(problems, fmt.Errorf("the manifest of %s: %w", module, err))
|
||||
continue
|
||||
}
|
||||
var man struct {
|
||||
Replaces map[string][]string `json:"replaces"`
|
||||
}
|
||||
if json.Unmarshal([]byte(kept.JSON), &man) != nil || len(man.Replaces) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, ModuleTools{Module: module, On: on[module], Replaces: man.Replaces})
|
||||
}
|
||||
return out, errors.Join(problems...)
|
||||
}
|
||||
|
||||
// firstSentence is what a verb does, short: its description to the first full stop, at most 70 characters.
|
||||
func firstSentence(s string) string {
|
||||
@@ -131,52 +215,33 @@ func firstSentence(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// ReplacementsOf reads the controller's `tools` and `modules` answers: the seats' verbs first, in the
|
||||
// records' order, then the modules' own tools by module and tool.
|
||||
func ReplacementsOf(tools, modules json.RawMessage) []Replacement {
|
||||
// ReplacementsOf is every seat verb and module tool that says it replaces a command: the seats' verbs first, in the
|
||||
// seats' order, then the modules' own tools by module and tool.
|
||||
func ReplacementsOf(seats []catalogue.Seat, modules []ModuleTools) []Replacement {
|
||||
var out []Replacement
|
||||
var seats struct {
|
||||
Seats []struct {
|
||||
Seat string `json:"seat"`
|
||||
Scope string `json:"scope"`
|
||||
Tools []struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Replaces []string `json:"replaces"`
|
||||
} `json:"tools"`
|
||||
} `json:"seats"`
|
||||
}
|
||||
if json.Unmarshal(tools, &seats) == nil {
|
||||
for _, s := range seats.Seats {
|
||||
for _, t := range s.Tools {
|
||||
if len(t.Replaces) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, Replacement{Address: s.Seat + "." + t.Name, NodeScoped: s.Scope == "node", Seat: true,
|
||||
Does: firstSentence(t.Description), Replaces: t.Replaces})
|
||||
for _, s := range seats {
|
||||
for _, v := range s.Serves {
|
||||
if len(v.Replaces) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == catalogue.ScopeNode, Seat: true,
|
||||
Does: firstSentence(v.Description), Replaces: v.Replaces})
|
||||
}
|
||||
}
|
||||
var listed []struct {
|
||||
Module string `json:"module"`
|
||||
On []string `json:"on"`
|
||||
Replaces map[string][]string `json:"replaces"`
|
||||
}
|
||||
if json.Unmarshal(modules, &listed) == nil {
|
||||
sort.SliceStable(listed, func(i, j int) bool { return listed[i].Module < listed[j].Module })
|
||||
for _, m := range listed {
|
||||
names := make([]string, 0, len(m.Replaces))
|
||||
for t := range m.Replaces {
|
||||
names = append(names, t)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, t := range names {
|
||||
if len(m.Replaces[t]) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
|
||||
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
|
||||
sorted := append([]ModuleTools(nil), modules...)
|
||||
sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Module < sorted[j].Module })
|
||||
for _, m := range sorted {
|
||||
names := make([]string, 0, len(m.Replaces))
|
||||
for t := range m.Replaces {
|
||||
names = append(names, t)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, t := range names {
|
||||
if len(m.Replaces[t]) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
|
||||
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -190,36 +255,24 @@ func lookup(name string) []string {
|
||||
return addrs
|
||||
}
|
||||
|
||||
// AskMeshTools asks the controller now. A part that does not answer is left out and said in the error;
|
||||
// what did answer is kept.
|
||||
func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
|
||||
// ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node placement names
|
||||
// with its domains and addresses. A part that cannot be read is left out and said in the error; what was read is kept.
|
||||
func ReadMeshToolsFrom(st States, seats []catalogue.Seat, resolve func(string) []string) (MeshTools, error) {
|
||||
var m MeshTools
|
||||
tools, terr := ask("seat:mesh-controller.tools", map[string]any{})
|
||||
modules, merr := ask("seat:mesh-controller.modules", map[string]any{})
|
||||
if terr != nil && merr != nil {
|
||||
return m, terr
|
||||
}
|
||||
m.Replacements = ReplacementsOf(verbAnswer(tools), verbAnswer(modules))
|
||||
nodesRaw, err := ask("seat:mesh-controller.nodes", map[string]any{})
|
||||
placed, err := placements(st)
|
||||
if err != nil {
|
||||
m.Replacements = ReplacementsOf(seats, nil)
|
||||
return m, err
|
||||
}
|
||||
var nodes []struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.Unmarshal(verbAnswer(nodesRaw), &nodes)
|
||||
for _, n := range nodes {
|
||||
if n.Name == "" {
|
||||
continue
|
||||
}
|
||||
machine := Machine{Name: n.Name, Domains: []string{n.Name + InternalSuffix}}
|
||||
if shown, err := ask("seat:mesh-controller.node", map[string]any{"node": n.Name}); err == nil {
|
||||
if d := publicDomain.FindStringSubmatch(outputOf(shown)); d != nil {
|
||||
machine.Domains = append(machine.Domains, strings.ToLower(d[1]))
|
||||
}
|
||||
modules, merr := modulesTools(st, runsOn(placed))
|
||||
m.Replacements = ReplacementsOf(seats, modules)
|
||||
for node, p := range placed {
|
||||
machine := Machine{Name: node, Domains: []string{node + InternalSuffix}}
|
||||
if d := strings.ToLower(strings.TrimSpace(p.Decided.PublicDomain)); d != "" {
|
||||
machine.Domains = append(machine.Domains, d)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, name := range append([]string{n.Name}, machine.Domains...) {
|
||||
for _, name := range append([]string{node}, machine.Domains...) {
|
||||
for _, a := range resolve(name) {
|
||||
if !seen[a] && !net.ParseIP(a).IsLoopback() {
|
||||
seen[a] = true
|
||||
@@ -231,7 +284,7 @@ func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
|
||||
m.Machines = append(m.Machines, machine)
|
||||
}
|
||||
sort.Slice(m.Machines, func(i, j int) bool { return m.Machines[i].Name < m.Machines[j].Name })
|
||||
return m, nil
|
||||
return m, merr
|
||||
}
|
||||
|
||||
// KeepMeshTools writes what was asked to the module's state when it differs from what was kept, and
|
||||
@@ -281,19 +334,19 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
|
||||
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
|
||||
}
|
||||
|
||||
// MeshToolsEvery is how often the controller is asked again.
|
||||
// MeshToolsEvery is how often the bus is read again.
|
||||
const MeshToolsEvery = 10 * time.Minute
|
||||
|
||||
// refreshMeshTools asks the controller at start and every few minutes, and renders when what it said changed.
|
||||
// refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed.
|
||||
func refreshMeshTools(p Paths) {
|
||||
for {
|
||||
m, err := AskMeshTools(ask, lookup)
|
||||
m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup)
|
||||
if err != nil {
|
||||
say("asking the controller what each tool replaces: %v", err)
|
||||
say("reading what each tool replaces and which machines the mesh has: %v", err)
|
||||
}
|
||||
if (err == nil || len(m.Replacements) > 0) && KeepMeshTools(p, m) {
|
||||
if _, err := RenderNow(p, writeManaged); err != nil {
|
||||
say("rendering what the controller said of its tools: %v", err)
|
||||
say("rendering what the bus said of the tools: %v", err)
|
||||
} else {
|
||||
say("the mesh's tools changed: %d replace a command, %d machines; rendered", len(m.Replacements), len(m.Machines))
|
||||
}
|
||||
@@ -306,7 +359,7 @@ func refreshMeshTools(p Paths) {
|
||||
func GuardStatus(p Paths, last int) map[string]any {
|
||||
m := ReadMeshTools(p)
|
||||
d := GuardDataOf(p, m)
|
||||
asked := "never: the controller has not answered yet"
|
||||
asked := "never: the bus has not been read whole yet"
|
||||
if m != nil {
|
||||
asked = m.Asked
|
||||
}
|
||||
|
||||
@@ -670,7 +670,7 @@ func TestTheRoutersRefusalIsSaidAndDiscards(t *testing.T) {
|
||||
r.kinds = nil
|
||||
refusing := &refusingRouter{router: r, p: p, store: store}
|
||||
within, every := RouterAnswersWithin, RouterAnswersEvery
|
||||
RouterAnswersWithin, RouterAnswersEvery = time.Second, 10*time.Millisecond
|
||||
RouterAnswersWithin, RouterAnswersEvery = 30*time.Second, 10*time.Millisecond
|
||||
t.Cleanup(func() { RouterAnswersWithin, RouterAnswersEvery = within, every })
|
||||
_, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "x", Text: "Text.\n"}, store, refusing, t0)
|
||||
if err == nil || !strings.Contains(err.Error(), "no channel can carry any of its answers now") ||
|
||||
|
||||
@@ -223,7 +223,7 @@ func TestTheRoutersRefusalIsSaidAndKept(t *testing.T) {
|
||||
store := newMemQuestions()
|
||||
refusing := &questionRefusingRouter{router: newRouter(), store: store}
|
||||
within, every := RouterAnswersWithin, RouterAnswersEvery
|
||||
RouterAnswersWithin, RouterAnswersEvery = time.Second, 10*time.Millisecond
|
||||
RouterAnswersWithin, RouterAnswersEvery = 30*time.Second, 10*time.Millisecond
|
||||
t.Cleanup(func() { RouterAnswersWithin, RouterAnswersEvery = within, every })
|
||||
_, err := AskQuestion(p, mergeTonight(), store, refusing, t0)
|
||||
if err == nil || !strings.Contains(err.Error(), "may not leave the mesh") {
|
||||
@@ -303,7 +303,7 @@ func TestAReadWaitsNoLongerThanItsBound(t *testing.T) {
|
||||
if err != nil || out["state"] != AnswerWaiting {
|
||||
t.Fatalf("%v %v", out, err)
|
||||
}
|
||||
if took := time.Since(start); took > 2*time.Second {
|
||||
if took := time.Since(start); took > 30*time.Second {
|
||||
t.Errorf("a read asked to wait an hour took %s", took)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
"withKey": {
|
||||
"managed-mcp.json": "{\n \"mcpServers\": {\n \"anton\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"/a/b.js\"\n ],\n \"env\": {}\n },\n \"docs\": {\n \"type\": \"stdio\",\n \"command\": \"docs-mcp\",\n \"args\": [\n \"--x\"\n ]\n },\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n },\n \"search\": {\n \"type\": \"http\",\n \"url\": \"https://s.example/mcp\"\n }\n }\n}\n",
|
||||
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true,\n \"apiKeyHelper\": \"/state/api-key-helper\"\n}\n",
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
},
|
||||
"plain": {
|
||||
"managed-mcp.json": "{\n \"mcpServers\": {\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n }\n }\n}\n",
|
||||
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true\n}\n",
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ module claude-code
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101023317-8311ecb84911
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101023317-8311ecb84911 h1:+82ygf2hiY5Mhwfmc1xfCe9e/KY/5ObBuE2LecM37qI=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101023317-8311ecb84911/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"waits"
|
||||
],
|
||||
"reads": [
|
||||
"claude-licence-manager.bindings"
|
||||
"claude-licence-manager.bindings",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.manifests"
|
||||
],
|
||||
"tools": [
|
||||
"claude_code_status",
|
||||
|
||||
@@ -3,7 +3,7 @@ module claude-licence-manager
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
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=
|
||||
|
||||
@@ -123,25 +123,27 @@ func TestTheLauncherUsesTheManagersDisplayAndWaitsOtherwise(t *testing.T) {
|
||||
os.MkdirAll(fakeBin, 0o755)
|
||||
// the manager's environment: empty until the file appears
|
||||
os.WriteFile(fakeBin+"/systemctl", []byte("#!/bin/sh\n[ -f \""+dir+"/env\" ] && cat \""+dir+"/env\"\nexit 0\n"), 0o755)
|
||||
run := func(env ...string) (string, bool) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2500*time.Millisecond)
|
||||
// wait is how long the launcher may run: a launcher that must answer gets a generous bound (the checker runs
|
||||
// thousands of tests at once, issue 1814); one that must keep waiting is cut off after a short one
|
||||
run := func(wait time.Duration, env ...string) (string, bool) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), wait)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "sh", file)
|
||||
cmd.Env = append([]string{"PATH=" + fakeBin + ":" + os.Getenv("PATH"), "HOME=" + dir}, env...)
|
||||
out, _ := cmd.Output()
|
||||
return strings.TrimSpace(string(out)), ctx.Err() == nil
|
||||
}
|
||||
if out, ok := run("DISPLAY=:7"); !ok || out != "DISPLAY=:7" {
|
||||
if out, ok := run(30*time.Second, "DISPLAY=:7"); !ok || out != "DISPLAY=:7" {
|
||||
t.Fatalf("with DISPLAY: %q %v", out, ok)
|
||||
}
|
||||
if _, ok := run(); ok {
|
||||
if _, ok := run(2500 * time.Millisecond); ok {
|
||||
t.Fatal("it exited with no X server")
|
||||
}
|
||||
for _, n := range []string{"X0", "X1"} {
|
||||
l, _ := net.Listen("unix", dir+"/x11/"+n)
|
||||
defer l.Close()
|
||||
}
|
||||
if _, ok := run(); ok {
|
||||
if _, ok := run(2500 * time.Millisecond); ok {
|
||||
t.Fatal("it chose one of two servers")
|
||||
}
|
||||
// the session hands the manager its display while the launcher waits
|
||||
@@ -149,7 +151,7 @@ func TestTheLauncherUsesTheManagersDisplayAndWaitsOtherwise(t *testing.T) {
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
os.WriteFile(dir+"/env", []byte("DISPLAY=:1\nOTHER=x\n"), 0o644)
|
||||
}()
|
||||
if out, ok := run(); !ok || out != "DISPLAY=:1" {
|
||||
if out, ok := run(30 * time.Second); !ok || out != "DISPLAY=:1" {
|
||||
t.Fatalf("the display that arrived while waiting: %q %v", out, ok)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ module clipmenu
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ module cups
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -422,11 +422,11 @@ func TestRunWithoutABusSaysItStalledAndReconnects(t *testing.T) {
|
||||
}, nil)
|
||||
w.state = filepath.Join(t.TempDir(), "bus")
|
||||
w.retry = 20 * time.Millisecond
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
|
||||
defer cancel()
|
||||
done := make(chan struct{})
|
||||
go func() { w.Run(ctx); close(done) }()
|
||||
deadline := time.Now().Add(6 * time.Second)
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for time.Now().Before(deadline) && !w.Snapshot().Connected {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ module dbus
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
)
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module deskchannel
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -181,10 +181,10 @@ func readingOf(s *memStore, key string) *health.Reading {
|
||||
|
||||
func waitFor(t *testing.T, done func() bool) {
|
||||
t.Helper()
|
||||
for end := time.Now().Add(5 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
|
||||
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
|
||||
if done() {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("not done within 5 s")
|
||||
t.Fatal("not done within 30 s")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module diskload
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -2,7 +2,7 @@ module dmenu
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module dnsmasq
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -43,8 +43,12 @@ func TestJobsASlowActIsAnsweredRunningWithItsID(t *testing.T) {
|
||||
t.Fatalf("%+v", j)
|
||||
}
|
||||
close(release)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
got, _ := jobByID(j.ID)
|
||||
var got Job
|
||||
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
|
||||
if got, _ = jobByID(j.ID); !got.Running {
|
||||
break
|
||||
}
|
||||
}
|
||||
if got.Running {
|
||||
t.Fatalf("finished afterwards: %+v", got)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ module docker-compose
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module docker
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestANotificationsChosenAnswerIsSaidAsAnEvent(t *testing.T) {
|
||||
if !strings.Contains(string(asked), "[--expire-time=0][--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") {
|
||||
t.Fatalf("notify-send was asked %s", asked)
|
||||
}
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for {
|
||||
mu.Lock()
|
||||
n := len(said)
|
||||
|
||||
@@ -2,7 +2,7 @@ module dunst
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -2,7 +2,7 @@ module fail2ban
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ module feh
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -43,8 +43,12 @@ func TestJobsASlowActIsAnsweredRunningWithItsID(t *testing.T) {
|
||||
t.Fatalf("%+v", j)
|
||||
}
|
||||
close(release)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
got, _ := jobByID(j.ID)
|
||||
var got Job
|
||||
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(5 * time.Millisecond) {
|
||||
if got, _ = jobByID(j.ID); !got.Running {
|
||||
break
|
||||
}
|
||||
}
|
||||
if got.Running {
|
||||
t.Fatalf("finished afterwards: %+v", got)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ module flatpak
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user