@aventara/testing 0.1.0-pilot.0 → 0.1.0-pilot.2
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.
- package/dist/adapters/fake-adapter.js +0 -8
- package/dist/protocol/corpus-contract.fixture.d.ts +27 -21
- package/dist/protocol/corpus-contract.fixture.js +9 -97
- package/dist/protocol/in-process.driver.d.ts +6 -7
- package/dist/protocol/in-process.driver.js +0 -9
- package/dist/protocol/per-route.driver.d.ts +7 -8
- package/dist/protocol/per-route.driver.js +0 -11
- package/dist/protocol/protocol-conformance.d.ts +11 -12
- package/dist/protocol/protocol-conformance.js +0 -30
- package/dist/protocol/protocol-driver.d.ts +6 -8
- package/dist/protocol/protocol-fixture.d.ts +6 -10
- package/dist/protocol/protocol-fixtures.js +24 -196
- package/package.json +3 -3
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Takes the caller's snapshot at the moment the boundary opens and returns the
|
|
3
|
-
* single restore action that undoes everything written after it. Returning a
|
|
4
|
-
* closure keeps the snapshot value's type local to the hook that owns it.
|
|
5
|
-
*/
|
|
6
1
|
function beginRollback(rollback) {
|
|
7
2
|
if (rollback === undefined) {
|
|
8
3
|
return undefined;
|
|
@@ -12,7 +7,6 @@ function beginRollback(rollback) {
|
|
|
12
7
|
rollback.restore(snapshot);
|
|
13
8
|
};
|
|
14
9
|
}
|
|
15
|
-
/** Reusable provider-free Adapter implementation for framework/conformance tests. */
|
|
16
10
|
export function createFakeAdapter(options) {
|
|
17
11
|
const operations = [];
|
|
18
12
|
const transactions = [];
|
|
@@ -54,8 +48,6 @@ export function createFakeAdapter(options) {
|
|
|
54
48
|
catch (error) {
|
|
55
49
|
restore?.();
|
|
56
50
|
settle(scoped, "rolled-back");
|
|
57
|
-
// By identity: the transaction runtime recognises its own abort
|
|
58
|
-
// sentinel by reference, never by shape.
|
|
59
51
|
throw error;
|
|
60
52
|
}
|
|
61
53
|
settle(scoped, "committed");
|
|
@@ -3,16 +3,14 @@ import { Decimal } from "@aventara/core";
|
|
|
3
3
|
/**
|
|
4
4
|
* The corpus's adapter model:
|
|
5
5
|
*
|
|
6
|
-
* - `authors` — every standard operation (family 2); its `secret` field is
|
|
7
|
-
*
|
|
6
|
+
* - `authors` — every standard operation (family 2); its `secret` field is hidden
|
|
7
|
+
* at client scope;
|
|
8
8
|
* - `books` — references `authors`, and deleting an author cascades into it
|
|
9
|
-
* ({@link CORPUS_REFERENTIAL_ACTIONS}; a plan's `V1019`); the client scope
|
|
10
|
-
*
|
|
11
|
-
* - `secrets` — its one operation restricted away at client scope
|
|
12
|
-
*
|
|
13
|
-
* - `
|
|
14
|
-
* - `samples` — one field of each scalar whose wire form is not its runtime
|
|
15
|
-
* form (`bigint`, `datetime`, `decimal`, `bytes`; family 13, P1, C1).
|
|
9
|
+
* ({@link CORPUS_REFERENTIAL_ACTIONS}; a plan's `V1019`); the client scope adds
|
|
10
|
+
* a computed `label` to it;
|
|
11
|
+
* - `secrets` — its one operation restricted away at client scope;
|
|
12
|
+
* - `hiddenRes` — hidden at client scope;
|
|
13
|
+
* - `samples` — one field of each scalar whose wire form is not its runtime form.
|
|
16
14
|
*/
|
|
17
15
|
export declare const CORPUS_MODEL: {
|
|
18
16
|
readonly transactions: "interactive";
|
|
@@ -290,7 +288,9 @@ export declare const CORPUS_REFERENTIAL_ACTIONS: {
|
|
|
290
288
|
* conflict code; `A3001` — it throws a plain `Error`;
|
|
291
289
|
* - `A3002` — a plan step carrying it runs, and the Adapter's transaction then
|
|
292
290
|
* fails to commit;
|
|
293
|
-
* - `A3003` — `samples.find.unique` answers a datetime the wire cannot spell
|
|
291
|
+
* - `A3003` — `samples.find.unique` answers a datetime the wire cannot spell;
|
|
292
|
+
* - `A3004` — the client pipe rewrites the arguments into ones the server
|
|
293
|
+
* contract refuses (a filter on a field `authors` does not have).
|
|
294
294
|
*/
|
|
295
295
|
export declare const CORPUS_TRIGGER: {
|
|
296
296
|
readonly unauthenticated: "A4000";
|
|
@@ -303,13 +303,19 @@ export declare const CORPUS_TRIGGER: {
|
|
|
303
303
|
readonly adapterThrows: "A3001";
|
|
304
304
|
readonly commitFails: "A3002";
|
|
305
305
|
readonly unencodable: "A3003";
|
|
306
|
+
readonly pipeOutputInvalid: "A3004";
|
|
306
307
|
};
|
|
307
308
|
/** The corpus's one client guard: it decides nothing unless a row names a trigger. */
|
|
308
309
|
declare function corpusGuard(context: PipelineContext): boolean;
|
|
310
|
+
/**
|
|
311
|
+
* The corpus's one client pipe: it rewrites nothing unless a row names the
|
|
312
|
+
* `A3004` trigger, and then adds a filter on a field the server does not have.
|
|
313
|
+
*/
|
|
314
|
+
declare function corpusPipe(args: unknown): unknown;
|
|
309
315
|
/**
|
|
310
316
|
* The corpus's client-scope configuration: what the ClientContract hides or
|
|
311
317
|
* restricts, and `books.label` — a VIRTUAL field added at client scope and
|
|
312
|
-
* computed on read from `id
|
|
318
|
+
* computed on read from `id`.
|
|
313
319
|
*/
|
|
314
320
|
export declare const CORPUS_CLIENT_CONFIG: {
|
|
315
321
|
readonly fields: {
|
|
@@ -340,6 +346,7 @@ export declare const CORPUS_CLIENT_CONFIG: {
|
|
|
340
346
|
};
|
|
341
347
|
readonly pipelines: {
|
|
342
348
|
readonly guards: readonly [typeof corpusGuard];
|
|
349
|
+
readonly pipes: readonly [typeof corpusPipe];
|
|
343
350
|
};
|
|
344
351
|
readonly restrictions: {
|
|
345
352
|
readonly authors: {
|
|
@@ -372,18 +379,17 @@ export declare const CORPUS_ENTRYPOINT = "/api";
|
|
|
372
379
|
export declare const CORPUS_CLIENT_CONTRACT_HASH = "sha256:07b1a75a6e8e88e30115ae81bcf7e3a12fa493bddce995a01867e2381bd158d0";
|
|
373
380
|
/**
|
|
374
381
|
* The corpus ClientContract as `GET /_contract` serves it: its JCS-canonical
|
|
375
|
-
* bytes
|
|
376
|
-
*
|
|
377
|
-
*
|
|
382
|
+
* bytes, `protocol.hash` included. A literal, as a host must serve it byte for
|
|
383
|
+
* byte; `corpus-contract.spec.ts` fails, naming this constant, when the compiled
|
|
384
|
+
* contract moves.
|
|
378
385
|
*/
|
|
379
386
|
export declare 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\"}";
|
|
380
387
|
/**
|
|
381
388
|
* The hash of the corpus's SECOND ClientContract — the same model and
|
|
382
|
-
* configuration with `transactions: "none"` (family 6's `/_transactions`
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* compiled value by `corpus-contract.spec.ts`.
|
|
389
|
+
* configuration with `transactions: "none"` (family 6's `/_transactions` absence).
|
|
390
|
+
* A row targets it by sending this hash, exactly as a client generated against it
|
|
391
|
+
* would; the runner gives the row the Framework whose ClientContract it names. A
|
|
392
|
+
* literal, held to the compiled value by `corpus-contract.spec.ts`.
|
|
387
393
|
*/
|
|
388
394
|
export declare const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:d333e8396b9bf9127e0859ed69b9874f230058a597e2129e6b9e9b380daef618";
|
|
389
395
|
/** The one record the corpus Adapter knows. */
|
|
@@ -394,8 +400,8 @@ export declare const CORPUS_AUTHOR: {
|
|
|
394
400
|
/** The `where.id` that matches nothing. */
|
|
395
401
|
export declare const CORPUS_NOBODY = "nobody";
|
|
396
402
|
/**
|
|
397
|
-
* The one `samples` record, in runtime forms — what an Adapter hands core,
|
|
398
|
-
*
|
|
403
|
+
* The one `samples` record, in runtime forms — what an Adapter hands core, and
|
|
404
|
+
* what the HTTP encoders must spell in wire forms.
|
|
399
405
|
*/
|
|
400
406
|
export declare const CORPUS_SAMPLE: {
|
|
401
407
|
readonly id: "s1";
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { createFramework, Decimal, FrameworkError } from "@aventara/core";
|
|
2
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
3
|
const identityString = {
|
|
13
4
|
kind: "scalar",
|
|
14
5
|
type: { scalar: "string" },
|
|
@@ -26,7 +17,6 @@ const writableString = {
|
|
|
26
17
|
update: [],
|
|
27
18
|
},
|
|
28
19
|
};
|
|
29
|
-
/** A non-JSON-native scalar the corpus reads (family 13): selectable, never filtered. */
|
|
30
20
|
const wired = (scalar) => ({
|
|
31
21
|
kind: "scalar",
|
|
32
22
|
type: { scalar },
|
|
@@ -35,22 +25,7 @@ const wired = (scalar) => ({
|
|
|
35
25
|
lifecycle: [],
|
|
36
26
|
capabilities: { select: true, create: [] },
|
|
37
27
|
});
|
|
38
|
-
/** One `find.many`: what the corpus's supporting Resources advertise. */
|
|
39
28
|
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
29
|
export const CORPUS_MODEL = {
|
|
55
30
|
transactions: "interactive",
|
|
56
31
|
scalars: {
|
|
@@ -105,23 +80,9 @@ export const CORPUS_MODEL = {
|
|
|
105
80
|
},
|
|
106
81
|
},
|
|
107
82
|
};
|
|
108
|
-
/** Deleting an author cascades into its books (an edge under `books`). */
|
|
109
83
|
export const CORPUS_REFERENTIAL_ACTIONS = {
|
|
110
84
|
books: [{ field: "author", target: "authors", onDelete: "cascade" }],
|
|
111
85
|
};
|
|
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
86
|
export const CORPUS_TRIGGER = {
|
|
126
87
|
unauthenticated: "A4000",
|
|
127
88
|
guardDenies: "A4001",
|
|
@@ -133,8 +94,8 @@ export const CORPUS_TRIGGER = {
|
|
|
133
94
|
adapterThrows: "A3001",
|
|
134
95
|
commitFails: "A3002",
|
|
135
96
|
unencodable: "A3003",
|
|
97
|
+
pipeOutputInvalid: "A3004",
|
|
136
98
|
};
|
|
137
|
-
/** The `where.id` an operation's arguments name, when it names one as a string. */
|
|
138
99
|
function whereIdOf(args) {
|
|
139
100
|
const where = typeof args === "object" && args !== null
|
|
140
101
|
? args.where
|
|
@@ -144,7 +105,6 @@ function whereIdOf(args) {
|
|
|
144
105
|
: undefined;
|
|
145
106
|
return typeof id === "string" ? id : undefined;
|
|
146
107
|
}
|
|
147
|
-
/** The corpus's one client guard: it decides nothing unless a row names a trigger. */
|
|
148
108
|
function corpusGuard(context) {
|
|
149
109
|
switch (whereIdOf(context.args)) {
|
|
150
110
|
case CORPUS_TRIGGER.unauthenticated:
|
|
@@ -159,11 +119,13 @@ function corpusGuard(context) {
|
|
|
159
119
|
return true;
|
|
160
120
|
}
|
|
161
121
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
122
|
+
function corpusPipe(args) {
|
|
123
|
+
if (whereIdOf(args) !== CORPUS_TRIGGER.pipeOutputInvalid) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const record = args;
|
|
127
|
+
return { ...record, where: { ...record.where, nope: "x" } };
|
|
128
|
+
}
|
|
167
129
|
export const CORPUS_CLIENT_CONFIG = {
|
|
168
130
|
fields: {
|
|
169
131
|
books: {
|
|
@@ -187,46 +149,19 @@ export const CORPUS_CLIENT_CONFIG = {
|
|
|
187
149
|
},
|
|
188
150
|
},
|
|
189
151
|
},
|
|
190
|
-
pipelines: { guards: [corpusGuard] },
|
|
152
|
+
pipelines: { guards: [corpusGuard], pipes: [corpusPipe] },
|
|
191
153
|
restrictions: {
|
|
192
154
|
authors: { fields: { secret: { hidden: true } } },
|
|
193
155
|
secrets: { operations: { find: { many: false } } },
|
|
194
156
|
hiddenRes: { hidden: true },
|
|
195
157
|
},
|
|
196
158
|
};
|
|
197
|
-
/** The corpus Framework's mount point; every row's path is relative to it. */
|
|
198
159
|
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
160
|
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
161
|
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
162
|
export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:d333e8396b9bf9127e0859ed69b9874f230058a597e2129e6b9e9b380daef618";
|
|
222
|
-
/** The one record the corpus Adapter knows. */
|
|
223
163
|
export const CORPUS_AUTHOR = { id: "a1", name: "Ada" };
|
|
224
|
-
/** The `where.id` that matches nothing. */
|
|
225
164
|
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
165
|
export const CORPUS_SAMPLE = {
|
|
231
166
|
id: "s1",
|
|
232
167
|
count: 9007199254740993n,
|
|
@@ -234,25 +169,16 @@ export const CORPUS_SAMPLE = {
|
|
|
234
169
|
amount: new Decimal("12.50"),
|
|
235
170
|
blob: new Uint8Array([255, 0, 16]),
|
|
236
171
|
};
|
|
237
|
-
/** The conflict codes the corpus Adapter raises, and the text it raises them with. */
|
|
238
172
|
const CONFLICTS = {
|
|
239
173
|
[CORPUS_TRIGGER.conflict]: "A2008",
|
|
240
174
|
[CORPUS_TRIGGER.concurrentModification]: "A2013",
|
|
241
175
|
[CORPUS_TRIGGER.staleSelection]: "A2014",
|
|
242
176
|
};
|
|
243
|
-
/** The one record of each supporting Resource. */
|
|
244
177
|
const SUPPORTING_RECORDS = {
|
|
245
178
|
books: { id: "b1", authorId: CORPUS_AUTHOR.id },
|
|
246
179
|
secrets: { id: "s1" },
|
|
247
180
|
hiddenRes: { id: "h1" },
|
|
248
181
|
};
|
|
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
182
|
function corpusAnswer(operation) {
|
|
257
183
|
const id = whereIdOf(operation.arguments);
|
|
258
184
|
const conflict = id === undefined ? undefined : CONFLICTS[id];
|
|
@@ -281,11 +207,6 @@ function corpusAnswer(operation) {
|
|
|
281
207
|
}
|
|
282
208
|
return CORPUS_AUTHOR;
|
|
283
209
|
}
|
|
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
210
|
function failingCommit(transaction) {
|
|
290
211
|
return async (work) => {
|
|
291
212
|
let doomed = false;
|
|
@@ -303,16 +224,7 @@ function failingCommit(transaction) {
|
|
|
303
224
|
return result;
|
|
304
225
|
};
|
|
305
226
|
}
|
|
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
227
|
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
228
|
const fake = createFakeAdapter({
|
|
317
229
|
model: contractHash === CORPUS_NONE_CLIENT_CONTRACT_HASH
|
|
318
230
|
? { ...CORPUS_MODEL, transactions: "none" }
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
2
|
/**
|
|
3
|
-
* Driver 1
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* answer every row identically through a real server.
|
|
3
|
+
* Driver 1: the protocol hosted in process, with no HTTP stack — a catch-all host
|
|
4
|
+
* that hands `/_contract` to `protocol.encodeContract`, `/_transactions` to the
|
|
5
|
+
* bound shortcut `protocol.handleTransaction`, every other request to
|
|
6
|
+
* `protocol.handleOperation`, and writes back exactly what they answer. What it
|
|
7
|
+
* proves is the framework's own answer, host-free; driver 2 must answer every row
|
|
8
|
+
* identically through a real server.
|
|
10
9
|
*/
|
|
11
10
|
export declare const inProcessDriver: AvProtocolDriver;
|
|
@@ -1,13 +1,4 @@
|
|
|
1
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
2
|
export const inProcessDriver = async (framework) => {
|
|
12
3
|
const protocol = AvProtocol.bind(framework);
|
|
13
4
|
return {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
2
|
/**
|
|
3
|
-
* The per-route driver
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* exactly as through driver 1's catch-all.
|
|
3
|
+
* The per-route driver: the protocol hosted in process as a host that mounts ONE
|
|
4
|
+
* route per `protocol.surface()` entry — each operation on `handleOperation`,
|
|
5
|
+
* `/_transactions` on `handleTransaction`, `/_contract` on `encodeContract`,
|
|
6
|
+
* matched by method and path — and `protocol.answerAbsent` as the fallback for
|
|
7
|
+
* everything it did not mount. `undefined` from the fallback is a path outside the
|
|
8
|
+
* protocol, which this host answers with its own bodiless 404. Every corpus row
|
|
9
|
+
* must answer here exactly as through driver 1's catch-all.
|
|
11
10
|
*/
|
|
12
11
|
export declare const perRouteDriver: AvProtocolDriver;
|
|
@@ -1,20 +1,9 @@
|
|
|
1
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
2
|
const HOST_NOT_FOUND = Object.freeze({
|
|
4
3
|
status: 404,
|
|
5
4
|
headers: Object.freeze({}),
|
|
6
5
|
body: "",
|
|
7
6
|
});
|
|
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
7
|
export const perRouteDriver = async (framework) => {
|
|
19
8
|
const protocol = AvProtocol.bind(framework);
|
|
20
9
|
const mounted = new Map(protocol.surface().map((route) => [`${route.method} ${route.path}`, route]));
|
|
@@ -4,12 +4,11 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Every row a host answers is driven — the rows whose `client.outcome` is
|
|
6
6
|
* `"transport-error"` describe what a client receives instead of a framework
|
|
7
|
-
* answer, so they are skipped
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* answer is held to the row's `server` block:
|
|
7
|
+
* answer, so they are skipped. Each driven row runs against a fresh corpus
|
|
8
|
+
* Framework, through a fresh driver session, with one request — the corpus
|
|
9
|
+
* Framework whose ClientContract hash the row's `Aventara-Contract-Hash` names, as
|
|
10
|
+
* a generated client names it — and its answer is held to the row's `server`
|
|
11
|
+
* block:
|
|
13
12
|
*
|
|
14
13
|
* - `status`, exactly;
|
|
15
14
|
* - `code`: the body is a framework envelope (`AvProtocol.isOperationResponse`,
|
|
@@ -17,14 +16,14 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
|
17
16
|
* - `causeKeys`: the exact sorted key set of `cause`;
|
|
18
17
|
* - `issues`: each issue's `code` and `path`, in order;
|
|
19
18
|
* - `adapterCalls`: the operations the corpus Adapter was handed, exactly;
|
|
20
|
-
* - `responseHeaders`: each named header present (names matched without
|
|
21
|
-
*
|
|
19
|
+
* - `responseHeaders`: each named header present (names matched without regard to
|
|
20
|
+
* case) with exactly that value — a host may add its own;
|
|
22
21
|
* - `bodyBytes`: the body text, exactly.
|
|
23
22
|
*
|
|
24
|
-
* Resolves one result per driven row, in corpus order, each with the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* Resolves one result per driven row, in corpus order, each with the mismatches
|
|
24
|
+
* found — none when the host conforms. A driver that throws is a mismatch of its
|
|
25
|
+
* row, not a rejection of the run. Test-runner free: a caller asserts on the
|
|
26
|
+
* results with whatever runner it uses.
|
|
28
27
|
*/
|
|
29
28
|
export declare function runAvProtocolConformance(driver: AvProtocolDriver): Promise<readonly {
|
|
30
29
|
readonly label: string;
|
|
@@ -1,33 +1,6 @@
|
|
|
1
1
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
2
2
|
import { createCorpusFramework } from "./corpus-contract.fixture.js";
|
|
3
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
4
|
export async function runAvProtocolConformance(driver) {
|
|
32
5
|
const results = [];
|
|
33
6
|
for (const row of AvProtocolFixtures) {
|
|
@@ -59,7 +32,6 @@ export async function runAvProtocolConformance(driver) {
|
|
|
59
32
|
}
|
|
60
33
|
return results;
|
|
61
34
|
}
|
|
62
|
-
/** Every way `response` departs from what the row's server block states. */
|
|
63
35
|
function mismatchesOf(server, response, adapterCalls) {
|
|
64
36
|
const mismatches = [];
|
|
65
37
|
const expect = (what, actual, expected) => {
|
|
@@ -96,12 +68,10 @@ function mismatchesOf(server, response, adapterCalls) {
|
|
|
96
68
|
}
|
|
97
69
|
return mismatches;
|
|
98
70
|
}
|
|
99
|
-
/** The row's `Aventara-Contract-Hash`, its name matched without regard to case. */
|
|
100
71
|
function contractHashOf(request) {
|
|
101
72
|
const value = Object.entries(request.headers).find(([name]) => name.toLowerCase() === "aventara-contract-hash")?.[1];
|
|
102
73
|
return typeof value === "string" ? value : undefined;
|
|
103
74
|
}
|
|
104
|
-
/** The parsed body, or `undefined` — which no envelope is — for text that is not JSON. */
|
|
105
75
|
function parsedBody(text) {
|
|
106
76
|
try {
|
|
107
77
|
return JSON.parse(text);
|
|
@@ -2,15 +2,13 @@ import type { Framework } from "@aventara/core";
|
|
|
2
2
|
import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
3
|
/**
|
|
4
4
|
* One host under test, as the protocol conformance run drives it
|
|
5
|
-
* (`runAvProtocolConformance`): given the corpus's Framework, mount the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `AvProtocolResponse`.
|
|
5
|
+
* (`runAvProtocolConformance`): given the corpus's Framework, mount the protocol
|
|
6
|
+
* the way that host does, then answer each corpus request as that host would
|
|
7
|
+
* answer it over HTTP — status, headers and body text, as an `AvProtocolResponse`.
|
|
9
8
|
*
|
|
10
|
-
* Driver 1 (`in-process.driver.ts`) calls the bound protocol's shortcuts
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `close` once per row, after its one request.
|
|
9
|
+
* Driver 1 (`in-process.driver.ts`) calls the bound protocol's shortcuts directly;
|
|
10
|
+
* a real host sends the request over its own stack and reads back what arrived.
|
|
11
|
+
* The run calls `close` once per row, after its one request.
|
|
14
12
|
*/
|
|
15
13
|
export type AvProtocolDriver = (framework: Framework) => Promise<{
|
|
16
14
|
readonly send: (request: AvProtocolRequest) => Promise<AvProtocolResponse>;
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import type { OperationCode, ValidationCode } from "@aventara/core";
|
|
2
2
|
import type { AvProtocolRequest } from "@aventara/core/protocol";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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`.
|
|
4
|
+
* Types only; the rows live in `protocol-fixtures.ts`.
|
|
8
5
|
*
|
|
9
6
|
* `request` is core's own request value, `AvProtocolRequest` from
|
|
10
|
-
* `@aventara/core/protocol
|
|
11
|
-
*
|
|
7
|
+
* `@aventara/core/protocol`, so a row is exactly what a host hands the bound
|
|
8
|
+
* protocol: no second request shape exists for the corpus to drift from.
|
|
12
9
|
*/
|
|
13
10
|
export type AvProtocolFixture = {
|
|
14
11
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* name (`find.many …`, `create.one …`) or with `transaction` for the
|
|
12
|
+
* A row that covers one of the specification's success rows begins with that row's
|
|
13
|
+
* operation name (`find.many …`, `create.one …`) or with `transaction` for the
|
|
18
14
|
* committed-plan row.
|
|
19
15
|
*/
|
|
20
16
|
readonly label: string;
|
|
21
|
-
/** Method, entrypoint-relative path, headers, and a raw or parsed body
|
|
17
|
+
/** Method, entrypoint-relative path, headers, and a raw or parsed body. */
|
|
22
18
|
readonly request: AvProtocolRequest;
|
|
23
19
|
readonly server: {
|
|
24
20
|
/**
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { CORPUS_AUTHOR, CORPUS_CLIENT_CONTRACT_DOCUMENT, CORPUS_CLIENT_CONTRACT_HASH, CORPUS_NOBODY, CORPUS_NONE_CLIENT_CONTRACT_HASH, CORPUS_TRIGGER, } from "./corpus-contract.fixture.js";
|
|
2
|
-
/**
|
|
3
|
-
* The Phase 10 protocol fixture corpus: passive data, read by the coverage
|
|
4
|
-
* gate (`protocol-coverage.spec.ts`) and, from later slices on, driven
|
|
5
|
-
* through each host driver. Each slice adds the rows it settles and removes
|
|
6
|
-
* what they cover from the gate's awaiting ledger.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* A well-formed generated-client request. A family-11 row's answer does not
|
|
10
|
-
* depend on it: the answer is what arrived instead of a framework envelope.
|
|
11
|
-
*/
|
|
12
2
|
const OPERATION_REQUEST = {
|
|
13
3
|
method: "POST",
|
|
14
4
|
path: "/_resources/authors/find/many",
|
|
@@ -19,21 +9,6 @@ const OPERATION_REQUEST = {
|
|
|
19
9
|
},
|
|
20
10
|
body: { kind: "raw", content: "{}" },
|
|
21
11
|
};
|
|
22
|
-
/**
|
|
23
|
-
* Family 11 — transport errors (§13.5; R5). Each row is a response that is NOT
|
|
24
|
-
* a framework envelope — produced by a proxy, a broken host or the network,
|
|
25
|
-
* never by the framework — so a client raises `TransportError`, never a
|
|
26
|
-
* `FrameworkError`. `AvProtocol.isOperationResponse` refuses every row's body
|
|
27
|
-
* (`envelope-guard.spec.ts`), and the generated client's emitted check is held
|
|
28
|
-
* to agree with it row by row (`packages/client`, the envelope pin).
|
|
29
|
-
*
|
|
30
|
-
* Every label begins `transport-error:`. These rows describe what a client
|
|
31
|
-
* receives, not what a host must answer, so a host driver does not drive them:
|
|
32
|
-
* it selects its rows by `client.outcome`, skipping every row whose outcome is
|
|
33
|
-
* `"transport-error"` (the director, 2026-10-05) — never by label or by
|
|
34
|
-
* `server.code === null`. `adapterCalls` is `0` because the framework produced
|
|
35
|
-
* none of these answers.
|
|
36
|
-
*/
|
|
37
12
|
const transportError = (label, status, bodyBytes) => ({
|
|
38
13
|
label: `transport-error: ${label}`,
|
|
39
14
|
request: OPERATION_REQUEST,
|
|
@@ -87,12 +62,6 @@ const TRANSPORT_ERRORS = [
|
|
|
87
62
|
transportError("a negative operation index", 409, envelopeText("A2008", null, { message: "x", operation: -1 })),
|
|
88
63
|
transportError("a fractional operation index", 409, envelopeText("A2008", null, { message: "x", operation: 0.5 })),
|
|
89
64
|
];
|
|
90
|
-
/**
|
|
91
|
-
* A generated client's request to the corpus Framework for one operation of
|
|
92
|
-
* `authors` (`corpus-contract.fixture.ts`): its identity headers name the
|
|
93
|
-
* corpus ClientContract, and it sends its own `Aventara-Request-Id`, so the
|
|
94
|
-
* echo every framework answer carries (Q13) is a fixed fact of the row.
|
|
95
|
-
*/
|
|
96
65
|
const corpusRequest = (operation, body, requestId, overrides = {}) => ({
|
|
97
66
|
method: "POST",
|
|
98
67
|
path: `/_resources/authors/${operation}`,
|
|
@@ -105,20 +74,10 @@ const corpusRequest = (operation, body, requestId, overrides = {}) => ({
|
|
|
105
74
|
body: { kind: "raw", content: JSON.stringify(body) },
|
|
106
75
|
...overrides,
|
|
107
76
|
});
|
|
108
|
-
/** The framework's own headers on an answer to a request that sent `requestId`. */
|
|
109
77
|
const answeredHeaders = (requestId) => ({
|
|
110
78
|
"Content-Type": "application/json",
|
|
111
79
|
"Aventara-Request-Id": requestId,
|
|
112
80
|
});
|
|
113
|
-
/**
|
|
114
|
-
* Family 2 — success statuses (§13.2; plan S8). Every standard operation
|
|
115
|
-
* through a found route: its success code at its status — `201` exactly for
|
|
116
|
-
* `A1002` and `A1003`, `200` otherwise, `upsert.unique`'s `A1008` included —
|
|
117
|
-
* with `cause: null`, the result in wire form, one Adapter call, and no
|
|
118
|
-
* `Location` (P2: the framework's two headers are all it writes). The label
|
|
119
|
-
* begins with the operation's name, which is how the coverage gate reads a
|
|
120
|
-
* success row.
|
|
121
|
-
*/
|
|
122
81
|
const success = (operation, body, status, code, data, label = "") => {
|
|
123
82
|
const requestId = `corpus-${operation.replace(".", "-")}${label === "" ? "" : "-miss"}`;
|
|
124
83
|
return {
|
|
@@ -157,13 +116,6 @@ const SUCCESS_STATUSES = [
|
|
|
157
116
|
success("delete.count", WHERE, 200, "A1007", 2),
|
|
158
117
|
success("upsert.unique", { ...WHERE, create: DATA, update: DATA }, 200, "A1008", CORPUS_AUTHOR),
|
|
159
118
|
];
|
|
160
|
-
/**
|
|
161
|
-
* F-828 (plan S12) — a computed field added under `client.fields` is read
|
|
162
|
-
* through the HTTP surface like one added at root: `books.label`, VIRTUAL,
|
|
163
|
-
* computed from `id` (`corpus-contract.fixture.ts`). It is in the default read,
|
|
164
|
-
* and an explicit `select` of it answers it alone; the Adapter is asked only for
|
|
165
|
-
* stored columns (core's `computed-read-runtime.scope-added-field.spec.ts`).
|
|
166
|
-
*/
|
|
167
119
|
const scopeAdded = (label, body, requestId, data) => ({
|
|
168
120
|
label: `scope-added: ${label}`,
|
|
169
121
|
request: corpusRequest("find/many", body, requestId, {
|
|
@@ -179,15 +131,9 @@ const scopeAdded = (label, body, requestId, data) => ({
|
|
|
179
131
|
client: { outcome: "resolve" },
|
|
180
132
|
});
|
|
181
133
|
const SCOPE_ADDED = [
|
|
182
|
-
scopeAdded("a computed field added under client.fields is in the default read
|
|
183
|
-
|
|
184
|
-
[{ authorId: CORPUS_AUTHOR.id, id: "b1", label: "book:b1" }]),
|
|
185
|
-
scopeAdded("an explicit select of it answers it alone (F-828)", { select: ["label"] }, "corpus-scope-added-select", [{ label: "book:b1" }]),
|
|
134
|
+
scopeAdded("a computed field added under client.fields is in the default read", {}, "corpus-scope-added-default", [{ authorId: CORPUS_AUTHOR.id, id: "b1", label: "book:b1" }]),
|
|
135
|
+
scopeAdded("an explicit select of it answers it alone", { select: ["label"] }, "corpus-scope-added-select", [{ label: "book:b1" }]),
|
|
186
136
|
];
|
|
187
|
-
/**
|
|
188
|
-
* A request the protocol answers itself, before any Adapter call: the
|
|
189
|
-
* framework's own envelope, exactly, at its code's status.
|
|
190
|
-
*/
|
|
191
137
|
const refused = (label, request, status, code, message, headers = {}) => ({
|
|
192
138
|
label,
|
|
193
139
|
request,
|
|
@@ -205,20 +151,10 @@ const refused = (label, request, status, code, message, headers = {}) => ({
|
|
|
205
151
|
client: { outcome: "framework-error", code },
|
|
206
152
|
});
|
|
207
153
|
const MALFORMED = "The request is malformed and cannot be interpreted.";
|
|
208
|
-
/** `{"pad":"x…"}`, one byte over the default 1 MiB `maxRequestBytes` (§20.1). */
|
|
209
154
|
const OVERSIZED = `{"pad":"${"x".repeat(1024 * 1024 + 1 - 10)}"}`;
|
|
210
155
|
const rawBody = (requestId, content) => corpusRequest("find/many", {}, requestId, {
|
|
211
156
|
body: { kind: "raw", content },
|
|
212
157
|
});
|
|
213
|
-
/**
|
|
214
|
-
* Family 7 — body and media (§12.3, §12.6, Q11; plan S7, driven from S8). On
|
|
215
|
-
* the raw arm: `{}` is valid (`find.many` above), and every other shape — a
|
|
216
|
-
* list, a scalar, `null`, no body, text that is not JSON — is `A2000`; a
|
|
217
|
-
* content type other than `application/json` is `A2011`; a decoded body over
|
|
218
|
-
* the limit is `A2010`. On the parsed arm only the shape is the decoder's.
|
|
219
|
-
* Routing metadata in the body is an argument like any other, refused by
|
|
220
|
-
* core's own validation (N16), so it reaches the Framework but no Adapter.
|
|
221
|
-
*/
|
|
222
158
|
const BODY_AND_MEDIA = [
|
|
223
159
|
refused("body: a JSON list → 400 A2000", rawBody("corpus-body-list", "[]"), 400, "A2000", MALFORMED),
|
|
224
160
|
refused("body: a JSON number → 400 A2000", rawBody("corpus-body-number", "3"), 400, "A2000", MALFORMED),
|
|
@@ -255,24 +191,16 @@ const BODY_AND_MEDIA = [
|
|
|
255
191
|
client: { outcome: "framework-error", code: "A2004" },
|
|
256
192
|
},
|
|
257
193
|
];
|
|
258
|
-
/**
|
|
259
|
-
* Family 8 — method (§12.1). A framework route accepts `POST` only; anything
|
|
260
|
-
* else is `405 A2012` carrying `Allow: POST` (RFC 9110 §15.5.6; the
|
|
261
|
-
* director, 2026-10-05).
|
|
262
|
-
*/
|
|
263
194
|
const METHOD = [
|
|
264
195
|
refused("method: GET on a resource route → 405 A2012 with Allow: POST", corpusRequest("find/many", {}, "corpus-method-get", { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
|
|
265
196
|
];
|
|
266
|
-
/** A generator's `GET /_contract`, with `headers` beside its own request id. */
|
|
267
197
|
const contractRequest = (requestId, headers = {}, method = "GET") => ({
|
|
268
198
|
method,
|
|
269
199
|
path: "/_contract",
|
|
270
200
|
headers: { "Aventara-Request-Id": requestId, ...headers },
|
|
271
201
|
body: { kind: "raw", content: "" },
|
|
272
202
|
});
|
|
273
|
-
/** The corpus document's entity tag: the quoted contract hash (§12.5). */
|
|
274
203
|
const CORPUS_ETAG = `"${CORPUS_CLIENT_CONTRACT_HASH}"`;
|
|
275
|
-
/** `200` with the document: its exact bytes, `ETag`, `Cache-Control: no-cache`. */
|
|
276
204
|
const contractServed = (label, requestId, headers = {}) => ({
|
|
277
205
|
label: `contract: ${label}`,
|
|
278
206
|
request: contractRequest(requestId, headers),
|
|
@@ -289,7 +217,6 @@ const contractServed = (label, requestId, headers = {}) => ({
|
|
|
289
217
|
},
|
|
290
218
|
client: { outcome: "resolve" },
|
|
291
219
|
});
|
|
292
|
-
/** `304 Not Modified`: no body, `ETag` and `Cache-Control` kept (RFC 9110 §15.4.5). */
|
|
293
220
|
const contractNotModified = (label, requestId, ifNoneMatch) => ({
|
|
294
221
|
label: `contract: ${label}`,
|
|
295
222
|
request: contractRequest(requestId, { "If-None-Match": ifNoneMatch }),
|
|
@@ -306,14 +233,6 @@ const contractNotModified = (label, requestId, ifNoneMatch) => ({
|
|
|
306
233
|
},
|
|
307
234
|
client: { outcome: "resolve" },
|
|
308
235
|
});
|
|
309
|
-
/**
|
|
310
|
-
* Family 1 — contract discovery (§12.5, Q8; plan S11). `GET /_contract`
|
|
311
|
-
* serves the ClientContract's JCS bytes with `ETag` the quoted hash and
|
|
312
|
-
* `Cache-Control: no-cache`, needs no identity header, and answers `304` with
|
|
313
|
-
* no body when `If-None-Match` matches — weakly, in a list, or `*`. Any other
|
|
314
|
-
* method is `405 A2012` with `Allow: GET`. A generator resolves both `200`
|
|
315
|
-
* and `304` ("up to date", Phase 12).
|
|
316
|
-
*/
|
|
317
236
|
const CONTRACT_DISCOVERY = [
|
|
318
237
|
contractServed("GET → 200 with the JCS bytes, ETag and no-cache", "corpus-contract"),
|
|
319
238
|
contractServed("no identity header is required, nor judged when sent", "corpus-contract-identity", {
|
|
@@ -329,24 +248,16 @@ const CONTRACT_DISCOVERY = [
|
|
|
329
248
|
}),
|
|
330
249
|
refused("method: POST on /_contract → 405 A2012 with Allow: GET", contractRequest("corpus-contract-post", {}, "POST"), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "GET" }),
|
|
331
250
|
];
|
|
332
|
-
/**
|
|
333
|
-
* One node of a generated client's plan (§14, M25): the operation, its
|
|
334
|
-
* arguments verbatim, and the fingerprint the client computed over them —
|
|
335
|
-
* a literal, as the client sends it; `corpus-contract.spec.ts` recomputes
|
|
336
|
-
* every one with core's `computeOperationFingerprint`.
|
|
337
|
-
*/
|
|
338
251
|
const planNode = (resource, operation, args, fingerprint) => {
|
|
339
252
|
const [family, variant] = operation.split(".");
|
|
340
253
|
return { resource, family, variant, args, fingerprint };
|
|
341
254
|
};
|
|
342
255
|
const AUTHORS_FIND_MANY = planNode("authors", "find.many", {}, "fp1:1aLMnUbKspahv3fbsV7pKg");
|
|
343
|
-
/** `POST /_transactions` with a plan of `nodes`, under the corpus identity. */
|
|
344
256
|
const planRequest = (requestId, nodes, overrides = {}) => ({
|
|
345
257
|
...corpusRequest("", { operations: nodes }, requestId),
|
|
346
258
|
path: "/_transactions",
|
|
347
259
|
...overrides,
|
|
348
260
|
});
|
|
349
|
-
/** A row refused by core's validation layer: its issues and cause keys, no Adapter call. */
|
|
350
261
|
const invalid = (label, request, status, code, issues, operation) => ({
|
|
351
262
|
label,
|
|
352
263
|
request,
|
|
@@ -362,20 +273,6 @@ const invalid = (label, request, status, code, issues, operation) => ({
|
|
|
362
273
|
},
|
|
363
274
|
client: { outcome: "framework-error", code },
|
|
364
275
|
});
|
|
365
|
-
/**
|
|
366
|
-
* Family 9 — transactions (§14; plan S9), through `/_transactions` under
|
|
367
|
-
* ClientContract rules only (R1). A committed plan is `200 A1009` with each
|
|
368
|
-
* step's data in plan order, a create included (P3); a rolled-back plan
|
|
369
|
-
* answers its failing step's own code at that code's status, cause keys
|
|
370
|
-
* `message` and `operation` (Q4, M15); a step that depends on rows an earlier
|
|
371
|
-
* step's cascade removed is refused before anything runs (`V1019` → `422`);
|
|
372
|
-
* and a plan node cannot carry an origin (N11).
|
|
373
|
-
*
|
|
374
|
-
* The R1 rows come in pairs, labelled `R1: <what> — single route` and
|
|
375
|
-
* `R1: <what> — /_transactions`: the same refusal through both routes, the
|
|
376
|
-
* plan's differing only by `cause.operation` (asserted pair by pair in
|
|
377
|
-
* `corpus-contract.spec.ts`).
|
|
378
|
-
*/
|
|
379
276
|
const TRANSACTIONS = [
|
|
380
277
|
{
|
|
381
278
|
label: "transaction — committed, a create included → 200 A1009 in plan order",
|
|
@@ -426,38 +323,31 @@ const TRANSACTIONS = [
|
|
|
426
323
|
...["scope", "origin"].map((key) => invalid(`transaction — a plan node carrying "${key}" cannot smuggle an origin → 422 A2004 / V1001`, planRequest(`corpus-tx-smuggle-${key}`, [
|
|
427
324
|
{ ...AUTHORS_FIND_MANY, [key]: "application" },
|
|
428
325
|
]), 422, "A2004", [{ code: "V1001", path: [key] }], 0)),
|
|
429
|
-
invalid("
|
|
430
|
-
invalid("
|
|
326
|
+
invalid("a client-hidden field — single route", corpusRequest("find/many", { select: ["id", "secret"] }, "corpus-r1-field"), 422, "A2004", [{ code: "V1005", path: ["arguments", "select", 1] }]),
|
|
327
|
+
invalid("a client-hidden field — /_transactions", planRequest("corpus-r1-field-tx", [
|
|
431
328
|
AUTHORS_FIND_MANY,
|
|
432
329
|
planNode("authors", "find.many", { select: ["id", "secret"] }, "fp1:1t8vSfPuohv1N5MIX8ccNw"),
|
|
433
330
|
]), 422, "A2004", [{ code: "V1005", path: ["arguments", "select", 1] }], 1),
|
|
434
|
-
invalid("
|
|
331
|
+
invalid("a client-restricted variant — single route", {
|
|
435
332
|
...corpusRequest("", {}, "corpus-r1-variant"),
|
|
436
333
|
path: "/_resources/secrets/find/many",
|
|
437
334
|
}, 404, "A2002", [{ code: "V1006", path: ["operation"] }]),
|
|
438
|
-
invalid("
|
|
335
|
+
invalid("a client-restricted variant — /_transactions", planRequest("corpus-r1-variant-tx", [
|
|
439
336
|
AUTHORS_FIND_MANY,
|
|
440
337
|
planNode("secrets", "find.many", {}, "fp1:KegI5OcquDHi8z39VH11EQ"),
|
|
441
338
|
]), 404, "A2002", [{ code: "V1006", path: ["operation"] }], 1),
|
|
442
|
-
invalid("
|
|
339
|
+
invalid("a client-hidden Resource — single route", {
|
|
443
340
|
...corpusRequest("", {}, "corpus-r1-resource"),
|
|
444
341
|
path: "/_resources/hiddenRes/find/many",
|
|
445
342
|
}, 404, "A2001", [{ code: "V1005", path: ["resource"] }]),
|
|
446
|
-
invalid("
|
|
343
|
+
invalid("a client-hidden Resource — /_transactions", planRequest("corpus-r1-resource-tx", [
|
|
447
344
|
AUTHORS_FIND_MANY,
|
|
448
345
|
planNode("hiddenRes", "find.many", {}, "fp1:UY6pLVLKM2LaIMnfQa0zGw"),
|
|
449
346
|
]), 404, "A2001", [{ code: "V1005", path: ["resource"] }], 1),
|
|
450
347
|
refused("method: GET on /_transactions → 405 A2012 with Allow: POST", planRequest("corpus-tx-get", [AUTHORS_FIND_MANY], { method: "GET" }), 405, "A2012", "The HTTP method is not allowed on this framework route.", { Allow: "POST" }),
|
|
451
348
|
];
|
|
452
|
-
/** A hash no corpus ClientContract has: a stale generated client's. */
|
|
453
349
|
const STALE_HASH = `sha256:${"0".repeat(64)}`;
|
|
454
|
-
/** The FVL's validation-failure text, which every A2001/A2002 envelope carries. */
|
|
455
350
|
const VALIDATION_FAILED = "Operation arguments failed framework validation.";
|
|
456
|
-
/**
|
|
457
|
-
* An absence answer (Q2's O3): core's own envelope for the same request,
|
|
458
|
-
* byte for byte (`corpus-contract.spec.ts` recomputes each operation row's
|
|
459
|
-
* through `framework.execute`), with zero Adapter calls.
|
|
460
|
-
*/
|
|
461
351
|
const absent = (label, request, status, code, cause) => ({
|
|
462
352
|
label: `absent: ${label}`,
|
|
463
353
|
request,
|
|
@@ -476,7 +366,6 @@ const absent = (label, request, status, code, cause) => ({
|
|
|
476
366
|
},
|
|
477
367
|
client: { outcome: "framework-error", code },
|
|
478
368
|
});
|
|
479
|
-
/** A corpus request to `path`, its identity headers overridden by `headers`. */
|
|
480
369
|
const absentRequest = (path, requestId, headers = {}, method = "POST") => {
|
|
481
370
|
const base = corpusRequest("", {}, requestId);
|
|
482
371
|
return { ...base, method, path, headers: { ...base.headers, ...headers } };
|
|
@@ -501,16 +390,6 @@ const unavailableResource = (resource) => ({
|
|
|
501
390
|
},
|
|
502
391
|
],
|
|
503
392
|
});
|
|
504
|
-
/**
|
|
505
|
-
* Family 6 — the absence responder (Q2's O3, Q3; plan S10). What a request
|
|
506
|
-
* for a route the ClientContract does not advertise answers, identically
|
|
507
|
-
* whether the host mounts one route per `protocol.surface()` entry with
|
|
508
|
-
* `protocol.answerAbsent` as its fallback or a catch-all whose decoder answers
|
|
509
|
-
* absence (Q12) — both styles drive these rows. Identity first, so a stale
|
|
510
|
-
* client hears `A2005` (F-716's server half), then the Resource, then the
|
|
511
|
-
* operation; zero Adapter calls. `_transactions` under `"none"` is asked of
|
|
512
|
-
* the corpus's second ClientContract, named by its hash.
|
|
513
|
-
*/
|
|
514
393
|
const ABSENCE = [
|
|
515
394
|
absent("an unadvertised operation, current hash → 404 A2002", absentRequest("/_resources/books/delete/many", "corpus-absent-op"), 404, "A2002", unavailableOperation("books", "delete.many")),
|
|
516
395
|
absent("an unadvertised operation, stale hash → 409 A2005 (regenerate)", absentRequest("/_resources/books/delete/many", "corpus-absent-stale", {
|
|
@@ -525,9 +404,9 @@ const ABSENCE = [
|
|
|
525
404
|
"Aventara-Protocol-Version": "2",
|
|
526
405
|
}), 400, "A2006", { message: "Aventara protocol version does not match the server." }),
|
|
527
406
|
absent("an unknown Resource → 404 A2001", absentRequest("/_resources/nope/find/many", "corpus-absent-resource"), 404, "A2001", unavailableResource("nope")),
|
|
528
|
-
absent("a Resource with operations: {} → 404 A2002
|
|
529
|
-
absent("a client-hidden Resource → 404 A2001
|
|
530
|
-
absent('/_transactions under transactions "none" → 404 A2002 at ["operations"]
|
|
407
|
+
absent("a Resource with operations: {} → 404 A2002", absentRequest("/_resources/secrets/find/many", "corpus-absent-empty"), 404, "A2002", unavailableOperation("secrets", "find.many")),
|
|
408
|
+
absent("a client-hidden Resource → 404 A2001", absentRequest("/_resources/hiddenRes/find/many", "corpus-absent-hidden"), 404, "A2001", unavailableResource("hiddenRes")),
|
|
409
|
+
absent('/_transactions under transactions "none" → 404 A2002 at ["operations"]', {
|
|
531
410
|
...planRequest("corpus-absent-tx-none", [AUTHORS_FIND_MANY]),
|
|
532
411
|
headers: {
|
|
533
412
|
...planRequest("corpus-absent-tx-none", [AUTHORS_FIND_MANY]).headers,
|
|
@@ -544,12 +423,6 @@ const ABSENCE = [
|
|
|
544
423
|
],
|
|
545
424
|
}),
|
|
546
425
|
];
|
|
547
|
-
/**
|
|
548
|
-
* A row whose whole answer is stated: its status, its envelope byte for byte
|
|
549
|
-
* (each object's keys in the order core writes them, as measured), its cause
|
|
550
|
-
* keys and issues read off that envelope, and its Adapter calls. A success
|
|
551
|
-
* code resolves; any other code is a framework error.
|
|
552
|
-
*/
|
|
553
426
|
const answered = (label, request, status, adapterCalls, envelope, responseHeaders = answeredHeaders(String(request.headers["Aventara-Request-Id"]))) => {
|
|
554
427
|
const { cause } = envelope;
|
|
555
428
|
const issues = Array.isArray(cause?.issues)
|
|
@@ -576,16 +449,11 @@ const answered = (label, request, status, adapterCalls, envelope, responseHeader
|
|
|
576
449
|
: { outcome: "framework-error", code: envelope.code },
|
|
577
450
|
};
|
|
578
451
|
};
|
|
579
|
-
/** A corpus request to any Resource's operation route. */
|
|
580
452
|
const routeRequest = (resource, operation, body, requestId, overrides = {}) => corpusRequest("", body, requestId, {
|
|
581
453
|
path: `/_resources/${resource}/${operation.replace(".", "/")}`,
|
|
582
454
|
...overrides,
|
|
583
455
|
});
|
|
584
456
|
const NO_MATCH = "No record matched the unique selector.";
|
|
585
|
-
/**
|
|
586
|
-
* Family 3 — absence semantics (§13.4): what a request that matches nothing
|
|
587
|
-
* answers, operation by operation. `find.first`'s `A1001` is family 2's.
|
|
588
|
-
*/
|
|
589
457
|
const ABSENCE_SEMANTICS = [
|
|
590
458
|
answered("absence: find.unique matching nothing → 404 A2003", routeRequest("authors", "find.unique", { where: { id: CORPUS_NOBODY } }, "corpus-none-find-unique"), 404, 1, { data: null, code: "A2003", cause: { message: NO_MATCH } }),
|
|
591
459
|
answered("absence: find.many matching nothing → 200 A1000 with []", routeRequest("authors", "find.many", { where: { id: CORPUS_NOBODY } }, "corpus-none-find-many"), 200, 1, { data: [], code: "A1000", cause: null }),
|
|
@@ -595,34 +463,19 @@ const ABSENCE_SEMANTICS = [
|
|
|
595
463
|
answered("absence: update.unique with no target → 404 A2003", routeRequest("authors", "update.unique", { where: { id: CORPUS_NOBODY }, data: DATA }, "corpus-none-update-unique"), 404, 1, { data: null, code: "A2003", cause: { message: NO_MATCH } }),
|
|
596
464
|
answered("absence: delete.unique with no target → 404 A2003", routeRequest("authors", "delete.unique", { where: { id: CORPUS_NOBODY } }, "corpus-none-delete-unique"), 404, 1, { data: null, code: "A2003", cause: { message: NO_MATCH } }),
|
|
597
465
|
];
|
|
598
|
-
/** `{ id: { equals: "a1" } }` wrapped in `depth` single-member `AND`s. */
|
|
599
466
|
const nested = (depth) => depth === 0
|
|
600
467
|
? { id: { equals: CORPUS_AUTHOR.id } }
|
|
601
468
|
: { AND: [nested(depth - 1)] };
|
|
602
|
-
/** 51 filter nodes under one `OR`: one over the default `maxBooleanNodes` (50). */
|
|
603
469
|
const TOO_MANY_NODES = {
|
|
604
470
|
OR: Array.from({ length: 51 }, () => ({ id: { equals: CORPUS_AUTHOR.id } })),
|
|
605
471
|
};
|
|
606
|
-
/** A plan reference to `path` of step `operation`, whose fingerprint is `fingerprint`. */
|
|
607
472
|
const ref = (operation, fingerprint, path) => ({
|
|
608
473
|
$ref: { operation, fingerprint, path: [path] },
|
|
609
474
|
});
|
|
610
|
-
/** Step 0 of every reference row: create an author, selecting its id. */
|
|
611
475
|
const CREATE_ID = planNode("authors", "create.one", { data: { name: CORPUS_AUTHOR.name }, select: ["id"] }, "fp1:aMKNYrUwcifOeY8ZYk4pow");
|
|
612
476
|
const CREATE_ID_PRINT = CREATE_ID.fingerprint;
|
|
613
|
-
/** A `find.first` that matches nothing: a source with no value to refer to. */
|
|
614
477
|
const FIRST_NOBODY = planNode("authors", "find.first", { where: { id: CORPUS_NOBODY } }, "fp1:qNR9mD7_iVygNtmuuXjymw");
|
|
615
478
|
const ARGUMENTS_FAILED = "Operation arguments failed framework validation.";
|
|
616
|
-
/**
|
|
617
|
-
* Family 4 — argument validation (§13.3; ADR 0008), core's own answers through
|
|
618
|
-
* the HTTP surface: an unknown field (`V1005`), an unadvertised operator
|
|
619
|
-
* (`V1006`), an invalid projection (`V1008`) → `A2004`; the three limits
|
|
620
|
-
* (`V1013` nesting, `V1014` a plan's operation count, `V1015` filter nodes) →
|
|
621
|
-
* `A2009`; a plan reference's index, fingerprint, path, type and value
|
|
622
|
-
* (`V1016`, `V1017`, `V1010`, `V1011`, `V1018`) → `A2007`. Rollup precedence
|
|
623
|
-
* (ADR 0008 decision 2), behaviourally: two `A2004` issues roll up together,
|
|
624
|
-
* and a limit refuses alone, before a field defect is read.
|
|
625
|
-
*/
|
|
626
479
|
const ARGUMENT_VALIDATION = [
|
|
627
480
|
answered("argument: an unknown field in where → 422 A2004 / V1005", corpusRequest("find/many", { where: { nope: { equals: "x" } } }, "corpus-arg-field"), 422, 0, {
|
|
628
481
|
data: null,
|
|
@@ -823,7 +676,6 @@ const ARGUMENT_VALIDATION = [
|
|
|
823
676
|
},
|
|
824
677
|
}),
|
|
825
678
|
];
|
|
826
|
-
/** The corpus identity headers, with `overrides` applied and `omit` left out. */
|
|
827
679
|
const identityHeaders = (requestId, overrides = {}, omit) => Object.fromEntries(Object.entries({
|
|
828
680
|
"Content-Type": "application/json",
|
|
829
681
|
"Aventara-Protocol-Version": "1",
|
|
@@ -832,19 +684,19 @@ const identityHeaders = (requestId, overrides = {}, omit) => Object.fromEntries(
|
|
|
832
684
|
...overrides,
|
|
833
685
|
}).filter(([name]) => name !== omit));
|
|
834
686
|
const STALE_CLIENT = "Generated client contract does not match the server. Regenerate the client.";
|
|
835
|
-
/**
|
|
836
|
-
* Family 5 — identity (§12.4; S7 follow-ups): a request missing either
|
|
837
|
-
* identity header is malformed (`A2000`), another protocol version is
|
|
838
|
-
* `A2006`, a stale or malformed hash is `A2005` with Appendix B.3's text (Q3),
|
|
839
|
-
* all on a route that exists; header names are matched without regard to case.
|
|
840
|
-
*/
|
|
841
687
|
const IDENTITY = [
|
|
842
688
|
refused("identity: no Aventara-Protocol-Version → 400 A2000", corpusRequest("find/many", {}, "corpus-id-no-version", {
|
|
843
689
|
headers: identityHeaders("corpus-id-no-version", {}, "Aventara-Protocol-Version"),
|
|
844
|
-
}), 400, "A2000",
|
|
690
|
+
}), 400, "A2000", "The Aventara-Protocol-Version header is missing."),
|
|
845
691
|
refused("identity: no Aventara-Contract-Hash → 400 A2000", corpusRequest("find/many", {}, "corpus-id-no-hash", {
|
|
846
692
|
headers: identityHeaders("corpus-id-no-hash", {}, "Aventara-Contract-Hash"),
|
|
847
|
-
}), 400, "A2000",
|
|
693
|
+
}), 400, "A2000", "The Aventara-Contract-Hash header is missing."),
|
|
694
|
+
refused("identity: neither identity header → 400 A2000 naming both", corpusRequest("find/many", {}, "corpus-id-none", {
|
|
695
|
+
headers: {
|
|
696
|
+
"Content-Type": "application/json",
|
|
697
|
+
"Aventara-Request-Id": "corpus-id-none",
|
|
698
|
+
},
|
|
699
|
+
}), 400, "A2000", "The Aventara-Protocol-Version and Aventara-Contract-Hash headers are missing."),
|
|
848
700
|
refused("identity: another protocol version → 400 A2006", corpusRequest("find/many", {}, "corpus-id-version", {
|
|
849
701
|
headers: identityHeaders("corpus-id-version", {
|
|
850
702
|
"Aventara-Protocol-Version": "2",
|
|
@@ -864,16 +716,7 @@ const IDENTITY = [
|
|
|
864
716
|
headers: Object.fromEntries(Object.entries(identityHeaders("corpus-id-case")).map(([name, value]) => [name.toLowerCase(), value])),
|
|
865
717
|
}), 200, 1, { data: [CORPUS_AUTHOR], code: "A1000", cause: null }, answeredHeaders("corpus-id-case")),
|
|
866
718
|
];
|
|
867
|
-
/** `authors.find.unique` naming the trigger `id` — what the guard or Adapter fails on. */
|
|
868
719
|
const triggered = (id, requestId) => corpusRequest("find/unique", { where: { id } }, requestId);
|
|
869
|
-
/**
|
|
870
|
-
* Family 10 — internal and authorization (§13.2; Q5, Q6), from the corpus's
|
|
871
|
-
* guard and Adapter failing on purpose (labelled by trigger,
|
|
872
|
-
* `CORPUS_TRIGGER`): `A3000`–`A3003` → 500 with `{ message }` only — no
|
|
873
|
-
* sub-code reaches the wire (Q5); `A4000` → 401, `A4001`/`A4002` → 403, with
|
|
874
|
-
* no reason member (Q6). Every one is a framework error, never a transport
|
|
875
|
-
* error: each body is an envelope.
|
|
876
|
-
*/
|
|
877
720
|
const INTERNAL = [
|
|
878
721
|
answered("internal: a guard that throws → 500 A3000", triggered(CORPUS_TRIGGER.guardThrows, "corpus-internal-a3000"), 500, 0, {
|
|
879
722
|
data: null,
|
|
@@ -893,6 +736,11 @@ const INTERNAL = [
|
|
|
893
736
|
code: "A3003",
|
|
894
737
|
cause: { message: "Result serialization failed." },
|
|
895
738
|
}),
|
|
739
|
+
answered("internal: a pipe whose output the server contract refuses → 500 A3004", routeRequest("authors", "find.many", { where: { id: CORPUS_TRIGGER.pipeOutputInvalid } }, "corpus-internal-a3004"), 500, 0, {
|
|
740
|
+
data: null,
|
|
741
|
+
code: "A3004",
|
|
742
|
+
cause: { message: "A server pipeline produced invalid arguments." },
|
|
743
|
+
}),
|
|
896
744
|
answered("internal: a guard that requires authentication → 401 A4000", triggered(CORPUS_TRIGGER.unauthenticated, "corpus-internal-a4000"), 401, 0, {
|
|
897
745
|
data: null,
|
|
898
746
|
code: "A4000",
|
|
@@ -909,10 +757,6 @@ const INTERNAL = [
|
|
|
909
757
|
cause: { message: "Denied by the corpus policy." },
|
|
910
758
|
}),
|
|
911
759
|
];
|
|
912
|
-
/**
|
|
913
|
-
* Family 12 — conflict codes (§13.2): the corpus Adapter raising `A2008`,
|
|
914
|
-
* `A2013` and `A2014` from an `update.unique` → 409, `cause.issues` absent.
|
|
915
|
-
*/
|
|
916
760
|
const CONFLICT = [
|
|
917
761
|
[CORPUS_TRIGGER.conflict, "A2008"],
|
|
918
762
|
[CORPUS_TRIGGER.concurrentModification, "A2013"],
|
|
@@ -922,7 +766,6 @@ const CONFLICT = [
|
|
|
922
766
|
code,
|
|
923
767
|
cause: { message: `The corpus Adapter answered ${code}.` },
|
|
924
768
|
}));
|
|
925
|
-
/** The one `samples` record in wire form (P1): what both encoders must write. */
|
|
926
769
|
const SAMPLE_WIRE = {
|
|
927
770
|
id: "s1",
|
|
928
771
|
count: "9007199254740993",
|
|
@@ -930,7 +773,6 @@ const SAMPLE_WIRE = {
|
|
|
930
773
|
amount: "12.50",
|
|
931
774
|
blob: "/wAQ",
|
|
932
775
|
};
|
|
933
|
-
/** A `samples` create whose `blob` is `blob`. */
|
|
934
776
|
const sampleCreate = (blob, requestId) => routeRequest("samples", "create.one", {
|
|
935
777
|
data: {
|
|
936
778
|
count: "1",
|
|
@@ -939,13 +781,6 @@ const sampleCreate = (blob, requestId) => routeRequest("samples", "create.one",
|
|
|
939
781
|
blob,
|
|
940
782
|
},
|
|
941
783
|
}, requestId);
|
|
942
|
-
/**
|
|
943
|
-
* Family 13 — wire scalars (P1, C1): a result carrying a `bigint`, a `Date`, a
|
|
944
|
-
* `Decimal` and a `Uint8Array` is written as their exact wire strings; a
|
|
945
|
-
* request `bytes` value whose padding carries non-zero bits is refused at the
|
|
946
|
-
* field, and its canonical spelling accepted. (The unencodable result is
|
|
947
|
-
* family 10's `A3003`.)
|
|
948
|
-
*/
|
|
949
784
|
const WIRE_SCALARS = [
|
|
950
785
|
answered("wire: a result's bigint, datetime, decimal and bytes in wire form → 200 A1000", routeRequest("samples", "find.unique", { where: { id: "s1" } }, "corpus-wire-result"), 200, 1, { data: SAMPLE_WIRE, code: "A1000", cause: null }),
|
|
951
786
|
answered('wire: bytes "AB==" (non-zero pad bits) → 422 A2004 / V1003, no Adapter call', sampleCreate("AB==", "corpus-wire-pad-bits"), 422, 0, {
|
|
@@ -964,13 +799,6 @@ const WIRE_SCALARS = [
|
|
|
964
799
|
}),
|
|
965
800
|
answered('wire: bytes "AA==" (canonical) → 201 A1002', sampleCreate("AA==", "corpus-wire-canonical"), 201, 1, { data: SAMPLE_WIRE, code: "A1002", cause: null }),
|
|
966
801
|
];
|
|
967
|
-
/**
|
|
968
|
-
* Family 14 — correlation (Q13): `Aventara-Request-Id` is adopted when it is
|
|
969
|
-
* 1–128 visible ASCII characters and echoed (every row above sends one and
|
|
970
|
-
* states its echo, refusals included); otherwise it is ignored — a fresh id
|
|
971
|
-
* is minted, whose value no row can state — and the request is never refused
|
|
972
|
-
* over it.
|
|
973
|
-
*/
|
|
974
802
|
const CORRELATION = [
|
|
975
803
|
answered("correlation: a 128-character id is adopted and echoed", corpusRequest("find/many", {}, "x".repeat(128)), 200, 1, { data: [CORPUS_AUTHOR], code: "A1000", cause: null }),
|
|
976
804
|
...[
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/testing",
|
|
3
|
-
"version": "0.1.0-pilot.
|
|
3
|
+
"version": "0.1.0-pilot.2",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Conformance and extension testing utilities for Aventara.",
|
|
6
6
|
"type": "module",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"LICENSE-ADDITIONAL-PERMISSION.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@aventara/core": "0.1.0-pilot.
|
|
23
|
+
"@aventara/core": "0.1.0-pilot.2"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"build": "tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
|
|
30
30
|
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
|
|
31
31
|
"test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
|
|
32
32
|
"lint": "biome lint src"
|