@openwop/openwop-conformance 1.20.0 → 1.23.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +61 -3
  2. package/README.md +61 -63
  3. package/api/asyncapi.yaml +54 -38
  4. package/api/openapi.yaml +34 -6
  5. package/coverage.md +381 -202
  6. package/fixtures/connection-packs/connection-pack-github.json +31 -0
  7. package/fixtures.md +120 -101
  8. package/package.json +2 -2
  9. package/schemas/README.md +1 -0
  10. package/schemas/agent-manifest.schema.json +6 -0
  11. package/schemas/capabilities.schema.json +75 -2
  12. package/schemas/connection-pack-manifest.schema.json +161 -0
  13. package/schemas/orchestrator-decision.schema.json +13 -0
  14. package/schemas/run-event-payloads.schema.json +23 -6
  15. package/schemas/run-event.schema.json +12 -2
  16. package/schemas/run-options.schema.json +1 -2
  17. package/schemas/run-snapshot.schema.json +2 -1
  18. package/schemas/suspend-request.schema.json +5 -0
  19. package/src/scenarios/agent-capability-degraded-projection.test.ts +108 -0
  20. package/src/scenarios/agent-channel-dispatch.test.ts +16 -8
  21. package/src/scenarios/agent-requires-capabilities-shape.test.ts +64 -0
  22. package/src/scenarios/agent-verifier-shape.test.ts +106 -0
  23. package/src/scenarios/aiproviders-input-shape.test.ts +69 -0
  24. package/src/scenarios/callai-multimodal.test.ts +86 -0
  25. package/src/scenarios/connection-pack-manifest-valid.test.ts +122 -0
  26. package/src/scenarios/connection-pack-no-credential-material.test.ts +125 -0
  27. package/src/scenarios/connection-pack-reach-exclusive.test.ts +85 -0
  28. package/src/scenarios/connection-pack-write-reconsent.test.ts +91 -0
  29. package/src/scenarios/connection-provider-resolution.test.ts +153 -0
  30. package/src/scenarios/cross-host-traceparent-propagation.test.ts +3 -3
  31. package/src/scenarios/experimental-tier-shape.test.ts +11 -0
  32. package/src/scenarios/fixtures-valid.test.ts +34 -0
  33. package/src/scenarios/grpc-transport.test.ts +108 -0
  34. package/src/scenarios/i18n-negotiation.test.ts +181 -0
  35. package/src/scenarios/interrupt-token-matrix.test.ts +2 -2
  36. package/src/scenarios/media-url-inline-cap.test.ts +5 -3
  37. package/src/scenarios/spec-corpus-validity.test.ts +129 -4
  38. package/src/scenarios/stream-text-fixture.test.ts +212 -0
  39. package/src/scenarios/verifier-gating.test.ts +73 -0
  40. package/src/scenarios/version-fold.test.ts +193 -0
  41. package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -2
  42. package/src/scenarios/webhook-tenant-isolation.test.ts +184 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Connection-pack reach exclusivity — `connection-packs.md` §Manifest clause 5
