diff --git a/modules/mesh-delivery-indexer/internal/index/validate.go b/modules/mesh-delivery-indexer/internal/index/validate.go index a394aa1e..103c3e6a 100644 --- a/modules/mesh-delivery-indexer/internal/index/validate.go +++ b/modules/mesh-delivery-indexer/internal/index/validate.go @@ -122,23 +122,62 @@ func placeKey(repository, path string) string { return strings.ToLower(trunk.RepositoryName(repository)) + " " + path } -// revisionDefinitions are the seat definitions the merges' trees carry, raw (§712 `seats`). +// revisionDefinitions are the validation's seat definitions, raw (§712 `seats`, A-241): the branch's revision's seat +// definitions with the definitions the merges' trees carry laid over them, so a revision that touches no definition is still +// judged against the whole seat list. A revision's own definitions are not kept yet (A-234), so the revision's are the +// trunk's, read from the state seat-definitions as the verb seat-definitions answers them. func (ix *Indexer) revisionDefinitions(ctx context.Context, ready []PendingEntry) ([]json.RawMessage, error) { - defs, err := ix.revisionSeatDefinitions(ctx, ready) + defs, err := ix.trunkDefinitions(ctx) if err != nil { return nil, err } - var out []json.RawMessage - for _, seat := range slices.Sorted(maps.Keys(defs)) { - raw, err := json.Marshal(defs[seat]) + merged, err := ix.revisionSeatDefinitions(ctx, ready) + if err != nil { + return nil, err + } + for seat, d := range merged { + raw, err := json.Marshal(d) if err != nil { return nil, fmt.Errorf("the definition of %s: %w", seat, err) } - out = append(out, raw) + defs[seat] = raw + } + var out []json.RawMessage + for _, seat := range slices.Sorted(maps.Keys(defs)) { + out = append(out, defs[seat]) } return out, nil } +// trunkDefinitions are the trunk's seat definitions of the state seat-definitions, raw, by seat: every `.trunk` entry +// that holds a definition; none where the indexer keeps no such state. +func (ix *Indexer) trunkDefinitions(ctx context.Context) (map[string]json.RawMessage, error) { + defs := map[string]json.RawMessage{} + if ix.SeatDefinitions == nil { + return defs, nil + } + keys, err := ix.SeatDefinitions.Keys(ctx) + if err != nil { + return nil, fmt.Errorf("the %s state cannot be listed: %w", SeatDefinitionsState, err) + } + for _, key := range keys { + seat, ok := strings.CutSuffix(key, "."+SourceTrunk) + if !ok || strings.HasPrefix(key, outOfStepPrefix) { + continue + } + v, _, found, err := ix.SeatDefinitions.Get(ctx, key) + if err != nil { + return nil, fmt.Errorf("%s.%s cannot be read: %w", SeatDefinitionsState, key, err) + } + var e SeatDefinitionEntry + if !found || json.Unmarshal(v, &e) != nil || e.Digest == "" || len(e.Definition) == 0 { + continue // an entry that holds no definition is no seat of the list + } + defs[seat] = e.Definition + } + return defs, nil +} + // validationJob is the run of the manifest validator for the request (§712): the program cmd/mesh-judge under the Go module // the indexer entry's Go artifact names, at the entry's path, built at head, the revision's head of the entry's repository; no // heads and no arguments; the input the request's bytes, served by the seat mesh-catalog's verb input. diff --git a/modules/mesh-delivery-indexer/internal/index/validate_test.go b/modules/mesh-delivery-indexer/internal/index/validate_test.go index d99a3358..5926cc31 100644 --- a/modules/mesh-delivery-indexer/internal/index/validate_test.go +++ b/modules/mesh-delivery-indexer/internal/index/validate_test.go @@ -1,11 +1,14 @@ package index import ( + "context" "encoding/json" + "reflect" "strings" "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer" @@ -72,3 +75,56 @@ func TestValidationRefusesWhatTheRevisionCauses(t *testing.T) { t.Errorf("no indexer entry: %v", err) } } + +// The validation's seats are the branch's revision's seat definitions with the merges' changed ones laid over them (A-241): a +// revision that touches no definition is judged against the whole seat list, and a merge's definition replaces its seat's. +func TestValidationSeatsAreTheRevisionsWithTheMergesOver(t *testing.T) { + ctx := context.Background() + defs := newMem() + for seat, raw := range map[string]string{"node-engine": `{"seat":"node-engine","verbs":[{"name":"old"}]}`, "git": `{"seat":"git"}`} { + v, _ := json.Marshal(SeatDefinitionEntry{Seat: seat, Source: SourceTrunk, Digest: "d-" + seat, Definition: json.RawMessage(raw)}) + defs.put(SeatDefinitionKey(seat, SourceTrunk, "", 0), v) + } + v, _ := json.Marshal(SeatDefinitionEntry{Seat: "pulled", Source: SourcePull, Pull: 3, Digest: "d", Definition: json.RawMessage(`{"seat":"pulled"}`)}) + defs.put(SeatDefinitionKey("pulled", SourcePull, "r", 3), v) + ix := &Indexer{SeatDefinitions: defs} + seatsOf := func(raws []json.RawMessage) map[string]string { + out := map[string]string{} + for _, raw := range raws { + var d struct { + Seat string `json:"seat"` + Verbs []struct { + Name string `json:"name"` + } `json:"verbs"` + } + if err := json.Unmarshal(raw, &d); err != nil { + t.Fatal(err) + } + out[d.Seat] = "" + if len(d.Verbs) > 0 { + out[d.Seat] = d.Verbs[0].Name + } + } + return out + } + got, err := ix.revisionDefinitions(ctx, nil) + if err != nil || !reflect.DeepEqual(seatsOf(got), map[string]string{"node-engine": "old", "git": ""}) { + t.Errorf("a revision touching no definition: %v (%v), want the trunk's whole list and no pull request's", seatsOf(got), err) + } + const contracts = "modules/seat-contracts" + f := forgemsg.Forge{Repository: "r", Merge: strings.Repeat("c", 40), Manifests: map[string]string{ + contracts: `{"module":"seat-contracts","version":"1.0","build":{"artifacts":[{"name":"go-seats","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`}} + ix.Files = func(_ context.Context, _, _ string, dirs []string) (map[string]map[string]string, error) { + return map[string]map[string]string{dirs[0]: {"node-engine/seat.json": "1"}}, nil + } + ix.ReadFile = func(_ context.Context, _, _, p string) ([]byte, error) { + if p == contracts+"/seats/node-engine/seat.json" { + return []byte(`{"format":1,"seat":"node-engine","scope":"node","grain":"seat-list","decision":"d","domain":"Core","verbs":[{"name":"new"}]}`), nil + } + return nil, nil + } + got, err = ix.revisionDefinitions(ctx, []PendingEntry{{Forge: f}}) + if err != nil || !reflect.DeepEqual(seatsOf(got), map[string]string{"node-engine": "new", "git": ""}) { + t.Errorf("a merge changing a definition: %v (%v), want its definition over the trunk's and the rest kept", seatsOf(got), err) + } +}