Files
mesh-catalog/modules/distribution/cmd/distribution-tools/contract_test.go
T
jschoubben 85fd46b4cc CAT-D2: distribution's verbs and events speak the two seats' contracts from mesh-contracts
The verbs answered the holder's own shapes (missing and ended, a part named digest, versions as bare strings, held as
the publish's event) while the seat definitions, now generated into mesh-contracts, name lacks and held, part,
versions of artifacts, tags with digests and a hold's own body. A build module using mesh-sdk's parts helper would
have read no lacking part and sent nothing. The bundle now takes and answers the contract's types and says each
event in its body; an image's manifest is the part whose digest is the publish's, as the contract names no media
type, and a hold on the image seat is of a copied base.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:07:14 +01:00

66 lines
2.6 KiB
Go

package main
import (
"bytes"
"encoding/json"
"reflect"
"strings"
"testing"
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
"distribution/internal/holder"
)
func jsonFields(v any) map[string]bool {
out := map[string]bool{}
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
out[name] = true
}
return out
}
// TestTheVerbsSpeakTheSeatsContract holds distribution's seat verbs to the contracts mesh-contracts generates from the
// two seats' definitions (ADR 0620 *How it is checked*): every argument a verb's tool takes is a field of the
// contract's input, and every event body decodes into the contract's type with no field left over.
func TestTheVerbsSpeakTheSeatsContract(t *testing.T) {
inputs := map[string]any{reg.VerbPublish: reg.PublishInput{}, reg.VerbPublishPart: reg.PublishPartInput{},
reg.VerbVersions: reg.VersionsInput{}, reg.VerbRemove: reg.RemoveInput{}, reg.VerbHold: reg.HoldInput{},
reg.VerbRelease: reg.ReleaseInput{}, reg.VerbMirror: reg.MirrorInput{}, reg.VerbTags: reg.TagsInput{}}
seen := 0
for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}) {
seat, verb, ok := strings.Cut(tool.Name, ".")
in, isVerb := inputs[verb]
if !ok || !isVerb || (seat != holder.ImageSeat && seat != holder.ArtifactSeat) {
continue
}
seen++
fields := jsonFields(in)
props, _ := tool.Input["properties"].(map[string]any)
for p := range props {
if !fields[p] {
t.Errorf("%s takes %q, which the contract's input does not name", tool.Name, p)
}
}
}
if seen != 14 {
t.Errorf("%d seat verbs served, want 14 (8 on the image seat, 6 on the artifact seat)", seen)
}
for event, want := range map[string]any{holder.EventPublished: &reg.Outcome{}, holder.EventHeld: &reg.Hold{}, holder.EventReleased: &reg.Hold{}} {
b, _ := json.Marshal(eventBody(event, holder.Event{Name: "m/a", Kind: holder.KindArchive, Digest: "sha256:aa", Holder: holder.Self, By: "x.artifact-store-tools"}))
dec := json.NewDecoder(bytes.NewReader(b))
dec.DisallowUnknownFields()
if err := dec.Decode(want); err != nil {
t.Errorf("%s's body %s is not the contract's: %v", event, b, err)
}
}
if a := publishAnswer(holder.PublishAnswer{Transfer: "t", Missing: []string{"sha256:bb"}}); !reflect.DeepEqual(a.Lacks, []string{"sha256:bb"}) || a.Held {
t.Errorf("publish answers %+v", a)
}
if r, err := partOf(reg.PublishPartInput{Transfer: "t", Part: "sha256:bb", Data: "aGk=", Last: true}); err != nil || string(r.Data) != "hi" || r.Digest != "sha256:bb" {
t.Errorf("publish-part read %+v, %v", r, err)
}
}