3
+ * (RFC 0095 §B.5).
4
+ *
5
+ * Always-on, server-free schema probe. `provider.reach` MUST specify exactly
6
+ * ONE of `mcp` / `openapi` / `integration` — the schema pins this with
7
+ * `minProperties: 1` + `maxProperties: 1` + `additionalProperties: false`:
8
+ *
9
+ * 1. Positive: each of the three reach modes validates alone.
10
+ * 2. Negative — two modes (`mcp` + `openapi`) → `maxProperties` violation.
11
+ * 3. Negative — zero modes (`reach: {}`) → `minProperties` violation.
12
+ * 4. Negative — an unknown mode (`grpc`) → `additionalProperties` violation.
13
+ *
14
+ * @see spec/v1/connection-packs.md
15
+ * @see schemas/connection-pack-manifest.schema.json
16
+ * @see RFCS/0095-connection-packs-portable-provider-definitions.md
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { readFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import Ajv2020 from 'ajv/dist/2020.js';
23
+ import addFormats from 'ajv-formats';
24
+ import type { ErrorObject } from 'ajv';
25
+ import { SCHEMAS_DIR, FIXTURES_DIR } from '../lib/paths.js';
26
+
27
+ const SCHEMA_PATH = join(SCHEMAS_DIR, 'connection-pack-manifest.schema.json');
28
+ const FIXTURE_PATH = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-github.json');
29
+
30
+ const MCP = { mcp: { server: { url: 'https://api.githubcopilot.com/mcp/', transport: 'http' } } };
31
+ const OPENAPI = { openapi: { ref: 'https://api.github.com/openapi.json' } };
32
+ const INTEGRATION = { integration: { node: 'core.openwop.integration.github' } };
33
+
34
+ type Manifest = Record<string, unknown> & { provider: Record<string, unknown> };
35
+
36
+ function withReach(reach: Record<string, unknown>): Manifest {
37
+ const m = JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as Manifest;
38
+ m.provider.reach = reach;
39
+ return m;
40
+ }
41
+
42
+ describe('connection-pack-reach-exclusive (RFC 0095 §B.5)', () => {
43
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
44
+ addFormats(ajv);
45
+ const validate = ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')));
46
+
47
+ const failsWith = (manifest: unknown, keyword: string): ErrorObject[] => {
48
+ const ok = validate(manifest);
49
+ expect(ok).toBe(false);
50
+ return (validate.errors ?? []).filter((e) => e.keyword === keyword);
51
+ };
52
+
53
+ it('positive: each reach mode validates alone', () => {
54
+ for (const reach of [MCP, OPENAPI, INTEGRATION]) {
55
+ expect(
56
+ validate(withReach(reach)),
57
+ `connection-packs.md §Manifest clause 5: a single reach mode (${Object.keys(reach)[0]}) MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
58
+ ).toBe(true);
59
+ }
60
+ });
61
+
62
+ it('negative: two reach modes are rejected (maxProperties:1)', () => {
63
+ const errs = failsWith(withReach({ ...MCP, ...OPENAPI }), 'maxProperties');
64
+ expect(
65
+ errs.length,
66
+ 'connection-packs.md §Manifest clause 5: reach MUST specify exactly one of mcp/openapi/integration',
67
+ ).toBeGreaterThan(0);
68
+ });
69
+
70
+ it('negative: an empty reach is rejected (minProperties:1)', () => {
71
+ const errs = failsWith(withReach({}), 'minProperties');
72
+ expect(
73
+ errs.length,
74
+ 'connection-packs.md §Manifest clause 5: reach MUST declare a mode — an empty object is invalid',
75
+ ).toBeGreaterThan(0);
76
+ });
77
+
78
+ it('negative: an unknown reach mode is rejected (additionalProperties:false)', () => {
79
+ const errs = failsWith(withReach({ grpc: { url: 'https://example.com' } }), 'additionalProperties');
80
+ expect(
81
+ errs.some((e) => (e.params as { additionalProperty?: string }).additionalProperty === 'grpc'),
82
+ 'connection-packs.md §Manifest clause 5: the reach vocabulary is closed (mcp | openapi | integration)',
83
+ ).toBe(true);
84
+ });
85
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Connection-pack write re-consent — `connection-packs.md` §Manifest clause 4
3
+ * (RFC 0095 §B.4) — behavioral.
4
+ *
5
+ * Capability-gated on `capabilities.connections.packsSupported: true`
6
+ * (soft-skips when unadvertised; hard-fails under
7
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`). Drives the
8
+ * `POST /v1/host/sample/connection-packs/consent-plan` test seam
9
+ * (`host-sample-test-seams.md`); hosts that haven't wired the seam
10
+ * soft-skip (404).
11
+ *
12
+ * For a `scopeModel: "groups"` oauth2 provider, requesting read AND write
13
+ * scope groups MUST plan write as a SEPARATE consent step — a host MUST NOT
14
+ * bundle write scopes into the initial read authorization (composes with the
15
+ * RFC 0047 write-re-consent pattern):
16
+ *
17
+ * 1. The plan has ≥ 2 steps when both read and write groups are requested.
18
+ * 2. The FIRST (initial) authorization step carries no write scope group.
19
+ * 3. A read-only request plans a single step (no spurious re-consent).
20
+ *
21
+ * @see spec/v1/connection-packs.md
22
+ * @see spec/v1/host-sample-test-seams.md
23
+ * @see RFCS/0095-connection-packs-portable-provider-definitions.md
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { readFileSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { FIXTURES_DIR } from '../lib/paths.js';
30
+ import { driver } from '../lib/driver.js';
31
+ import { behaviorGate } from '../lib/behavior-gate.js';
32
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
33
+
34
+ const FIXTURE_PATH = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-github.json');
35
+
36
+ interface ConsentStep {
37
+ groups?: Array<{ key?: string; access?: 'read' | 'write' }>;
38
+ includesWrite?: boolean;
39
+ }
40
+
41
+ interface ConsentPlan {
42
+ steps?: ConsentStep[];
43
+ }
44
+
45
+ describe('connection-pack-write-reconsent (RFC 0095 §B.4)', () => {
46
+ it('write scope groups are a separate consent step, never bundled into the initial read authorization', async () => {
47
+ const connections = await readCapabilityFamily<{ packsSupported?: boolean }>('connections');
48
+ if (!behaviorGate('connections.packsSupported', connections?.packsSupported === true)) return;
49
+
50
+ const manifest = JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as Record<string, unknown>;
51
+ await driver.post('/v1/host/sample/connection-packs/install', { manifest });
52
+
53
+ const res = await driver.post('/v1/host/sample/connection-packs/consent-plan', {
54
+ provider: 'github',
55
+ requested: ['read', 'write'],
56
+ });
57
+ if (res.status === 404 || res.status === 403) return; // seam unwired — soft-skip
58
+
59
+ const plan = res.json as ConsentPlan | undefined;
60
+ const steps = plan?.steps ?? [];
61
+ expect(
62
+ steps.length >= 2,
63
+ driver.describe(
64
+ 'connection-packs.md §Manifest clause 4',
65
+ 'requesting read + write scope groups MUST plan write as a SEPARATE consent step (≥ 2 steps)',
66
+ ),
67
+ ).toBe(true);
68
+ const first = steps[0] ?? {};
69
+ const firstHasWrite =
70
+ first.includesWrite === true || (first.groups ?? []).some((g) => g.access === 'write');
71
+ expect(
72
+ firstHasWrite,
73
+ driver.describe(
74
+ 'connection-packs.md §Manifest clause 4',
75
+ 'the INITIAL authorization step MUST NOT bundle write scopes',
76
+ ),
77
+ ).toBe(false);
78
+
79
+ const readOnly = await driver.post('/v1/host/sample/connection-packs/consent-plan', {
80
+ provider: 'github',
81
+ requested: ['read'],
82
+ });
83
+ if (readOnly.status !== 404 && readOnly.status !== 403) {
84
+ const roSteps = (readOnly.json as ConsentPlan | undefined)?.steps ?? [];
85
+ expect(
86
+ roSteps.length,
87
+ driver.describe('connection-packs.md §Manifest clause 4', 'a read-only request plans a single consent step'),
88
+ ).toBe(1);
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Connection-pack provider resolution — `connection-packs.md` §Manifest
3
+ * clauses 6 + 8 (RFC 0095 §B.6/§B.8) — behavioral.
4
+ *
5
+ * Capability-gated on `capabilities.connections.packsSupported: true`
6
+ * (soft-skips when unadvertised; hard-fails under
7
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`). Drives the host through the
8
+ * `POST /v1/host/sample/connection-packs/{install,resolve}` test seams
9
+ * (`host-sample-test-seams.md`); hosts that haven't wired the seams
10
+ * soft-skip (404).
11
+ *
12
+ * 1. INSTALL + RESOLVE (§B.6) — installing the `connection-pack-github`
13
+ * fixture makes `provider: "github"` resolve with `source: "pack"`.
14
+ * 2. UNRESOLVED (§B.6) — a provider with no installed pack and no
15
+ * built-in fails with `connection_provider_unresolved`.
16
+ * 3. PRERELEASE CONFLICT (§B.6, SemVer §11) — an installed prerelease
17
+ * (`1.0.0-alpha.1`) does NOT outrank a built-in `1.0.0` (prerelease <
18
+ * release); the host MUST surface `connection_provider_conflict`
19
+ * rather than silently choosing.
20
+ * 4. REJECTION ISOLATION (§B.8) — a rejected manifest (credential
21
+ * material) means NOT INSTALLED, nothing more: a subsequent valid
22
+ * install on the same host MUST still succeed (one bad pack never
23
+ * takes down the install path).
24
+ *
25
+ * @see spec/v1/connection-packs.md
26
+ * @see spec/v1/host-sample-test-seams.md
27
+ * @see RFCS/0095-connection-packs-portable-provider-definitions.md
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { readFileSync } from 'node:fs';
32
+ import { join } from 'node:path';
33
+ import { FIXTURES_DIR } from '../lib/paths.js';
34
+ import { driver } from '../lib/driver.js';
35
+ import { behaviorGate } from '../lib/behavior-gate.js';
36
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
37
+
38
+ const FIXTURE_PATH = join(FIXTURES_DIR, 'connection-packs', 'connection-pack-github.json');
39
+
40
+ type Manifest = Record<string, unknown> & {
41
+ provider: Record<string, unknown> & { id: string };
42
+ };
43
+
44
+ interface InstallResult {
45
+ installed?: boolean;
46
+ errors?: Array<{ code?: string; path?: string }>;
47
+ }
48
+
49
+ interface ResolveResult {
50
+ resolved?: boolean;
51
+ source?: 'pack' | 'builtin';
52
+ version?: string;
53
+ code?: string;
54
+ }
55
+
56
+ function fixture(): Manifest {
57
+ return JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as Manifest;
58
+ }
59
+
60
+ async function gate(): Promise<boolean> {
61
+ const connections = await readCapabilityFamily<{ packsSupported?: boolean }>('connections');
62
+ return behaviorGate('connections.packsSupported', connections?.packsSupported === true);
63
+ }
64
+
65
+ describe('connection-provider-resolution (RFC 0095 §B.6/§B.8)', () => {
66
+ it('an installed pack resolves its provider id; an unknown provider is unresolved', async () => {
67
+ if (!(await gate())) return;
68
+
69
+ const install = await driver.post('/v1/host/sample/connection-packs/install', { manifest: fixture() });
70
+ if (install.status === 404 || install.status === 403) return; // seam unwired — soft-skip
71
+ const installed = install.json as InstallResult | undefined;
72
+ expect(
73
+ installed?.installed,
74
+ driver.describe('connection-packs.md §Manifest clause 1', 'a well-formed connection pack MUST install'),
75
+ ).toBe(true);
76
+
77
+ const hit = await driver.post('/v1/host/sample/connection-packs/resolve', { provider: 'github' });
78
+ const resolved = hit.json as ResolveResult | undefined;
79
+ expect(
80
+ resolved?.resolved,
81
+ driver.describe('connection-packs.md §Manifest clause 6', 'provider "github" MUST resolve against the installed pack whose provider.id matches'),
82
+ ).toBe(true);
83
+ expect(
84
+ resolved?.source,
85
+ driver.describe('connection-packs.md §Manifest clause 6', 'the installed pack is the resolution source'),
86
+ ).toBe('pack');
87
+
88
+ const miss = await driver.post('/v1/host/sample/connection-packs/resolve', {
89
+ provider: 'conformance-nonexistent-provider-xyz',
90
+ });
91
+ const unresolved = miss.json as ResolveResult | undefined;
92
+ expect(
93
+ unresolved?.resolved,
94
+ driver.describe('connection-packs.md §Manifest clause 6', 'a provider with no installed pack and no built-in MUST NOT resolve'),
95
+ ).toBe(false);
96
+ expect(
97
+ unresolved?.code,
98
+ driver.describe('connection-packs.md §Manifest clause 6', 'the refusal code MUST be connection_provider_unresolved'),
99
+ ).toBe('connection_provider_unresolved');
100
+ });
101
+
102
+ it('an installed prerelease does not outrank a built-in release — conflict surfaces (SemVer §11)', async () => {
103
+ if (!(await gate())) return;
104
+
105
+ const prerelease = { ...fixture(), version: '1.0.0-alpha.1' };
106
+ prerelease.provider = { ...prerelease.provider, id: 'conformance-prerelease-probe' };
107
+ const install = await driver.post('/v1/host/sample/connection-packs/install', { manifest: prerelease });
108
+ if (install.status === 404 || install.status === 403) return; // seam unwired — soft-skip
109
+ expect(
110
+ (install.json as InstallResult | undefined)?.installed,
111
+ driver.describe('connection-packs.md §Manifest clause 1', 'a prerelease-versioned pack is shape-valid and MUST install'),
112
+ ).toBe(true);
113
+
114
+ const res = await driver.post('/v1/host/sample/connection-packs/resolve', {
115
+ provider: 'conformance-prerelease-probe',
116
+ simulateBuiltinVersion: '1.0.0',
117
+ });
118
+ if (res.status === 404 || res.status === 403) return; // simulate knob unwired — soft-skip
119
+ const body = res.json as ResolveResult | undefined;
120
+ expect(
121
+ body?.code,
122
+ driver.describe(
123
+ 'connection-packs.md §Manifest clause 6',
124
+ 'SemVer §11: 1.0.0-alpha.1 < 1.0.0 — the installed pack is NOT greater-or-equal, so the host MUST surface connection_provider_conflict rather than silently choosing',
125
+ ),
126
+ ).toBe('connection_provider_conflict');
127
+ });
128
+
129
+ it('rejection isolation: one rejected pack never takes down the install path (§B.8)', async () => {
130
+ if (!(await gate())) return;
131
+
132
+ const leaky = fixture();
133
+ leaky.provider = { ...leaky.provider, id: 'conformance-isolation-probe' };
134
+ (leaky.provider.auth as Record<string, unknown>).clientSecret = 'ghs_conformance_canary';
135
+ const bad = await driver.post('/v1/host/sample/connection-packs/install', { manifest: leaky });
136
+ if (bad.status === 404 || bad.status === 403) return; // seam unwired — soft-skip
137
+ expect(
138
+ (bad.json as InstallResult | undefined)?.installed,
139
+ driver.describe('connection-packs.md §Manifest clause 2', 'the credential-carrying manifest MUST NOT install'),
140
+ ).toBe(false);
141
+
142
+ const good = { ...fixture() };
143
+ good.provider = { ...good.provider, id: 'conformance-isolation-survivor' };
144
+ const after = await driver.post('/v1/host/sample/connection-packs/install', { manifest: good });
145
+ expect(
146
+ (after.json as InstallResult | undefined)?.installed,
147
+ driver.describe(
148
+ 'connection-packs.md §Manifest clause 8',
149
+ 'a rejected pack means NOT INSTALLED — nothing more; a subsequent valid install MUST succeed',
150
+ ),
151
+ ).toBe(true);
152
+ });
153
+ });
@@ -35,10 +35,10 @@
35
35
 
36
36
  import { describe, it } from 'vitest';
37
37
 
38
- // Behavioral assertions in this file are currently `it.todo` placeholders;
38
+ // Behavioral assertions in this file are currently `it.skip` placeholders;
39
39
  // the cross-host MCP / A2A peer harness (gated on OPENWOP_MCP_REAL_SERVER_URL
40
40
  // / OPENWOP_A2A_REAL_PEER_URL) hasn't landed yet. When it does, the
41
- // `it.todo` calls flip back to runnable `it(...)` bodies that read discovery
41
+ // `it.skip` calls flip back to runnable `it(...)` bodies that read discovery
42
42
  // (via `driver.get('/.well-known/openwop')`), gate on `Phase 3` advertisement,
43
43
  // and drive the workflow through the configured real peer.
44
44
 
@@ -48,7 +48,7 @@ describe('cross-host-traceparent-propagation: behavioral (RFC 0040 §B)', () =>
48
48
  // OPENWOP_MCP_REAL_SERVER_URL) records inbound headers; the test reads
49
49
  // the recorded headers and asserts `traceparent` is present + matches
50
50
  // the format `00-{traceId}-{spanId}-{flags}` per W3C tracecontext.
51
- // Until the peer harness lands, the assertion is surfaced as `todo` so
51
+ // Until the peer harness lands, the assertion is surfaced as `it.skip` so
52
52
  // test reporters track the gap rather than reporting a vacuous PASS.
53
53
  // Marked out of stable profile via RFC 0042 §B (experimental tier):
54
54
  // RFC 0040 remains Active. Hosts that wire Phase 3 cross-host causation
@@ -29,6 +29,7 @@
29
29
  import { describe, it, expect } from 'vitest';
30
30
  import { driver } from '../lib/driver.js';
31
31
  import { experimentalGate } from '../lib/behavior-gate.js';
32
+ import { __resetEnvCacheForTests } from '../lib/env.js';
32
33
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
33
34
 
34
35
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -180,6 +181,12 @@ describe.skipIf(HTTP_SKIP)('experimental-tier-shape: §D experimentalGate helper
180
181
  it('experimentalGate routes through behaviorGate when tier === undefined or "stable"', () => {
181
182
  const prevReqBeh = process.env.OPENWOP_REQUIRE_BEHAVIOR;
182
183
  delete process.env.OPENWOP_REQUIRE_BEHAVIOR;
184
+ // behaviorGate/experimentalGate read a memoized loadEnv() snapshot. Under a
185
+ // strict suite run (e.g. the conformance-soak sets OPENWOP_REQUIRE_BEHAVIOR=true
186
+ // process-wide) an earlier scenario has already cached requireBehavior=true, so
187
+ // the delete above is a no-op against the cache and the default-mode assertions
188
+ // below would wrongly throw. Bust the memo so this self-test sees default mode.
189
+ __resetEnvCacheForTests();
183
190
  try {
184
191
  // Stable + advertised → proceed.
185
192
  expect(experimentalGate('test-stable', true, 'stable')).toBe(true);
@@ -188,6 +195,10 @@ describe.skipIf(HTTP_SKIP)('experimental-tier-shape: §D experimentalGate helper
188
195
  expect(experimentalGate('test-not-adv', false, 'stable')).toBe(false);
189
196
  } finally {
190
197
  if (prevReqBeh !== undefined) process.env.OPENWOP_REQUIRE_BEHAVIOR = prevReqBeh;
198
+ // Restore the real env into the memo so later scenarios gate correctly (a
199
+ // leaked default-mode cache would turn their strict behaviorGates into
200
+ // silent soft-skips — a coverage hole).
201
+ __resetEnvCacheForTests();
191
202
  }
192
203
  });
193
204
  });
@@ -154,6 +154,40 @@ describe('fixtures: node-pack-manifest schema validity', () => {
154
154
  });
155
155
  });
156
156
 
157
+ describe('fixtures: connection-pack-manifest schema validity', () => {
158
+ // Connection-pack fixtures live in `fixtures/connection-packs/` (RFC 0095)
159
+ // so the node-pack validator above doesn't apply the wrong schema. They are
160
+ // schema-level proof points AND the canonical install payloads for the
161
+ // capability-gated `connection-provider-resolution` behavioral scenario.
162
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
163
+ addFormats(ajv);
164
+ const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'connection-pack-manifest.schema.json'), 'utf8'));
165
+ const validate = ajv.compile(schema);
166
+
167
+ const dir = join(FIXTURES_DIR, 'connection-packs');
168
+ const files = readdirSync(dir)
169
+ .filter((f) => f.endsWith('.json'))
170
+ .sort();
171
+
172
+ it('finds at least one connection-pack fixture (RFC 0095 coverage)', () => {
173
+ expect(files.length).toBeGreaterThan(0);
174
+ });
175
+
176
+ for (const file of files) {
177
+ it(`connection-packs/${file} validates against connection-pack-manifest.schema.json`, () => {
178
+ const data = JSON.parse(readFileSync(join(dir, file), 'utf8'));
179
+ const ok = validate(data);
180
+ const errors = (validate.errors ?? [])
181
+ .map((e: ErrorObject) => `${e.instancePath || '/'}: ${e.message}`)
182
+ .join('\n');
183
+ expect(
184
+ ok,
185
+ `Fixture connection-packs/${file} fails connection-pack-manifest schema:\n${errors}`,
186
+ ).toBe(true);
187
+ });
188
+ }
189
+ });
190
+
157
191
  describe('fixtures: prompt-template schema validity', () => {
158
192
  // PromptTemplate fixtures live in `fixtures/prompt-templates/` per
159
193
  // RFC 0027 §A. Like pack manifests, they're schema-level proof points,
@@ -0,0 +1,108 @@
1
+ /**
2
+ * grpc-transport — `spec/v1/grpc-transport.md` advertisement-shape scenario
3
+ * (capability-gated on `capabilities.grpc`; the schema block lands with
4
+ * RFC 0094 §H on this branch).
5
+ *
6
+ * SHAPE-ONLY by design: this scenario validates the advertised
7
+ * `capabilities.grpc` block against the doc's MUSTs without dialing gRPC
8
+ * (the suite ships no gRPC client — see grpc-transport.md §Implementation
9
+ * notes). The end-to-end legs (GetCapabilities equivalence, run
10
+ * round-trip, error-envelope mapping, auth metadata) are listed in
11
+ * grpc-transport.md §Conformance and stay future work for a host-side
12
+ * gRPC harness.
13
+ *
14
+ * Gated via `behaviorGate('openwop-grpc-transport', grpc !== undefined)`
15
+ * — soft-skip when the block is absent (absent ⇒ no gRPC transport,
16
+ * which is conformant), hard-fail under `OPENWOP_REQUIRE_BEHAVIOR=true`
17
+ * unless honestly opted out (root-first family read per RFC 0073).
18
+ *
19
+ * Asserted MUSTs (grpc-transport.md §"Field semantics"):
20
+ * - `supported` is a boolean;
21
+ * - `service` is the canonical `openwop.v1.Engine` for v1;
22
+ * - `tls` ∈ {"required", "optional", "disabled"};
23
+ * - `endpoint`, when present, is a `grpc://` or `grpcs://` URI;
24
+ * - when `supported: true`, root `supportedTransports` includes
25
+ * `"grpc"` ("presence required when gRPC is exposed");
26
+ * - a production-profile claimant (root `production` block advertised)
27
+ * MUST set `tls: "required"`.
28
+ *
29
+ * @see spec/v1/grpc-transport.md §"Field semantics" + §Conformance
30
+ * @see RFCS/0094-wire-shape-reconciliation.md §H
31
+ */
32
+
33
+ import { describe, it, expect } from 'vitest';
34
+ import { driver } from '../lib/driver.js';
35
+ import { behaviorGate } from '../lib/behavior-gate.js';
36
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
+
38
+ const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
39
+
40
+ const TLS_VALUES = ['required', 'optional', 'disabled'];
41
+ const V1_SERVICE = 'openwop.v1.Engine';
42
+
43
+ interface GrpcCap {
44
+ readonly supported?: unknown;
45
+ readonly endpoint?: unknown;
46
+ readonly service?: unknown;
47
+ readonly tls?: unknown;
48
+ }
49
+
50
+ describe.skipIf(HTTP_SKIP)('grpc-transport: advertisement shape (grpc-transport.md §Field semantics)', () => {
51
+ it('an advertised capabilities.grpc block satisfies every doc MUST', async () => {
52
+ const res = await driver.get('/.well-known/openwop');
53
+ if (res.status !== 200) return;
54
+ const grpc = capabilityFamily<GrpcCap>(res.json, 'grpc');
55
+ if (!behaviorGate('openwop-grpc-transport', grpc !== undefined)) return;
56
+
57
+ expect(
58
+ typeof grpc!.supported,
59
+ driver.describe('grpc-transport.md §Field semantics', 'capabilities.grpc.supported MUST be a boolean'),
60
+ ).toBe('boolean');
61
+
62
+ expect(
63
+ grpc!.service,
64
+ driver.describe('grpc-transport.md §Field semantics', 'v1 hosts MUST use the canonical service name openwop.v1.Engine'),
65
+ ).toBe(V1_SERVICE);
66
+
67
+ expect(
68
+ TLS_VALUES,
69
+ driver.describe(
70
+ 'grpc-transport.md §Field semantics',
71
+ `capabilities.grpc.tls MUST be one of ${TLS_VALUES.join(' / ')} (got ${String(grpc!.tls)})`,
72
+ ),
73
+ ).toContain(grpc!.tls);
74
+
75
+ if (grpc!.endpoint !== undefined) {
76
+ expect(
77
+ typeof grpc!.endpoint === 'string' && /^grpcs?:\/\/.+/.test(grpc!.endpoint),
78
+ driver.describe(
79
+ 'grpc-transport.md §Field semantics',
80
+ `capabilities.grpc.endpoint MUST be a grpc:// (cleartext) or grpcs:// (TLS) URI (got ${String(grpc!.endpoint)})`,
81
+ ),
82
+ ).toBe(true);
83
+ }
84
+
85
+ if (grpc!.supported === true) {
86
+ const transports = capabilityFamily<unknown>(res.json, 'supportedTransports');
87
+ expect(
88
+ Array.isArray(transports) && transports.includes('grpc'),
89
+ driver.describe(
90
+ 'grpc-transport.md §Field semantics',
91
+ 'supportedTransports MUST include "grpc" when the gRPC surface is exposed',
92
+ ),
93
+ ).toBe(true);
94
+ }
95
+
96
+ // Production-profile claimants MUST require TLS on the gRPC listener.
97
+ const production = capabilityFamily<unknown>(res.json, 'production');
98
+ if (production !== undefined && grpc!.supported === true) {
99
+ expect(
100
+ grpc!.tls,
101
+ driver.describe(
102
+ 'grpc-transport.md §Field semantics',
103
+ 'production hosts MUST set capabilities.grpc.tls: "required"',
104
+ ),
105
+ ).toBe('required');
106
+ }
107
+ });
108
+ });