Merge remote-tracking branch 'origin/main' into feat/47-G-join11

# Conflicts:
#	modules/mesh-delivery-indexer/judge/rules.go
#	modules/mesh-delivery-indexer/judge/rules_test.go
This commit is contained in:
2026-11-12 10:28:05 +01:00
4 changed files with 93 additions and 5 deletions
@@ -1021,11 +1021,7 @@ func (n *askNotes) note(ctx context.Context, language, id, step string) error {
v.Asks = map[string]string{}
}
v.Asks[language] = id
if err := n.b.Claimer.Finish(ctx, n.cl, v); err != nil {
return err
}
n.b.handAsk(n.cl.Key, language, id)
return nil
return n.b.Claimer.Finish(ctx, n.cl, v)
}
// runLane puts the lane's part of the build as one ask on its build seat's stream (the delivery stream for a trunk key, the
@@ -1100,6 +1096,9 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
l.err = err
return
}
// A request to build the key again by hand is answered the ask as asked once it is put, not once it is noted: a
// put the stream refuses after the note answers the key's failure (BuildAgainResult.Asks).
b.handAsk(asked.cl.Key, language, ask.ID)
}
step, ask, err := b.answer(ctx, l, ask, asked, module, m.Fp2)
if err != nil {
@@ -891,6 +891,8 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
during bool
// standing is a key taken as it stands (left building): its claim is the key's own revision, not a new one.
standing bool
// putRefused is a stream that refuses the ask after its note was written to the key.
putRefused bool
}{
{name: "build again: a look the builder queued during the by-hand build still runs afterwards", again: true, during: true,
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
@@ -908,6 +910,10 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
{name: "build again: a build that fails before its asks says the key failed, not that it was asked", again: true, failed: "cannot be read",
edit: func(m *meshcatalog.ModulesByModuleValue) { m.Manifest = mdigest('b') },
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
// An ask is answered as asked only once it is put, not once it is noted in the key (BuildAgainResult.Asks): a put
// the stream refuses after the note answers the key's failure and no ask id.
{name: "build again: an ask its stream refuses after its note answers the key failed, not asked", again: true, putRefused: true,
failed: "the stream refused the ask", key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
{name: "build again: a failed key is written building as a new revision and asks the seat", again: true,
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: the step failed: no tun"}},
{name: "build again: a done key is refused, nothing to do", again: true, refuse: "is done: nothing to do",
@@ -965,6 +971,9 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
r.b.hands[r.key] = &handWatch{}
}
r.b.known["m"] = m // the builder has read the modules value, and looks at nothing until asked
if c.putRefused {
r.b.Asks = buildrun.Asker{Publisher: refusingPublisher{}, Waiter: r.asks, Bound: 2 * time.Second}
}
if c.standing {
r.b.tried[r.key] = true // its look of the key ended before its write: a later look takes over nothing by itself
}
@@ -1259,6 +1268,13 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
})
}
// refusingPublisher is an asks stream that refuses every ask.
type refusingPublisher struct{}
func (refusingPublisher) PublishAsk(context.Context, string, string, []byte) error {
return errors.New("the stream refused the ask")
}
// heldAsks says an ask is held when its id is among held.
type heldAsks map[string]bool
@@ -7970,3 +7970,22 @@ func ObjectsCatalogueProblems(shelf catalogue.Shelf) []string {
}
return problems
}
// NodeEngineSeat is the seat the node-engine's module claims as one module (design 47 G §508).
const NodeEngineSeat = "node-engine"
// nodeEngineClaimProblems refuses a revision in which more than one module claims the seat node-engine, naming them: one
// module claims it, the node-engine's own (design 47 G §508), in place of refusing every claim.
func nodeEngineClaimProblems(shelf catalogue.Shelf) []string {
var holders []string
for module, m := range shelf {
if slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == NodeEngineSeat }) {
holders = append(holders, module)
}
}
if len(holders) < 2 {
return nil
}
slices.Sort(holders)
return []string{fmt.Sprintf("%s claim the seat %s, which one module claims (design 47 G §508)", strings.Join(holders, " and "), NodeEngineSeat)}
}
@@ -6415,6 +6415,60 @@ func TestRulesOther(t *testing.T) {
t.Run("AskingForTheRuleSetWithNowhereToPutItIsRefused", caseAskingForTheRuleSetWithNowhereToPutItIsRefused)
t.Run("TakesSettingsIsRefusedWhereItCannotMeanAnything", caseTakesSettingsIsRefusedWhereItCannotMeanAnything)
t.Run("TheInstallationCheckReadsADefault", caseTheInstallationCheckReadsADefault)
t.Run("ASeatNamedViewIsJudgedAgainstTheSeatsViews", caseASeatNamedViewIsJudgedAgainstTheSeatsViews)
t.Run("OneModuleClaimsTheNodeEnginesSeat", caseOneModuleClaimsTheNodeEnginesSeat)
}
// A consumed view, `seat:<seat>.views.<state>`, is accepted where the seat offers the state as a view and refused, by name,
// where it does not or the seat is unknown (design 47 G §616, §699, §621).
func caseASeatNamedViewIsJudgedAgainstTheSeatsViews(t *testing.T) {
was := catalogue.Seats()
t.Cleanup(func() { catalogue.UseSeats(was) })
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-module-assigner", Scope: catalogue.ScopeMesh, Decision: "test",
Views: []string{"assignments"}}})
for _, row := range []struct{ consume, says string }{
{"seat:mesh-module-assigner.views.assignments", ""},
{"seat:mesh-module-assigner.views.placement", "does not offer the state"},
{"seat:mesh-unknown.views.assignments", "mesh-unknown"},
{"seat:mesh-module-assigner.assignments", "does not name a seat and its view"},
} {
problems := strings.Join(EventProblems(catalogue.Manifest{Module: "reader", Consumes: []string{row.consume}}), "\n")
if row.says == "" && problems != "" || row.says != "" && !strings.Contains(problems, row.says) {
t.Errorf("%s: problems %q, want %q", row.consume, problems, row.says)
}
}
}
// One module of a revision claims the seat node-engine and is accepted; a second is refused naming both (design 47 G §508).
func caseOneModuleClaimsTheNodeEnginesSeat(t *testing.T) {
was := catalogue.Seats()
t.Cleanup(func() { catalogue.UseSeats(was) })
catalogue.UseSeats([]catalogue.Seat{{Name: NodeEngineSeat, Scope: catalogue.ScopeNode, Decision: "test", HeldByTheEngine: true}})
engine := func(module string) []byte {
return []byte(`{"module":"` + module + `","version":"1","claims":[{"name":"node-engine","scope":"node"}]}`)
}
for _, row := range []struct {
name string
raws map[string][]byte
wants string
}{
{"one claim", map[string][]byte{"mesh-host": engine("mesh-host")}, ""},
{"two claims", map[string][]byte{"mesh-host": engine("mesh-host"), "other": engine("other")},
"mesh-host and other claim the seat node-engine, which one module claims"},
} {
shelf := catalogue.Shelf{}
for name, raw := range row.raws {
m, problems := Manifest(raw, nil)
if len(problems) > 0 {
t.Fatalf("%s: %s refused alone: %v", row.name, name, problems)
}
shelf[name] = m
}
got := strings.Join(Between(shelf, 0, nil), "\n")
if row.wants == "" && strings.Contains(got, "node-engine") || row.wants != "" && !strings.Contains(got, row.wants) {
t.Errorf("%s: %s", row.name, got)
}
}
}
// A read of a seat holder's state, `seat:<seat>.<state>`, is accepted where the seat declares the state and refused, by name,