From 1801f1178e0877e81a6a8224c416f659981d81aa Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:40:59 +0200 Subject: [PATCH] Hold the Go emitter to the shared fixtures Every required header set, each value in the pinned shape, and the subject derived the same way. Read from the sdk's conformance directory by sibling path, never copied. --- internal/link/conformance_test.go | 107 ++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 internal/link/conformance_test.go diff --git a/internal/link/conformance_test.go b/internal/link/conformance_test.go new file mode 100644 index 0000000..50f60da --- /dev/null +++ b/internal/link/conformance_test.go @@ -0,0 +1,107 @@ +package link + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "testing" + "time" +) + +// The Go implementation, held to the shared fixtures (novox/hq ADR 0074, design 19). +// +// **Read from the sdk's conformance directory by sibling path**, the way the lab finds its +// siblings — deliberately not copied here. A fixture copied into each implementation is two +// fixtures, and two fixtures drift, which is the exact failure the suite exists to prevent. +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"` +} + +func loadFixture(t *testing.T, name string) fixture { + t.Helper() + path := filepath.Join("..", "..", "..", "mesh-sdk", "conformance", name) + raw, err := os.ReadFile(path) + if err != nil { + t.Skipf("the sdk's conformance fixtures are not beside this checkout: %v", err) + } + var f fixture + if err := json.Unmarshal(raw, &f); err != nil { + t.Fatalf("%s: %v", name, err) + } + return f +} + +// 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", + } +}