B5g6-2: the indexer validates every pending revision before its take-in

The program puts the validation's run ask on node-build-go's check queue
under its own asker and hears its end on step-ended; the manifest invokes
seat:node-build-go.accept.check. The revision validated is the branch its
merges landed on (the merged message's base, else the trunk), never the pull
request's branch. take-in.pending may name a refusal or why the revision
could not validate instead of the build it waits for. The whole case runs a
stand-in build seat: an accepted revision is taken in as before, by one ask
whose job names the indexer entry's repository, path and the revision's
head and no heads; a merge whose changed manifest gains a problem stays
pending, refused naming it.

Builds: design 47 G §712, §713, §714, A-231, A-232, A-247, A-254

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 09:50:37 +01:00
parent 722d3a03b7
commit fb62b14512
4 changed files with 59 additions and 13 deletions
@@ -120,11 +120,21 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
if _, err := tc.Put(ctx, key, value); err != nil {
t.Fatal(err)
}
// The trunk holds the indexer's entry, its Go artifact naming its package (§676): the validation's program is read from it (§712).
seedBuildModules(t, ctx, js, map[string]meshcatalog.CatalogEntry{index.Module: {V: index.CatalogEntryLayout, Module: index.Module,
Repository: "novox/mesh-delivery", Path: "modules/" + index.Module, Commit: "4444444444444444444444444444444444444444", None: sdkstep.ToolchainNoReport,
Artifacts: []meshcatalog.Artifact{{Kind: "bundle", Language: "go", Ref: index.Module + ":program",
Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: index.Module}}}}})
// The trunk holds the indexer's entry, taken in at an earlier commit with its own manifest; the seat git answers its go.mod, so
// its Go artifacts carry the Go module the validation's program is read under (§712, §676, A-263).
seedBuildModules(t, ctx, js, map[string]meshcatalog.CatalogEntry{index.Module: trunkIndexerEntry(t, ctx, js)})
if _, err := js.Conn().Subscribe("mesh.seat.git.tool.file", func(m *nats.Msg) {
var a git.FileInput
_ = json.Unmarshal(m.Data, &a)
answer := git.FileAnswer{Path: a.Path, Ref: a.Ref, Absent: true}
if a.Path == "modules/"+index.Module+"/go.mod" {
answer = git.FileAnswer{Path: a.Path, Ref: a.Ref, Content: "module " + index.Module + "\n"}
}
raw, _ := json.Marshal(map[string]any{"result": answer, "node": "n0"})
_ = m.Respond(raw)
}); err != nil {
t.Fatal(err)
}
validations = standInBuildSeat(t, ctx, js, &refusing)
// The node-engine's composition on n1 (process.Conn.Call's subjects): the build of the indexer that runs there.
answer := func(m *nats.Msg) {
@@ -430,8 +440,8 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
if refused, ok := said(takein.TakeInPending, m3Key); !ok || !strings.Contains(refused.Refused, "a: the stand-in's problem") || refused.Failed != "" {
t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the revision refused naming a's problem", m3Key, refused, ok)
}
if w = waits(); len(w) != 1 || w[0].Key != m3Key || w[0].Validation == nil || w[0].Validation.Accepted != 0 || len(validations.asked()) != 2 {
t.Fatalf("indexer_pending = %+v, asks %d: want the refused revision pending with its validation", w, len(validations.asked()))
if w = waits(); len(w) != 1 || w[0].Key != m3Key || w[0].Validation == nil || w[0].Validation.Accepted != 0 || len(validations.asked()) != 3 {
t.Fatalf("indexer_pending = %+v, asks %d: want the refused revision pending with its validation, asked once after m1 and the group", w, len(validations.asked()))
}
if raw, ok := get(index.ModulesState, "a"); !ok || strings.Contains(string(raw), m3.Merge) {
t.Errorf("modules.a = %s: the refused revision was taken in", raw)
@@ -439,6 +449,37 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
}
// trunkIndexerEntry keeps the indexer's own manifest by its digest, accepted, and answers its trunk entry at an earlier commit.
func trunkIndexerEntry(t *testing.T, ctx context.Context, js jetstream.JetStream) meshcatalog.CatalogEntry {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
digest := meshcatalogclient.ManifestDigest(raw)
manifests, err := js.KeyValue(ctx, index.ManifestsBucket)
if err != nil {
t.Fatal(err)
}
mkey, _ := meshcatalogclient.ManifestsKey(digest)
mdata, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: index.Module, Manifest: string(raw)})
if _, err := manifests.Put(ctx, mkey, mdata); err != nil {
t.Fatal(err)
}
held := meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-delivery", Commit: "4444444444444444444444444444444444444444", Path: "modules/" + index.Module,
Manifest: digest, None2: sdkstep.ToolchainNoReport}
e := index.CatalogEntryOf(index.Module, held, m, string(raw), graph.Node{None: held.None2})
for _, a := range m.Build.Artifacts {
e.Artifacts = append(e.Artifacts, meshcatalog.Artifact{Kind: a.Kind, Language: a.Language, Ref: index.ArtifactRefOf(index.Module, a.Name),
Package: &meshcatalog.ArtifactPackage{Ecosystem: index.EcosystemGo, Name: index.Module}})
}
return e
}
// standIn is the build seat node-build-go as the indexer's validation meets it: the asks it heard, oldest first.
type standIn struct {
mu sync.Mutex
@@ -783,7 +824,7 @@ func startIndexerRun(t *testing.T, put func(context.Context, buildLog), before .
start := func() {
runCtx, cancel := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- run(runCtx, func(k string) string { return env[k] }, os.Stderr) }()
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
stop = func() { cancel(); <-done }
}
start()
@@ -1536,7 +1577,8 @@ func serveAssignerAssignments(t *testing.T, ctx context.Context, js jetstream.Je
t.Fatal(err)
}
if _, err := js.CreateOrUpdateConsumer(ctx, "EVENTS", jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-indexer", AckPolicy: jetstream.AckExplicitPolicy,
FilterSubjects: []string{"mesh.mod." + assignerHolderModule + ".event.views.>", "mesh.seat.mesh-delivery.event.>"}}); err != nil {
// and the build seat's step-ended the manifest's consumes names (design 47 G §713: the end of a validation ask).
FilterSubjects: []string{"mesh.mod." + assignerHolderModule + ".event.views.>", "mesh.seat.mesh-delivery.event.>", nodebuildgo.SubjectStepEndedAll}}); err != nil {
t.Fatal(err)
}
em := views.Emitter{Owner: assignerHolderModule, Subject: process.Account{Node: "n1", Module: assignerHolderModule}.EventSubject,
@@ -1731,4 +1773,3 @@ func TestIndexerTakesInAWatchedBranchOtherThanTheFirstIntoItsOwnRevision(t *test
t.Errorf("modules.avahi = %s, want the trunk's modules left", e.Value())
}
}
var _ = io.Discard
@@ -35,9 +35,11 @@ func newestOfRevision(ready []PendingEntry) int {
return n
}
// branchOfRevision is the watched branch the revision's merges landed on, its newest merged message's base (design 47 G §714);
// a message that names none is the trunk's. A pending value's own branch is its pull request's, never the branch validated.
func branchOfRevision(ready []PendingEntry) string {
if ref := ready[newestOfRevision(ready)].Value.Ref; ref != nil && ref.Branch != "" {
return ref.Branch
if base := ready[newestOfRevision(ready)].Forge.Base; base != "" {
return base
}
return TrunkBranch
}
@@ -179,7 +179,9 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
// commit is in another (issue 1900).
if want := []string{"seat:git.trees", "seat:git.file", "seat:git.contains", "seat:git.open-pulls", "seat:git.branch", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-module-assigner.assignments", "seat:mesh-module-assigner.placement", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.steps", "seat:node-build-node.steps", "seat:node-build-image.steps", "mesh-delivery-builder.builder_versions"}; !reflect.DeepEqual(m.Invokes, want) {
if want := []string{"seat:git.trees", "seat:git.file", "seat:git.contains", "seat:git.open-pulls", "seat:git.branch", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-module-assigner.assignments", "seat:mesh-module-assigner.placement", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.steps",
"seat:node-build-go.accept.check", // design 47 G A-231: the validation's run ask (§712) on the check queue
"seat:node-build-node.steps", "seat:node-build-image.steps", "mesh-delivery-builder.builder_versions"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
@@ -175,6 +175,7 @@
"seat:mesh-hand-acts.hand-act",
"seat:mesh-watchdogs.presence",
"seat:node-build-go.steps",
"seat:node-build-go.accept.check",
"seat:node-build-node.steps",
"seat:node-build-image.steps",
"mesh-delivery-builder.builder_versions"