package link import ( "bytes" "encoding/json" "fmt" "os" "os/exec" "path" "path/filepath" "regexp" "strings" "sync" "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 pin the clone lacks fails the test; a // missing clone reads the captured copy and says so loudly, until mesh-controller #220 has rolled out // and the build seat clones mesh-sdk (then its follow-up makes a missing clone fail again); // - 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) if strings.HasPrefix(from, notJudged) { // Said on the check's own output too, where `go test` without -v prints no log of a passing test. saidNotJudged.Do(func() { fmt.Fprintln(os.Stderr, "internal/link conformance: "+from) }) } var f fixture if err := json.Unmarshal(raw, &f); err != nil { t.Fatalf("%s: %v", name, err) } return f } // notJudged opens what a merge check without the SDK's clone says; saidNotJudged says it once on stderr. const notJudged = "NOT JUDGED AGAINST THE CLONE" var saidNotJudged sync.Once // 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) readCaptured := func() ([]byte, error) { 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, nil } if root == "" { raw, err := readCaptured() return raw, "the copy captured in testdata/beside (see CAPTURED); a merge check reads the SDK's clone " + "at this tree's pin", err } clone := filepath.Join(root, "mesh-sdk") if _, err := os.Stat(clone); err != nil { // A build seat asked by a controller from before mesh-controller #220 places no mesh-sdk beside the // check, so #220 could never pass its own check if this failed. Until #220 has rolled out, the // captured copy is read and the log says so loudly; the follow-up of novox/hq issue 449 makes this // a failure again. at, cerr := capturedCommit(capturedLog) if cerr != nil { return nil, "", cerr } raw, rerr := readCaptured() return raw, fmt.Sprintf(notJudged+": the seat placed no mesh-sdk beside this check in "+ "%s=%s (it does once mesh-controller #220 has rolled out); read the captured copy at %s", beside.Env, root, at), rerr } 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", } }