@openwop/openwop-conformance 1.67.0 → 1.67.1
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/package.json +1 -1
- package/src/scenarios/connection-pack-reach-exclusive.test.ts +6 -0
- package/src/scenarios/model-capability-substituted.test.ts +10 -2
- package/src/scenarios/run-transport-economy.test.ts +28 -5
- package/src/scenarios/webhook-negative.test.ts +8 -1
- package/src/scenarios/webhook-signed-delivery.test.ts +9 -1
package/package.json
CHANGED
|
@@ -36,6 +36,12 @@ type Manifest = Record<string, unknown> & { provider: Record<string, unknown> };
|
|
|
36
36
|
function withReach(reach: Record<string, unknown>): Manifest {
|
|
37
37
|
const m = JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as Manifest;
|
|
38
38
|
m.provider.reach = reach;
|
|
39
|
+
// openapi reach REQUIRES provider.apiHosts (RFC 0120 §A, schema
|
|
40
|
+
// provider/allOf/0/then/required); other modes MUST NOT carry it. Set it here
|
|
41
|
+
// so the manifest validates against a live corpus root, not just the vendored
|
|
42
|
+
// snapshot. (Suite defect, fixed 2026-08-09.)
|
|
43
|
+
if ('openapi' in reach) m.provider.apiHosts = ['api.github.com'];
|
|
44
|
+
else delete m.provider.apiHosts;
|
|
39
45
|
return m;
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -79,7 +79,15 @@ describe.skipIf(HTTP_SKIP)('model-capability-substituted: advertisement shape (R
|
|
|
79
79
|
Array.isArray(mc.advertised),
|
|
80
80
|
driver.describe('RFCS/0031-envelope-variants-and-model-capabilities.md §E', 'modelCapabilities.advertised MUST be an array of capability identifiers'),
|
|
81
81
|
).toBe(true);
|
|
82
|
-
|
|
82
|
+
// RFC 0031 §C's five, PLUS the four modality identifiers RFC 0055 §A
|
|
83
|
+
// promoted into the formal vocabulary (Accepted 2026-05-26; registered in
|
|
84
|
+
// `capabilities.schema.json` advertised.description + the RFC 0031 §C
|
|
85
|
+
// table). A host advertising `vision-input` etc. is conformant; the
|
|
86
|
+
// pre-0055 five-element list rejected it. (Suite defect, fixed 2026-08-09.)
|
|
87
|
+
const SPEC_RESERVED = [
|
|
88
|
+
'structured-output', 'discriminator-enum', 'long-context', 'reasoning', 'function-calling',
|
|
89
|
+
'vision-input', 'audio-input', 'audio-output', 'image-output',
|
|
90
|
+
];
|
|
83
91
|
for (const id of mc.advertised as unknown[]) {
|
|
84
92
|
expect(typeof id, 'each advertised identifier MUST be a string').toBe('string');
|
|
85
93
|
const idStr = String(id);
|
|
@@ -89,7 +97,7 @@ describe.skipIf(HTTP_SKIP)('model-capability-substituted: advertisement shape (R
|
|
|
89
97
|
isReserved || isHostExt,
|
|
90
98
|
driver.describe(
|
|
91
99
|
'RFCS/0031-envelope-variants-and-model-capabilities.md §C',
|
|
92
|
-
`advertised identifier "${idStr}" MUST be spec-reserved (structured-output, discriminator-enum, long-context, reasoning, function-calling) or match the x-host-<host>-<key> extension pattern`,
|
|
100
|
+
`advertised identifier "${idStr}" MUST be spec-reserved (RFC 0031: structured-output, discriminator-enum, long-context, reasoning, function-calling; RFC 0055: vision-input, audio-input, audio-output, image-output) or match the x-host-<host>-<key> extension pattern`,
|
|
93
101
|
),
|
|
94
102
|
).toBe(true);
|
|
95
103
|
}
|
|
@@ -202,8 +202,18 @@ describe.skipIf(HTTP_SKIP)('run-transport-economy: Content-Encoding round-trips
|
|
|
202
202
|
: undefined;
|
|
203
203
|
|
|
204
204
|
for (const enc of encodings) {
|
|
205
|
-
//
|
|
206
|
-
//
|
|
205
|
+
// Set Accept-Encoding to the target so the host tags Content-Encoding.
|
|
206
|
+
// NOTE (suite defect, fixed 2026-08-09): the prior code assumed a manual
|
|
207
|
+
// Accept-Encoding makes undici return RAW compressed bytes. It does not —
|
|
208
|
+
// Node's global fetch (undici) AUTO-DECOMPRESSES `gzip`/`br`/`zstd`
|
|
209
|
+
// regardless of who set the header, so `res.arrayBuffer()` already yields
|
|
210
|
+
// the identity body and feeding it to `gunzipSync` throws
|
|
211
|
+
// "Decompression failed". (`zstd` passed only because undici did not yet
|
|
212
|
+
// recognise it.) We therefore decode-tolerantly: attempt the declared
|
|
213
|
+
// decode, and if it throws OR the bytes already equal identity, treat the
|
|
214
|
+
// body as already-decompressed by undici. The Content-Encoding header
|
|
215
|
+
// assertion below still proves the host advertised and tagged the
|
|
216
|
+
// encoding; the byte-compare proves the round-trip is lossless.
|
|
207
217
|
const res = await fetch(url, { headers: { ...auth, 'Accept-Encoding': enc } });
|
|
208
218
|
expect(res.status).toBe(200);
|
|
209
219
|
const contentEncoding = res.headers.get('content-encoding');
|
|
@@ -215,15 +225,28 @@ describe.skipIf(HTTP_SKIP)('run-transport-economy: Content-Encoding round-trips
|
|
|
215
225
|
),
|
|
216
226
|
).toBe(enc);
|
|
217
227
|
|
|
218
|
-
const
|
|
228
|
+
const responseBytes = Buffer.from(await res.arrayBuffer());
|
|
219
229
|
const decode = enc === 'gzip' ? gunzipSync : enc === 'br' ? brotliDecompressSync : zstdDecode;
|
|
220
230
|
if (!decode) {
|
|
221
231
|
// zstd decode unavailable in this runtime: negotiation already
|
|
222
232
|
// asserted above; skip only the byte-compare for this encoding.
|
|
223
|
-
expect(
|
|
233
|
+
expect(responseBytes.length).toBeGreaterThan(0);
|
|
224
234
|
continue;
|
|
225
235
|
}
|
|
226
|
-
|
|
236
|
+
// Tolerate undici auto-decompression: if the body already equals identity,
|
|
237
|
+
// it was decoded in transit; otherwise it is raw and we decode it here.
|
|
238
|
+
let decoded: Buffer;
|
|
239
|
+
if (responseBytes.equals(identityBytes)) {
|
|
240
|
+
decoded = responseBytes;
|
|
241
|
+
} else {
|
|
242
|
+
try {
|
|
243
|
+
decoded = Buffer.from(decode(responseBytes));
|
|
244
|
+
} catch {
|
|
245
|
+
// Not decompressible ⇒ already decompressed by undici to something
|
|
246
|
+
// other than identity ⇒ genuine mismatch; surface it as the raw body.
|
|
247
|
+
decoded = responseBytes;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
227
250
|
expect(
|
|
228
251
|
decoded.equals(identityBytes),
|
|
229
252
|
driver.describe(
|
|
@@ -37,7 +37,14 @@ describe('webhook-negative: SSRF guard rejects private destinations', () => {
|
|
|
37
37
|
console.warn('[webhook-negative] host does not advertise webhook support; skipping');
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
// Spec-complete except for the private URL, so validation passes and the
|
|
41
|
+
// request reaches the SSRF guard. `{ url }` alone 400s `validation_error`
|
|
42
|
+
// (missing `events`) before the guard runs. (Suite defect, fixed 2026-08-09.)
|
|
43
|
+
const reg = await driver.post('/v1/webhooks', {
|
|
44
|
+
url: 'http://127.0.0.1:65535/',
|
|
45
|
+
events: ['run.completed'],
|
|
46
|
+
tenantId: 'conformance-tenant',
|
|
47
|
+
});
|
|
41
48
|
if (reg.status === 201) {
|
|
42
49
|
// Host accepted — SSRF guard not implemented or bypassed.
|
|
43
50
|
// Soft-skip; this is acceptable per spec.
|
|
@@ -89,7 +89,15 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
|
89
89
|
activeServer = receiver.server;
|
|
90
90
|
|
|
91
91
|
// Register the webhook.
|
|
92
|
-
|
|
92
|
+
// webhooks.md §Register: `events` + `tenantId` are REQUIRED (empty events → 400).
|
|
93
|
+
// The pre-fix `{ url }`-only body 400s on validation before delivery can occur.
|
|
94
|
+
// (Suite defect, fixed 2026-08-09.) The delivered run's tenant must match this
|
|
95
|
+
// subscription's tenantId — verified end-to-end against the openwop-app host.
|
|
96
|
+
const reg = await driver.post('/v1/webhooks', {
|
|
97
|
+
url: receiver.url,
|
|
98
|
+
events: ['run.completed'],
|
|
99
|
+
tenantId: 'conformance-tenant',
|
|
100
|
+
});
|
|
93
101
|
|
|
94
102
|
// SSRF guard skip: if the host rejects loopback destinations,
|
|
95
103
|
// honor the operator contract and skip rather than fail.
|