Hosting

Testing and conformance

@aventara/testing - a fake adapter, and the protocol corpus for holding a host or a client written by you to Aventara's HTTP answers: run it in-process, serve it, or read it as plain JSON.

Read as Markdown

@aventara/testing is for people who write something that speaks Aventara's HTTP protocol or runs a framework: a host for another server, a client in another language, an adapter, or application code you want to test without a database. It depends on @aventara/core only.

Code
npm install --save-dev @aventara/testing@rc

Node ^22.23.4 || >=24.22.1. The package is ES modules, and it also loads from CommonJS (below).

ExportWhat it is
createFakeAdapterAn in-memory adapter, with transaction tracing, for code that runs an AvFramework without a database.
runAvProtocolConformance(driver)Drives the protocol corpus through one host and reports, per row, every mismatch.
AvProtocolCorpusThe corpus's server side: a corpus AvFramework you can serve through your own host or client.
AvProtocolFixturesThe corpus rows, as data.
@aventara/testing/fixtures/protocol-v1.jsonThe same corpus as plain JSON, for a client or host written in any language.

The protocol corpus

The corpus is a list of rows. Each row is an HTTP request a host receives, the answer the host must give, and what a generated client does with that answer:

  • request: method, a path relative to the entrypoint, headers and a body (what you would hand protocol.handleOperation, Custom host).
  • server: the status, the framework code (or null: no envelope at all), the exact key set of cause, each issue's code and path, how many operations the corpus adapter was handed (adapterCalls), the response headers that must be present with exactly those values, and, where bytes matter, the exact bodyBytes.
  • client: the outcome a generated client produces: "resolve", "framework-error" (with its code) or "transport-error".

A row whose client.outcome is "transport-error" describes something no host produces (a proxy's HTML page, a dropped connection); skip it when serving, as the conformance run does. For protocol version 1 the corpus has 170 rows, 143 of which a host answers. They include POST /_all (Operation Sets), its streamed answer (a row whose server.code is null, since the lines are no envelope), and cached reads as GET (Caching reads), whose request carries a query.

Check your host

runAvProtocolConformance builds the corpus AvFramework itself and drives every row a host answers through a driver you supply. A driver is a function that receives an AvFramework and returns a session: send takes one protocol request and returns the answer, and close ends the session. Each row runs against a fresh framework through a fresh session.

This driver goes straight to the protocol binding, the way a host's route handlers do. Written for your own host, send would call it over a socket instead:

Code
const { runAvProtocolConformance } = require("@aventara/testing");
const { AvProtocol } = require("@aventara/core/protocol");

const myHost = async (framework) => {
  const protocol = AvProtocol.bind(framework);
  const routes = new Map(protocol.surface().map((r) => [`${r.method} ${r.path}`, r.kind]));
  return {
    send: async (request) => {
      switch (routes.get(`${request.method} ${request.path}`)) {
        case "contract": return protocol.encodeContract(request);
        case "transactions": return protocol.handleTransaction(request);
        case "operation": return protocol.handleOperation(request);
        default: return protocol.answerAbsent(request) ?? { status: 404, headers: {}, body: "" };
      }
    },
    close: async () => {},
  };
};

runAvProtocolConformance(myHost).then((results) => {
  const bad = results.filter((r) => r.mismatches.length > 0);
  console.log(`${results.length} rows driven, ${bad.length} with mismatches`);
  for (const r of bad) console.log(r.label, r.mismatches);
});
Code
143 rows driven, 0 with mismatches

The result is one entry per driven row, in corpus order, each with its mismatches (none when the host conforms). A driver that throws is a mismatch of its row, not a failure of the run. Assert on the results with whichever test runner you use. @aventara/nest is held to this corpus on Express and Fastify in its own tests.

Serve the corpus through your own stack

To point a client you are writing at a running corpus, or to test a host with your own bench, take the corpus framework from AvProtocolCorpus and mount it with your host:

Code
import { AvProtocolCorpus } from "@aventara/testing";

const framework = await AvProtocolCorpus.framework();
// Mount `framework` under AvProtocolCorpus.entrypoint ("/api") with your host,
// then send each row's `request` and compare the answer with the row's `server`.
  • AvProtocolCorpus.framework(contractHash?) returns a fresh corpus framework, backed by an in-memory adapter whose answers every row states. Pass a row's Aventara-Contract-Hash to get the contract that row targets; a cached read's GET sends none, so pass its query's v. A few rows target a contract without transactions, the cached-read rows one with cache stages, and every other hash (a stale one included) gets the main contract.
  • AvProtocolCorpus.entrypoint is where the corpus is mounted: "/api". Every row's path is relative to it.
  • AvProtocolCorpus.clientContractHash is the main ClientContract's hash: what a client generated against the corpus sends, and what GET /_contract names.

A row's adapterCalls counts the operations the corpus adapter was handed, which only the conformance run can observe; when you serve the corpus yourself, compare status, code, headers and body.

The corpus as JSON

For a client or a host written in another language, @aventara/testing/fixtures/protocol-v1.json holds the whole corpus for protocol version 1, in the same order and with the same fields as AvProtocolFixtures. It is generated from the TypeScript corpus when the package is built.

Code
{
  "protocolVersion": 1,
  "entrypoint": "/api",
  "clientContractHash": "sha256:07b1a75...",
  "contracts": [{ "hash": "sha256:07b1a75...", "document": "{\"enums\":{},..." }, ...],
  "rows": [{ "label": "...", "request": { ... }, "server": { ... }, "client": { ... } }, ...]
}
  • contracts lists each ClientContract a row targets: its hash and the exact bytes of its GET /_contract answer (document, a string). The first is the main contract, clientContractHash; a row targets another by sending its hash (a cached read as its query's v).

  • rows are the rows described above. A real one:

    Code
    {
      "label": "transaction — committed, a create included → 200 A1009 in plan order",
      "request": {
        "method": "POST",
        "path": "/_transactions",
        "headers": { "Content-Type": "application/json", "Aventara-Protocol-Version": "1",
                     "Aventara-Contract-Hash": "sha256:07b1a75...", "Aventara-Request-Id": "corpus-tx-committed" },
        "body": { "kind": "raw", "content": "{\"operations\":[ ... ]}" }
      },
      "server": { "status": 200, "code": "A1009", "adapterCalls": 2,
                  "responseHeaders": { "Content-Type": "application/json", "Aventara-Request-Id": "corpus-tx-committed" },
                  "bodyBytes": "{\"data\":[ ... ],\"code\":\"A1009\",\"cause\":null}" },
      "client": { "outcome": "resolve" }
    }
  • One body is a recipe. The one request too large to write out (one byte over the 1 MiB request limit) has a request.body.content that is not a string but { "prefix": "...", "repeat": "x", "times": 1048567, "suffix": "..." }, standing for prefix followed by repeat repeated times times, then suffix. Every other content is the body's text.

From JavaScript, require("@aventara/testing/fixtures/protocol-v1.json"), or import it with with { type: "json" }.

From CommonJS

require("@aventara/testing") returns the same module import does, so a CommonJS test setup can use the whole kit. The package stays ES modules; Node's require() of an ES module needs Node 22.23.4 or later, which the package already requires.

Code
const t = require("@aventara/testing");
Object.keys(t).sort().join(", ");
// AvProtocolCorpus, AvProtocolFixtures, createFakeAdapter, runAvProtocolConformance

See also