diff --git a/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go b/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go index 5b34dfe1..6856bacb 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go @@ -2,9 +2,12 @@ package main import ( "bytes" + "context" "encoding/json" + "errors" "io/fs" "os" + "path" "path/filepath" "reflect" "strings" @@ -14,6 +17,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "git.novox.be/novox/mesh-delivery/sdk/seatdefs" "git.novox.be/novox/mesh-sdk/go/seatdef" ) @@ -158,12 +162,8 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { if err != nil || len(whole.Seats) < 10 { t.Fatalf("the repository's own definitions: %d, %v", len(whole.Seats), err) } - group.SeatDigests = map[string]string{} - for _, raw := range whole.Seats { - var d struct{ Seat string } - _ = json.Unmarshal(raw, &d) - group.SeatDigests[d.Seat] = rules.SeatDigest(raw) - } + // The reader's digests are of each definition whole, as the seat mesh-catalog answers it; the answer is its judging view. + group.SeatDigests = wholeDigests(t, real, "novox/mesh-delivery") same, err := rules.SeatsOf(real, group) answer, _ := json.Marshal(same) if err != nil || len(same.Seats) != 0 || same.Same != len(whole.Seats) || len(answer) > buildstep.RunInputBound { @@ -218,6 +218,13 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { t.Errorf("one changed definition: answered %d, %v", len(one.Seats), err) } }) + // The lead's ruling after G's group check of 616c42434ac2: with digests given too, a changed definition whose verb input + // has a property named credentials is answered as the validator and the composer read it, with no such field. + changed, err := rules.SeatsOf(os.DirFS(heads), rules.Facts{V: rules.FactsVersion, Members: facts.Members, + SeatDigests: map[string]string{"node-build-x": "changed"}}) + if err != nil || len(changed.Seats) != 1 || bytes.Contains(changed.Seats[0], []byte("credentials")) || bytes.Contains(changed.Seats[0], []byte("Sealed")) { + t.Errorf("a changed definition with digests given: %d answered, %s, %v; want it alone, with no verb's schema", len(changed.Seats), changed.Seats, err) + } // Review round 1 of milestone 47/B0: the module graph's cycle rule is the program's (ADR 0603 §2, ADR 0656 §5), over the // edges the running checker gathers. A cycle the change makes fails; one the trunk has among the same modules is noted. web, mail := rules.Edge{From: "web", To: "mail", Kind: rules.EdgeAwaits}, rules.Edge{From: "mail", To: "web", Kind: rules.EdgeAwaits} @@ -270,3 +277,66 @@ func mustJSON(t *testing.T, v any) []byte { } return out } + +// wholeDigests are the digests of a head's seat definitions whole (rules.SeatDigest), read by the domain SDK as the +// indexer reads them: what a checker's reader holds for heads that change none. +func wholeDigests(t *testing.T, root fs.FS, repository string) map[string]string { + t.Helper() + head, err := fs.Sub(root, repository) + if err != nil { + t.Fatal(err) + } + manifests := map[string]string{} + _ = fs.WalkDir(head, ".", func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() && (d.Name() == ".git" || d.Name() == "testdata" || d.Name() == "node_modules") { + return fs.SkipDir + } + if !d.IsDir() && d.Name() == "module.json" { + raw, _ := fs.ReadFile(head, p) + manifests[path.Dir(p)] = string(raw) + } + return nil + }) + forge := seatdefs.Forge{ + Files: func(_ context.Context, _, _ string, dirs []string) (map[string]map[string]string, error) { + out := map[string]map[string]string{} + for _, dir := range dirs { + files := map[string]string{} + if err := fs.WalkDir(head, dir, func(p string, d fs.DirEntry, err error) error { + if err == nil && !d.IsDir() { + files[strings.TrimPrefix(p, dir+"/")] = "" + } + return err + }); err == nil { + out[dir] = files + } + } + return out, nil + }, + File: func(_ context.Context, _, _, file string) ([]byte, error) { + raw, err := fs.ReadFile(head, file) + if errors.Is(err, fs.ErrNotExist) { + return nil, nil + } + return raw, err + }, + } + out := map[string]string{} + if len(manifests) == 0 { + t.Fatalf("no manifest at the head of %s", repository) + } + for _, c := range seatdefs.ContractsModules(manifests) { + defs, _, err := forge.Read(context.Background(), repository, "h", c) + if err != nil { + t.Fatal(err) + } + for seat, d := range defs { + raw, _ := json.Marshal(d) + out[seat] = rules.SeatDigest(raw) + } + } + return out +} diff --git a/modules/mesh-delivery-checker/rules/seats.go b/modules/mesh-delivery-checker/rules/seats.go index 576240bc..1c51289a 100644 --- a/modules/mesh-delivery-checker/rules/seats.go +++ b/modules/mesh-delivery-checker/rules/seats.go @@ -137,14 +137,15 @@ func SeatsOf(heads fs.FS, f Facts) (SeatsAnswer, error) { continue } raw := by[seat].raw + // The answer is the judging view of a seat, what the manifest validator and the composer read (the lead's ruling after + // the group check of 616c42434ac2), with digests or without: no verb's or message's schema, which neither reads, and + // which the build seat's step state, keeping this answer, would refuse where a property is named like a credential + // (mesh-vault's verb input `credentials`). + raw = withoutMessageSchemas(raw) if f.SeatDigests == nil { // A checker from before G hands no digests, so every definition is answered: whole they are over a run answer's - // bound, so every description but the seat's own is left out, of its verbs, events and their schemas, which - // decides no verdict (neither the manifest validator nor the composer reads one); review round 2 of G. Nor do they - // read a verb's or a message's schema, and the build seat's step state, which keeps this answer, refuses a value - // carrying a field named like a credential, as a schema's property may be named (the group check of - // 616c42434ac2: mesh-vault's verb input property `credentials`): those schemas are left out too. - raw = withoutMessageSchemas(withoutInnerDescriptions(raw)) + // bound, so every description but the seat's own is left out too, which decides no verdict; review round 2 of G. + raw = withoutInnerDescriptions(raw) } out.Seats = append(out.Seats, raw) }