@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.67.0",
3
+ "version": "1.67.1",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- const SPEC_RESERVED = ['structured-output', 'discriminator-enum', 'long-context', 'reasoning', 'function-calling'];
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
- // Manually set Accept-Encoding so undici returns the raw compressed
206
- // bytes (it only auto-decompresses encodings it negotiated itself).
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 compressedBytes = Buffer.from(await res.arrayBuffer());
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(compressedBytes.length).toBeGreaterThan(0);
233
+ expect(responseBytes.length).toBeGreaterThan(0);
224
234
  continue;
225
235
  }
226
- const decoded = Buffer.from(decode(compressedBytes));
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
- const reg = await driver.post('/v1/webhooks', { url: 'http://127.0.0.1:65535/' });
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
- const reg = await driver.post('/v1/webhooks', { url: receiver.url });
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.