mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheNatsBusEmitsTheEnvelopeTheFixturePins (0.03s)
mesh/delivery superseded: a newer head of the same pull request
A pull request moving the SDK passed its check against the old SDK and then failed everywhere once it rolled out. In a merge check the test now reads the clone's history at the tree's own go.mod pin, and holds the captured copy to the clone byte for byte.
254 lines
9.6 KiB
Go
254 lines
9.6 KiB
Go
package link
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/beside"
|
|
)
|
|
|
|
// The Go implementation, held to the shared fixtures (novox/hq ADR 0074, design 19).
|
|
//
|
|
// **Read from the SDK's own conformance directory, never copied by hand**: a fixture written into each
|
|
// implementation is two fixtures, and two fixtures drift, which is the failure the suite exists to prevent.
|
|
// Which SDK is read is the one this tree's go.mod pins, never the one a desktop happens to hold beside this
|
|
// checkout (novox/hq issue 449):
|
|
//
|
|
// - in a merge check, the fixture as it stands at that pin in the mesh-sdk clone the build seat places
|
|
// beside the check — read with `git show`, because the clone is checked out at the *running*
|
|
// controller's pin, and a pull request moving the SDK must be judged against the SDK it moves to, or it
|
|
// passes the check and fails everywhere after it rolls out. A missing clone or a pin the clone lacks
|
|
// fails the test;
|
|
// - elsewhere, the copy captured in testdata/beside at the commit testdata/beside/CAPTURED names, held to
|
|
// go.mod by TestTheCapturedSDKIsTheOneGoModPins.
|
|
//
|
|
// In a merge check the captured copy is also compared with the clone at the captured commit, byte for byte,
|
|
// so a hand-edited copy cannot pass for the SDK's.
|
|
type fixture struct {
|
|
Name string `json:"name"`
|
|
Given struct {
|
|
Module string `json:"module"`
|
|
Node string `json:"node"`
|
|
Key string `json:"key"`
|
|
Body map[string]any `json:"body"`
|
|
Headers map[string]string `json:"headers"`
|
|
} `json:"given"`
|
|
Wire struct {
|
|
Subject string `json:"subject"`
|
|
RequiredHeaders []string `json:"requiredHeaders"`
|
|
HeaderFormats map[string]string `json:"headerFormats"`
|
|
} `json:"wire"`
|
|
}
|
|
|
|
// sdkModule is the Go module of the SDK the controller is built against.
|
|
const sdkModule = "git.novox.be/novox/mesh-sdk/go"
|
|
|
|
// The tree's go.mod, and the captured copy, as this package finds them.
|
|
var (
|
|
goModFile = filepath.Join("..", "..", "go.mod")
|
|
capturedSDK = filepath.Join(beside.Captured(), "mesh-sdk")
|
|
capturedLog = filepath.Join(beside.Captured(), "CAPTURED")
|
|
)
|
|
|
|
func loadFixture(t *testing.T, name string) fixture {
|
|
t.Helper()
|
|
raw, from, err := sdkFixture(os.Getenv(beside.Env), goModFile, capturedSDK, capturedLog, name)
|
|
if err != nil {
|
|
// Failed, never skipped: a skip here passed the suite with nothing judged.
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("%s: %s", name, from)
|
|
var f fixture
|
|
if err := json.Unmarshal(raw, &f); err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// sdkFixture is one conformance fixture and where it was read: from the mesh-sdk clone in root (a merge
|
|
// check's MESH_CHECK_BESIDE) at the pin of goMod, or, with no root, from the captured copy.
|
|
func sdkFixture(root, goMod, captured, capturedLog, name string) ([]byte, string, error) {
|
|
file := path.Join("conformance", name)
|
|
if root == "" {
|
|
raw, err := os.ReadFile(filepath.Join(captured, filepath.FromSlash(file)))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the captured SDK's %s: %w", file, err)
|
|
}
|
|
return raw, "the copy captured in testdata/beside (see CAPTURED); a merge check reads the SDK's clone " +
|
|
"at this tree's pin", nil
|
|
}
|
|
clone := filepath.Join(root, "mesh-sdk")
|
|
if _, err := os.Stat(clone); err != nil {
|
|
return nil, "", fmt.Errorf("mesh-sdk is not beside this check in %s=%s, so the SDK this controller "+
|
|
"pins cannot be judged here: %w", beside.Env, root, err)
|
|
}
|
|
ref, err := sdkRef(goMod)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
raw, err := gitShow(clone, ref, file)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the mesh-sdk clone beside this check has no %s at %s, the SDK this tree's "+
|
|
"go.mod pins: %w", file, ref, err)
|
|
}
|
|
// The captured copy is the SDK's own, never edited by hand: the clone at the captured commit says so.
|
|
at, err := capturedCommit(capturedLog)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
theirs, err := gitShow(clone, at, file)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the mesh-sdk clone has no %s at %s, the commit CAPTURED names: %w", file, at, err)
|
|
}
|
|
ours, err := os.ReadFile(filepath.Join(captured, filepath.FromSlash(file)))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the captured SDK's %s: %w", file, err)
|
|
}
|
|
if !bytes.Equal(ours, theirs) {
|
|
return nil, "", fmt.Errorf("the captured %s is not mesh-sdk's at %s, the commit CAPTURED names: it was "+
|
|
"edited or captured wrong; capture it again as CAPTURED says", file, at)
|
|
}
|
|
return raw, "mesh-sdk cloned beside this check, at " + ref + " (this tree's go.mod)", nil
|
|
}
|
|
|
|
// gitShow is one file of a repository at a ref. safe.directory, because the clone is the build seat's and
|
|
// the test may run as another user.
|
|
func gitShow(repository, ref, file string) ([]byte, error) {
|
|
cmd := exec.Command("git", "-c", "safe.directory=*", "-C", repository, "show", ref+":"+file)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("git show %s:%s: %v: %s", ref, file, err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// pseudoCommit is the commit a Go pseudo-version names: v0.1.11-0.20261009143344-f047d0a4a970 → f047d0a4a970.
|
|
var pseudoCommit = regexp.MustCompile(`-([0-9a-f]{12})$`)
|
|
|
|
// sdkRef is the SDK repository's ref a go.mod pins: a pseudo-version's commit, or a release's tag (the SDK
|
|
// tags its Go module under go/).
|
|
func sdkRef(goMod string) (string, error) {
|
|
raw, err := os.ReadFile(goMod)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
version := ""
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require "))
|
|
if len(fields) >= 2 && fields[0] == sdkModule {
|
|
version = fields[1]
|
|
}
|
|
}
|
|
if m := pseudoCommit.FindStringSubmatch(version); m != nil {
|
|
return m[1], nil
|
|
}
|
|
if strings.HasPrefix(version, "v") {
|
|
return "go/" + version, nil
|
|
}
|
|
return "", fmt.Errorf("%s pins no version of %s that names a commit or a tag", goMod, sdkModule)
|
|
}
|
|
|
|
// capturedCommit is the commit testdata/beside/CAPTURED names for mesh-sdk.
|
|
func capturedCommit(capturedLog string) (string, error) {
|
|
raw, err := os.ReadFile(capturedLog)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
at := regexp.MustCompile(`(?m)^mesh-sdk\s+([0-9a-f]{40})\s`).FindSubmatch(raw)
|
|
if at == nil {
|
|
return "", fmt.Errorf("%s names no commit for mesh-sdk", capturedLog)
|
|
}
|
|
return string(at[1]), nil
|
|
}
|
|
|
|
// The captured SDK is the one this controller is built against: when go.mod moves the SDK, the copy moves
|
|
// with it, or the tests away from a merge check judge an SDK the controller no longer uses (novox/hq issue
|
|
// 449).
|
|
func TestTheCapturedSDKIsTheOneGoModPins(t *testing.T) {
|
|
pinned, err := sdkRef(goModFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
at, err := capturedCommit(capturedLog)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.HasPrefix(pinned, "go/") || !strings.HasPrefix(at, pinned) {
|
|
t.Errorf("the SDK is captured at %s but go.mod pins %s: capture it again at the pinned commit, as "+
|
|
"testdata/beside/CAPTURED says", at, pinned)
|
|
}
|
|
}
|
|
|
|
// Every header the fixture requires is one this implementation actually sets.
|
|
func TestTheGoEmitterSetsEveryRequiredHeader(t *testing.T) {
|
|
f := loadFixture(t, "events/module-event.json")
|
|
sent := goEventHeaders(f.Given.Key, f.Given.Module, f.Given.Node)
|
|
for _, want := range f.Wire.RequiredHeaders {
|
|
if _, ok := sent[want]; !ok {
|
|
t.Errorf("the Go emitter does not set %q, which the fixture requires — an event it "+
|
|
"emits is one a conforming consumer refuses", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And each value is in the shape the fixture pins, because a header present but differently
|
|
// formatted is the disagreement that does not announce itself.
|
|
func TestTheGoEmittersHeaderFormatsMatch(t *testing.T) {
|
|
f := loadFixture(t, "events/module-event.json")
|
|
sent := goEventHeaders(f.Given.Key, f.Given.Module, f.Given.Node)
|
|
|
|
if got := sent["content-type"]; got != f.Wire.HeaderFormats["content-type"] {
|
|
t.Errorf("content-type is %q, the fixture says %q", got, f.Wire.HeaderFormats["content-type"])
|
|
}
|
|
if _, err := time.Parse(time.RFC3339, sent["x-time"]); err != nil {
|
|
t.Errorf("x-time %q is not RFC3339, which the fixture requires: %v", sent["x-time"], err)
|
|
}
|
|
if pattern := f.Wire.HeaderFormats["x-event-id"]; pattern != "" {
|
|
if !regexp.MustCompile(pattern).MatchString(sent["x-event-id"]) {
|
|
t.Errorf("x-event-id %q does not match %q", sent["x-event-id"], pattern)
|
|
}
|
|
}
|
|
// The origin the envelope claims is the one the bus enforces by namespace. A disagreement
|
|
// here means the envelope is lying about where it came from.
|
|
if sent["x-source"] != f.Given.Module {
|
|
t.Errorf("x-source is %q for module %q", sent["x-source"], f.Given.Module)
|
|
}
|
|
}
|
|
|
|
// The subject a module's event lands on is derived, not carried — so this implementation must
|
|
// derive the same one the fixture names.
|
|
func TestTheGoSubjectMatchesTheFixture(t *testing.T) {
|
|
f := loadFixture(t, "events/module-event.json")
|
|
got := "mesh.mod." + f.Given.Module + ".event." + f.Given.Key
|
|
if got != f.Wire.Subject {
|
|
t.Errorf("this implementation would publish on %q; the fixture says %q", got, f.Wire.Subject)
|
|
}
|
|
}
|
|
|
|
// goEventHeaders is the header set EmitEvent produces, factored so conformance can see it
|
|
// without a broker. Kept beside the emitter so the two cannot drift apart silently.
|
|
func goEventHeaders(eventType, source, node string) map[string]string {
|
|
id, err := eventID()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return map[string]string{
|
|
"x-event-id": id,
|
|
"x-source": source,
|
|
"x-node": node,
|
|
"x-time": time.Now().UTC().Format(time.RFC3339),
|
|
"content-type": "application/json",
|
|
}
|
|
}
|