DLV-M2r1: the checker fails a pull request whose head's seat definition does not parse, naming the seat and where
The lead's ruling on A2i's finding 1 (design 26): the indexer keeps an unparsed definition of a pull request's head as its entry of the state seat-definitions with `unparsed`, and cannot refuse the manifest. The checker follows that state as its seventh first view (invokes indexer_read_seat_definitions, consumes views.seat-definitions; checks start once it is answered too, as M2's six) and fails each pull request with an entry holding `unparsed`, naming the seat, its module, its directory and the indexer's words; a state that cannot be read fails it too. Rows: TestTheIndexersJudgeJudgesTheChangesManifests (fails first without the refusal), the adapter's key match in TestThePublishedVersionsAreTheIndexersPackagesState (fails first matching every key), the manifest's invokes and consumes; the integration bed serves the state (without it every check waits and five tests fail). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -443,7 +443,7 @@ func startBed(t *testing.T) *bed {
|
||||
}
|
||||
// The indexer and the assigner as the owners of the states the checker follows: their read tools and views families
|
||||
// (ADR 0615, DLV-M2).
|
||||
for _, st := range []string{candidatesState, modulesState, manifestsState, delivery.PackagesState, delivery.IndexerPredefinedState} {
|
||||
for _, st := range []string{candidatesState, modulesState, manifestsState, delivery.PackagesState, delivery.IndexerPredefinedState, seatDefinitionsState} {
|
||||
firstviewtest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, "n1_"+delivery.CheckerModule)
|
||||
}
|
||||
firstviewtest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, "n1_"+delivery.CheckerModule)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
type judgeDeps struct {
|
||||
asks check.Asks
|
||||
renames check.Renames
|
||||
seatDefs check.SeatDefinitions
|
||||
tools *buildrun.ToolchainSet
|
||||
trunks *check.Trunks
|
||||
kept check.KV
|
||||
@@ -69,6 +71,68 @@ func (k keptPredefined) PredefinedList(ctx context.Context) (json.RawMessage, er
|
||||
return kept.List, nil
|
||||
}
|
||||
|
||||
// keptSeatDefinitions reads a pull request's entries of the indexer's seat definitions from its first view: each whose
|
||||
// `unparsed` says why its head's definition does not parse (the entry's shape is the indexer's read tool's answer).
|
||||
type keptSeatDefinitions struct {
|
||||
keys func(ctx context.Context) ([]string, error)
|
||||
get func(ctx context.Context, key string) ([]byte, error)
|
||||
}
|
||||
|
||||
// seatDefinitionsOf reads the state's picture (a key gone between the keys and its get is none).
|
||||
func seatDefinitionsOf(kv jetstream.KeyValue) keptSeatDefinitions {
|
||||
return keptSeatDefinitions{keys: func(ctx context.Context) ([]string, error) {
|
||||
keys, err := kv.Keys(ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return keys, err
|
||||
}, get: func(ctx context.Context, key string) ([]byte, error) {
|
||||
e, err := kv.Get(ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value(), nil
|
||||
}}
|
||||
}
|
||||
|
||||
func (k keptSeatDefinitions) Unparsed(ctx context.Context, repository string, pull int) ([]check.UnparsedSeat, error) {
|
||||
suffix := ".pull." + delivery.EscapeName(repository) + "." + strconv.Itoa(pull)
|
||||
keys, err := k.keys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var out []check.UnparsedSeat
|
||||
for _, key := range keys {
|
||||
if !strings.HasSuffix(key, suffix) {
|
||||
continue
|
||||
}
|
||||
raw, err := k.get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw == nil {
|
||||
continue
|
||||
}
|
||||
var v struct {
|
||||
Seat string `json:"seat"`
|
||||
Module string `json:"module"`
|
||||
Directory string `json:"directory"`
|
||||
Unparsed string `json:"unparsed"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return nil, fmt.Errorf("%s is no seat definition entry: %w", key, err)
|
||||
}
|
||||
if v.Unparsed != "" {
|
||||
out = append(out, check.UnparsedSeat{Seat: v.Seat, Module: v.Module, Directory: v.Directory, Why: v.Unparsed})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// nodeNames reads every node's name from the node-engine's have, one key `<node>.node` per node with a record (novox/hq
|
||||
// ADR 0567 §12), at each ask: a node enrolled since is named in the next check.
|
||||
type nodeNames struct{ kv jetstream.KeyValue }
|
||||
@@ -156,7 +220,7 @@ func newJudge(d judgeDeps) check.Judge {
|
||||
nodes = d.nodes
|
||||
}
|
||||
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, Logf: d.logf}
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, Logf: d.logf}
|
||||
if d.nodes != nil && d.builds != nil && d.repos != nil {
|
||||
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,18 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-checker/internal/check"
|
||||
)
|
||||
|
||||
type memManifests map[string][]byte
|
||||
@@ -142,6 +146,20 @@ func TestThePublishedVersionsAreTheIndexersPackagesState(t *testing.T) {
|
||||
if got := p.LatestPackages(); len(got) != 1 || got[sdk] == "" {
|
||||
t.Errorf("after a delete: %v", got)
|
||||
}
|
||||
t.Run("a pull request's unparsed seat definitions are its entries of the indexer's seat-definitions", func(t *testing.T) {
|
||||
state := map[string]string{
|
||||
"web-api.trunk": `{"seat":"web-api","source":"trunk","digest":"d"}`,
|
||||
"web-api.pull." + delivery.EscapeName("novox/web") + ".7": `{"seat":"web-api","source":"pull","module":"web-contracts","directory":"go","unparsed":"seat.json: bad"}`,
|
||||
"mail-api.pull." + delivery.EscapeName("novox/web") + ".7": `{"seat":"mail-api","source":"pull","digest":"e"}`,
|
||||
"web-api.pull." + delivery.EscapeName("novox/web") + ".17": `{"seat":"web-api","source":"pull","unparsed":"another pull request's"}`,
|
||||
}
|
||||
k := keptSeatDefinitions{keys: func(context.Context) ([]string, error) { return slices.Collect(maps.Keys(state)), nil },
|
||||
get: func(_ context.Context, key string) ([]byte, error) { return []byte(state[key]), nil }}
|
||||
got, err := k.Unparsed(context.Background(), "novox/web", 7)
|
||||
if want := []check.UnparsedSeat{{Seat: "web-api", Module: "web-contracts", Directory: "go", Why: "seat.json: bad"}}; err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("unparsed of novox/web#7 = %+v, %v; want %+v", got, err, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A manifest whose config value falls back to a predefined key (ADR 0348 §7) is read, not logged as unreadable for the
|
||||
|
||||
@@ -74,8 +74,11 @@ var (
|
||||
|
||||
const (
|
||||
candidatesState = "candidates"
|
||||
modulesState = "modules"
|
||||
manifestsState = "manifests"
|
||||
// seatDefinitionsState is the indexer's state of the seat definitions (design 26), keyed `<seat>.trunk` and
|
||||
// `<seat>.pull.<escaped repository>.<number>`.
|
||||
seatDefinitionsState = "seat-definitions"
|
||||
modulesState = "modules"
|
||||
manifestsState = "manifests"
|
||||
)
|
||||
|
||||
// run is the program: it returns when ctx ends or the bus cannot be used.
|
||||
@@ -144,6 +147,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
packages := reader.Follow(delivery.IndexerModule, delivery.PackagesState)
|
||||
predefined := reader.Follow(delivery.IndexerModule, delivery.IndexerPredefinedState)
|
||||
placement := reader.Follow(delivery.AssignerModule, delivery.PlacementState)
|
||||
// The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails.
|
||||
seatDefs := reader.Follow(delivery.IndexerModule, seatDefinitionsState)
|
||||
|
||||
stepBound, err := durationEnv(getenv, "MESH_CHECKER_STEP_BOUND", check.DefaultStepBound)
|
||||
if err != nil {
|
||||
@@ -264,6 +269,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
nodes: openNodeNames(ctx, js, logger.Printf),
|
||||
predefined: keptPredefined{predefined},
|
||||
renames: seatRenames{call: caller.Call},
|
||||
seatDefs: seatDefinitionsOf(seatDefs),
|
||||
})
|
||||
ch.Announce = announce
|
||||
tryer.Judge = ch.Judge
|
||||
|
||||
@@ -1041,7 +1041,11 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
// renames is what the change does to web's renamed-from ("bring", "drop") and what the seat mesh-config's verb
|
||||
// renames answers ("served", "open" on g14, "not served", "unasked" with no seat to ask, "error"); DLV-C5.
|
||||
renames, seat string
|
||||
// unparsed is the indexer's words of why the pull request's head's seat definition of web-api does not parse.
|
||||
unparsed string
|
||||
}{
|
||||
{name: "a seat definition that does not parse", unparsed: "seats/web-api/seat.json: unexpected end of JSON input",
|
||||
fails: "novox/mesh-delivery#7: the seat definition of web-api (module web-contracts, go) does not parse: seats/web-api/seat.json: unexpected end of JSON input"},
|
||||
{name: "a rename brought, the verb not served", renames: "bring", seat: "not served",
|
||||
fails: "web (novox/mesh-delivery#7) renames new-key from old-key: refused, the holder of the seat mesh-config does not serve its verb renames"},
|
||||
{name: "a rename brought, the verb served", renames: "bring", seat: "served"},
|
||||
@@ -1122,6 +1126,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
b.ex.Renames = fake
|
||||
}
|
||||
}
|
||||
if row.unparsed != "" {
|
||||
b.ex.SeatDefinitions = fakeSeatDefinitions{"novox/mesh-delivery#7": {{Seat: "web-api", Module: "web-contracts", Directory: "go", Why: row.unparsed}}}
|
||||
}
|
||||
var judged []delivery.ComposeJob
|
||||
answer := b.asks.Answer
|
||||
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
|
||||
@@ -1178,3 +1185,10 @@ type fakeRenames struct {
|
||||
func (f fakeRenames) Renames(context.Context, string) (meshconfig.RenamesAnswer, error) {
|
||||
return f.answer, f.err
|
||||
}
|
||||
|
||||
// fakeSeatDefinitions are the indexer's unparsed seat definitions by pull request (pullKey).
|
||||
type fakeSeatDefinitions map[string][]UnparsedSeat
|
||||
|
||||
func (f fakeSeatDefinitions) Unparsed(_ context.Context, repository string, pull int) ([]UnparsedSeat, error) {
|
||||
return f[fmt.Sprintf("%s#%d", repository, pull)], nil
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ type Examiner struct {
|
||||
// Manifests are the indexer's manifest digests by module: the catalogue a change's manifests are judged in by the
|
||||
// indexer's judge (manifests_judge.go, novox/hq ADR 0578 §2); nil judges no manifest.
|
||||
Manifests TrunkManifests
|
||||
// SeatDefinitions reads the indexer's seat definitions of a pull request (seatdefs.go, design 26); nil reads none.
|
||||
SeatDefinitions SeatDefinitions
|
||||
// Renames asks the seat mesh-config's verb renames for a change that brings or drops a `renamed-from` (renames.go,
|
||||
// design 32); nil fails such a change, saying it cannot ask.
|
||||
Renames Renames
|
||||
@@ -273,7 +275,7 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
|
||||
// 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02).
|
||||
tests := st.judgeAll(ctx)
|
||||
st.seriesChanges(ctx) // DLV-C4: named in the verdict's line, whatever the verdict
|
||||
if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx), st.judgeRenames(ctx))); v != nil {
|
||||
if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx), st.judgeRenames(ctx), st.refuseUnparsed(ctx))); v != nil {
|
||||
return st.finish(ctx, *v), nil
|
||||
}
|
||||
return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// A pull request whose head's seat definition does not parse fails its check, naming the seat and where it lives (design
|
||||
// 26; the lead's ruling on A2i's finding 1): the indexer keeps such a definition as the pull request's entry of its state
|
||||
// seat-definitions with `unparsed` set, and cannot refuse the manifest itself, whose digest the trunk may share.
|
||||
|
||||
// UnparsedSeat is one seat definition of a pull request's head that does not parse: the seat (the contracts module's name
|
||||
// when its index did not parse), its module's directory and the indexer's words of why.
|
||||
type UnparsedSeat struct {
|
||||
Seat, Module, Directory, Why string
|
||||
}
|
||||
|
||||
// SeatDefinitions reads the indexer's seat definitions of one pull request (its state seat-definitions, through its
|
||||
// first view, DLV-M2).
|
||||
type SeatDefinitions interface {
|
||||
Unparsed(ctx context.Context, repository string, pull int) ([]UnparsedSeat, error)
|
||||
}
|
||||
|
||||
// refuseUnparsed fails the check of each pull request with a seat definition that does not parse; a state that cannot be
|
||||
// read fails it too, saying so.
|
||||
func (st *state) refuseUnparsed(ctx context.Context) *delivery.Verdict {
|
||||
if st.j.SeatDefinitions == nil || st.c.Try != nil {
|
||||
return nil
|
||||
}
|
||||
var out *delivery.Verdict
|
||||
failed := func(words string) {
|
||||
f := delivery.FailedFirst{Machine: SeatDefinitionsMachine, Words: words}
|
||||
if out == nil {
|
||||
out = &delivery.Verdict{State: delivery.VerdictFail, Failed: f}
|
||||
return
|
||||
}
|
||||
addFailure(out, f)
|
||||
}
|
||||
for _, p := range st.c.Pulls {
|
||||
bad, err := st.j.SeatDefinitions.Unparsed(ctx, p.Repository, p.Number)
|
||||
if err != nil {
|
||||
failed(fmt.Sprintf("%s: the indexer's seat definitions of it cannot be read, so none is known to parse: %v", pullKey(p), err))
|
||||
continue
|
||||
}
|
||||
for _, u := range bad {
|
||||
failed(fmt.Sprintf("%s: the seat definition of %s (module %s, %s) does not parse: %s", pullKey(p), u.Seat, u.Module, u.Directory, u.Why))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SeatDefinitionsMachine names what refused a seat definition in a verdict's failure.
|
||||
const SeatDefinitionsMachine = "seat-definitions"
|
||||
@@ -123,6 +123,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
|
||||
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_seat_definitions",
|
||||
"seat:mesh-config.renames", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
|
||||
t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
|
||||
}
|
||||
@@ -133,7 +134,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
// And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*).
|
||||
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
|
||||
"mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
||||
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages",
|
||||
"mesh-delivery-indexer.views.predefined",
|
||||
"mesh-delivery-indexer.views.seat-definitions",
|
||||
"npm-package-registry.published"
|
||||
],
|
||||
"streams": [
|
||||
@@ -79,6 +80,7 @@
|
||||
"mesh-delivery-indexer.indexer_read_modules",
|
||||
"mesh-delivery-indexer.indexer_read_packages",
|
||||
"mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_seat_definitions",
|
||||
"seat:mesh-config.renames",
|
||||
"seat:node-engine.compose-candidate"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user