Compare commits

..
Author SHA1 Message Date
jschoubben de2de44c98 mesh-build-go: give each ask a cache layer in its workspace, so what its digest and vet compile its tests find compiled
Pins mesh-sdk fix/check-speed-s2 (2a867018), whose Isolation.CacheLayer keeps what one
command of an ask writes into the caches for the ask's later commands, the shared cache
unchanged. A trunk build and the lint's vet still write the shared cache directly.
Ported from feat/try-speed S2 (issue 1921; ADR 0541 §2).
2026-10-29 00:52:25 +01:00
jschoubben 1989dd7cf2 mesh-build-go: start the integration level's store and bus beside its compile, and compile the go.mod directories side by side
The setup started the store, then the bus, then compiled each directory one after
another: 132 s of setup in the L3 group check. Now the three start together and the
directories compile up to the bound at once; the compile runs no test binary
(-exec true), so it needs neither server and is given nothing a test reads. Ported
from feat/try-speed S5 (issue 1921; ADR 0541 §1).
2026-10-29 00:48:50 +01:00
jschoubben 31c860bc92 mesh-build-go: let a check's go vet warm the shared Go build cache with cgo off, and say each linted directory as it ends
A check's lint recompiled every package from a cold layer on each ask (g14 linted
mesh-catalog's 8 directories in 141 s where ace, warm, took 17 s), and said nothing
until it ended. ADR 0541 §2 and ADR 0542 §1 decided this; the built speed-up on
feat/try-speed (S11) was parked. Ported here alone: CGO_ENABLED=0 in every check
command's go environment, vet runs with the shared caches writable, and the lint
says a phase per finished directory (issue 1921).
2026-10-29 00:46:19 +01:00
46 changed files with 444 additions and 1116 deletions
-26
View File
@@ -62,29 +62,3 @@ func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
t.Errorf("the build packs files/ into %d archives, not one", packs)
}
}
// The forge's sshd takes a burst of the mesh's machines fetching at once: the image's MaxStartups raised from OpenSSH's
// default 10:30:100 to 100:30:200 through SSH_MAX_STARTUPS, which dropped ace's thirteen connections at once.
func TestTheForgesSshdTakesABurstOfTheMeshsMachines(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if err != nil {
t.Fatal(err)
}
var m struct {
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
for _, r := range m.Resources {
if r["id"] != "server" {
continue
}
env, _ := r["env"].(map[string]any)
if env["SSH_MAX_STARTUPS"] != "100:30:200" {
t.Errorf("SSH_MAX_STARTUPS is %v, not 100:30:200", env["SSH_MAX_STARTUPS"])
}
return
}
t.Error("the manifest has no server container")
}
+1 -2
View File
@@ -266,8 +266,7 @@
"env": {
"DB_TYPE": "postgres",
"USER_UID": "1000",
"USER_GID": "1000",
"SSH_MAX_STARTUPS": "100:30:200"
"USER_GID": "1000"
},
"env-file": [
"${dir:state}/server.env"
@@ -296,6 +296,7 @@ func (m *Module) goEnv() []string {
modcache = m.GoModCache
}
return append([]string{
"CGO_ENABLED=0", // as a production build compiles (build.go), so a check tests what ships and the warm cache matches (ADR 0542 §1)
"GOTOOLCHAIN=local",
"GOFLAGS=-mod=mod",
"GOMODCACHE=" + modcache,
@@ -12,6 +12,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
@@ -62,30 +63,73 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
if err != nil {
return nil, err
}
postgres, stopStore, err := m.startStore(ctx, run)
if err != nil {
return nil, fmt.Errorf("the integration level's store: %w", err)
}
defer stopStore()
nats, stopBus, err := m.startBus(ctx, filepath.Join(run, "nats"))
if err != nil {
return nil, fmt.Errorf("the integration level's bus: %w", err)
}
defer stopBus()
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace, nil)...)
// Compiling counts as setup: the tests are built once, here, with the addresses a TestMain may read, and the run
// below finds them built. Whether they compile is for the run to say; its failure is not read here.
// Only the directories the selected packages lie in compile (ADR 0536 §3); no selection is every directory.
byDir, err := packagesByDir(tree, dirs, packages)
if err != nil {
return nil, err
}
// The store, the bus and the compile start together, and the directories compile side by side up to the bound
// (issue 1921, try speed S5): the setup is the longest of them, not their sum. The compile runs no test binary
// (-exec true), so it needs neither server and is given nothing a test reads; whether the tests compile is for the
// run to say, its failure is not read here, and the run below finds them built.
var (
wg sync.WaitGroup
postgres, nats string
stopStore, stopBus func()
storeErr, busErr error
crash any
crashMu sync.Mutex
)
guard := func() {
if p := recover(); p != nil {
crashMu.Lock()
crash = p
crashMu.Unlock()
}
}
wg.Add(2)
go func() {
defer wg.Done()
defer guard()
postgres, stopStore, storeErr = m.startStore(ctx, run)
}()
go func() {
defer wg.Done()
defer guard()
nats, stopBus, busErr = m.startBus(ctx, filepath.Join(run, "nats"))
}()
slots := make(chan struct{}, m.parallel())
for _, d := range dirs {
if len(byDir[d]) == 0 {
continue
}
m.Run(ctx, append(m.goEnvFor(filepath.Join(tree, d)), env...), "go", append([]string{"-C", filepath.Join(tree, d), "test", "-tags", strings.Join(integrationTags, ","), "-run", "^$", "-count=1"}, byDir[d]...)...)
wg.Add(1)
slots <- struct{}{}
go func() {
defer wg.Done()
defer func() { <-slots }()
defer guard()
m.Run(ctx, m.goEnvFor(filepath.Join(tree, d)), "go", append([]string{"-C", filepath.Join(tree, d), "test", "-tags", strings.Join(integrationTags, ","), "-exec", "true", "-count=1"}, byDir[d]...)...)
}()
}
wg.Wait()
// Whatever started is stopped however the level ends, a half-started store included.
if stopStore != nil {
defer stopStore()
}
if stopBus != nil {
defer stopBus()
}
if crash != nil {
panic(crash)
}
if storeErr != nil {
return nil, fmt.Errorf("the integration level's store: %w", storeErr)
}
if busErr != nil {
return nil, fmt.Errorf("the integration level's bus: %w", busErr)
}
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace, nil)...)
setup := m.clock().Sub(began)
return m.runLevel(ctx, tree, levelOpts{
@@ -35,6 +35,9 @@ type integFake struct {
// command runs, with its line.
gomods string
onTest func(line string)
// onCompile is called, unlocked, as each compile of the setup runs, and onStart as the store starts (issue 1921, S5).
onCompile func(line string, env []string)
onStart func()
}
func newIntegFake(t *testing.T, captured string) *integFake {
@@ -71,7 +74,10 @@ func (f *integFake) run(ctx context.Context, env []string, name string, args ...
return forgekey.Ran{Stdout: f.gomods}
}
return forgekey.Ran{Stdout: "go.mod\n"}
case name == "go" && strings.Contains(line, "-run ^$"):
case name == "go" && strings.Contains(line, "-exec true"):
if f.onCompile != nil {
f.onCompile(line, env)
}
f.tick("compile")
case name == "go":
if f.onTest != nil {
@@ -84,6 +90,9 @@ func (f *integFake) run(ctx context.Context, env []string, name string, args ...
}
return forgekey.Ran{Stdout: f.captured, Status: status}
case name == "pg_ctl" && strings.HasSuffix(line, " start"):
if f.onStart != nil {
f.onStart()
}
f.tick("start")
for i, a := range args {
if a == "-D" {
@@ -314,8 +323,8 @@ func TestAStoreThatDoesNotStartEndsTheStepFailedAndLeavesNothing(t *testing.T) {
if !strings.Contains(f.lines(), " stop") {
t.Errorf("the half-started store was not stopped:\n%s", f.lines())
}
if f.busDir != "" {
t.Errorf("the bus was started after the store failed")
if f.busDir != "" && !f.busDown {
t.Errorf("the bus, started beside the store, was not stopped after the store failed")
}
if entries, _ := os.ReadDir(filepath.Join(m.Work, "ws1")); len(entries) != 1 || entries[0].Name() != "tree" {
t.Errorf("the workspace holds %v, want only its tree", entries)
@@ -29,23 +29,32 @@ func (m *Module) caches() []string {
}
// isolation is how a run step of the workspace is shut off: the workspace (its tree, its siblings, its run directories,
// its packed artifacts) read-write, the caches beneath a throwaway layer unless write, and an environment of a PATH and
// its packed artifacts) read-write, the caches beneath the ask's own layer unless write, and an environment of a PATH and
// a locale alone (the step's HOME is its own, inside its /tmp).
func (m *Module) isolation(workspace string, write bool) buildstep.Isolation {
path := os.Getenv("PATH")
if path == "" {
path = defaultPath
}
return buildstep.Isolation{
iso := buildstep.Isolation{
Workspace: filepath.Join(m.Work, workspace),
Caches: m.caches(),
WriteCaches: write,
Env: append([]string{"PATH=" + path, "LC_ALL=C"}, m.threadEnv()...),
}
if !write {
// What one command of the ask compiles is kept for its later commands (the vet, the unit tests, the integration
// level's compile), in the workspace and gone with it (issue 1921, try speed S2).
iso.CacheLayer = filepath.Join(iso.Workspace, cacheLayerDir)
}
return iso
}
// cacheLayerDir is the directory in a workspace's root where an ask keeps what its commands wrote into the caches.
const cacheLayerDir = "cachelayer"
// stepRunner is the runner of a run step of the workspace: buildstep.Isolated over m.Run, so every command it runs is
// shut off; write is true for a trunk build alone (the shared caches are written by trunk builds only). Inside is the
// shut off; write is true for a trunk build and for a check's go vet, compile steps that run nothing of the change (ADR 0541 §2). Inside is the
// integration level's own process, already running shut off (level.go): it runs its commands as they are.
func (m *Module) stepRunner(workspace string, write bool) (forgekey.Runner, error) {
if m.Inside {
@@ -20,6 +20,7 @@ import (
type shutOff struct {
flags []string
binds, roBinds, overlays []string
uppers map[string]string // the writable layer each --overlay gives a cache: cache -> upper
env []string
command string
chdir string
@@ -27,7 +28,7 @@ type shutOff struct {
// bwrapArity is how many words each bubblewrap option the SDK writes takes.
var bwrapArity = map[string]int{"--unshare-all": 0, "--die-with-parent": 0, "--new-session": 0, "--clearenv": 0,
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1,
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1, "--overlay": 3,
"--ro-bind": 2, "--ro-bind-try": 2, "--bind": 2, "--symlink": 2, "--setenv": 2}
// throughBubblewrap is the fake runner of a step's tests: a bubblewrap command is read back (recorded in seen) and its
@@ -56,6 +57,11 @@ func throughBubblewrap(t *testing.T, run forgekey.Runner, seen *[]shutOff) forge
s.roBinds = append(s.roBinds, args[i+1])
case "--overlay-src":
s.overlays = append(s.overlays, args[i+1])
case "--overlay":
if s.uppers == nil {
s.uppers = map[string]string{}
}
s.uppers[args[i+3]] = args[i+1]
case "--setenv":
s.env = append(s.env, args[i+1]+"="+args[i+2])
case "--chdir":
@@ -105,6 +111,9 @@ func checkShutOff(t *testing.T, m *Module, s shutOff, write bool) {
t.Errorf("a trunk build's %s does not write the shared cache %s (bound %v, layered %v)", s.command, c, bound, layered)
case !write && (bound || !layered):
t.Errorf("%s may write the shared cache %s (bound %v, layered %v): only a trunk build does", s.command, c, bound, layered)
case !write && !strings.HasPrefix(s.uppers[c], filepath.Join(m.Work, "ws1", "cachelayer")+"/"):
// 1921/S2: what one command of the ask compiles is kept for its later commands, in the workspace
t.Errorf("%s writes the cache %s into %q, not the ask's cache layer in its workspace", s.command, c, s.uppers[c])
}
}
if want := filepath.Join(m.Work, "ws1"); !slices.Contains(s.binds, want) {
@@ -130,8 +139,23 @@ func TestTheUnitLevelRunsEveryGoTestShutOff(t *testing.T) {
if ran == 0 || len(seen) != ran {
t.Fatalf("%d go commands ran, %d of them shut off: %v", ran, len(seen), *calls)
}
// A check compiles with cgo off as a build does (ADR 0542 §1), and the lint's go vet, a compile that runs nothing
// of the change, writes the shared caches; every other command reads them beneath its own layer (ADR 0541 §2).
for _, c := range *calls {
if strings.HasPrefix(c.line, "go ") && !slices.Contains(c.env, "CGO_ENABLED=0") {
t.Errorf("%s ran without CGO_ENABLED=0: %v", c.line, c.env)
}
}
vets := 0
for _, s := range seen {
checkShutOff(t, m, s, false)
vet := strings.Contains(s.command, " vet ")
if vet {
vets++
}
checkShutOff(t, m, s, vet)
}
if vets == 0 {
t.Error("no go vet ran shut off")
}
}
@@ -13,6 +13,7 @@ import (
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/forgekey"
@@ -144,6 +145,18 @@ func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string,
var wg sync.WaitGroup
var crash any // a panic of a lint goes on in the caller, as a panic of a test run does
var crashMu sync.Mutex
// Each directory's end is said as a phase, so a long lint is readable while it runs (issue 1921): which directories
// are done and how long the last one took.
var doneMu sync.Mutex
done := 0
said := func(d string, began time.Time) {
doneMu.Lock()
defer doneMu.Unlock()
done++
if done < len(dirs) {
buildstep.Phase(ctx, fmt.Sprintf("unit: lint of %d directories, %d done (%s in %s)", len(dirs), done, d, m.clock().Sub(began).Round(time.Second)))
}
}
for i, d := range dirs {
wg.Add(1)
slots <- struct{}{}
@@ -157,7 +170,9 @@ func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string,
crashMu.Unlock()
}
}()
began := m.clock()
answers[i].failed, answers[i].err = m.lint(ctx, run, tree, d, env(d))
said(d, began)
}()
}
wg.Wait()
@@ -290,8 +290,8 @@ func TestTheIntegrationLevelIsGivenTheBesideDirectoryOnlyWhenSiblingsWerePlaced(
}
tested := 0
for _, c := range f.calls {
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") {
continue
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") || strings.Contains(c.line, "-exec true") {
continue // the setup's compile runs no test binary, so it is given nothing a test reads (issue 1921, S5)
}
tested++
got := ""
@@ -72,7 +72,7 @@ func TestOnlyTheSelectedDirectoryCompilesAndRuns(t *testing.T) {
if !strings.HasPrefix(l, "go -C "+filepath.Join(tree, "b")+" test ") {
t.Errorf("go test ran outside the selected directory: %s", l)
}
if strings.Contains(l, "-run ^$") {
if strings.Contains(l, "-exec true") {
compiled++
} else {
ran++
@@ -91,7 +91,7 @@ func TestWithoutASelectionEveryDirectoryCompilesAndRuns(t *testing.T) {
compiled, ran := 0, 0
for _, l := range strings.Split(f.lines(), "\n") {
if strings.HasPrefix(l, "go ") && strings.Contains(l, " test ") {
if strings.Contains(l, "-run ^$") {
if strings.Contains(l, "-exec true") {
compiled++
} else {
ran++
@@ -146,27 +146,85 @@ func TestSelectedDirectoriesRunAtOnceAndAnswerInDirectoryOrder(t *testing.T) {
}
}
// The bound holds: with one directory at a time, no two go test commands run at once.
// The bound holds for the setup's compiles and for the tests: no more of them run at once than the bound, and with a
// bound of 3 the three directories compile at once, beside the store's start, given nothing a test reads, since the
// compile runs no test binary (issue 1921, try speed S5).
func TestTheDirectoriesAtOnceNeverPassTheBound(t *testing.T) {
f := newIntegFake(t, "slow")
var mu sync.Mutex
running, most := 0, 0
f.onTest = func(string) {
mu.Lock()
running++
if running > most {
most = running
}
mu.Unlock()
time.Sleep(20 * time.Millisecond)
mu.Lock()
running--
mu.Unlock()
}
m, states := threeModules(t, f)
m.Parallel = 1
runIntegrationOf(t, m, states)
if most != 1 {
t.Errorf("%d go test commands ran at once with a bound of 1", most)
for _, tc := range []struct {
name string
compile bool // the setup's compiles are counted, not the tests
bound int
want int // how many must run at once (at most the bound always)
}{
{"tests one at a time", false, 1, 1},
{"compiles one at a time", true, 1, 1},
{"compiles at once beside the store", true, 3, 3},
} {
t.Run(tc.name, func(t *testing.T) {
f := newIntegFake(t, "slow")
var mu sync.Mutex
running, most := 0, 0
reached := make(chan struct{})
storeStarting, beside := false, false
count := func() {
mu.Lock()
running++
most = max(most, running)
if running == tc.want && tc.want > 1 {
close(reached)
}
mu.Unlock()
if tc.want > 1 {
select { // held until all of them run, so "at once" does not hang on timing
case <-reached:
case <-time.After(2 * time.Second):
}
} else {
time.Sleep(20 * time.Millisecond)
}
mu.Lock()
running--
mu.Unlock()
}
if tc.compile {
f.onCompile = func(_ string, env []string) {
for _, e := range env {
if strings.HasPrefix(e, "MESH_TEST_") {
t.Errorf("the compile was given %s: it runs no test binary", e)
}
}
mu.Lock()
beside = beside || storeStarting
mu.Unlock()
count()
}
if tc.want > 1 {
f.onStart = func() { // the store starts until the compiles have all run at once, or 2 s pass
mu.Lock()
storeStarting = true
mu.Unlock()
select {
case <-reached:
case <-time.After(2 * time.Second):
}
}
}
} else {
f.onTest = func(string) { count() }
}
m, states := threeModules(t, f)
m.Parallel = tc.bound
if _, st := runIntegrationOf(t, m, states); st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
mu.Lock()
defer mu.Unlock()
if most > tc.bound || most < tc.want {
t.Errorf("%d ran at once with a bound of %d, want %d", most, tc.bound, tc.want)
}
if tc.compile && tc.want > 1 && !beside {
t.Errorf("no compile ran while the store was starting:\n%s", f.lines())
}
})
}
}
@@ -251,8 +251,15 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
if o.level == LevelUnit {
// gofmt and go vet of each directory the level runs come before its tests (ADR 0529 §1; issue 1887 D7-go): a
// directory that fails them fails the level at once, and their time is the result's Lint.
// go vet compiles and runs nothing of the change, with cgo off (goEnv), so it writes the shared cache and warms it
// for every later check, a pull request's head included (ADR 0541 §2, ADR 0542 §1); the tests run beneath their
// own throwaway layer.
compile, err := m.stepRunner(o.workspace, true)
if err != nil {
return nil, err
}
linted := m.clock()
failed, err := m.lintAll(ctx, run, tree, todo, func(d string) []string {
failed, err := m.lintAll(ctx, compile, tree, todo, func(d string) []string {
return append(m.goEnvFor(filepath.Join(tree, d)), o.env...)
})
if err != nil {
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildgo
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028235126-2a8670189188
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+4 -2
View File
@@ -1,5 +1,7 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5 h1:L7EJbCPZfJRCYHyco5Ck3ui+pWc//sp+h5nnPqSarng=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028235126-2a8670189188 h1:0ZJZmp4wqiJlBlVHAdGonVIRaLf0Z79Ql4DFR2k8E8c=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028235126-2a8670189188/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildimage
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5 h1:L7EJbCPZfJRCYHyco5Ck3ui+pWc//sp+h5nnPqSarng=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5/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=
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildnode
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771 h1:Tt16HJk8JZRce1i3JzzDSNdCWOrv7dw8FQ/bckNQ5Ig=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029233535-78c9ec881771/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5 h1:L7EJbCPZfJRCYHyco5Ck3ui+pWc//sp+h5nnPqSarng=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
+3 -8
View File
@@ -21,16 +21,11 @@ account alone writes its buckets.
## How a value resolves
For every module the seat mesh-placement places on a node (`mesh-delivery-assigner.placement`), from the module's
own manifests as the delivery built them, never from the controller (issue 1953): the trunk's, which the indexer's
`modules.<module>` names in its manifests by digest (`mesh-delivery-indexer.modules`, `.manifests`), with the build the
node's want names (`mesh-delivery-assigner.wants`, the builder's `mesh-delivery-builder.builds` naming its manifest)
laid over it key by key on that node. So a module from its own repository resolves as a catalogue module does. A placed
module none of whose manifests can be read is listed under `unresolved` with why and said in the log, and keeps the
values last resolved for it. Then: the manifest's default; mesh config for the predefined key the value falls back
From the controller's mirrors of the assignments and of each module's declarations (`mesh-controller.assignments`,
`mesh-controller.declared-config`): the manifest's default; mesh config for the predefined key the value falls back
to; node config for that key on the node; the module's own value on that node, the most specific winning. A value
naming no predefined key resolves from its default and the module's own value only (ADR 0348 §2). It resolves at
start, whenever a placement, a want or a trunk module moves, and every five minutes, writing only the values that moved.
start, whenever a mirror moves, and every five minutes, writing only the values that moved.
## The verb
@@ -97,10 +97,6 @@ type Config struct {
RouterWithin, RouterEvery time.Duration
endedMu sync.Mutex
ended map[string]endedWord
// manifests is each manifest's declarations read, by digest: a manifest never changes (manifestDeclarations).
manifestsMu sync.Mutex
manifests map[string]ModuleDeclarations
}
// Call is one call of the verb, its arguments read.
@@ -340,8 +336,8 @@ func (c *Config) declared(s Sources, level Level, key string) (rule, bool, error
joinKeys(decl.Config))
}
if !assigned(s, level.Node, level.Module) {
return rule{}, false, fmt.Errorf("%s is not placed on %s, and a module's own value is set for a node it "+
"is placed on (novox/hq ADR 0348 §3); it is placed on: %s", level.Module, level.Node,
return rule{}, false, fmt.Errorf("%s is not assigned to %s, and a module's own value is set for a node it "+
"is assigned to (novox/hq ADR 0348 §3); it is assigned to: %s", level.Module, level.Node,
strings.Join(nodesOf(s, level.Module), ", "))
}
trusted := d.Trusted
@@ -9,8 +9,6 @@ import (
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// memStore is the bus as mesh-config reaches it, in memory: its states and the mirrors it reads, its history log,
@@ -122,81 +120,9 @@ func (m *memStore) mirror(t *testing.T, read, key, raw string) {
m.states[read][key] = []byte(raw)
}
// assign places a module on a node, in the seat mesh-placement's key for the node.
func (m *memStore) assign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, module, true)
}
// unassign takes a module off a node's placement.
func (m *memStore) unassign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, module, false)
}
func (m *memStore) place(t *testing.T, node, module string, on bool) {
t.Helper()
type placed struct {
Module string `json:"module"`
}
p := struct {
V int `json:"v"`
Node string `json:"node"`
Modules []placed `json:"modules"`
}{V: 1, Node: node}
if e, _ := m.Get(PlacementRead, "placement."+node); e != nil {
if err := json.Unmarshal(e.Value, &p); err != nil {
t.Fatal(err)
}
}
var kept []placed
for _, x := range p.Modules {
if x.Module != module {
kept = append(kept, x)
}
}
if on {
kept = append(kept, placed{Module: module})
}
p.Modules = kept
if err := m.Put(PlacementRead, "placement."+node, p); err != nil {
t.Fatal(err)
}
}
// manifest keeps a manifest in the indexer's manifests by its digest, as the indexer writes it, and answers the digest.
func (m *memStore) manifest(t *testing.T, module, raw string) string {
t.Helper()
if !json.Valid([]byte(raw)) {
t.Fatalf("not JSON: %s", raw)
}
digest := delivery.ManifestDigest([]byte(raw))
value, err := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
if err != nil {
t.Fatal(err)
}
m.mirror(t, ManifestsRead, digest, string(value))
return digest
}
// declare makes raw the module's manifest at the trunk: kept by digest, and named by the indexer's modules.<module>.
func (m *memStore) declare(t *testing.T, module, raw string) {
t.Helper()
digest := m.manifest(t, module, raw)
m.mirror(t, ModulesRead, module, fmt.Sprintf(`{"v":1,"manifest":%q,"repository":"novox/%s","commit":"c1"}`,
digest, module))
}
// want makes a node's want name a build of the module whose manifest is raw, as the assigner and the builder keep them.
func (m *memStore) want(t *testing.T, node, module, fp, raw string) {
t.Helper()
digest := m.manifest(t, module, raw)
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil {
t.Fatal(err)
}
m.mirror(t, BuildsRead, key, fmt.Sprintf(`{"v":1,"state":"done","manifest":%q}`, digest))
m.mirror(t, WantsRead, "want."+node+"."+module, fmt.Sprintf(`{"v":1,"fp":%q,"self":true}`, fp))
m.mirror(t, AssignmentsRead, node+"."+module,
fmt.Sprintf(`{"node":%q,"module":%q,"assigned":"2026-10-14T00:00:00Z"}`, node, module))
}
func (m *memStore) resolved(t *testing.T, node, module, key string) Resolved {
@@ -228,10 +154,10 @@ const theList = `{"keys":{"agent-managed-settings":{"type":"object","description
func aMesh(t *testing.T) (*Config, *memStore) {
t.Helper()
m := newStore()
m.declare(t, "dunst", dunstDeclared)
m.declare(t, "claude-code", `{"module":"claude-code","settings":{
m.mirror(t, DeclaredConfigRead, "dunst", dunstDeclared)
m.mirror(t, DeclaredConfigRead, "claude-code", `{"module":"claude-code","settings":{
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`)
m.declare(t, "old", `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`)
m.mirror(t, DeclaredConfigRead, "old", `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`)
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList)
for _, node := range []string{"g14", "novox"} {
m.assign(t, node, "dunst")
@@ -372,8 +298,8 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
{"a key not on the predefined list, at a node", Call{Key: "width", Value: 400.0, Node: "g14"}, "is not one"},
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"},
"no module value for the whole mesh"},
{"a module on a node it is not placed on", Call{Key: "width", Value: 400.0, Node: "ace", Module: "dunst"},
"it is placed on: g14, novox"},
{"a module on a node it is not assigned to", Call{Key: "width", Value: 400.0, Node: "ace", Module: "dunst"},
"it is assigned to: g14, novox"},
{"a module not registered", Call{Key: "width", Value: 400.0, Node: "g14", Module: "nobody"}, "not a registered module"},
{"a value of another type", Call{Key: "font-size", Value: "12"}, `"12" is not a number`},
{"a value below its minimum", Call{Key: "font-size", Value: 2.0, Node: "g14"}, "below the minimum 6"},
@@ -546,7 +472,7 @@ func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T
}
}
// **A placement, a declaration or its removal moves the resolved values**, written only where they moved, and
// **An assignment, a declaration or its removal moves the resolved values**, written only where they moved, and
// said as config-changed; a pass over unchanged sources writes nothing.
func TestAnAssignmentMovesTheResolvedValuesAndAQuietPassWritesNothing(t *testing.T) {
c, m := aMesh(t)
@@ -563,7 +489,7 @@ func TestAnAssignmentMovesTheResolvedValuesAndAQuietPassWritesNothing(t *testing
if len(m.events) != 1 || m.events[0].Action != "resolve" {
t.Errorf("an assignment raised %+v", m.events)
}
m.unassign(t, "ace", "dunst")
_ = m.Delete(AssignmentsRead, "ace.dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("an unassignment moved %+v", r.Moved)
}
@@ -626,110 +552,42 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
}
}
// despiegel is de-spiegel's manifest as its own repository declares it (novox/de-spiegel at e25e1beb, its config
// alone): required trusted values, no default.
const despiegel = `{"module":"de-spiegel","version":"1","requires":["route","smtp"],"config":{
"praktijk-name":{"type":"string","description":"the practice's public name","trusted":true,"required":true},
"praktijk-www-name":{"type":"string","description":"its www form","trusted":true,"required":true}}}`
// Issue 1953: **each placed module's declarations come from its own manifests as built**, the trunk's at the indexer
// and the wanted build's at the builder, never from the controller's mirror; a module placed with no manifest to read
// is said unresolved with why, in the listing and the log, and keeps what was resolved for it.
func TestDeclarationsComeFromTheManifestsAsBuilt(t *testing.T) {
fp := "src2:" + strings.Repeat("ab", 32)
cases := []struct {
name string
given func(t *testing.T, m *memStore)
// resolved is each key's resolved value on novox, `<module>.<key>` to source and value ("" for none).
resolved map[string]string
absent []string
unresolved string
}{
{name: "a module of its own repository resolves from the trunk's manifest, its value set",
given: func(t *testing.T, m *memStore) {
m.declare(t, "de-spiegel", despiegel)
m.assign(t, "novox", "de-spiegel")
must(t, m.Put(ValuesState, Level{Kind: LevelModule, Node: "novox", Module: "de-spiegel"}.ValueKey(
"praktijk-name"), Value{Value: json.RawMessage(`"praktijkdespiegel.be"`), By: "the operator"}))
// The controller's mirror says otherwise, and is not read.
m.mirror(t, "mesh-controller.declared-config", "de-spiegel", `{"module":"de-spiegel","settings":{}}`)
},
resolved: map[string]string{"de-spiegel.praktijk-name": `module "praktijkdespiegel.be"`,
"de-spiegel.praktijk-www-name": "unset "}},
{name: "the wanted build's manifest lays its keys over the trunk's on its node",
given: func(t *testing.T, m *memStore) {
m.declare(t, "de-spiegel", despiegel)
m.assign(t, "novox", "de-spiegel")
m.want(t, "novox", "de-spiegel", fp, `{"module":"de-spiegel","config":{
"praktijk-name":{"type":"string","description":"d","default":"old.example"},
"old-key":{"type":"number","description":"d","default":3}}}`)
},
resolved: map[string]string{"de-spiegel.praktijk-name": `default "old.example"`,
"de-spiegel.old-key": "default 3", "de-spiegel.praktijk-www-name": "unset "}},
{name: "a removed want resolves from the trunk alone",
given: func(t *testing.T, m *memStore) {
m.declare(t, "de-spiegel", despiegel)
m.assign(t, "novox", "de-spiegel")
m.want(t, "novox", "de-spiegel", fp, `{"module":"de-spiegel","config":{
"old-key":{"type":"number","description":"d","default":3}}}`)
m.mirror(t, WantsRead, "want.novox.de-spiegel", `{"v":1,"removed":true}`)
},
resolved: map[string]string{"de-spiegel.praktijk-name": "unset "}, absent: []string{"de-spiegel.old-key"}},
{name: "a placed module the indexer holds no manifest of is unresolved, said, its values kept",
given: func(t *testing.T, m *memStore) {
must(t, m.Put(ResolvedState, ResolvedKey("novox", "de-spiegel", "praktijk-name"),
Resolved{Node: "novox", Module: "de-spiegel", Key: "praktijk-name", Source: SourceUnset}))
m.assign(t, "novox", "de-spiegel")
},
resolved: map[string]string{"de-spiegel.praktijk-name": "unset "},
unresolved: "the indexer holds no modules.de-spiegel"},
// Issue 1098: a value of the assignments bucket with `ended` set, or with a `state`, is not an assignment: it
// resolves nothing and takes the module's resolved values away, as a removed key does.
func TestAnEndedAssignmentIsNotAnAssignment(t *testing.T) {
c, m := aMesh(t)
m.assign(t, "ace", "dunst")
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
t.Fatalf("the assignment moved %+v (%v)", r.Moved, err)
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m := newStore()
tc.given(t, m)
var said []string
c := &Config{Store: m, Now: time.Now, Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
for k, want := range tc.resolved {
module, key, _ := strings.Cut(k, ".")
r := m.resolved(t, "novox", module, key)
if got := r.Source + " " + string(r.Value); got != want {
t.Errorf("%s resolved as %s, want %s", k, got, want)
}
}
for _, k := range tc.absent {
module, key, _ := strings.Cut(k, ".")
if e, _ := m.Get(ResolvedState, ResolvedKey("novox", module, key)); e != nil {
t.Errorf("%s is resolved: %s", k, e.Value)
}
}
l, err := c.List(Call{Module: "de-spiegel"})
if err != nil {
t.Fatal(err)
}
if tc.unresolved == "" {
if len(l.Unresolved) != 0 {
t.Errorf("unresolved: %+v", l.Unresolved)
}
return
}
if len(l.Unresolved) != 1 || l.Unresolved[0].Node != "novox" ||
!strings.Contains(l.Unresolved[0].Why, tc.unresolved) {
t.Errorf("the listing says %+v, want %q", l.Unresolved, tc.unresolved)
}
if !strings.Contains(strings.Join(said, "\n"), tc.unresolved) {
t.Errorf("the log does not say it: %q", said)
}
})
for _, ended := range []string{`"ended":"2026-10-18T01:00:00Z"`, `"state":"pending"`} {
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+ended+`}`)
r, err := c.Reconcile()
if err != nil || len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("%s moved %+v (%v)", ended, r.Moved, err)
}
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil {
t.Errorf("%s: the module keeps its resolved values", ended)
}
m.assign(t, "ace", "dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 {
t.Fatalf("the assignment standing again moved %+v", r.Moved)
}
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
// Issue 1098: the fields the controller adds to an assignment change nothing, under the same key.
func TestAnAssignmentCarriesBuildCommitAndRolloutWithoutChangingItsKey(t *testing.T) {
c, m := aMesh(t)
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","assigned":"2026-10-14T00:00:00Z",`+
`"build":"b-1","artifact":{"name":"dunst","version":"1.2"},"commit":"abc123","rollout":"r-9",`+
`"since":"2026-10-18T00:00:00Z","ended":"","state":"","future":{"x":1}}`)
r, err := c.Reconcile()
if err != nil || len(r.Moved) != 4 {
t.Fatalf("an assignment with the new fields moved %+v (%v)", r.Moved, err)
}
expect(t, m, "ace", "10", SourceDefault)
if r, _ = c.Reconcile(); len(r.Moved) != 0 {
t.Errorf("a quiet pass moved %+v", r.Moved)
}
}
+20 -33
View File
@@ -11,8 +11,8 @@ import (
"strings"
)
// What mesh-config resolves from (novox/hq ADR 0348 §2, §4, §7): each module's declarations as its manifests declare
// them, the list of predefined keys, and where each module is placed (issue 1953).
// What mesh-config resolves from (novox/hq ADR 0348 §2, §4, §7): each module's declarations, the list of predefined
// keys, and the assignments, as the controller mirrors them on the bus (design 57 F3, K3).
// The types a config value may have (ADR 0348 §4).
const (
@@ -69,34 +69,7 @@ type ModuleDeclarations struct {
Config map[string]Declaration `json:"settings"`
}
// ManifestDeclarations is the config a manifest declares (ADR 0348 §1, §4): its `config`, or for a module not yet
// moved its `settings` (kind, default and why), the two never both. Only these two fields are read, so a manifest
// field this module's SDK does not know yet never leaves a module's config unresolved.
func ManifestDeclarations(raw []byte) (ModuleDeclarations, error) {
var m struct {
Module string `json:"module"`
Config map[string]Declaration `json:"config"`
Settings map[string]Declaration `json:"settings"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return ModuleDeclarations{}, fmt.Errorf("not a manifest: %w", err)
}
if m.Config != nil && m.Settings != nil {
return ModuleDeclarations{}, fmt.Errorf("%s declares both config and settings, and a module uses one form "+
"(novox/hq ADR 0348 §1)", m.Module)
}
decl := m.Config
if decl == nil {
decl = m.Settings
}
raw, err := json.Marshal(ModuleDeclarations{Module: m.Module, Config: decl})
if err != nil {
return ModuleDeclarations{}, err
}
return ParseModuleDeclarations(raw)
}
// ParseModuleDeclarations reads one module's declarations in the layout ModuleDeclarations writes.
// ParseModuleDeclarations reads one key of the declared-config mirror.
func ParseModuleDeclarations(raw []byte) (ModuleDeclarations, error) {
var m ModuleDeclarations
if err := json.Unmarshal(raw, &m); err != nil {
@@ -148,10 +121,24 @@ type Published struct {
Commit string `json:"commit"`
}
// Assignment is a module placed on a node, as the seat mesh-placement's key `placement.<node>` names it.
// Assignment is one key of the assignments bucket: a module on a node. The value also carries the build, commit and
// rollout that put it there and more; those are read where they are reported, and an unknown field never fails a
// parse. **Ended** and **State** are set on a value that is not a standing assignment (novox/hq ADR 0408 clause 4):
// it reads exactly as a removed key (issue 1098).
type Assignment struct {
Node string `json:"node"`
Module string `json:"module"`
Node string `json:"node"`
Module string `json:"module"`
Ended json.RawMessage `json:"ended,omitempty"`
State json.RawMessage `json:"state,omitempty"`
}
// Standing is whether the value is a standing assignment: neither ended nor in a state.
func (a Assignment) Standing() bool { return !fieldSet(a.Ended) && !fieldSet(a.State) }
// fieldSet is whether a field of an assignment's value carries something: not absent, null or empty.
func fieldSet(f json.RawMessage) bool {
s := strings.TrimSpace(string(f))
return s != "" && s != "null" && s != `""`
}
// rule is the validation a value is held to, from a module's declaration or a predefined key.
+3 -11
View File
@@ -17,8 +17,6 @@ type Listed struct {
Predefined map[string]Published `json:"predefined,omitempty"`
// Withheld is why the newest list of predefined keys is not published, when it is not.
Withheld string `json:"withheld,omitempty"`
// Unresolved is each placed module the call narrows to whose declarations cannot be read, with why (issue 1953).
Unresolved []Unresolved `json:"unresolved,omitempty"`
// Held is the approved values held for open pull requests, not yet set (novox/hq ADR 0502).
Held []HeldValue `json:"held,omitempty"`
}
@@ -44,18 +42,12 @@ func (c *Config) List(call Call) (*Listed, error) {
if err != nil {
return nil, err
}
out := &Listed{Resolved: []Resolved{}, Set: []SetValue{}, Withheld: withheld}
for _, u := range s.Unresolved {
if (call.Module == "" || u.Module == call.Module) && (call.Node == "" || u.Node == call.Node) {
out.Unresolved = append(out.Unresolved, u)
}
}
if call.Module != "" {
if _, ok := s.Declared[call.Module]; !ok && len(out.Unresolved) == 0 {
return nil, fmt.Errorf("%s is placed on no node with a manifest the indexer holds, so it declares no "+
"config", call.Module)
if _, ok := s.Declared[call.Module]; !ok {
return nil, fmt.Errorf("%s is not a registered module, so it declares no config", call.Module)
}
}
out := &Listed{Resolved: []Resolved{}, Set: []SetValue{}, Withheld: withheld}
resolved := Resolve(s)
for _, k := range sortedKeys(resolved) {
r := resolved[k]
+3 -6
View File
@@ -4,9 +4,8 @@
// node), mesh-config_resolved (each module's value on each node, with its source), mesh-config_predefined (the
// published list of predefined keys) and mesh-config_proposals (each value proposed for the operator's Approve,
// asked under this module's own name, design 57 K4).
// It resolves from where the seat mesh-placement places each module, and each module's declarations as its own
// manifests declare them, the trunk's and its wanted build's (the indexer's and the builder's state, issue 1953); the
// list of predefined keys still comes from the controller's mirror until the indexer keeps it (ADR 0565 §2). stdout is the MCP channel; what this module says, it says on stderr.
// It resolves from the controller's mirrors of the assignments, the modules' declarations and the list it kept
// (design 57 F3, K3). stdout is the MCP channel; what this module says, it says on stderr.
package main
import (
@@ -116,9 +115,7 @@ func resolving(c *Config) {
}
return nil
}
// A build's manifest is written before a want names the build, so a want, a trunk module or a placement moving is
// every change of what a module declares where (issue 1953); the builds themselves are read, not watched.
for _, read := range []string{PlacementRead, WantsRead, ModulesRead, PredefinedRead} {
for _, read := range []string{AssignmentsRead, PlacementRead, DeclaredConfigRead, PredefinedRead} {
go func(read string) {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
time.Sleep(wait)
@@ -573,7 +573,7 @@ func TestTheProposalArgumentsAreReadStrictly(t *testing.T) {
// not refused as carrying a secret: modules/asus-zephyrus-g14, i3status-rust and powerlevel10k are names.
func TestAnOwnersListOfNamesWithDigitsIsProposed(t *testing.T) {
c, m, r := aProposingMesh(t)
m.declare(t, "mesh-issues", `{"module":"mesh-issues","settings":{
m.mirror(t, DeclaredConfigRead, "mesh-issues", `{"module":"mesh-issues","settings":{
"owners":{"kind":"","default":[],"why":"","type":"list","description":"owners","trusted":true}}}`)
m.assign(t, "novox", "mesh-issues")
raw, err := os.ReadFile(filepath.Join("testdata", "owners-1439.json"))
@@ -0,0 +1,36 @@
package main
import "testing"
// A module only pulled in by another on a node, and handed to the provisioner there, has a value of its own in the
// assignments mirror (mesh-controller internal/inventory AssignmentValues, issue 765): what requires it, its plan,
// and no time it was assigned. Its values resolve there like an assigned module's, so the node's client can fill its
// templates; they go when the value goes.
func TestAModulePulledInHasItsValuesResolvedOnItsNode(t *testing.T) {
c, m := aMesh(t)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","required-by":"desk",`+
`"provisioning":{"templates":{"source":"x","digest":"y"},"files":[]}}`)
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
t.Fatalf("a pulled-in module's value moved %+v (%v)", r.Moved, err)
}
expect(t, m, "ace", "10", SourceDefault)
_ = m.Delete(AssignmentsRead, "ace.dunst")
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("a module no longer pulled in moved %+v", r.Moved)
}
// 1126 (ADR 0549 §3, §5): a module the catalogue's placement names on a node resolves there as an assigned one does,
// while the two sources stand; it goes when neither names it.
m.mirror(t, PlacementRead, "placement.ace", `{"v":1,"node":"ace","modules":[{"module":"dunst"}]}`)
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
t.Fatalf("a placed module's values moved %+v (%v)", r.Moved, err)
}
expect(t, m, "ace", "10", SourceDefault)
_ = m.Delete(PlacementRead, "placement.ace")
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("a module no longer placed moved %+v", r.Moved)
}
}
+2 -2
View File
@@ -378,8 +378,8 @@ func heldRule(s Sources, call Call, level Level) (rule, PullDecl, string, error)
return rule{}, PullDecl{}, "", fmt.Errorf("%s is not a registered module, so a value cannot be held for it", level.Module)
}
if !assigned(s, level.Node, level.Module) {
return rule{}, PullDecl{}, "", fmt.Errorf("%s is not placed on %s, and a module's own value is set for a node "+
"it is placed on (novox/hq ADR 0348 §3); it is placed on: %s", level.Module, level.Node,
return rule{}, PullDecl{}, "", fmt.Errorf("%s is not assigned to %s, and a module's own value is set for a node "+
"it is assigned to (novox/hq ADR 0348 §3); it is assigned to: %s", level.Module, level.Node,
strings.Join(nodesOf(s, level.Module), ", "))
}
return d.rule(), pd, declDigest(d), nil
@@ -23,7 +23,7 @@ func n8nPullDeclares() map[string]any {
func aHoldingMesh(t *testing.T) (*Config, *memStore, *standIn, *standForge) {
t.Helper()
c, m, r := aProposingMesh(t)
m.declare(t, "n8n", `{"module":"n8n","settings":{}}`)
m.mirror(t, DeclaredConfigRead, "n8n", `{"module":"n8n","settings":{}}`)
m.assign(t, "ace", "n8n")
f := &standForge{state: map[string][2]bool{}}
c.Forge = f
@@ -112,7 +112,7 @@ func TestAPullRequestsDeclarationsAreTakenFromTheControllerAlone(t *testing.T) {
}
// 1826/K2: a proposal for a key only the pull request declares is refused without its declaration, for a mesh or
// node level, for a clear, for an undeclared key, and for a module not placed on the node; and trunk-only
// node level, for a clear, for an undeclared key, and for a module not assigned to the node; and trunk-only
// proposals are judged as before.
func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T) {
c, m, _, _ := aHoldingMesh(t)
@@ -142,7 +142,7 @@ func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T)
"a key not added": {func(c *Call) { c.Key = "other" }, "does not add or change a value"},
"a bad value": {func(c *Call) { c.Value = 0.0 }, "below the minimum"},
"a wrong type": {func(c *Call) { c.Value = "2000" }, "is not a number"},
"another node": {func(c *Call) { c.Node = "g14" }, "is not placed on g14"},
"another node": {func(c *Call) { c.Node = "g14" }, "is not assigned to g14"},
"another pull": {func(c *Call) { c.Pull = "novox/mesh-catalog#435" }, "holds no declaration for"},
"a malformed pull": {func(c *Call) { c.Pull = "434" }, "owner/repository#number"},
} {
@@ -342,7 +342,7 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); e != nil {
t.Errorf("resolved before the trunk declares the key: %s", e.Value)
}
m.declare(t, "n8n", `{"module":"n8n","settings":{"media-gid":{"kind":"","default":null,"why":"",`+
m.mirror(t, DeclaredConfigRead, "n8n", `{"module":"n8n","settings":{"media-gid":{"kind":"","default":null,"why":"",`+
`"type":"number","required":true,"minimum":1,"maximum":65535}}}`)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
+13 -59
View File
@@ -6,8 +6,6 @@ import (
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The four sources (novox/hq ADR 0348 §2, §3, §9): a module's value on a node resolves, the most specific winning,
@@ -22,21 +20,12 @@ const (
PredefinedState = "predefined"
ProposalsState = "proposals"
AssignmentsRead = "mesh-controller.assignments"
// PlacementRead is the seat mesh-placement's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
// node's key `placement.<node>` with the modules placed there. It is the one source of which module runs where
// (issue 1953): the controller's assignments are no longer read.
PlacementRead = delivery.PlacementRead
// WantsRead is the assigner's wants, `want.<node>.<module>`: the build each node is to run (ADR 0539 §1).
WantsRead = delivery.AssignerModule + ".wants"
// BuildsRead is the builder's builds, `<module>.<fingerprint hex>`: each build's manifest digest (ADR 0525).
BuildsRead = delivery.BuilderModule + ".builds"
// ModulesRead is the indexer's modules, `<module>`: the trunk's manifest digest of each module, from whichever
// repository holds it (ADR 0437 §5, ADR 0478 §9); ManifestsRead its manifests by digest (ADR 0429 §16).
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
// PredefinedRead is the controller's list of predefined keys, which ADR 0565 §2 moves to the indexer's
// `predefined`: read here until that state exists.
PredefinedRead = "mesh-controller.predefined"
// node's key `placement.<node>` with the modules placed there.
PlacementRead = "mesh-delivery-assigner.placement"
DeclaredConfigRead = "mesh-controller.declared-config"
PredefinedRead = "mesh-controller.predefined"
// HistoryLog is the log of every set and clear, with the proven caller and why (ADR 0348 §9).
HistoryLog = "history"
@@ -134,59 +123,24 @@ type Resolved struct {
// ResolvedKey is a resolved value's key in mesh-config_resolved.
func ResolvedKey(node, module, key string) string { return node + "." + module + "." + key }
// Sources is everything a resolution reads: where each module is placed, every module's declarations, the published
// list of predefined keys, and every value set.
// Sources is everything a resolution reads: the assignments, every module's declarations, the published list of
// predefined keys, and every value set.
type Sources struct {
Assignments []Assignment
// Declared is each module's declarations as its manifests declare them: the trunk's, with every key only a wanted
// build still declares (declarationsOf). It is what a set is judged against.
Declared map[string]ModuleDeclarations
// OnNode is, by `<node>.<module>`, the declarations of the build the node's want names laid over the trunk's: what
// that node resolves. A placed module absent here resolves from Declared.
OnNode map[string]ModuleDeclarations
// Unresolved is each placed module whose declarations could not be read, with why: never skipped silently
// (issue 1953). Its resolved values are kept as they were until its declarations read again.
Unresolved []Unresolved
Predefined map[string]Published
Values map[string]Value
Declared map[string]ModuleDeclarations
Predefined map[string]Published
Values map[string]Value
// Pulls is what the controller told of each open pull request's declarations, and Kept the approved values held
// for them (pulls.go); neither is resolved.
Pulls map[string]PullDecl
Kept map[string]KeptValue
}
// Unresolved is a module placed on a node whose values are not resolved, and why.
type Unresolved struct {
Node string `json:"node"`
Module string `json:"module"`
Why string `json:"why"`
}
// declarationsOn is the declarations a module on a node resolves from, and whether any are held.
func (s Sources) declarationsOn(node, module string) (ModuleDeclarations, bool) {
if d, ok := s.OnNode[node+"."+module]; ok {
return d, true
}
d, ok := s.Declared[module]
return d, ok
}
// unresolved is whether a module on a node is unresolved.
func (s Sources) unresolved(node, module string) bool {
for _, u := range s.Unresolved {
if u.Node == node && u.Module == module {
return true
}
}
return false
}
// Resolve is every placed module's every declared value on its node, keyed as mesh-config_resolved is. A placed
// module with no declarations is in s.Unresolved, said there (sources).
// Resolve is every assigned module's every declared value on its node, keyed as mesh-config_resolved is.
func Resolve(s Sources) map[string]Resolved {
out := map[string]Resolved{}
for _, a := range s.Assignments {
decl, ok := s.declarationsOn(a.Node, a.Module)
decl, ok := s.Declared[a.Module]
if !ok {
continue
}
@@ -242,7 +196,7 @@ type Moved struct {
From json.RawMessage `json:"from,omitempty"`
To json.RawMessage `json:"to,omitempty"`
Source string `json:"source"`
// Gone says the module no longer takes the key there: no longer placed, or no longer declared.
// Gone says the module no longer takes the key there: unassigned, or no longer declared.
Gone bool `json:"gone,omitempty"`
}
+58 -233
View File
@@ -4,30 +4,73 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Reading the sources, writing the resolved values, and publishing the list of predefined keys.
// sources reads everything a resolution reads, from the bus as it is now: which modules are placed where from the
// seat mesh-placement, and each placed module's declarations from its own manifests as built, never from the
// controller (issue 1953, ADR 0565 §3).
// sources reads everything a resolution reads, from the bus as it is now.
func (c *Config) sources() (Sources, error) {
s := Sources{Declared: map[string]ModuleDeclarations{}, OnNode: map[string]ModuleDeclarations{},
Predefined: map[string]Published{}, Values: map[string]Value{}}
s := Sources{Declared: map[string]ModuleDeclarations{}, Predefined: map[string]Published{},
Values: map[string]Value{}}
var problems []string
if err := c.readPlacement(&s); err != nil {
return s, fmt.Errorf("the seat mesh-placement's placement cannot be read: %w", err)
if err := c.each(AssignmentsRead, func(key string, raw []byte) error {
var a Assignment
if err := json.Unmarshal(raw, &a); err != nil || a.Node == "" || a.Module == "" {
return fmt.Errorf("the assignment %s is not one: %s", key, raw)
}
if !a.Standing() {
return nil // ended or pending: read as a removed key (issue 1098)
}
s.Assignments = append(s.Assignments, a)
return nil
}); err != nil {
return s, fmt.Errorf("the assignments cannot be read: %w", err)
}
if err := c.readDeclarations(&s); err != nil {
// And every module the seat's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both
// sources stand (ADR 0549 §5), so a pair either names is resolved, and none loses its values while the two differ.
// Placement that cannot be read yet (the seat's state not on the bus) leaves the assignments alone, said.
standing := map[string]bool{}
for _, a := range s.Assignments {
standing[a.Node+"."+a.Module] = true
}
if err := c.each(PlacementRead, func(key string, raw []byte) error {
node, ok := strings.CutPrefix(key, "placement.")
if !ok {
return nil
}
var p struct {
Node string `json:"node"`
Modules []struct {
Module string `json:"module"`
} `json:"modules"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.Node != node {
problems = append(problems, fmt.Sprintf("the placement %s is not one", key))
return nil
}
for _, m := range p.Modules {
if m.Module != "" && !standing[node+"."+m.Module] {
standing[node+"."+m.Module] = true
s.Assignments = append(s.Assignments, Assignment{Node: node, Module: m.Module})
}
}
return nil
}); err != nil {
problems = append(problems, fmt.Sprintf("the seat's placement cannot be read, so the assignments alone are resolved: %v", err))
}
if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error {
m, err := ParseModuleDeclarations(raw)
if err != nil {
// One module's declarations unread leaves that module unresolved, said; the rest resolve.
problems = append(problems, fmt.Sprintf("%s: %v", key, err))
return nil
}
s.Declared[m.Module] = m
return nil
}); err != nil {
return s, fmt.Errorf("the modules' declarations cannot be read: %w", err)
}
for _, u := range s.Unresolved {
problems = append(problems, fmt.Sprintf("%s on %s: %s", u.Module, u.Node, u.Why))
}
if err := c.each(PredefinedState, func(key string, raw []byte) error {
var p Published
if err := json.Unmarshal(raw, &p); err != nil {
@@ -52,222 +95,11 @@ func (c *Config) sources() (Sources, error) {
return s, err
}
for _, p := range problems {
c.logf("[mesh-config] a placed module's declarations are not read, so its values are not resolved (they "+
"stay as last resolved): %s", p)
c.logf("[mesh-config] a module's declarations are not read, so its values are not resolved: %s", p)
}
return s, nil
}
// readPlacement is every module the seat mesh-placement places on a node (ADR 0567 §1, §9): each node's key
// `placement.<node>`.
func (c *Config) readPlacement(s *Sources) error {
placed := map[string]bool{}
return c.each(PlacementRead, func(key string, raw []byte) error {
node, err := delivery.ParsePlacementKey(key)
if err != nil {
return nil // a key of another kind
}
var p struct {
Node string `json:"node"`
Modules []struct {
Module string `json:"module"`
} `json:"modules"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.Node != node {
return fmt.Errorf("the placement %s is not one", key)
}
for _, m := range p.Modules {
if m.Module != "" && !placed[node+"."+m.Module] {
placed[node+"."+m.Module] = true
s.Assignments = append(s.Assignments, Assignment{Node: node, Module: m.Module})
}
}
return nil
})
}
// readDeclarations reads each placed module's declarations from its manifests as the delivery holds them: the trunk's
// (the indexer's `modules.<module>` and its manifest by digest), and, on each node whose want names a build, that
// build's (the builder's `<module>.<fp>` names its manifest digest). On a node the want's build is laid over the
// trunk's, key by key, so a key only the build it runs declares still resolves there and a key the trunk adds resolves
// before its build is wanted; Declared is the trunk's with every key only a wanted build declares. A placed module
// none of whose manifests can be read is Unresolved, with why.
func (c *Config) readDeclarations(s *Sources) error {
wanted := map[string]string{} // <node>.<module> → the fingerprint its want names
if err := c.each(WantsRead, func(key string, raw []byte) error {
node, module, err := delivery.ParseWantKey(key)
if err != nil {
return nil // a held key or a node's own want
}
var w struct {
FP string `json:"fp"`
Removed bool `json:"removed"`
}
if json.Unmarshal(raw, &w) == nil && !w.Removed && w.FP != "" {
wanted[node+"."+module] = w.FP
}
return nil
}); err != nil {
return fmt.Errorf("the assigner's wants: %w", err)
}
trunk := map[string]ModuleDeclarations{}
trunkWhy := map[string]string{}
for _, module := range modulesOf(s.Assignments) {
d, err := c.trunkDeclarations(module)
if err != nil {
trunkWhy[module] = err.Error()
continue
}
trunk[module] = d
s.Declared[module] = d
}
for _, a := range s.Assignments {
t, hasTrunk := trunk[a.Module]
fp, isWanted := wanted[a.Node+"."+a.Module]
var built ModuleDeclarations
var builtErr error
if isWanted {
built, builtErr = c.builtDeclarations(a.Module, fp)
}
switch {
case isWanted && builtErr == nil:
on := overlay(t, built)
s.OnNode[a.Node+"."+a.Module] = on
d := s.Declared[a.Module]
s.Declared[a.Module] = overlay(built, d) // the trunk's wins in what a set is judged against
case hasTrunk:
if isWanted {
c.logf("[mesh-config] %s on %s resolves from the trunk's manifest alone: the build its want names "+
"cannot be read: %v", a.Module, a.Node, builtErr)
}
default:
why := trunkWhy[a.Module]
if isWanted {
why += "; and the build its want names: " + builtErr.Error()
} else {
why += "; and no want names a build of it on " + a.Node
}
s.Unresolved = append(s.Unresolved, Unresolved{Node: a.Node, Module: a.Module, Why: why})
}
}
return nil
}
// overlay is base with every key of over laid on it, the module's name kept.
func overlay(base, over ModuleDeclarations) ModuleDeclarations {
out := ModuleDeclarations{Module: over.Module, Config: map[string]Declaration{}}
if out.Module == "" {
out.Module = base.Module
}
for k, d := range base.Config {
out.Config[k] = d
}
for k, d := range over.Config {
out.Config[k] = d
}
return out
}
func modulesOf(as []Assignment) []string {
seen := map[string]bool{}
var out []string
for _, a := range as {
if !seen[a.Module] {
seen[a.Module] = true
out = append(out, a.Module)
}
}
sort.Strings(out)
return out
}
// trunkDeclarations is a module's declarations at the trunk: the manifest the indexer's `modules.<module>` names.
func (c *Config) trunkDeclarations(module string) (ModuleDeclarations, error) {
key, err := delivery.ModulesKey(module)
if err != nil {
return ModuleDeclarations{}, err
}
e, err := c.Store.Get(ModulesRead, key)
if err != nil {
return ModuleDeclarations{}, fmt.Errorf("the indexer's modules.%s cannot be read: %w", module, err)
}
if e == nil {
return ModuleDeclarations{}, fmt.Errorf("the indexer holds no modules.%s, so no manifest of it is known", module)
}
var m struct {
Manifest string `json:"manifest"`
}
if err := json.Unmarshal(e.Value, &m); err != nil || m.Manifest == "" {
return ModuleDeclarations{}, fmt.Errorf("the indexer's modules.%s names no manifest", module)
}
return c.manifestDeclarations(module, m.Manifest)
}
// builtDeclarations is a module's declarations at a build: the manifest the builder's done key names.
func (c *Config) builtDeclarations(module, fp string) (ModuleDeclarations, error) {
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil {
return ModuleDeclarations{}, err
}
e, err := c.Store.Get(BuildsRead, key)
if err != nil {
return ModuleDeclarations{}, fmt.Errorf("the builder's %s cannot be read: %w", key, err)
}
if e == nil {
return ModuleDeclarations{}, fmt.Errorf("the builder holds no build %s", key)
}
var b struct {
Manifest string `json:"manifest"`
}
if err := json.Unmarshal(e.Value, &b); err != nil || b.Manifest == "" {
return ModuleDeclarations{}, fmt.Errorf("the builder's %s names no manifest", key)
}
return c.manifestDeclarations(module, b.Manifest)
}
// manifestDeclarations is the declarations of the manifest of a digest in the indexer's manifests, read once: a
// manifests key is written once and never changes (ADR 0429 §16), so what it declares is kept by digest.
func (c *Config) manifestDeclarations(module, digest string) (ModuleDeclarations, error) {
c.manifestsMu.Lock()
d, ok := c.manifests[digest]
c.manifestsMu.Unlock()
if ok {
return d, nil
}
key, err := delivery.ManifestsKey(digest)
if err != nil {
return ModuleDeclarations{}, err
}
e, err := c.Store.Get(ManifestsRead, key)
if err != nil {
return ModuleDeclarations{}, fmt.Errorf("the indexer's manifest %s cannot be read: %w", short(digest), err)
}
if e == nil {
return ModuleDeclarations{}, fmt.Errorf("the indexer holds no manifest %s", short(digest))
}
var m struct {
Module string `json:"module"`
JSON string `json:"manifest"`
}
if err := json.Unmarshal(e.Value, &m); err != nil || m.JSON == "" {
return ModuleDeclarations{}, fmt.Errorf("the indexer's manifest %s is not one", short(digest))
}
if m.Module != module {
return ModuleDeclarations{}, fmt.Errorf("the indexer's manifest %s is %s's, not %s's", short(digest), m.Module, module)
}
d, err = ManifestDeclarations([]byte(m.JSON))
if err != nil {
return ModuleDeclarations{}, fmt.Errorf("the manifest %s: %w", short(digest), err)
}
c.manifestsMu.Lock()
if c.manifests == nil {
c.manifests = map[string]ModuleDeclarations{}
}
c.manifests[digest] = d
c.manifestsMu.Unlock()
return d, nil
}
// each hands every key of a state to visit.
func (c *Config) each(state string, visit func(key string, raw []byte) error) error {
keys, err := c.Store.Keys(state)
@@ -313,13 +145,6 @@ func (c *Config) writeResolved(s Sources) ([]Moved, error) {
return nil, fmt.Errorf("the resolved values cannot be read: %w", err)
}
want := Resolve(s)
// A placed module whose declarations cannot be read keeps what was resolved for it: not knowing is no reason to take
// a node's values away (issue 1953).
for key, h := range held {
if _, ok := want[key]; !ok && h.Source != "unreadable" && s.unresolved(h.Node, h.Module) {
want[key] = h
}
}
moved := diff(held, want)
var problems []error
for _, m := range moved {
+2 -4
View File
@@ -28,11 +28,9 @@
"gitea.gitea_get_pull_request"
],
"reads": [
"mesh-controller.assignments",
"mesh-delivery-assigner.placement",
"mesh-delivery-assigner.wants",
"mesh-delivery-builder.builds",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests",
"mesh-controller.declared-config",
"mesh-controller.predefined"
],
"state": [
@@ -330,7 +330,7 @@ func (h *Holder) endStaleAsks() {
h.endStaleSettles()
for _, d := range h.deliveries {
r := d.Ready
if r == nil || r.Merged != nil || d.MergedAs != "" || r.declined() || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
if r == nil || r.Merged != nil || d.MergedAs != "" || r.Declined != nil || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
continue
}
switch {
@@ -418,7 +418,6 @@ func (h *Holder) approval(d *Delivery, now time.Time) (string, func()) {
h.runOf(m, now)
m.Ready.AskID, m.Ready.Ask, m.Ready.Heads, m.Ready.AskedAt, m.Ready.Asks = id, &ask, heads, now, max(m.Ready.Asks, r.Asks)
m.Ready.AskedKey, m.Ready.Asking, m.Ready.AskCheck = id, asking, check.Key
m.Ready.Declined, m.Ready.AnsweredAsk = nil, "" // a new ask: an answer to an earlier one does not stand on it
h.keep(m)
taken = append(taken, m)
reads = append(reads, readOf(m, m.Commit))
@@ -609,7 +608,7 @@ func (h *Holder) pendingAsks() []string {
seen := map[string]bool{}
var out []string
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.AskID != "" && r.Approved == nil && !r.declined() && !seen[r.AskID] {
if r := d.Ready; r != nil && r.AskID != "" && r.Approved == nil && r.Declined == nil && !seen[r.AskID] {
seen[r.AskID] = true
out = append(out, r.AskID)
}
@@ -706,19 +705,16 @@ func (h *Holder) answered(key string, ans sdk.Answer) []func() {
h.mergeAgainNow(d) // the operator chose: the merge is asked at once
switch ans.Option {
case OptionApprove:
r.Approved, r.Declined, r.AnsweredAsk = &now, nil, key
r.Approved = &now
h.Logf("[mesh-delivery] %s: the operator approved its merge into main (%s)", d.ID, r.Warrant)
case OptionDecline:
r.Declined, r.AnsweredAsk = &now, key
r.Declined = &now
h.Logf("[mesh-delivery] %s: the operator declined its merge into main (%s)", d.ID, r.Warrant)
said = append(said, h.sayOn(d, fmt.Sprintf("mesh-delivery: **not merged.** The merge of this pull request into main at %s was "+
"declined: %s. A new head asks the operator again.", d.Commit, r.Warrant)))
}
h.keep(d)
}
if ans.Option == OptionApprove {
h.oweApproved(covered[0], now) // said on the bus at once, and again at its bound if its merge has not started (unacted.go)
}
return said
}
@@ -1,7 +1,6 @@
package main
import (
"fmt"
"strings"
"testing"
"time"
@@ -406,140 +405,3 @@ func TestAnAnswerToAnEarlierAskingIsNotTakenAndTheMergeNamesTheAnsweredAsking(t
t.Fatalf("the merge is announced %v; wanted the ask id of asking 2, %s", say, sdk.AskingID(key, 2))
}
}
// 2026-10-29 (hq#495): a group declined under one ask, a member pushed, and the group asked and approved again at the new
// heads: the member whose head did not move kept the earlier Decline beside the new Approve, and nothing was handed, with
// no word. An answer stands only for the ask it answers; whichever member is pushed, both merge on the new Approve.
func TestADeclineOfAnEarlierAskDoesNotHoldTheApproveOfTheNext(t *testing.T) {
const ctlHead, catHead, pushed = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
for _, push := range []struct {
repo string
number int
trunk string
}{{"novox/mesh-catalog", 2, "t3t3t3t3t3t3"}, {"novox/mesh-controller", 1, "t1t1t1t1t1t1"}} {
t.Run(push.repo, func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
heads := map[string]string{"novox/mesh-controller": ctlHead, "novox/mesh-catalog": catHead}
paths := map[string]string{"novox/mesh-controller": "internal/a.go", "novox/mesh-catalog": "modules/app/module.json"}
numbers := map[string]int{"novox/mesh-controller": 1, "novox/mesh-catalog": 2}
trunks := map[string]string{"novox/mesh-controller": "t1t1t1t1t1t1", "novox/mesh-catalog": "t3t3t3t3t3t3"}
round := func(check string) {
var open []OpenPull
for _, repo := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
w.h.PullUpdated(pr(repo, numbers[repo], heads[repo], "feat/x", paths[repo]))
w.h.Checked(judgedAt(repo, numbers[repo], heads[repo], "pass", trunks[repo]))
open = append(open, openPull(repo, numbers[repo], heads[repo]))
}
w.forge.open = open
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: check, Verdict: "pass", Summary: "both compose"})
w.settleAll()
}
round("check-1")
w.op.answer(w.op.open()[0], OptionDecline)
w.settleAll()
heads[push.repo] = pushed
round("check-2")
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the group at its new heads is not asked once: %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("approved at the new heads, yet merged %v; stalled %+v", w.forge.merges, w.h.Stalled())
}
})
}
}
// 2026-10-30 (feat/774-l3-reland): approved at 10:14 and never handed, with no word. Each row an Approve: one whose runs
// were kept by a gate before the fix above, a Decline of an earlier ask beside the Approve, merges; one whose merge
// cannot start is said on the bus (mesh-delivery-gate.approval) and as an urgent line within approvalBound, and merges
// once what held it ends.
func TestAnApproveIsActedOnOrSaidWithinItsBound(t *testing.T) {
for _, row := range []struct {
name string
kept bool // the runs carry a Decline of an earlier ask, kept before answers named their ask
forgeDown bool // the members' statuses cannot be read, so the pass cannot start
}{
{name: "a Decline kept before the Approve, as the gate kept it before answers named their ask", kept: true},
{name: "an Approve whose merge cannot start is said within its bound", forgeDown: true},
} {
t.Run(row.name, func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
heads := map[string]string{"novox/mesh-controller": "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "novox/mesh-catalog": "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"}
paths := map[string]string{"novox/mesh-controller": "internal/a.go", "novox/mesh-catalog": "modules/app/module.json"}
numbers := map[string]int{"novox/mesh-controller": 1, "novox/mesh-catalog": 2}
trunks := map[string]string{"novox/mesh-controller": "t1t1t1t1t1t1", "novox/mesh-catalog": "t3t3t3t3t3t3"}
var open []OpenPull
for _, repo := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
w.h.PullUpdated(pr(repo, numbers[repo], heads[repo], "feat/x", paths[repo]))
w.h.Checked(judgedAt(repo, numbers[repo], heads[repo], "pass", trunks[repo]))
open = append(open, openPull(repo, numbers[repo], heads[repo]))
}
w.forge.open = open
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
asked := w.op.open()
if len(asked) != 1 {
t.Fatalf("the group is not asked once: %v", asked)
}
w.op.answer(asked[0], OptionApprove)
w.forge.mu.Lock()
w.forge.down = row.forgeDown
w.forge.mu.Unlock()
w.h.hearAsks()
if row.kept {
w.h.mu.Lock()
for _, d := range w.h.deliveries {
if d.Ready != nil && d.Ready.Approved != nil {
before := d.Ready.Approved.Add(-38 * time.Minute)
d.Ready.Declined, d.Ready.AnsweredAsk = &before, ""
}
}
w.h.mu.Unlock()
}
w.settleAll()
w.later(approvalBound + time.Second)
w.settleAll()
states := map[string]bool{}
for _, e := range w.said {
if e["event-kind"] == approvalEvent && e["group"] == "feat/x" {
states[fmt.Sprint(e["state"])] = true
}
}
if !states["approved"] {
t.Fatalf("the Approve is not said on the bus: %v", w.said)
}
var urgent []StalledLine
for _, l := range w.h.Stalled() {
if l.Bound == approvalBound.String() && l.Severity == "urgent" && strings.Contains(l.Says, "feat/x was approved") {
urgent = append(urgent, l)
}
}
if !row.forgeDown {
if len(w.forge.merges) != 2 || states["not-handed"] || len(urgent) != 0 {
t.Fatalf("approved, yet merged %v (said not handed: %v, lines %+v)", w.forge.merges, states["not-handed"], urgent)
}
return
}
if len(w.forge.merges) != 0 || !states["not-handed"] || len(urgent) != 1 {
t.Fatalf("an Approve held past its bound: merged %v, said not handed %v, urgent lines %+v", w.forge.merges, states["not-handed"], urgent)
}
w.forge.mu.Lock()
w.forge.down = false
w.forge.mu.Unlock()
w.later(askAgainAfter)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("the forge back, yet merged %v; stalled %+v", w.forge.merges, w.h.Stalled())
}
})
}
}
@@ -96,7 +96,7 @@ func (h *Holder) withdrawForStatus(e ForgeStatusEvent) {
done := map[string]bool{}
for _, d := range h.deliveries {
r := d.Ready
if !strings.EqualFold(d.Repository, repository) || r == nil || r.AskID == "" || r.Approved != nil || r.declined() || done[r.AskID] {
if !strings.EqualFold(d.Repository, repository) || r == nil || r.AskID == "" || r.Approved != nil || r.Declined != nil || done[r.AskID] {
continue
}
head := d.Commit
@@ -208,7 +208,7 @@ func (h *Holder) passes() []pass {
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
if r == nil || r.Approved == nil || r.Declined != nil || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
d.Base != MergeBranch || h.allMerged(r.Heads) {
continue
}
@@ -335,7 +335,7 @@ func (h *Holder) MergeAgain(id, why string) (string, error) {
return "", fmt.Errorf("say why: the merge asked again is the operator's word, kept in the log")
}
c := h.covering(d)
if c == nil || c.Approved == nil || c.declined() || c.Heads[d.ID] != d.Commit {
if c == nil || c.Approved == nil || c.Declined != nil || c.Heads[d.ID] != d.Commit {
return "", fmt.Errorf("%s is not approved at its head %s: nothing to ask again (a new head is asked on its own)", id, shortOf(d.Commit, 12))
}
if d.MergedAs != "" || (d.Ready != nil && d.Ready.Merged != nil) {
@@ -524,7 +524,7 @@ func (h *Holder) PullEdited(p PullEvent) {
if after, _ := reviewWords(d); after == before {
return
}
if r := h.covering(d); r != nil && r.AskID != "" && r.Approved == nil && !r.declined() {
if r := h.covering(d); r != nil && r.AskID != "" && r.Approved == nil && r.Declined == nil {
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, d.ID+"'s review record changed: "+shortOf(d.ReviewedAt, 12)))
}
}
@@ -89,7 +89,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
if !reflect.DeepEqual(m.Consumes, followed) || !slices.Contains(m.Consumes, "mesh-delivery-forge-bridge.forge.**") {
t.Fatalf("consumes %v, follows %v (the bridge's forge messages among them)", m.Consumes, followed)
}
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged", "approval"}) {
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
t.Fatalf("emits %v", m.Emits)
}
// Nine buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3, the conditions it raised (issue 1275) and the
@@ -149,10 +149,11 @@ func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
return nil
}
for _, m := range order {
if h.notHandable(m) != "" {
return nil // the others are not approved at their heads yet: nothing is merged before they are (said by notHanded)
mc := h.covering(m)
if m.Ready == nil || m.Ready.Declined != nil || mc == nil || mc.Approved == nil || mc.Heads[m.ID] != m.Commit || h.readyWaits(m) != "" {
return nil // the others are not approved at their heads yet: nothing is merged before they are
}
reads = append(reads, readOf(m, h.covering(m).Heads[m.ID]))
reads = append(reads, readOf(m, mc.Heads[m.ID]))
}
}
if c == nil || c.Approved == nil || c.Heads[d.ID] == "" {
@@ -168,66 +169,6 @@ func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
return func() { h.hand(gid, name, key, target, reads, approved) }
}
// notHandable is why a group member cannot be handed in its group's pass, empty when it can: handTurn's own test, in words.
// Under the lock.
func (h *Holder) notHandable(m *Delivery) string {
mc := h.covering(m)
switch {
case m.Ready == nil:
return m.ID + " has no ready run (" + string(m.State) + ")"
case m.Ready.declined():
return m.ID + " stands declined since " + m.Ready.Declined.UTC().Format(time.RFC3339)
case mc == nil || mc.Approved == nil:
return m.ID + " is not approved at its head " + shortOf(m.Commit, 12)
case mc.Heads[m.ID] != m.Commit:
return m.ID + "'s head " + shortOf(m.Commit, 12) + " is not the head approved"
}
if w := h.readyWaits(m); w != "" {
return m.ID + " waits: " + w
}
return ""
}
// notHanded is why the approved merge of d, which waits for nothing, is not handed: its group's pass cannot start, and
// says which member holds it and why. Empty when nothing holds it, or when the pass is under way or its refusal is said
// already. Under the lock.
func (h *Holder) notHanded(g *Group, d *Delivery) string {
if g == nil {
return ""
}
order := h.unmergedLive(g)
if len(order) == 0 {
return ""
}
if first := order[0].Ready; first != nil && (first.merging || !first.LastTry.IsZero() || first.RefusedSince != nil) {
return "" // the pass is under way, or its refusal is said on its own line
}
for _, m := range order {
if why := h.notHandable(m); why != "" {
return fmt.Sprintf("its group %s is not handed to the forge bridge: %s", g.ID, why)
}
}
return ""
}
// noteNotHanded keeps on d's run why its approved merge is not handed, said once in the log when it changes, and as a
// stalled line meanwhile (readyLines). Under the lock.
func (h *Holder) noteNotHanded(d *Delivery, why string, now time.Time) {
r := d.Ready
if r == nil || r.NotHanded == why {
return
}
r.NotHanded = why
if why == "" {
r.NotHandedSince = nil
} else {
at := now
r.NotHandedSince = &at
h.Logf("[mesh-delivery] %s is approved, waits for nothing, and is NOT handed to merge: %s", d.ID, why)
}
h.keep(d)
}
// hand reads a group's required statuses and, when all are a success, writes the target as the member being merged.
func (h *Holder) hand(gid, name, key, target string, reads []memberRead, approved map[string]string) {
if gid != "" && !h.groupMergeable(gid, target, reads) {
@@ -120,13 +120,11 @@ type ReadyRun struct {
// in new words, so it never keeps a claim the check no longer holds.
AskCheck string `json:"ask_check,omitempty"`
// Approved and Declined are when the operator chose, Warrant what the router's warrant says of who and how; Refused
// why an answer was not taken (it did not hold to the ask). AnsweredAsk is the ask the answer answers: an answer stands
// only for its own ask, so a Decline of an earlier ask never stands beside the Approve of a later one (declined).
Approved *time.Time `json:"approved,omitempty"`
Declined *time.Time `json:"declined,omitempty"`
AnsweredAsk string `json:"answered_ask,omitempty"`
Warrant string `json:"warrant,omitempty"`
Refused string `json:"refused,omitempty"`
// why an answer was not taken (it did not hold to the ask).
Approved *time.Time `json:"approved,omitempty"`
Declined *time.Time `json:"declined,omitempty"`
Warrant string `json:"warrant,omitempty"`
Refused string `json:"refused,omitempty"`
// One check per delivery (novox/hq ADR 0548 §2, §4, turns.go): HeldSince is when the approved merge began to wait for the
// check of other heads HeldFor names (its verdict key), running on a repository it goes into; HandNow the operator's word
@@ -139,28 +137,6 @@ type ReadyRun struct {
// (writes.go, issue 1945); CannotWriteSince since when. Said at once as a condition.
CannotWrite string `json:"cannot_write,omitempty"`
CannotWriteSince *time.Time `json:"cannot_write_since,omitempty"`
// NotHanded is why an approved merge that waits for nothing is not handed to the forge bridge (a member of its group that
// cannot be handed, and why), NotHandedSince since when: said once in the log and as a stalled line, never silent.
NotHanded string `json:"not_handed,omitempty"`
NotHandedSince *time.Time `json:"not_handed_since,omitempty"`
// UnactedSaid is the ask whose Approve was said not acted on, approvalBound after it (unacted.go): said once per ask.
UnactedSaid string `json:"unacted_said,omitempty"`
}
// declined is whether the operator's Decline stands for this run: it answered the ask the run holds now, or the run holds
// no ask any more. A Decline of an earlier ask, the run asked again since at other heads (a sibling's new head), does not
// stand (2026-10-29: hq#495 declined under one ask and approved under the next was never handed). A run kept before answers
// named their ask stands declined unless an Approve came after the Decline.
func (r *ReadyRun) declined() bool {
if r == nil || r.Declined == nil {
return false
}
if r.AnsweredAsk != "" {
return r.AskID == "" || r.AskID == r.AnsweredAsk
}
return r.Approved == nil || r.Declined.After(*r.Approved)
}
// MergeBranch is the one branch this owner merges into (novox/hq ADR 0398 §4): a merge into any other is its author's.
@@ -197,9 +173,6 @@ func (h *Holder) runOf(d *Delivery, now time.Time) *ReadyRun {
func (h *Holder) readyAsks(now time.Time) []func() {
var asks []func()
h.endStaleAsks()
// An Approve whose merge has not started is said, whatever holds it (unacted.go): read before this look hands, so a pass
// handed and given back within the last look (a read that failed) counts as not started.
h.sayUnacted(now)
gates := h.checkGates()
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
@@ -224,7 +197,7 @@ func (h *Holder) readyAsks(now time.Time) []func() {
asks = append(asks, a)
}
r := d.Ready
if r == nil || r.Merged != nil || r.declined() {
if r == nil || r.Merged != nil || r.Declined != nil {
continue
}
if d.State == Rejected {
@@ -303,11 +276,9 @@ func (h *Holder) readyAsks(now time.Time) []func() {
asks = append(asks, approvalAsk)
}
if waits != "" || r.merging {
h.noteNotHanded(d, "", now) // what it waits for is said by its waits, or it is handed
continue
}
if !r.LastTry.IsZero() {
h.noteNotHanded(d, "", now)
continue // refused: asked again at the next event that decides whether it may merge (mergeAgainNow), with no timer
}
// A group is merged one member at a time, handed to the forge bridge, and the pass stops at the first refusal (ADR
@@ -317,10 +288,7 @@ func (h *Holder) readyAsks(now time.Time) []func() {
g = nil
}
if a := h.handTurn(g, d, now); a != nil {
h.noteNotHanded(d, "", now)
asks = append(asks, a)
} else {
h.noteNotHanded(d, h.notHanded(g, d), now) // a merge never handed says why (2026-10-29: silent for an hour)
}
}
asks = append(asks, h.turnWrites()...) // the merge turns, after the holds of this look (ADR 0548 §3)
@@ -607,11 +575,6 @@ func (h *Holder) readyLines(now time.Time) []StalledLine {
Bound: "0s", H2: "none: a new head, which asks the operator again — the operator's",
Says: fmt.Sprintf("%s#%d at %s is not merged: the operator's answer %s", d.Repository,
d.Number, shortOf(r.Head, 12), r.Refused)})
case r.NotHanded != "" && r.NotHandedSince != nil && r.Approved != nil:
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.NotHandedSince).Round(time.Second).String(), Bound: "0s",
H2: "none: the member named is approved at its head and ready, or its group is asked anew by a new head — the operator's",
Says: fmt.Sprintf("%s#%d at %s is approved and %s", d.Repository, d.Number, shortOf(r.Head, 12), r.NotHanded)})
case h.answering(d, now):
// The trunk moved and its check was asked again: the gate is answering it, so it is no stall yet (ADR 0505
// §4). `show` lists the refusal meanwhile.
@@ -110,7 +110,7 @@ func (h *Holder) approvedMerges() []approvedMerge {
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
if r == nil || r.Approved == nil || r.Declined != nil || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
d.State.Final() || d.State == Rejected || d.Base != MergeBranch || r.RefusedSince != nil || h.allMerged(r.Heads) {
continue
}
@@ -382,10 +382,7 @@ type TurnLine struct {
func (h *Holder) TurnsRead() map[string]any {
h.mu.Lock()
defer h.mu.Unlock()
want := map[string]sdk.Turn{}
if h.checkGates() {
want = h.wantedTurns() // as turnWrites: while the check gates no merge, no turn is written by design
}
want := h.wantedTurns()
repos := make([]string, 0, len(want))
for repo := range want {
repos = append(repos, repo)
@@ -407,9 +404,6 @@ func (h *Holder) TurnsRead() map[string]any {
lines = append(lines, l)
}
out := map[string]any{"turns": lines, "check-gates-merge": h.checkGates(), "bound": turnBound.String()}
if !h.checkGates() {
out["why-none"] = "the check gates no merge (check-gates-merge false): no merge waits for a check, so no turn is written"
}
if h.Turns != nil {
out["written"] = h.turnsLoaded
var unwritten []string
@@ -1,167 +0,0 @@
package main
import (
"fmt"
"sort"
"strings"
"time"
)
// **An Approve is acted on, or said at once** (the operator, 2026-10-30: "an approval given by me must get handled
// accordingly and you should know about it immediately"). That morning the group feat/774-l3-reland was approved at 10:14
// and nothing was handed for an hour, with no word: six of its seven members kept a Decline of an earlier ask beside the
// new Approve (the fix of 2026-10-29, reverted with L3, came back with this file), readyAsks skipped them and handTurn
// returned nil for the seventh.
//
// So, whatever holds it: every Approve is said on the bus as `mesh-delivery-gate.approval` {state: approved} when it is
// taken; an approved merge whose pass has not started approvalBound after its Approve is said again as {state: not-handed}
// with what it waits on, once per ask, and is an urgent stalled line (so an urgent condition) until its pass starts, unless
// a line of its own says it already (a held group; a merge waiting for another delivery's check, said at its bound). A
// rejected delivery is its author's, not an Approve unacted. A session waits on `mesh-delivery-gate.approval` to hear both.
// approvalBound is how long after the operator's Approve a merge's pass may take to start before it is said.
const approvalBound = 60 * time.Second
// approvalEvent is the event family of an Approve taken and of an Approve not acted on.
const approvalEvent = "approval"
// unacted is an approved ask whose pass has not started: the run holding its Approve, its owner, its members, and what
// the gate says it waits on.
type unacted struct {
owner *Delivery
run *ReadyRun
name string
members []string
waits string
// checkHeld is whether a member waits for the check of other heads (ADR 0548 §2): said by the event at once, and as a
// line by turnLines at its own bound, not twice.
checkHeld bool
}
// approvalStands is whether a run's Approve still stands: no Decline after it, not merged, into the merge branch.
func approvalStands(d *Delivery) bool {
r := d.Ready
if r == nil || r.Approved == nil || r.AskID == "" || len(r.Heads) == 0 || r.Merged != nil || d.MergedAs != "" ||
d.State.Final() || d.State == Rejected || d.Base != MergeBranch {
return false
}
return r.Declined == nil || !r.Declined.After(*r.Approved)
}
// unactedAsks are the approved asks whose pass has not started, each once, in the order of their ids. A pass has started
// when a member it covers is handed (merging), was tried (refused or asked again), or merged. Under the lock.
func (h *Holder) unactedAsks() []unacted {
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
seen := map[string]bool{}
var out []unacted
for _, id := range ids {
d := h.deliveries[id]
if !approvalStands(d) || seen[d.Ready.AskID] {
continue
}
if g := h.groups[d.Group]; d.Group != "" && g != nil && g.Hold != nil {
seen[d.Ready.AskID] = true
continue // a held group is said by its own urgent line, naming the member and why (groupLines)
}
r := d.Ready
seen[r.AskID] = true
u := unacted{owner: d, run: r, name: askGroup(d)}
started := false
var says []string
for mid := range r.Heads {
u.members = append(u.members, mid)
m := h.deliveries[mid]
if m == nil {
continue
}
if m.MergedAs != "" || (m.Ready != nil && (m.Ready.Merged != nil || m.Ready.merging || !m.Ready.LastTry.IsZero() ||
m.Ready.RefusedSince != nil)) {
started = true
break
}
if m.Ready != nil {
u.checkHeld = u.checkHeld || m.Ready.HeldSince != nil
if w := firstOf(m.Ready.NotHanded, m.Ready.Waits); w != "" {
says = append(says, mid+": "+w)
}
}
}
if started {
continue
}
sort.Strings(u.members)
sort.Strings(says)
u.waits = firstOf(strings.Join(says, "; "), "nothing the gate says: its merge was never handed")
out = append(out, u)
}
return out
}
// sayUnacted owes the event {state: not-handed} once per ask for every approved merge past approvalBound whose pass has
// not started, and wants a look at the bound of each not yet due. Under the lock; the event is owed, said by Flush.
func (h *Holder) sayUnacted(now time.Time) {
for _, u := range h.unactedAsks() {
due := u.run.Approved.Add(approvalBound)
if now.Before(due) {
h.lookAt(due, "an Approve whose merge has not started, said at its bound")
continue
}
if u.run.UnactedSaid == u.run.AskID {
continue
}
u.run.UnactedSaid = u.run.AskID
h.Logf("[mesh-delivery] %s is approved since %s and its merge is NOT handed: %s", u.name,
u.run.Approved.UTC().Format(time.RFC3339), u.waits)
u.owner.Owed = append(u.owner.Owed, Effect{Kind: EffectEmit, Event: approvalEvent, Since: now,
Line: string(mustJSON(approvalBody(u.owner, "not-handed", u.members, now, u.waits)))})
h.keep(u.owner)
}
}
// oweApproved owes the event {state: approved} for an ask the operator approved, on the first delivery it covers, and wants
// a look at its bound. Under the lock.
func (h *Holder) oweApproved(d *Delivery, now time.Time) {
r := d.Ready
members := make([]string, 0, len(r.Heads))
for id := range r.Heads {
members = append(members, id)
}
sort.Strings(members)
d.Owed = append(d.Owed, Effect{Kind: EffectEmit, Event: approvalEvent, Since: now,
Line: string(mustJSON(approvalBody(d, "approved", members, now, "")))})
h.lookAt(now.Add(approvalBound), "an Approve whose merge has not started, said at its bound")
}
// approvalBody is what `mesh-delivery-gate.approval` says: no secret, no address.
func approvalBody(d *Delivery, state string, members []string, now time.Time, waits string) map[string]any {
r := d.Ready
e := map[string]any{"state": state, "group": askGroup(d), "ask": r.AskID, "approved": r.Approved.UTC(), "at": now.UTC(),
"members": members, "by": r.Warrant}
if waits != "" {
e["waits"] = waits
e["for"] = now.Sub(*r.Approved).Round(time.Second).String()
}
return e
}
// unactedLines are the urgent stalled lines of approved merges whose pass has not started past approvalBound. Under the
// lock.
func (h *Holder) unactedLines(now time.Time) []StalledLine {
var out []StalledLine
for _, u := range h.unactedAsks() {
since := now.Sub(*u.run.Approved)
if since < approvalBound || u.checkHeld {
continue
}
out = append(out, StalledLine{ID: u.owner.ID, Number: u.owner.Number, State: u.owner.State,
For: since.Round(time.Second).String(), Bound: approvalBound.String(), Severity: "urgent",
H2: "none: the merge is handed once what it waits on ends; delivery_turns and mesh-delivery.show say what holds it",
Says: fmt.Sprintf("%s was approved at %s (%d pull requests) and its merge is not handed: %s", u.name,
u.run.Approved.UTC().Format(time.RFC3339), len(u.members), u.waits)})
}
return out
}
@@ -230,7 +230,6 @@ func (h *Holder) Stalled() []StalledLine {
out = append(out, h.readyLines(now)...)
out = append(out, h.turnLines(now)...) // an approved merge held past its bound by another delivery's check (ADR 0548 §4)
out = append(out, h.groupLines(now)...)
out = append(out, h.unactedLines(now)...) // an Approve whose merge has not started past its bound (unacted.go)
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
+1 -2
View File
@@ -33,8 +33,7 @@
"emits": [
"transition",
"group",
"merged",
"approval"
"merged"
],
"uses": [
"operator-channel"
@@ -161,11 +161,7 @@ func (m *memAsks) PutLinksAsked(key string, times []time.Time) error {
var rootFreeOf = map[*Holder]map[string]string{}
// told is what the router published to askers, by subject.
type told struct {
by map[string][]asks.Warrant
// asked are the asks the router said it took, by subject (sayAsked).
asked map[string][]map[string]any
}
type told struct{ by map[string][]asks.Warrant }
func (w *told) of(asker string) []asks.Warrant { return w.by[asks.DecidedSubject(asker)] }
@@ -175,7 +171,7 @@ func newRouter(t *testing.T) (*Holder, *fakeChannel, *fakeChannel, *clock, *memA
t.Helper()
h, tg, dt, c, _ := newHolder(t)
store := newMemAsks()
w := &told{by: map[string][]asks.Warrant{}, asked: map[string][]map[string]any{}}
w := &told{by: map[string][]asks.Warrant{}}
bench := NewBench(c.now)
bench.SetKinds([]KindInfo{
{Kind: "telegram", Module: "telegram", Node: "anchor", Capabilities: []string{"deliver", "reaches-away", "choice", "edit",
@@ -208,9 +204,6 @@ func newRouter(t *testing.T) (*Holder, *fakeChannel, *fakeChannel, *clock, *memA
if wt, ok := body.(asks.Warrant); ok {
w.by[subject] = append(w.by[subject], wt)
}
if m, ok := body.(map[string]any); ok && strings.HasPrefix(subject, "mesh.seat."+asks.Seat+".event."+AskedEvent+".") {
w.asked[subject] = append(w.asked[subject], m)
}
return nil
}
return h, tg, dt, c, store, w
@@ -332,10 +325,6 @@ func TestWithoutALinkedAccountOnlyTheMeshMCPServerApproves(t *testing.T) {
!strings.Contains(rec.HeldWhy, "no Telegram account is linked") {
t.Fatalf("not held: %+v %+v", rec, w.of("mesh-delivery"))
}
// Held or shown, the ask is said taken on the bus for its asker, so a session hears it open (2026-10-30).
if said := w.asked[askedSubject("mesh-delivery")]; len(said) != 1 || said[0]["ask"] != "d-1" || said[0]["state"] != StateHeld {
t.Fatalf("the ask taken is not said on %s: %+v", askedSubject("mesh-delivery"), said)
}
if len(dt.sends) != 0 {
t.Errorf("the desk was offered an ask with an approval: %+v", dt.sends)
}
-26
View File
@@ -560,7 +560,6 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
} else {
h.keepAsk(key, rec, 0)
}
h.sayAsked(asker, a, rec)
if rec.State == StateHeld {
h.Logf("[messenger] %s asks %q (%s): held, not shown: %s", asker, a.Headline, a.ID, rec.HeldWhy)
return nil
@@ -569,31 +568,6 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
return nil
}
// AskedEvent is the seat's event that an ask was taken, said for its asker as decided is (ADR 0259 §3): a session
// waits on `operator-channel.asked.<asker>` to hear an ask open, and on `operator-channel.decided.<asker>` to hear
// its answer (the operator, 2026-10-30: "you should be able to hook into approval requests so you also get a
// notification"). The SDK names decided's subject; asked's is named here until the SDK's asks contract carries it.
const AskedEvent = "asked"
// askedSubject is where the router says an ask of an asker was taken.
func askedSubject(asker string) string {
return "mesh.seat." + asks.Seat + ".event." + AskedEvent + "." + asker
}
// sayAsked says on the bus that an ask was taken, open or held: its id, asker, what it is about, its headline (the
// words the operator's phone shows, already judged to carry nothing that may not leave the mesh) and when it ends.
// Never its options' bindings or its whole words. A failure is said in the log and holds nothing: the ask stands.
func (h *Holder) sayAsked(asker string, a asks.Ask, rec AskRecord) {
if h.Publish == nil {
return
}
body := map[string]any{"ask": a.ID, "asker": asker, "about": a.About, "headline": a.Headline, "state": rec.State,
"opened": rec.Opened.UTC(), "expires": a.Expires}
if err := h.Publish(askedSubject(asker), body, "asked."+ticket()); err != nil {
h.Logf("[messenger] the ask %s of %s is taken and could not be said on the bus: %v", a.ID, asker, err)
}
}
// ticketsFor is every option's ticket on every kind that can carry the ask's highest level now: an ask with an
// approval is answered only where it could be approved (answerLevel). None when no ready channel can.
func (h *Holder) ticketsFor(a asks.Ask) map[string]Ticket {
@@ -64,8 +64,8 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
m, _ := readManifest(t)
verbs := []string{"open", "history", "notify", "tell", "asks"}
want := []seat{
{Name: asks.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided", "asked"},
ByCaller: []string{"ask", "cancel", "condition", "decided", "asked"}, Records: []string{"asks"}},
{Name: asks.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided"},
ByCaller: []string{"ask", "cancel", "condition", "decided"}, Records: []string{"asks"}},
{Name: asks.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{asks.Show, asks.Edit, asks.Send}},
{Name: asks.IntakeSeat, Scope: "mesh", Kinded: true,
Emits: []string{asks.Chosen, asks.Link, asks.FailedWhat, asks.StandingWhat}, Proofs: []string{asks.CodeProof}},
+2 -4
View File
@@ -20,15 +20,13 @@
"condition"
],
"emits": [
"decided",
"asked"
"decided"
],
"by-caller": [
"ask",
"cancel",
"condition",
"decided",
"asked"
"decided"
],
"records": [
"asks"