The sdk names no transport, and fixtures say what two implementations may not disagree about #8
@@ -0,0 +1,32 @@
|
|||||||
|
# Conformance fixtures
|
||||||
|
|
||||||
|
What two implementations of the mesh's module protocol may not disagree about
|
||||||
|
([novox/hq ADR 0074](https://git.novox.be), design 19). A fixture is data, not prose: every
|
||||||
|
implementation produces it and consumes it, and an implementation that passes the fixtures for a
|
||||||
|
capability **claims** that capability.
|
||||||
|
|
||||||
|
## What "byte-for-byte" means, and what it cannot
|
||||||
|
|
||||||
|
ADR 0074 says the fixtures must match byte for byte. Taken literally that is more than the mesh
|
||||||
|
needs and more than two languages can give:
|
||||||
|
|
||||||
|
- **The envelope is exact.** The subject, the set of required headers, each header's name, and
|
||||||
|
the format of each value. This is what an implementation can get wrong in a way that makes two
|
||||||
|
meshes silently ignore each other, so this is what is pinned.
|
||||||
|
- **The body is not.** It is the module's own payload, opaque to the mesh, and its serialisation
|
||||||
|
is the language's. Go's `encoding/json` sorts a map's keys; JavaScript's `JSON.stringify` keeps
|
||||||
|
insertion order. Demanding identical bytes there would mean committing every implementation to
|
||||||
|
a canonical JSON encoder — a large promise, bought for a property the mesh never uses. The
|
||||||
|
fixture pins that the body **round-trips**: what goes in comes out equal.
|
||||||
|
|
||||||
|
So: exact where disagreement is invisible, semantic where it is not. Stated here because
|
||||||
|
"byte-for-byte" read strictly would have sent somebody implementing a canonical encoder.
|
||||||
|
|
||||||
|
## Running them
|
||||||
|
|
||||||
|
The TypeScript implementation: `node conformance/run.mjs` (against a NATS server; see the file).
|
||||||
|
The Go implementations read this directory by sibling path, the way the lab finds its siblings.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
events/ one emitted event, and what it must look like on the wire
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"capability": "events",
|
||||||
|
"name": "a module emits an event",
|
||||||
|
"why": "The envelope is what two implementations can disagree about without either failing: a missing header, a header spelled differently, or a body nested where metadata belongs. None of those stop a mesh running; they stop it reacting.",
|
||||||
|
"given": {
|
||||||
|
"module": "shop",
|
||||||
|
"node": "one",
|
||||||
|
"key": "order.placed",
|
||||||
|
"body": { "id": "a1", "total": 12 },
|
||||||
|
"headers": {
|
||||||
|
"x-event-id": "0123456789abcdef0123456789abcdef",
|
||||||
|
"x-source": "shop",
|
||||||
|
"x-node": "one",
|
||||||
|
"x-time": "2026-09-26T12:00:00Z",
|
||||||
|
"content-type": "application/json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wire": {
|
||||||
|
"subject": "mesh.mod.shop.event.order.placed",
|
||||||
|
"requiredHeaders": ["x-event-id", "x-source", "x-node", "x-time", "content-type"],
|
||||||
|
"optionalHeaders": ["x-causation-id", "x-schema"],
|
||||||
|
"headerFormats": {
|
||||||
|
"x-time": "RFC3339",
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-event-id": "^[0-9a-f]{32}$"
|
||||||
|
},
|
||||||
|
"payloadIs": "the body alone, not the envelope",
|
||||||
|
"keyRecoveredFrom": "the subject, after the .event. token"
|
||||||
|
},
|
||||||
|
"refuses": [
|
||||||
|
{
|
||||||
|
"what": "an envelope nested in the payload",
|
||||||
|
"why": "an implementation that publishes the whole envelope as the body passes all of its own tests and is unreadable to every other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"what": "a missing x-event-id",
|
||||||
|
"why": "delivery is at-least-once and only the emitter can say which of two messages is a redelivery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"what": "an x-source that differs from the subject's module",
|
||||||
|
"why": "the bus enforces the namespace, so a disagreement means the envelope is lying about its origin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export interface ToolDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata that rides an event as AMQP headers (novox/hq ADR 0042). An event's identity and
|
* The metadata that rides an event as message headers (novox/hq ADR 0042). An event's identity and
|
||||||
* provenance live here, not in the body, so a consumer — or the broker, or an audit tool — reads
|
* provenance live here, not in the body, so a consumer — or the broker, or an audit tool — reads
|
||||||
* who/when/what without parsing the payload. An unknown `x-` header is ignored, not refused: an
|
* who/when/what without parsing the payload. An unknown `x-` header is ignored, not refused: an
|
||||||
* event is observed by parties that need not all understand every header.
|
* event is observed by parties that need not all understand every header.
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
// credential — just the broker's topic routing). Declared as emits/consumes on the manifest so the
|
// credential — just the broker's topic routing). Declared as emits/consumes on the manifest so the
|
||||||
// mesh knows the event graph.
|
// mesh knows the event graph.
|
||||||
//
|
//
|
||||||
// Identity and provenance ride as AMQP headers, not in the body (novox/hq ADR 0042): a consumer —
|
// Identity and provenance ride as message headers, not in the body (novox/hq ADR 0042): a consumer —
|
||||||
// or the broker, or an audit tool — reads who/when/what without parsing the payload, and the body
|
// or the broker, or an audit tool — reads who/when/what without parsing the payload, and the body
|
||||||
// is only the domain payload. This surface hides the header/exchange/queue mechanics; a module
|
// is only the domain payload. This surface hides the header/exchange/queue mechanics; a module
|
||||||
// names a type and a body and never sees the wire.
|
// names a type and a body and never sees the wire.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export interface Broker {
|
|||||||
/**
|
/**
|
||||||
* How a module obtains its broker. The hosting runtime sets this once; module code calls broker()
|
* How a module obtains its broker. The hosting runtime sets this once; module code calls broker()
|
||||||
* without knowing the concrete binding. Keeping the binding out of the sdk is deliberate — the sdk
|
* without knowing the concrete binding. Keeping the binding out of the sdk is deliberate — the sdk
|
||||||
* carries the contract, not a specific AMQP client build.
|
* carries the contract, not a specific broker client build.
|
||||||
*/
|
*/
|
||||||
let binding: (() => Broker) | undefined;
|
let binding: (() => Broker) | undefined;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user