@openwop/openwop-conformance 1.135.0 → 1.135.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/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/schemas/capabilities.schema.json +10 -0
- package/src/scenarios/a2a-version-negotiation.test.ts +4 -2
- package/src/scenarios/discovery.test.ts +1 -2
- package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -4
- package/src/scenarios/mcp-version-negotiation.test.ts +4 -1
- package/src/scenarios/media-url-inline-cap.test.ts +3 -5
- package/src/scenarios/metric-emission.test.ts +3 -4
- package/src/scenarios/node-module-required-capabilities-shape.test.ts +2 -1
- package/src/scenarios/otel-emission-grpc.test.ts +3 -6
- package/src/scenarios/otel-emission.test.ts +2 -1
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +2 -1
- package/src/scenarios/otel-trace-propagation.test.ts +2 -1
- package/src/scenarios/self-hosted-runner.test.ts +9 -17
- package/src/scenarios/versioned-composition-profiles.test.ts +18 -0
- package/src/scenarios/webhook-negative.test.ts +3 -3
- package/src/scenarios/webhook-sig-algorithm.test.ts +2 -1
- package/src/scenarios/webhook-signed-delivery.test.ts +3 -2
- package/src/scenarios/workflow-chain-host-expansion.test.ts +3 -6
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.135.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.135.2",
|
|
4
|
+
"corpusCommit": "101bf981f0431710e81a3e7ac108c5b6b49cddd9"
|
|
5
5
|
}
|
|
@@ -3332,6 +3332,16 @@
|
|
|
3332
3332
|
"type": "boolean",
|
|
3333
3333
|
"description": "Host advertises a client-side MCP surface (ctx.mcp.*). See spec/v1/mcp-integration.md."
|
|
3334
3334
|
},
|
|
3335
|
+
"serverUrls": {
|
|
3336
|
+
"type": "array",
|
|
3337
|
+
"minItems": 1,
|
|
3338
|
+
"uniqueItems": true,
|
|
3339
|
+
"items": {
|
|
3340
|
+
"type": "string",
|
|
3341
|
+
"minLength": 1
|
|
3342
|
+
},
|
|
3343
|
+
"description": "Where the host's MCP server mount(s) answer JSON-RPC \u2014 an absolute URL or a path relative to the discovery origin (S25: a path is joined to the base URL, an absolute URL is used as-is). The v1.0 conformance baseline (`mcp-discoverability.test.ts`, spec/v1/mcp-integration.md \u00a7Conformance + interop) REQUIRES a non-empty `serverUrls` whenever `supported: true`, and since suite 1.134.0 every server-side scenario POSTs to `serverUrls[0]`. Required by the scenario and absent from this schema until 2026-08-17 (S27) \u2014 the second sibling host emitted it and measured the contradiction."
|
|
3344
|
+
},
|
|
3335
3345
|
"serverMount": {
|
|
3336
3346
|
"type": "object",
|
|
3337
3347
|
"description": "Server-side MCP composition (workflow IS an MCP server). When supported, the host mounts an MCP endpoint and routes inbound tools/call, resources/read, prompts/get into workflows. Inbound MUST be treated as `trustBoundary: 'untrusted'`.",
|
|
@@ -28,6 +28,7 @@ import { driver } from '../lib/driver.js';
|
|
|
28
28
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
29
29
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
30
30
|
import { getA2AFakePeer } from '../lib/a2a-fake-peer.js';
|
|
31
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Callback-shaped: the host issues A2A calls to the suite's fake peer, which records the negotiated version header.
|
|
@@ -153,9 +154,10 @@ describe('RFC 0152 §B — A2A version negotiation', () => {
|
|
|
153
154
|
});
|
|
154
155
|
if (drive.status === 404 || drive.status === 403) return;
|
|
155
156
|
expect(drive.status >= 400, driver.describe('RFCS/0152 §B', 'an unsupported version MUST fail')).toBe(true);
|
|
156
|
-
|
|
157
|
+
// S28: canonical envelope is FLAT (`error: "<code>"`, error-envelope.schema.json);
|
|
158
|
+
// the nested `error.code` is tolerated by the lib only through the deprecation window.
|
|
157
159
|
expect(
|
|
158
|
-
|
|
160
|
+
readErrorCode(drive.json),
|
|
159
161
|
driver.describe(
|
|
160
162
|
'RFCS/0152 §B',
|
|
161
163
|
'the upstream version error MUST be projected through the canonical OpenWOP interop error ' +
|
|
@@ -183,8 +183,7 @@ const AUTH_SCOPED_PROFILE = 'openwop-discovery-auth-scoped';
|
|
|
183
183
|
|
|
184
184
|
async function readDiscoveryCaps(): Promise<DiscoveryCaps | undefined> {
|
|
185
185
|
const disco = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
186
|
-
return (disco.json as {
|
|
187
|
-
?.discovery;
|
|
186
|
+
return (discoveryFamilies(disco.json) as { discovery?: DiscoveryCaps }).discovery;
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
function isAuthScopedAdvertised(disc: DiscoveryCaps | undefined): boolean {
|
|
@@ -22,13 +22,12 @@
|
|
|
22
22
|
import { describe, it, expect } from 'vitest';
|
|
23
23
|
import { softSkip } from '../lib/soft-skip.js';
|
|
24
24
|
import { driver } from '../lib/driver.js';
|
|
25
|
-
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
25
|
+
import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
26
26
|
|
|
27
27
|
async function isMcpClientSupported(): Promise<boolean> {
|
|
28
28
|
const disco = await driver.get('/.well-known/openwop');
|
|
29
|
-
const caps = (disco.json as {
|
|
30
|
-
|
|
31
|
-
return caps?.mcpClient?.supported === true;
|
|
29
|
+
const caps = discoveryFamilies(disco.json) as { mcpClient?: { supported?: boolean } };
|
|
30
|
+
return caps.mcpClient?.supported === true;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
describe('mcp-toolcall-redaction: capability advertisement contract', () => {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
29
30
|
import { seamAbsent } from '../lib/soft-skip.js';
|
|
30
31
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
31
32
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -154,8 +155,10 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
154
155
|
drive.status >= 400,
|
|
155
156
|
driver.describe('RFCS/0153 §B', 'an unsupported revision MUST fail rather than silently proceed'),
|
|
156
157
|
).toBe(true);
|
|
158
|
+
// S28: canonical envelope is FLAT (`error: "<code>"`, error-envelope.schema.json);
|
|
159
|
+
// the nested `error.code` is tolerated by the lib only through the deprecation window.
|
|
157
160
|
expect(
|
|
158
|
-
(drive.json
|
|
161
|
+
readErrorCode(drive.json),
|
|
159
162
|
driver.describe(
|
|
160
163
|
'RFCS/0153 §B',
|
|
161
164
|
'the upstream error MUST be projected through the canonical OpenWOP interop envelope — a ' +
|
|
@@ -32,7 +32,7 @@ import { readFileSync } from 'node:fs';
|
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { driver } from '../lib/driver.js';
|
|
34
34
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
35
|
-
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
35
|
+
import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
36
36
|
|
|
37
37
|
const MEDIA_KINDS = ['media.image', 'media.audio', 'media.file'] as const;
|
|
38
38
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
@@ -136,10 +136,8 @@ describe.skipIf(HTTP_SKIP)('media-url-inline-cap: advertisement shape (RFC 0055
|
|
|
136
136
|
// that emits a media.* envelope into a run (so no media payload appears).
|
|
137
137
|
const disc = await driver.get('/.well-known/openwop');
|
|
138
138
|
if (disc.status !== 200) return;
|
|
139
|
-
const caps = (disc.json as {
|
|
140
|
-
|
|
141
|
-
}).capabilities;
|
|
142
|
-
if (caps?.aiProviders?.maxInlineMediaBytes === undefined || caps.debugBundle?.supported !== true) {
|
|
139
|
+
const caps = discoveryFamilies(disc.json) as { aiProviders?: { maxInlineMediaBytes?: unknown }; debugBundle?: { supported?: unknown } };
|
|
140
|
+
if (caps.aiProviders?.maxInlineMediaBytes === undefined || caps.debugBundle?.supported !== true) {
|
|
143
141
|
return; // host doesn't serve media + export debug bundles — contract not exercisable
|
|
144
142
|
}
|
|
145
143
|
// Find a recent run and inspect its debug bundle for any media.* event.
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { softSkip } from '../lib/soft-skip.js';
|
|
23
23
|
import { driver } from '../lib/driver.js';
|
|
24
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
24
25
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
25
26
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
26
27
|
import { getCollector } from '../lib/otel-collector.js';
|
|
@@ -43,10 +44,8 @@ interface MetricsCaps {
|
|
|
43
44
|
async function metricsAdvertised(): Promise<MetricsCaps | null> {
|
|
44
45
|
try {
|
|
45
46
|
const disco = await driver.get('/.well-known/openwop');
|
|
46
|
-
const caps = (disco.json as {
|
|
47
|
-
|
|
48
|
-
}).capabilities;
|
|
49
|
-
return caps?.observability?.metrics ?? null;
|
|
47
|
+
const caps = discoveryFamilies(disco.json) as { observability?: { metrics?: MetricsCaps } };
|
|
48
|
+
return caps.observability?.metrics ?? null;
|
|
50
49
|
} catch {
|
|
51
50
|
return null;
|
|
52
51
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
/** RFC 0031 §C — spec-reserved capability identifiers. */
|
|
25
26
|
const RESERVED_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
@@ -58,7 +59,7 @@ beforeAll(async () => {
|
|
|
58
59
|
SKIP_REASON = 'discovery doc unreachable';
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
|
-
const caps = (disco.json as DiscoveryDoc
|
|
62
|
+
const caps = discoveryFamilies(disco.json) as NonNullable<DiscoveryDoc['capabilities']>;
|
|
62
63
|
if (caps.modelCapabilities?.supported !== true) {
|
|
63
64
|
SKIP_REASON = 'host does not advertise capabilities.modelCapabilities.supported: true';
|
|
64
65
|
return;
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { describe, it, expect } from 'vitest';
|
|
29
29
|
import { softSkip } from '../lib/soft-skip.js';
|
|
30
30
|
import { driver } from '../lib/driver.js';
|
|
31
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
31
32
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
32
33
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
33
34
|
import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
|
|
@@ -45,12 +46,8 @@ const FIXTURE = 'conformance-noop';
|
|
|
45
46
|
async function advertisesGrpcExport(): Promise<boolean> {
|
|
46
47
|
try {
|
|
47
48
|
const disco = await driver.get('/.well-known/openwop');
|
|
48
|
-
const caps = (disco.json as {
|
|
49
|
-
|
|
50
|
-
observability?: { otel?: { exportProtocols?: unknown } };
|
|
51
|
-
};
|
|
52
|
-
}).capabilities;
|
|
53
|
-
const protocols = caps?.observability?.otel?.exportProtocols;
|
|
49
|
+
const caps = discoveryFamilies(disco.json) as { observability?: { otel?: { exportProtocols?: unknown } } };
|
|
50
|
+
const protocols = caps.observability?.otel?.exportProtocols;
|
|
54
51
|
return Array.isArray(protocols) && protocols.includes('grpc');
|
|
55
52
|
} catch {
|
|
56
53
|
return false;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { describe, it, expect } from 'vitest';
|
|
27
27
|
import { softSkip } from '../lib/soft-skip.js';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
29
30
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
30
31
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
31
32
|
import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
|
|
@@ -43,7 +44,7 @@ const FIXTURE = 'conformance-noop';
|
|
|
43
44
|
async function isObservabilityAdvertised(): Promise<boolean> {
|
|
44
45
|
try {
|
|
45
46
|
const disco = await driver.get('/.well-known/openwop');
|
|
46
|
-
const caps = (disco.json as {
|
|
47
|
+
const caps = discoveryFamilies(disco.json) as { observability?: unknown };
|
|
47
48
|
return caps.observability !== undefined;
|
|
48
49
|
} catch {
|
|
49
50
|
return false;
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { describe, it, expect } from 'vitest';
|
|
37
37
|
import { softSkip } from '../lib/soft-skip.js';
|
|
38
38
|
import { driver } from '../lib/driver.js';
|
|
39
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
39
40
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
40
41
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
41
42
|
import { isScenarioOptedOut } from '../lib/env.js';
|
|
@@ -70,7 +71,7 @@ function makeTraceparent(): { header: string; traceId: string } {
|
|
|
70
71
|
async function isObservabilityAdvertised(): Promise<boolean> {
|
|
71
72
|
try {
|
|
72
73
|
const disco = await driver.get('/.well-known/openwop');
|
|
73
|
-
const caps = (disco.json as {
|
|
74
|
+
const caps = discoveryFamilies(disco.json) as { observability?: unknown };
|
|
74
75
|
return caps.observability !== undefined;
|
|
75
76
|
} catch {
|
|
76
77
|
return false;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { describe, it, expect } from 'vitest';
|
|
21
21
|
import { softSkip } from '../lib/soft-skip.js';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
23
24
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
24
25
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
25
26
|
import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
|
|
@@ -45,7 +46,7 @@ function makeTraceparent(): { header: string; traceId: string } {
|
|
|
45
46
|
async function isObservabilityAdvertised(): Promise<boolean> {
|
|
46
47
|
try {
|
|
47
48
|
const disco = await driver.get('/.well-known/openwop');
|
|
48
|
-
const caps = (disco.json as {
|
|
49
|
+
const caps = discoveryFamilies(disco.json) as { observability?: unknown };
|
|
49
50
|
return caps.observability !== undefined;
|
|
50
51
|
} catch {
|
|
51
52
|
return false;
|
|
@@ -35,6 +35,7 @@ import { driver } from '../lib/driver.js';
|
|
|
35
35
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
36
36
|
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
37
37
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
38
|
+
import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
|
|
38
39
|
|
|
39
40
|
const GATE = 'openwop-self-hosted-runner';
|
|
40
41
|
|
|
@@ -51,23 +52,14 @@ function readSchema(name: string): JsonSchema {
|
|
|
51
52
|
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as JsonSchema;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Read a boolean `retriable` flag from either `{retriable}` or `{error:{retriable}}`. */
|
|
65
|
-
function retriable(json: unknown): boolean | undefined {
|
|
66
|
-
const j = json as { retriable?: unknown; error?: { retriable?: unknown } };
|
|
67
|
-
if (typeof j?.retriable === 'boolean') return j.retriable;
|
|
68
|
-
if (j?.error && typeof j.error.retriable === 'boolean') return j.error.retriable;
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
55
|
+
// S28 (2026-08-17): the HTTP error envelope is READ through lib/error-envelope.ts —
|
|
56
|
+
// flat `{ error: "<code>", message, details.retriable }` per error-envelope.schema.json
|
|
57
|
+
// (S22), with the pre-S22 nested `{ error: { code, retriable } }` tolerated only through
|
|
58
|
+
// the deprecation window. The local readers this replaced accepted top-level `retriable`
|
|
59
|
+
// or nested `error.retriable` and never `details.retriable`, so a schema-valid host could
|
|
60
|
+
// not pass the retriable-flag leg at all (openwop-app H27 → S28).
|
|
61
|
+
const errCode = readErrorCode;
|
|
62
|
+
const retriable = readRetriable;
|
|
71
63
|
|
|
72
64
|
interface SelfHostedRunner {
|
|
73
65
|
supported?: boolean;
|
|
@@ -137,6 +137,24 @@ describe('RFC 0153 §A — MCP versioned discovery', () => {
|
|
|
137
137
|
).toBe(true);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it('the v1.0 baseline `serverUrls` (required by mcp-discoverability when supported:true) validates — path or absolute URL, non-empty, no duplicates', () => {
|
|
141
|
+
// S27 (2026-08-17): the schema was `additionalProperties: false` and did not
|
|
142
|
+
// list `serverUrls`, while mcp-discoverability.test.ts REQUIRED it — a host
|
|
143
|
+
// could not satisfy the scenario and the schema at once. The second sibling
|
|
144
|
+
// host measured the contradiction on its own advert.
|
|
145
|
+
expect(
|
|
146
|
+
validate({ supported: true, serverUrls: ['/v1/host/openwop-app/mcp'], protocolVersions: ['2026-07-28'] }),
|
|
147
|
+
JSON.stringify(validate.errors),
|
|
148
|
+
).toBe(true);
|
|
149
|
+
expect(
|
|
150
|
+
validate({ supported: true, serverUrls: ['https://example.test/canvasApi/v1/mcp'], protocolVersions: ['2024-11-05'], preferredVersion: '2024-11-05' }),
|
|
151
|
+
JSON.stringify(validate.errors),
|
|
152
|
+
).toBe(true);
|
|
153
|
+
expect(validate({ supported: true, serverUrls: [] }), 'empty serverUrls is the bare `supported: true` problem').toBe(false);
|
|
154
|
+
expect(validate({ supported: true, serverUrls: ['/a', '/a'] }), 'duplicates').toBe(false);
|
|
155
|
+
expect(validate({ supported: true, serverUrls: [''] }), 'an empty string is not a mount').toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
140
158
|
it('versions use MCP date form exactly', () => {
|
|
141
159
|
// MCP revisions ARE dates. `latest` or a non-padded month would let two
|
|
142
160
|
// hosts disagree about which revision they share while both validate.
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
import { describe, it, expect } from 'vitest';
|
|
24
24
|
import { driver } from '../lib/driver.js';
|
|
25
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
25
26
|
import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
|
|
26
27
|
|
|
27
28
|
async function isWebhookSupported(): Promise<boolean> {
|
|
28
29
|
const disco = await driver.get('/.well-known/openwop');
|
|
29
|
-
const caps = (disco.json as {
|
|
30
|
-
|
|
31
|
-
return caps?.webhooks?.supported === true;
|
|
30
|
+
const caps = discoveryFamilies(disco.json) as { webhooks?: { supported?: boolean } };
|
|
31
|
+
return caps.webhooks?.supported === true;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
describe('webhook-negative: SSRF guard rejects private destinations', () => {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect } from 'vitest';
|
|
17
17
|
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
18
19
|
|
|
19
20
|
interface DiscoveryWebhooks {
|
|
20
21
|
webhooks?: {
|
|
@@ -26,7 +27,7 @@ interface DiscoveryWebhooks {
|
|
|
26
27
|
describe('webhook-sig-algorithm: host advertises supported algorithm set', () => {
|
|
27
28
|
it('discovery surfaces a webhooks.signatureAlgorithms array including "v1"', async () => {
|
|
28
29
|
const disco = await driver.get('/.well-known/openwop');
|
|
29
|
-
const caps = (disco.json as
|
|
30
|
+
const caps = discoveryFamilies(disco.json) as DiscoveryWebhooks;
|
|
30
31
|
const webhooks = caps.webhooks;
|
|
31
32
|
|
|
32
33
|
if (!webhooks?.supported) {
|
|
@@ -28,6 +28,7 @@ import { softSkip } from '../lib/soft-skip.js';
|
|
|
28
28
|
import { createHmac } from 'node:crypto';
|
|
29
29
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
30
30
|
import { driver } from '../lib/driver.js';
|
|
31
|
+
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
31
32
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
32
33
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
33
34
|
import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
|
|
@@ -70,8 +71,8 @@ afterEach(async () => {
|
|
|
70
71
|
|
|
71
72
|
async function isWebhookSupported(): Promise<boolean> {
|
|
72
73
|
const disco = await driver.get('/.well-known/openwop');
|
|
73
|
-
const caps = (disco.json as {
|
|
74
|
-
return caps
|
|
74
|
+
const caps = discoveryFamilies(disco.json) as { webhooks?: { supported?: boolean } };
|
|
75
|
+
return caps.webhooks?.supported === true;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
@@ -46,7 +46,7 @@ import { loadEnv } from '../lib/env.js';
|
|
|
46
46
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
47
47
|
import { FIXTURES_DIR } from '../lib/paths.js';
|
|
48
48
|
import { expandChain, expandChainWithCompensation, type WorkflowChain, type WorkflowChainWithCompensation } from '../lib/workflow-chain-expansion.js';
|
|
49
|
-
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
49
|
+
import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
50
50
|
import { softSkip } from '../lib/soft-skip.js';
|
|
51
51
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
52
52
|
|
|
@@ -84,9 +84,7 @@ interface ChainCaps {
|
|
|
84
84
|
|
|
85
85
|
async function isExpansionAdvertised(): Promise<boolean> {
|
|
86
86
|
const disco = await driver.get('/.well-known/openwop');
|
|
87
|
-
const caps =
|
|
88
|
-
(disco.json as { capabilities?: { workflowChainPacks?: ChainCaps } }).capabilities
|
|
89
|
-
?.workflowChainPacks ?? {};
|
|
87
|
+
const caps = (discoveryFamilies(disco.json) as { workflowChainPacks?: ChainCaps }).workflowChainPacks ?? {};
|
|
90
88
|
return caps.hostExpansionSeam === true;
|
|
91
89
|
}
|
|
92
90
|
|
|
@@ -105,8 +103,7 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
|
|
|
105
103
|
if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
|
|
106
104
|
|
|
107
105
|
const disco = await driver.get('/.well-known/openwop');
|
|
108
|
-
const caps = (disco.json as {
|
|
109
|
-
?.workflowChainPacks;
|
|
106
|
+
const caps = (discoveryFamilies(disco.json) as { workflowChainPacks?: ChainCaps }).workflowChainPacks;
|
|
110
107
|
expect(
|
|
111
108
|
caps,
|
|
112
109
|
driver.describe(
|