@hypequery/protocol-conformance 0.10.1 → 0.10.3
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/README.md +31 -0
- package/dist/bin/conformance.js +9 -0
- package/dist/family-expectations.d.ts +9 -0
- package/dist/family-expectations.d.ts.map +1 -0
- package/dist/family-expectations.js +44 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +10 -0
- package/fixtures/README.md +48 -0
- package/fixtures/fuzz-seeds-v1/README.md +2 -0
- package/fixtures/fuzz-seeds-v1/identifiers.json +17 -0
- package/fixtures/manifest.json +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,21 @@ hypequery-protocol-conformance run \
|
|
|
35
35
|
hypequery-protocol-conformance list
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
`--families` selects cases but deliberately permits partial adapters. Release
|
|
39
|
+
gates should also pass `--expect-families` with the complete expected adapter
|
|
40
|
+
family set. The run then fails during the handshake if a family was added or
|
|
41
|
+
dropped without updating the gate. When both options are present, their family
|
|
42
|
+
sets must match so the gate cannot assert a family while filtering out its
|
|
43
|
+
cases. Every expected family must also retain at least one case after
|
|
44
|
+
`--skip-fuzz` or `--only-fuzz` filtering:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
hypequery-protocol-conformance run \
|
|
48
|
+
--families tagged-values-v1,identifiers-v1 \
|
|
49
|
+
--expect-families tagged-values-v1,identifiers-v1 \
|
|
50
|
+
-- python -m hypequery.protocol.adapter
|
|
51
|
+
```
|
|
52
|
+
|
|
38
53
|
Exit code `0` means every case passed, `1` means conformance failures, and `2` means the runner or adapter protocol could not be set up correctly.
|
|
39
54
|
|
|
40
55
|
## Adapter shape
|
|
@@ -51,6 +66,22 @@ The exported `createStdioAdapter` helper handles the loop. Your handler maps `(f
|
|
|
51
66
|
|
|
52
67
|
The package includes a pinned fixture snapshot, so a pinned package version is also a pinned conformance target. Pass `--fixtures` to test local or newer specifications.
|
|
53
68
|
|
|
69
|
+
## Repository gates
|
|
70
|
+
|
|
71
|
+
With Node.js, pnpm, and `uv` installed, build the workspaces and run the
|
|
72
|
+
TypeScript reference, SQL-portability, and Python adapters against the same
|
|
73
|
+
local fixtures:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm build
|
|
77
|
+
pnpm conformance
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The Python leg asserts its exact announced family set, so removing a supported
|
|
81
|
+
family cannot turn cases into a green “not run” result. See the [fixture update
|
|
82
|
+
runbook](../../specs/security-protocol/fixtures/README.md) when changing a
|
|
83
|
+
fixture family or adding an implementation.
|
|
84
|
+
|
|
54
85
|
## License
|
|
55
86
|
|
|
56
87
|
Apache-2.0.
|
package/dist/bin/conformance.js
CHANGED
|
@@ -57,12 +57,21 @@ async function main() {
|
|
|
57
57
|
const families = typeof options.families === 'string'
|
|
58
58
|
? options.families.split(',').map((f) => f.trim()).filter(Boolean)
|
|
59
59
|
: undefined;
|
|
60
|
+
const expectedFamiliesOption = options['expect-families'];
|
|
61
|
+
if (expectedFamiliesOption === true) {
|
|
62
|
+
process.stderr.write('error: --expect-families requires a comma-separated value\n');
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
const expectedFamilies = typeof expectedFamiliesOption === 'string'
|
|
66
|
+
? expectedFamiliesOption.split(',').map((f) => f.trim()).filter(Boolean)
|
|
67
|
+
: undefined;
|
|
60
68
|
let summary;
|
|
61
69
|
try {
|
|
62
70
|
summary = await runConformance({
|
|
63
71
|
adapterCommand,
|
|
64
72
|
fixturesDir,
|
|
65
73
|
families,
|
|
74
|
+
expectedFamilies,
|
|
66
75
|
timeoutMs: typeof options['timeout-ms'] === 'string' ? Number(options['timeout-ms']) : undefined,
|
|
67
76
|
skipFuzz: options['skip-fuzz'] === true,
|
|
68
77
|
onlyFuzz: options['only-fuzz'] === true,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Validates a release gate's independently maintained fixture-family list. */
|
|
2
|
+
export declare function validateExpectedFamilies(expectedFamilies: readonly string[], manifestFamilies: ReadonlySet<string>): void;
|
|
3
|
+
/** Fails unless an adapter announces exactly the release gate's family set. */
|
|
4
|
+
export declare function assertExpectedFamilies(announcedFamilies: readonly string[], expectedFamilies: readonly string[] | undefined): void;
|
|
5
|
+
/** Prevents a release gate from asserting families while filtering some out. */
|
|
6
|
+
export declare function assertSelectedFamilies(selectedFamilies: readonly string[] | undefined, expectedFamilies: readonly string[]): void;
|
|
7
|
+
/** Ensures filtering cannot leave an asserted family without executable cases. */
|
|
8
|
+
export declare function assertExpectedFamiliesHaveCases(expectedFamilies: readonly string[], effectiveCaseFamilies: ReadonlySet<string>): void;
|
|
9
|
+
//# sourceMappingURL=family-expectations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"family-expectations.d.ts","sourceRoot":"","sources":["../src/family-expectations.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,wBAAwB,CACtC,gBAAgB,EAAE,SAAS,MAAM,EAAE,EACnC,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,GACpC,IAAI,CAWN;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,SAAS,MAAM,EAAE,EACpC,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAC9C,IAAI,CAaN;AAED,gFAAgF;AAChF,wBAAgB,sBAAsB,CACpC,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAC/C,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAClC,IAAI,CAaN;AAED,kFAAkF;AAClF,wBAAgB,+BAA+B,CAC7C,gBAAgB,EAAE,SAAS,MAAM,EAAE,EACnC,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,GACzC,IAAI,CAON"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Validates a release gate's independently maintained fixture-family list. */
|
|
2
|
+
export function validateExpectedFamilies(expectedFamilies, manifestFamilies) {
|
|
3
|
+
if (expectedFamilies.some((family) => typeof family !== 'string' || family.length === 0)
|
|
4
|
+
|| new Set(expectedFamilies).size !== expectedFamilies.length) {
|
|
5
|
+
throw new Error('expected families must be unique non-empty strings');
|
|
6
|
+
}
|
|
7
|
+
const absent = expectedFamilies.filter((family) => !manifestFamilies.has(family)).sort();
|
|
8
|
+
if (absent.length > 0) {
|
|
9
|
+
throw new Error(`expected families are absent from the manifest: ${absent.join(', ')}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Fails unless an adapter announces exactly the release gate's family set. */
|
|
13
|
+
export function assertExpectedFamilies(announcedFamilies, expectedFamilies) {
|
|
14
|
+
if (!expectedFamilies)
|
|
15
|
+
return;
|
|
16
|
+
const announced = [...announcedFamilies].sort();
|
|
17
|
+
const expected = [...expectedFamilies].sort();
|
|
18
|
+
if (announced.length !== expected.length
|
|
19
|
+
|| announced.some((family, index) => family !== expected[index])) {
|
|
20
|
+
throw new Error(`adapter families did not match expectation: expected ${expected.join(', ') || '(none)'}; `
|
|
21
|
+
+ `announced ${announced.join(', ') || '(none)'}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Prevents a release gate from asserting families while filtering some out. */
|
|
25
|
+
export function assertSelectedFamilies(selectedFamilies, expectedFamilies) {
|
|
26
|
+
if (!selectedFamilies)
|
|
27
|
+
return;
|
|
28
|
+
const selected = [...selectedFamilies].sort();
|
|
29
|
+
const expected = [...expectedFamilies].sort();
|
|
30
|
+
if (selected.length !== expected.length
|
|
31
|
+
|| selected.some((family, index) => family !== expected[index])) {
|
|
32
|
+
throw new Error(`selected families did not match expected families: expected `
|
|
33
|
+
+ `${expected.join(', ') || '(none)'}; selected ${selected.join(', ') || '(none)'}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Ensures filtering cannot leave an asserted family without executable cases. */
|
|
37
|
+
export function assertExpectedFamiliesHaveCases(expectedFamilies, effectiveCaseFamilies) {
|
|
38
|
+
const untested = expectedFamilies
|
|
39
|
+
.filter((family) => !effectiveCaseFamilies.has(family))
|
|
40
|
+
.sort();
|
|
41
|
+
if (untested.length > 0) {
|
|
42
|
+
throw new Error(`expected families have no selected cases: ${untested.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface RunConformanceOptions {
|
|
|
4
4
|
readonly fixturesDir?: string;
|
|
5
5
|
/** Restrict to these families (intersected with what the adapter announces). */
|
|
6
6
|
readonly families?: readonly string[];
|
|
7
|
+
/** Fail setup unless the adapter announces exactly these fixture families. */
|
|
8
|
+
readonly expectedFamilies?: readonly string[];
|
|
7
9
|
readonly timeoutMs?: number;
|
|
8
10
|
readonly skipFuzz?: boolean;
|
|
9
11
|
readonly onlyFuzz?: boolean;
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAcA,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAyJD,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6HxF"}
|
package/dist/runner.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { compareCase } from './compare.js';
|
|
7
|
+
import { assertExpectedFamilies, assertExpectedFamiliesHaveCases, assertSelectedFamilies, validateExpectedFamilies, } from './family-expectations.js';
|
|
7
8
|
import { createJsonLoader, loadManifest, resolveFixturesDir } from './fs.js';
|
|
8
9
|
import { enumerateAllCases } from './manifest.js';
|
|
9
10
|
import { CONFORMANCE_MANIFEST_VERSION, CONFORMANCE_PROTOCOL_VERSION, } from './types.js';
|
|
@@ -145,6 +146,10 @@ export async function runConformance(options) {
|
|
|
145
146
|
const fixturesDir = resolveFixturesDir(options.fixturesDir);
|
|
146
147
|
const manifest = loadManifest(fixturesDir);
|
|
147
148
|
const loadJson = createJsonLoader(fixturesDir);
|
|
149
|
+
if (options.expectedFamilies) {
|
|
150
|
+
validateExpectedFamilies(options.expectedFamilies, new Set(manifest.families.map((family) => family.name)));
|
|
151
|
+
assertSelectedFamilies(options.families, options.expectedFamilies);
|
|
152
|
+
}
|
|
148
153
|
const allCases = enumerateAllCases(manifest, loadJson);
|
|
149
154
|
const hostModelFamilies = new Set(allCases
|
|
150
155
|
.filter((c) => {
|
|
@@ -163,6 +168,9 @@ export async function runConformance(options) {
|
|
|
163
168
|
const requested = new Set(options.families);
|
|
164
169
|
cases = cases.filter((c) => requested.has(c.family));
|
|
165
170
|
}
|
|
171
|
+
if (options.expectedFamilies) {
|
|
172
|
+
assertExpectedFamiliesHaveCases(options.expectedFamilies, new Set(cases.map((conformanceCase) => conformanceCase.family)));
|
|
173
|
+
}
|
|
166
174
|
// The handshake covers process spawn plus the adapter's first write, which
|
|
167
175
|
// can be slow under load; it gets a generous timeout independent of the
|
|
168
176
|
// per-case timeout so a busy machine never mistakes startup for a hang.
|
|
@@ -171,6 +179,7 @@ export async function runConformance(options) {
|
|
|
171
179
|
let hello;
|
|
172
180
|
try {
|
|
173
181
|
hello = await connection.handshake(handshakeTimeoutMs, hostModelFamilies);
|
|
182
|
+
assertExpectedFamilies(hello.families, options.expectedFamilies);
|
|
174
183
|
}
|
|
175
184
|
catch (error) {
|
|
176
185
|
connection.kill();
|
|
@@ -228,6 +237,7 @@ export async function runConformance(options) {
|
|
|
228
237
|
connection = new AdapterConnection(options.adapterCommand);
|
|
229
238
|
try {
|
|
230
239
|
hello = await connection.handshake(handshakeTimeoutMs, hostModelFamilies);
|
|
240
|
+
assertExpectedFamilies(hello.families, options.expectedFamilies);
|
|
231
241
|
}
|
|
232
242
|
catch {
|
|
233
243
|
// A replacement that cannot even complete a handshake ends the run
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Conformance fixture update runbook
|
|
2
|
+
|
|
3
|
+
This directory is the language-neutral conformance source of truth. A fixture
|
|
4
|
+
change is a protocol change, not an implementation-specific test edit.
|
|
5
|
+
|
|
6
|
+
## Before editing
|
|
7
|
+
|
|
8
|
+
1. Identify the RFC and extension version that owns the behavior. Accepted
|
|
9
|
+
grammar, limits, comparison rules, or canonical bytes cannot be changed in
|
|
10
|
+
place; introduce a new extension version when the RFC requires one.
|
|
11
|
+
2. Decide whether the case is a success, rejection, identity, portability, or
|
|
12
|
+
deterministic fuzz case. Rejection cases must use the stable code owned by
|
|
13
|
+
their manifest family.
|
|
14
|
+
3. Keep fixture IDs unique and inputs deterministic. Never put credentials,
|
|
15
|
+
customer data, nondeterministic timestamps, or host-specific paths in the
|
|
16
|
+
corpus.
|
|
17
|
+
|
|
18
|
+
## Update workflow
|
|
19
|
+
|
|
20
|
+
1. Edit the owning family files and its README. Add new files or fuzz targets
|
|
21
|
+
to `manifest.json`; do not bypass the manifest.
|
|
22
|
+
2. Update the TypeScript reference implementation and every implementation
|
|
23
|
+
that already announces the family. A family may be implemented in a later
|
|
24
|
+
stacked PR, but no release gate may silently drop an announced family.
|
|
25
|
+
3. When an adapter gains a family, update its exact `--expect-families` list in
|
|
26
|
+
the same PR. For Python, that list is owned by `conformance:python` in the
|
|
27
|
+
root `package.json`.
|
|
28
|
+
4. Add a Changeset whenever the bundled `@hypequery/protocol-conformance`
|
|
29
|
+
fixture snapshot or runner behavior changes.
|
|
30
|
+
5. Rebuild the conformance package after fixture edits; its build copies the
|
|
31
|
+
current corpus into the published package.
|
|
32
|
+
|
|
33
|
+
## Verification
|
|
34
|
+
|
|
35
|
+
From the repository root:
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
pnpm --filter @hypequery/protocol build
|
|
39
|
+
pnpm --filter @hypequery/datasets build
|
|
40
|
+
pnpm --filter @hypequery/protocol-conformance build
|
|
41
|
+
pnpm --filter @hypequery/protocol-conformance test
|
|
42
|
+
pnpm conformance
|
|
43
|
+
uv run --project python/hypequery --frozen pytest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Review the report's implementation, announced families, hostile-object suite,
|
|
47
|
+
and not-run count—not only its exit status. CI release gates must use an exact
|
|
48
|
+
family assertion for partial adapters.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
These adversarial inputs replay on every conformance run under RFC 0012. An implementation may accept a seed within documented limits or reject it with a stable `HQ_*` code; it must never crash, hang, partially execute input, or allocate without bounds.
|
|
4
4
|
|
|
5
5
|
- `value-sources.json` targets duplicate-aware JSON decoding and tagged values.
|
|
6
|
+
- `identifiers.json` targets Unicode ambiguity, reserved names, separators,
|
|
7
|
+
validation limits, and non-string identifier inputs.
|
|
6
8
|
- `structured-values.json` targets selected structural validator families.
|
|
7
9
|
- `sql-expressions.json` targets the SQL portability compiler.
|
|
8
10
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "id": "identifier-null", "mode": "simple", "value": null },
|
|
3
|
+
{ "id": "identifier-control-character", "mode": "simple", "value": "order\u0000id" },
|
|
4
|
+
{ "id": "identifier-sql-shaped", "mode": "simple", "value": "orders;DROP_TABLE_users" },
|
|
5
|
+
{ "id": "identifier-cyrillic-confusable", "mode": "simple", "value": "аdmin" },
|
|
6
|
+
{ "id": "identifier-combining-mark", "mode": "simple", "value": "café" },
|
|
7
|
+
{ "id": "identifier-fullwidth-dot", "mode": "qualified", "value": "orders.customer" },
|
|
8
|
+
{ "id": "identifier-lone-high-surrogate", "mode": "simple", "value": "\ud800" },
|
|
9
|
+
{ "id": "identifier-lone-low-surrogate", "mode": "simple", "value": "\udc00" },
|
|
10
|
+
{ "id": "identifier-surrogate-pair", "mode": "simple", "value": "\ud83d\ude00" },
|
|
11
|
+
{ "id": "identifier-reserved-mixed-case", "mode": "simple", "value": "__HyPeQuErY_admin" },
|
|
12
|
+
{ "id": "identifier-qualified-leading-dot", "mode": "qualified", "value": ".orders" },
|
|
13
|
+
{ "id": "identifier-qualified-trailing-dot", "mode": "qualified", "value": "orders." },
|
|
14
|
+
{ "id": "identifier-qualified-repeated-dot", "mode": "qualified", "value": "orders..customer" },
|
|
15
|
+
{ "id": "identifier-long-segment", "mode": "simple", "generator": { "type": "repeat-string", "value": "a", "count": 10000 } },
|
|
16
|
+
{ "id": "identifier-many-segments", "mode": "qualified", "generator": { "type": "qualified-segments", "segment": "a", "count": 1000 } }
|
|
17
|
+
]
|
package/fixtures/manifest.json
CHANGED