@aventara/testing 0.0.0-stage → 0.1.0-pilot.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
1
+ import { createFramework, Decimal, FrameworkError } from "@aventara/core";
2
+ import { createFakeAdapter } from "../adapters/fake-adapter.js";
3
+ /**
4
+ * The Framework every driven corpus row runs against: one model, one
5
+ * configuration, and an in-memory Adapter whose answer is a pure function of
6
+ * the operation it is handed — so a row's expected bytes are fixed facts, and
7
+ * no row needs a provider (plan §13).
8
+ *
9
+ * Grows with the corpus (plan §6): each slice adds the resources its rows
10
+ * need, and moves {@link CORPUS_CLIENT_CONTRACT_HASH} with them.
11
+ */
12
+ const identityString = {
13
+ kind: "scalar",
14
+ type: { scalar: "string" },
15
+ nullable: false,
16
+ list: false,
17
+ lifecycle: [],
18
+ capabilities: { select: true, filter: ["equals"] },
19
+ };
20
+ const writableString = {
21
+ ...identityString,
22
+ capabilities: {
23
+ select: true,
24
+ filter: ["equals"],
25
+ create: [],
26
+ update: [],
27
+ },
28
+ };
29
+ /** A non-JSON-native scalar the corpus reads (family 13): selectable, never filtered. */
30
+ const wired = (scalar) => ({
31
+ kind: "scalar",
32
+ type: { scalar },
33
+ nullable: false,
34
+ list: false,
35
+ lifecycle: [],
36
+ capabilities: { select: true, create: [] },
37
+ });
38
+ /** One `find.many`: what the corpus's supporting Resources advertise. */
39
+ const findMany = { find: { many: true } };
40
+ /**
41
+ * The corpus's adapter model:
42
+ *
43
+ * - `authors` — every standard operation (family 2); its `secret` field is
44
+ * hidden at client scope (R1's client-hidden field);
45
+ * - `books` — references `authors`, and deleting an author cascades into it
46
+ * ({@link CORPUS_REFERENTIAL_ACTIONS}; a plan's `V1019`); the client scope
47
+ * adds a computed `label` to it ({@link CORPUS_CLIENT_CONFIG}, F-828);
48
+ * - `secrets` — its one operation restricted away at client scope (R1's
49
+ * client-restricted variant; M6's `operations: {}`);
50
+ * - `hiddenRes` — hidden at client scope (R1's client-hidden Resource, M7);
51
+ * - `samples` — one field of each scalar whose wire form is not its runtime
52
+ * form (`bigint`, `datetime`, `decimal`, `bytes`; family 13, P1, C1).
53
+ */
54
+ export const CORPUS_MODEL = {
55
+ transactions: "interactive",
56
+ scalars: {
57
+ string: { builtin: true },
58
+ bigint: { builtin: true },
59
+ datetime: { builtin: true },
60
+ decimal: { builtin: true },
61
+ bytes: { builtin: true },
62
+ },
63
+ enums: {},
64
+ resources: {
65
+ authors: {
66
+ fields: {
67
+ id: identityString,
68
+ name: writableString,
69
+ secret: identityString,
70
+ },
71
+ identifiers: [["id"]],
72
+ operations: {
73
+ find: { first: true, unique: true, many: true, count: true },
74
+ create: { one: true, many: true, count: true },
75
+ update: { first: true, unique: true, many: true, count: true },
76
+ delete: { first: true, unique: true, many: true, count: true },
77
+ upsert: { unique: true },
78
+ },
79
+ },
80
+ books: {
81
+ fields: { id: identityString, authorId: identityString },
82
+ identifiers: [["id"]],
83
+ operations: findMany,
84
+ },
85
+ secrets: {
86
+ fields: { id: identityString },
87
+ identifiers: [["id"]],
88
+ operations: findMany,
89
+ },
90
+ hiddenRes: {
91
+ fields: { id: identityString },
92
+ identifiers: [["id"]],
93
+ operations: findMany,
94
+ },
95
+ samples: {
96
+ fields: {
97
+ id: identityString,
98
+ count: wired("bigint"),
99
+ at: wired("datetime"),
100
+ amount: wired("decimal"),
101
+ blob: wired("bytes"),
102
+ },
103
+ identifiers: [["id"]],
104
+ operations: { find: { unique: true }, create: { one: true } },
105
+ },
106
+ },
107
+ };
108
+ /** Deleting an author cascades into its books (an edge under `books`). */
109
+ export const CORPUS_REFERENTIAL_ACTIONS = {
110
+ books: [{ field: "author", target: "authors", onDelete: "cascade" }],
111
+ };
112
+ /**
113
+ * The `where.id` a row sends to make the corpus Framework fail one way
114
+ * (families 10 and 12, "a fake adapter returning the code, labelled so"):
115
+ *
116
+ * - `A4000`, `A4001`, `A4002`, `A3000` — the client guard throws
117
+ * `FrameworkError("A4000")`, denies (`false`), throws
118
+ * `FrameworkError("A4002")`, or throws a plain `Error`;
119
+ * - `A2008`, `A2013`, `A2014` — the Adapter throws `FrameworkError` with that
120
+ * conflict code; `A3001` — it throws a plain `Error`;
121
+ * - `A3002` — a plan step carrying it runs, and the Adapter's transaction then
122
+ * fails to commit;
123
+ * - `A3003` — `samples.find.unique` answers a datetime the wire cannot spell.
124
+ */
125
+ export const CORPUS_TRIGGER = {
126
+ unauthenticated: "A4000",
127
+ guardDenies: "A4001",
128
+ policyDenies: "A4002",
129
+ guardThrows: "A3000",
130
+ conflict: "A2008",
131
+ concurrentModification: "A2013",
132
+ staleSelection: "A2014",
133
+ adapterThrows: "A3001",
134
+ commitFails: "A3002",
135
+ unencodable: "A3003",
136
+ };
137
+ /** The `where.id` an operation's arguments name, when it names one as a string. */
138
+ function whereIdOf(args) {
139
+ const where = typeof args === "object" && args !== null
140
+ ? args.where
141
+ : undefined;
142
+ const id = typeof where === "object" && where !== null
143
+ ? where.id
144
+ : undefined;
145
+ return typeof id === "string" ? id : undefined;
146
+ }
147
+ /** The corpus's one client guard: it decides nothing unless a row names a trigger. */
148
+ function corpusGuard(context) {
149
+ switch (whereIdOf(context.args)) {
150
+ case CORPUS_TRIGGER.unauthenticated:
151
+ throw new FrameworkError("A4000", "Authentication is required.");
152
+ case CORPUS_TRIGGER.guardDenies:
153
+ return false;
154
+ case CORPUS_TRIGGER.policyDenies:
155
+ throw new FrameworkError("A4002", "Denied by the corpus policy.");
156
+ case CORPUS_TRIGGER.guardThrows:
157
+ throw new Error("the corpus guard failed");
158
+ default:
159
+ return true;
160
+ }
161
+ }
162
+ /**
163
+ * The corpus's client-scope configuration: what the ClientContract hides or
164
+ * restricts, and `books.label` — a VIRTUAL field added at client scope and
165
+ * computed on read from `id` (F-828, plan S12).
166
+ */
167
+ export const CORPUS_CLIENT_CONFIG = {
168
+ fields: {
169
+ books: {
170
+ label: {
171
+ type: { scalar: "string" },
172
+ nullable: false,
173
+ list: false,
174
+ lifecycle: ["VIRTUAL"],
175
+ },
176
+ },
177
+ },
178
+ behaviors: {
179
+ books: {
180
+ fields: {
181
+ label: {
182
+ read: {
183
+ dependsOn: ["id"],
184
+ compute: ({ record, }) => `book:${String(record.id)}`,
185
+ },
186
+ },
187
+ },
188
+ },
189
+ },
190
+ pipelines: { guards: [corpusGuard] },
191
+ restrictions: {
192
+ authors: { fields: { secret: { hidden: true } } },
193
+ secrets: { operations: { find: { many: false } } },
194
+ hiddenRes: { hidden: true },
195
+ },
196
+ };
197
+ /** The corpus Framework's mount point; every row's path is relative to it. */
198
+ export const CORPUS_ENTRYPOINT = "/api";
199
+ /**
200
+ * The corpus ClientContract's hash: what a generated client for it sends as
201
+ * `Aventara-Contract-Hash`. A literal, as a generated client holds it;
202
+ * `corpus-contract.spec.ts` fails, naming this constant, when the compiled
203
+ * contract moves.
204
+ */
205
+ export const CORPUS_CLIENT_CONTRACT_HASH = "sha256:07b1a75a6e8e88e30115ae81bcf7e3a12fa493bddce995a01867e2381bd158d0";
206
+ /**
207
+ * The corpus ClientContract as `GET /_contract` serves it: its JCS-canonical
208
+ * bytes (Q8), `protocol.hash` included (M22). A literal, as a host must
209
+ * serve it byte for byte; `corpus-contract.spec.ts` fails, naming this
210
+ * constant, when the compiled contract moves.
211
+ */
212
+ export const CORPUS_CLIENT_CONTRACT_DOCUMENT = '{"enums":{},"limits":{"maxBooleanNodes":50,"maxListLimit":250,"maxNestingDepth":12,"maxRequestBytes":1048576,"maxTransactionOperations":20},"protocol":{"hash":"sha256:07b1a75a6e8e88e30115ae81bcf7e3a12fa493bddce995a01867e2381bd158d0","version":1},"resources":{"authors":{"fields":{"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}},"name":{"capabilities":{"create":[],"filter":["equals"],"select":true,"update":[]},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{"create":{"count":true,"many":true,"one":true},"delete":{"count":true,"first":true,"many":true,"unique":true},"find":{"count":true,"first":true,"many":true,"unique":true},"update":{"count":true,"first":true,"many":true,"unique":true},"upsert":{"unique":true}}},"books":{"fields":{"authorId":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}},"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}},"label":{"capabilities":{"select":true},"kind":"scalar","lifecycle":["COMPUTED_ON_READ","VIRTUAL"],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{"find":{"many":true}}},"samples":{"fields":{"amount":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"decimal"}},"at":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"datetime"}},"blob":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"bytes"}},"count":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"bigint"}},"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{"create":{"one":true},"find":{"unique":true}}},"secrets":{"fields":{"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{}}},"scalars":{"bigint":{"builtin":true},"bytes":{"builtin":true},"datetime":{"builtin":true},"decimal":{"builtin":true},"string":{"builtin":true}},"transactions":"interactive"}';
213
+ /**
214
+ * The hash of the corpus's SECOND ClientContract — the same model and
215
+ * configuration with `transactions: "none"` (family 6's `/_transactions`
216
+ * absence). A row targets it by sending this hash, exactly as a client
217
+ * generated against it would; the runner gives the row the Framework whose
218
+ * ClientContract it names (the director, 2026-10-05). A literal, held to the
219
+ * compiled value by `corpus-contract.spec.ts`.
220
+ */
221
+ export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:d333e8396b9bf9127e0859ed69b9874f230058a597e2129e6b9e9b380daef618";
222
+ /** The one record the corpus Adapter knows. */
223
+ export const CORPUS_AUTHOR = { id: "a1", name: "Ada" };
224
+ /** The `where.id` that matches nothing. */
225
+ export const CORPUS_NOBODY = "nobody";
226
+ /**
227
+ * The one `samples` record, in runtime forms — what an Adapter hands core,
228
+ * and what the HTTP encoders must spell in wire forms (P1).
229
+ */
230
+ export const CORPUS_SAMPLE = {
231
+ id: "s1",
232
+ count: 9007199254740993n,
233
+ at: new Date("2026-10-05T12:34:56.789Z"),
234
+ amount: new Decimal("12.50"),
235
+ blob: new Uint8Array([255, 0, 16]),
236
+ };
237
+ /** The conflict codes the corpus Adapter raises, and the text it raises them with. */
238
+ const CONFLICTS = {
239
+ [CORPUS_TRIGGER.conflict]: "A2008",
240
+ [CORPUS_TRIGGER.concurrentModification]: "A2013",
241
+ [CORPUS_TRIGGER.staleSelection]: "A2014",
242
+ };
243
+ /** The one record of each supporting Resource. */
244
+ const SUPPORTING_RECORDS = {
245
+ books: { id: "b1", authorId: CORPUS_AUTHOR.id },
246
+ secrets: { id: "s1" },
247
+ hiddenRes: { id: "h1" },
248
+ };
249
+ /**
250
+ * What the corpus Adapter answers an operation. A trigger `where.id` fails it
251
+ * ({@link CORPUS_TRIGGER}). Otherwise: for {@link CORPUS_NOBODY}, nothing — `0`
252
+ * for a count, `[]` for a many, `null` for a single record; else `2` for a
253
+ * count, the Resource's one record in a list for a many, and the one record
254
+ * for a single-record operation (`samples`: {@link CORPUS_SAMPLE}).
255
+ */
256
+ function corpusAnswer(operation) {
257
+ const id = whereIdOf(operation.arguments);
258
+ const conflict = id === undefined ? undefined : CONFLICTS[id];
259
+ if (conflict !== undefined) {
260
+ throw new FrameworkError(conflict, `The corpus Adapter answered ${conflict}.`);
261
+ }
262
+ if (id === CORPUS_TRIGGER.adapterThrows) {
263
+ throw new Error("the corpus Adapter failed");
264
+ }
265
+ const nothing = id === CORPUS_NOBODY;
266
+ if (operation.variant === "count") {
267
+ return nothing ? 0 : 2;
268
+ }
269
+ if (operation.variant === "many") {
270
+ return nothing
271
+ ? []
272
+ : [SUPPORTING_RECORDS[operation.resource] ?? CORPUS_AUTHOR];
273
+ }
274
+ if (nothing) {
275
+ return null;
276
+ }
277
+ if (operation.resource === "samples") {
278
+ return id === CORPUS_TRIGGER.unencodable
279
+ ? { ...CORPUS_SAMPLE, at: new Date("+010000-01-01T00:00:00.000Z") }
280
+ : CORPUS_SAMPLE;
281
+ }
282
+ return CORPUS_AUTHOR;
283
+ }
284
+ /**
285
+ * The fake Adapter's transaction, made to fail its commit when a step of the
286
+ * plan named {@link CORPUS_TRIGGER.commitFails} — an infrastructure failure
287
+ * after the work ran (`A3002`).
288
+ */
289
+ function failingCommit(transaction) {
290
+ return async (work) => {
291
+ let doomed = false;
292
+ const result = await transaction((scoped) => work({
293
+ execute: (operation) => {
294
+ if (whereIdOf(operation.arguments) === CORPUS_TRIGGER.commitFails) {
295
+ doomed = true;
296
+ }
297
+ return scoped.execute(operation);
298
+ },
299
+ }));
300
+ if (doomed) {
301
+ throw new Error("the corpus Adapter failed to commit");
302
+ }
303
+ return result;
304
+ };
305
+ }
306
+ /**
307
+ * A fresh corpus Framework, and the count of operations its Adapter has been
308
+ * handed so far — a row's `adapterCalls` is read from it. `contractHash`
309
+ * selects which corpus ClientContract it serves: the `transactions: "none"`
310
+ * one when it is {@link CORPUS_NONE_CLIENT_CONTRACT_HASH}, the main one
311
+ * otherwise (a stale or absent hash included).
312
+ */
313
+ export async function createCorpusFramework(contractHash) {
314
+ // Over the widened model type: which model is discovered is a runtime fact,
315
+ // and a literal-typed Framework costs type-checking the corpus never uses.
316
+ const fake = createFakeAdapter({
317
+ model: contractHash === CORPUS_NONE_CLIENT_CONTRACT_HASH
318
+ ? { ...CORPUS_MODEL, transactions: "none" }
319
+ : CORPUS_MODEL,
320
+ execute: corpusAnswer,
321
+ });
322
+ const adapter = Object.assign(fake, {
323
+ referentialActions: CORPUS_REFERENTIAL_ACTIONS,
324
+ ...(fake.transaction === undefined
325
+ ? {}
326
+ : { transaction: failingCommit(fake.transaction) }),
327
+ });
328
+ const framework = await createFramework({
329
+ entrypoint: CORPUS_ENTRYPOINT,
330
+ adapter,
331
+ client: CORPUS_CLIENT_CONFIG,
332
+ });
333
+ return { framework, adapterCalls: () => adapter.operations.length };
334
+ }
@@ -0,0 +1,11 @@
1
+ import type { AvProtocolDriver } from "./protocol-driver.js";
2
+ /**
3
+ * Driver 1 (plan S8, S9): the protocol hosted in process, with no HTTP stack —
4
+ * a catch-all host (Q12) that hands `/_contract` to `protocol.encodeContract`,
5
+ * `/_transactions` to the bound shortcut `protocol.handleTransaction`, every
6
+ * other request to `protocol.handleOperation`, and writes back exactly what
7
+ * they answer. What
8
+ * it proves is the framework's own answer, host-free; driver 2 (S13) must
9
+ * answer every row identically through a real server.
10
+ */
11
+ export declare const inProcessDriver: AvProtocolDriver;
@@ -0,0 +1,26 @@
1
+ import { AvProtocol } from "@aventara/core/protocol";
2
+ /**
3
+ * Driver 1 (plan S8, S9): the protocol hosted in process, with no HTTP stack —
4
+ * a catch-all host (Q12) that hands `/_contract` to `protocol.encodeContract`,
5
+ * `/_transactions` to the bound shortcut `protocol.handleTransaction`, every
6
+ * other request to `protocol.handleOperation`, and writes back exactly what
7
+ * they answer. What
8
+ * it proves is the framework's own answer, host-free; driver 2 (S13) must
9
+ * answer every row identically through a real server.
10
+ */
11
+ export const inProcessDriver = async (framework) => {
12
+ const protocol = AvProtocol.bind(framework);
13
+ return {
14
+ send: async (request) => {
15
+ switch (request.path) {
16
+ case "/_contract":
17
+ return protocol.encodeContract(request);
18
+ case "/_transactions":
19
+ return protocol.handleTransaction(request);
20
+ default:
21
+ return protocol.handleOperation(request);
22
+ }
23
+ },
24
+ close: async () => undefined,
25
+ };
26
+ };
@@ -0,0 +1,12 @@
1
+ import type { AvProtocolDriver } from "./protocol-driver.js";
2
+ /**
3
+ * The per-route driver (plan S10): the protocol hosted in process as a host
4
+ * that mounts ONE route per `protocol.surface()` entry — each operation on
5
+ * `handleOperation`, `/_transactions` on `handleTransaction`, `/_contract` on
6
+ * `encodeContract`, matched by method and path — and `protocol.answerAbsent`
7
+ * as the fallback for everything it did not mount (Q12's first style).
8
+ * `undefined` from the fallback is a path outside the protocol, which this
9
+ * host answers with its own bodiless 404. Every corpus row must answer here
10
+ * exactly as through driver 1's catch-all.
11
+ */
12
+ export declare const perRouteDriver: AvProtocolDriver;
@@ -0,0 +1,36 @@
1
+ import { AvProtocol } from "@aventara/core/protocol";
2
+ /** What this host answers a path the protocol says is the host's: its own bodiless 404. */
3
+ const HOST_NOT_FOUND = Object.freeze({
4
+ status: 404,
5
+ headers: Object.freeze({}),
6
+ body: "",
7
+ });
8
+ /**
9
+ * The per-route driver (plan S10): the protocol hosted in process as a host
10
+ * that mounts ONE route per `protocol.surface()` entry — each operation on
11
+ * `handleOperation`, `/_transactions` on `handleTransaction`, `/_contract` on
12
+ * `encodeContract`, matched by method and path — and `protocol.answerAbsent`
13
+ * as the fallback for everything it did not mount (Q12's first style).
14
+ * `undefined` from the fallback is a path outside the protocol, which this
15
+ * host answers with its own bodiless 404. Every corpus row must answer here
16
+ * exactly as through driver 1's catch-all.
17
+ */
18
+ export const perRouteDriver = async (framework) => {
19
+ const protocol = AvProtocol.bind(framework);
20
+ const mounted = new Map(protocol.surface().map((route) => [`${route.method} ${route.path}`, route]));
21
+ return {
22
+ send: async (request) => {
23
+ switch (mounted.get(`${request.method} ${request.path}`)?.kind) {
24
+ case "operation":
25
+ return protocol.handleOperation(request);
26
+ case "transactions":
27
+ return protocol.handleTransaction(request);
28
+ case "contract":
29
+ return protocol.encodeContract(request);
30
+ default:
31
+ return protocol.answerAbsent(request) ?? HOST_NOT_FOUND;
32
+ }
33
+ },
34
+ close: async () => undefined,
35
+ };
36
+ };
@@ -0,0 +1,32 @@
1
+ import type { AvProtocolDriver } from "./protocol-driver.js";
2
+ /**
3
+ * `runAvProtocolConformance(driver)` — the corpus, driven through one host.
4
+ *
5
+ * Every row a host answers is driven — the rows whose `client.outcome` is
6
+ * `"transport-error"` describe what a client receives instead of a framework
7
+ * answer, so they are skipped (the director, 2026-10-05). Each driven row runs
8
+ * against a fresh corpus Framework, through a fresh driver session, with one
9
+ * request — the corpus Framework whose ClientContract hash the row's
10
+ * `Aventara-Contract-Hash` names, as a generated client names it (the main
11
+ * corpus contract for any other hash; the director, 2026-10-05) — and its
12
+ * answer is held to the row's `server` block:
13
+ *
14
+ * - `status`, exactly;
15
+ * - `code`: the body is a framework envelope (`AvProtocol.isOperationResponse`,
16
+ * the one envelope test) carrying that code — or, for `null`, is none;
17
+ * - `causeKeys`: the exact sorted key set of `cause`;
18
+ * - `issues`: each issue's `code` and `path`, in order;
19
+ * - `adapterCalls`: the operations the corpus Adapter was handed, exactly;
20
+ * - `responseHeaders`: each named header present (names matched without
21
+ * regard to case) with exactly that value — a host may add its own;
22
+ * - `bodyBytes`: the body text, exactly.
23
+ *
24
+ * Resolves one result per driven row, in corpus order, each with the
25
+ * mismatches found — none when the host conforms. A driver that throws is a
26
+ * mismatch of its row, not a rejection of the run. Test-runner free: a
27
+ * caller asserts on the results with whatever runner it uses.
28
+ */
29
+ export declare function runAvProtocolConformance(driver: AvProtocolDriver): Promise<readonly {
30
+ readonly label: string;
31
+ readonly mismatches: readonly string[];
32
+ }[]>;
@@ -0,0 +1,112 @@
1
+ import { AvProtocol } from "@aventara/core/protocol";
2
+ import { createCorpusFramework } from "./corpus-contract.fixture.js";
3
+ import { AvProtocolFixtures } from "./protocol-fixtures.js";
4
+ /**
5
+ * `runAvProtocolConformance(driver)` — the corpus, driven through one host.
6
+ *
7
+ * Every row a host answers is driven — the rows whose `client.outcome` is
8
+ * `"transport-error"` describe what a client receives instead of a framework
9
+ * answer, so they are skipped (the director, 2026-10-05). Each driven row runs
10
+ * against a fresh corpus Framework, through a fresh driver session, with one
11
+ * request — the corpus Framework whose ClientContract hash the row's
12
+ * `Aventara-Contract-Hash` names, as a generated client names it (the main
13
+ * corpus contract for any other hash; the director, 2026-10-05) — and its
14
+ * answer is held to the row's `server` block:
15
+ *
16
+ * - `status`, exactly;
17
+ * - `code`: the body is a framework envelope (`AvProtocol.isOperationResponse`,
18
+ * the one envelope test) carrying that code — or, for `null`, is none;
19
+ * - `causeKeys`: the exact sorted key set of `cause`;
20
+ * - `issues`: each issue's `code` and `path`, in order;
21
+ * - `adapterCalls`: the operations the corpus Adapter was handed, exactly;
22
+ * - `responseHeaders`: each named header present (names matched without
23
+ * regard to case) with exactly that value — a host may add its own;
24
+ * - `bodyBytes`: the body text, exactly.
25
+ *
26
+ * Resolves one result per driven row, in corpus order, each with the
27
+ * mismatches found — none when the host conforms. A driver that throws is a
28
+ * mismatch of its row, not a rejection of the run. Test-runner free: a
29
+ * caller asserts on the results with whatever runner it uses.
30
+ */
31
+ export async function runAvProtocolConformance(driver) {
32
+ const results = [];
33
+ for (const row of AvProtocolFixtures) {
34
+ if (row.client.outcome === "transport-error") {
35
+ continue;
36
+ }
37
+ const corpus = await createCorpusFramework(contractHashOf(row.request));
38
+ let response;
39
+ try {
40
+ const session = await driver(corpus.framework);
41
+ try {
42
+ response = await session.send(row.request);
43
+ }
44
+ finally {
45
+ await session.close();
46
+ }
47
+ }
48
+ catch (error) {
49
+ results.push({
50
+ label: row.label,
51
+ mismatches: [`driver threw: ${error}`],
52
+ });
53
+ continue;
54
+ }
55
+ results.push({
56
+ label: row.label,
57
+ mismatches: mismatchesOf(row.server, response, corpus.adapterCalls()),
58
+ });
59
+ }
60
+ return results;
61
+ }
62
+ /** Every way `response` departs from what the row's server block states. */
63
+ function mismatchesOf(server, response, adapterCalls) {
64
+ const mismatches = [];
65
+ const expect = (what, actual, expected) => {
66
+ const [a, e] = [JSON.stringify(actual), JSON.stringify(expected)];
67
+ if (a !== e) {
68
+ mismatches.push(`${what}: expected ${e}, received ${a}`);
69
+ }
70
+ };
71
+ expect("status", response.status, server.status);
72
+ expect("adapter calls", adapterCalls, server.adapterCalls);
73
+ const body = parsedBody(response.body);
74
+ if (server.code === null) {
75
+ expect("a framework envelope", AvProtocol.isOperationResponse(body), false);
76
+ }
77
+ else if (AvProtocol.isOperationResponse(body)) {
78
+ expect("code", body.code, server.code);
79
+ const cause = body.cause;
80
+ if (server.causeKeys !== undefined) {
81
+ expect("cause keys", Object.keys(cause ?? {}).sort(), [...server.causeKeys].sort());
82
+ }
83
+ if (server.issues !== undefined) {
84
+ expect("issues", (cause?.issues ?? []).map(({ code, path }) => path === undefined ? { code } : { code, path }), server.issues);
85
+ }
86
+ }
87
+ else {
88
+ mismatches.push(`a framework envelope carrying ${server.code}: none`);
89
+ }
90
+ for (const [name, value] of Object.entries(server.responseHeaders ?? {})) {
91
+ const received = Object.entries(response.headers).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase());
92
+ expect(`header ${name}`, received?.[1], value);
93
+ }
94
+ if (server.bodyBytes !== undefined) {
95
+ expect("body", response.body, server.bodyBytes);
96
+ }
97
+ return mismatches;
98
+ }
99
+ /** The row's `Aventara-Contract-Hash`, its name matched without regard to case. */
100
+ function contractHashOf(request) {
101
+ const value = Object.entries(request.headers).find(([name]) => name.toLowerCase() === "aventara-contract-hash")?.[1];
102
+ return typeof value === "string" ? value : undefined;
103
+ }
104
+ /** The parsed body, or `undefined` — which no envelope is — for text that is not JSON. */
105
+ function parsedBody(text) {
106
+ try {
107
+ return JSON.parse(text);
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
@@ -0,0 +1,18 @@
1
+ import type { Framework } from "@aventara/core";
2
+ import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
3
+ /**
4
+ * One host under test, as the protocol conformance run drives it
5
+ * (`runAvProtocolConformance`): given the corpus's Framework, mount the
6
+ * protocol the way that host does, then answer each corpus request as that
7
+ * host would answer it over HTTP — status, headers and body text, as an
8
+ * `AvProtocolResponse`.
9
+ *
10
+ * Driver 1 (`in-process.driver.ts`) calls the bound protocol's shortcuts
11
+ * directly; a real host (a `node:http` server, Phase 11's Nest module) sends
12
+ * the request over its own stack and reads back what arrived. The run calls
13
+ * `close` once per row, after its one request.
14
+ */
15
+ export type AvProtocolDriver = (framework: Framework) => Promise<{
16
+ readonly send: (request: AvProtocolRequest) => Promise<AvProtocolResponse>;
17
+ readonly close: () => Promise<void>;
18
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import type { OperationCode, ValidationCode } from "@aventara/core";
2
+ import type { AvProtocolRequest } from "@aventara/core/protocol";
3
+ /**
4
+ * One row of the Phase 10 protocol fixture corpus: the HTTP request a host
5
+ * receives (`request`), what the host must answer for it (`server`), and what
6
+ * a generated client must do with that answer (`client`). Types only; the
7
+ * rows live in `protocol-fixtures.ts`.
8
+ *
9
+ * `request` is core's own request value, `AvProtocolRequest` from
10
+ * `@aventara/core/protocol` (Q9), so a row is exactly what a host hands the
11
+ * bound protocol: no second request shape exists for the corpus to drift from.
12
+ */
13
+ export type AvProtocolFixture = {
14
+ /**
15
+ * Stable across slices; the coverage gate reads it. A row that covers one
16
+ * of the specification's success rows begins with that row's operation
17
+ * name (`find.many …`, `create.one …`) or with `transaction` for the
18
+ * committed-plan row.
19
+ */
20
+ readonly label: string;
21
+ /** Method, entrypoint-relative path, headers, and a raw or parsed body (Q11). */
22
+ readonly request: AvProtocolRequest;
23
+ readonly server: {
24
+ /**
25
+ * The HTTP status. `0` only on a transport-error row (family 11) where no
26
+ * HTTP response arrived at all — the Fetch API's network-error status.
27
+ */
28
+ readonly status: number;
29
+ /** `null`: the host produces no framework envelope at all. */
30
+ readonly code: OperationCode | null;
31
+ /** The exact, sorted key set of `cause`. */
32
+ readonly causeKeys?: readonly string[];
33
+ readonly issues?: readonly {
34
+ readonly code: ValidationCode;
35
+ readonly path?: readonly (string | number)[];
36
+ }[];
37
+ /** Always stated: `0` is an assertion, not a default. */
38
+ readonly adapterCalls: number;
39
+ readonly responseHeaders?: Readonly<Record<string, string>>;
40
+ /** The exact body, where bytes matter. */
41
+ readonly bodyBytes?: string;
42
+ };
43
+ readonly client: {
44
+ /**
45
+ * What a generated client does with the answer. `"transport-error"`
46
+ * also marks a row no host produces (family 11): a host driver filters
47
+ * on `client.outcome === "transport-error"` and does not drive those rows.
48
+ */
49
+ readonly outcome: "resolve" | "framework-error" | "transport-error";
50
+ /** Present iff `outcome` is `"framework-error"`. */
51
+ readonly code?: OperationCode;
52
+ };
53
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { AvProtocolFixture } from "./protocol-fixture.js";
2
+ export declare const AvProtocolFixtures: readonly AvProtocolFixture[];