@openwop/openwop-conformance 2.45.13 → 2.45.15
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/CHANGELOG.md +24 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +1446 -4
- package/scenario-majors.json +65 -2
- package/schemas/CORPUS-STAMP.json +14 -12
- package/src/lib/backpressure-witness.ts +2 -2
- package/src/lib/budget-witness.ts +3 -3
- package/src/lib/envelope-schema-static.ts +166 -0
- package/src/lib/exhaustion-facet-witness.ts +1 -1
- package/src/lib/family-advert-witness.ts +212 -0
- package/src/lib/i18n-double.ts +99 -0
- package/src/lib/i18n-negotiation-witness.ts +173 -0
- package/src/lib/localized-content-witness.ts +49 -0
- package/src/lib/model-capability-insufficient-witness.ts +121 -0
- package/src/lib/prompt-double.ts +217 -0
- package/src/lib/prompt-events-witness.ts +211 -0
- package/src/lib/prompt-library-witness.ts +406 -0
- package/src/lib/run-bounds-witness.ts +111 -0
- package/src/lib/scratch-host.ts +4 -1
- package/src/lib/table-schema-witness.ts +3 -1
- package/src/scenarios/v2-ai-envelope-shape-static.test.ts +93 -0
- package/src/scenarios/v2-ai-providers-advertisement.test.ts +94 -0
- package/src/scenarios/v2-envelope-reasoning-shape.test.ts +81 -0
- package/src/scenarios/v2-envelope-tier-one-subset-static.test.ts +77 -0
- package/src/scenarios/v2-envelope-variant-discriminator-static.test.ts +57 -0
- package/src/scenarios/v2-envelopes-advertisement.test.ts +85 -0
- package/src/scenarios/v2-i18n-negotiation.test.ts +91 -0
- package/src/scenarios/v2-limits-envelope-caps-advertisement.test.ts +61 -0
- package/src/scenarios/v2-localized-content-delivery.test.ts +136 -0
- package/src/scenarios/v2-media-inline-cap-static.test.ts +59 -0
- package/src/scenarios/v2-model-capabilities-advertisement.test.ts +61 -0
- package/src/scenarios/v2-model-capability-insufficient.test.ts +63 -0
- package/src/scenarios/v2-prompt-all-four-kinds-events.test.ts +69 -0
- package/src/scenarios/v2-prompt-end-to-end-events.test.ts +71 -0
- package/src/scenarios/v2-prompt-list-and-fetch.test.ts +103 -0
- package/src/scenarios/v2-prompt-mutable-lifecycle.test.ts +130 -0
- package/src/scenarios/v2-prompt-pack-install.test.ts +78 -0
- package/src/scenarios/v2-prompt-render-deterministic.test.ts +87 -0
- package/src/scenarios/v2-prompt-resolution-chain-event.test.ts +46 -0
- package/src/scenarios/v2-prompt-template-shape.test.ts +112 -0
- package/src/scenarios/v2-run-execution-bounds.test.ts +67 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — localized content (`spec/v2/core/i18n.md` §Localized content;
|
|
3
|
+
* `schemas/v2/localized-content-*.schema.json`; RFCs 0103, 0206). The v1 twin
|
|
4
|
+
* is `localized-content-delivery`.
|
|
5
|
+
*
|
|
6
|
+
* Two layers:
|
|
7
|
+
*
|
|
8
|
+
* A. Server-free — the v2 `content` capability record (no `supported` seat;
|
|
9
|
+
* presence is the claim), the four v2 content schemas, and the section
|
|
10
|
+
* merge every host MUST resolve identically (exact tag → `ll-Ssss` script
|
|
11
|
+
* family → language → `data`, shallow overlay), against the suite's one
|
|
12
|
+
* reference copy in `lib/localized-content.ts`.
|
|
13
|
+
*
|
|
14
|
+
* B. Live — the advertised `content` record is coherent with the advertised
|
|
15
|
+
* `i18n` record (`lib/localized-content-witness.ts`, a pure judge proven in
|
|
16
|
+
* `localized-content-witness.test.ts`). Gated by the presence of the v2
|
|
17
|
+
* `content` record: absent ⇒ `inapplicable`.
|
|
18
|
+
*
|
|
19
|
+
* Not ported here: the `/content/*` delivery legs (published-only, tenant
|
|
20
|
+
* isolation, no enumeration); v1 never drove them either.
|
|
21
|
+
*
|
|
22
|
+
* @see spec/v2/core/i18n.md §Localized content
|
|
23
|
+
* @see SECURITY/invariants.yaml id: content-published-cache-no-draft, content-response-tenant-scoped
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
30
|
+
import { familyAdvertised, v2Discovery, v2RefValidator, v2Validator } from '../lib/v2.js';
|
|
31
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
32
|
+
import { req } from '../lib/requirement-ids.js';
|
|
33
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
34
|
+
import { resolveSection, type Section } from '../lib/localized-content.js';
|
|
35
|
+
import { coherenceLeg } from '../lib/localized-content-witness.js';
|
|
36
|
+
|
|
37
|
+
const PROFILE = majorProfile(2);
|
|
38
|
+
const DOC = 'spec/v2/core/i18n.md §Localized content';
|
|
39
|
+
const ID_RECORD = 'openwop.requirement.content.capability-record-shape';
|
|
40
|
+
const ID_SECTION = 'openwop.requirement.content.section-schema';
|
|
41
|
+
const ID_PAGE = 'openwop.requirement.content.page-schema';
|
|
42
|
+
const ID_SETTINGS = 'openwop.requirement.content.language-settings-schema';
|
|
43
|
+
const ID_RESPONSE = 'openwop.requirement.content.page-response-schema';
|
|
44
|
+
const ID_MERGE = 'openwop.requirement.content.section-merge';
|
|
45
|
+
const ID_COHERENT = 'openwop.requirement.content.advertisement-coherent';
|
|
46
|
+
const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
|
|
47
|
+
|
|
48
|
+
const caps = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'capabilities.schema.json'), 'utf8')) as { properties: Record<string, { required?: string[]; properties?: Record<string, unknown> }> };
|
|
49
|
+
const section = v2Validator('localized-content-section');
|
|
50
|
+
const page = v2Validator('localized-content-page');
|
|
51
|
+
const settings = v2Validator('localized-content-language-settings');
|
|
52
|
+
const response = v2Validator('localized-content-page-response');
|
|
53
|
+
const contentRecord = v2RefValidator('capabilities.schema.json#/properties/content');
|
|
54
|
+
|
|
55
|
+
const goodSection = {
|
|
56
|
+
sectionId: 'hero', sectionType: 'hero', data: { heading: 'Welcome', cta: 'Get started' },
|
|
57
|
+
localizations: { es: { heading: 'Bienvenido', cta: 'Empezar' }, 'pt-BR': { heading: 'Bem-vindo' } },
|
|
58
|
+
status: 'published', enabled: true, order: 0,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('v2 localized content: the capability record (server-free)', () => {
|
|
62
|
+
it('the v2 content record requires baseLocale and supportedLocales and has no supported seat', () => {
|
|
63
|
+
const content = caps.properties['content'];
|
|
64
|
+
expect(content?.required, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §content', 'content MUST require baseLocale and supportedLocales')).toEqual(expect.arrayContaining(['baseLocale', 'supportedLocales']));
|
|
65
|
+
expect(content?.properties?.['supported'], req(ID_RECORD, 'spec/v2/core/capabilities.md (presence is the claim)', 'the v2 content record MUST NOT declare a supported seat')).toBeUndefined();
|
|
66
|
+
const base = { status: 'experimental', since: '2.0', until: '2.9', witness: 'witnessable-gated' };
|
|
67
|
+
expect(contentRecord({ ...base, baseLocale: 'en', supportedLocales: ['es'] }).ok, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §content', 'a conforming content record MUST validate')).toBe(true);
|
|
68
|
+
expect(contentRecord({ ...base, supported: true, baseLocale: 'en', supportedLocales: ['es'] }).ok, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §content', 'a content record carrying supported MUST be rejected (closed record)')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('v2 localized content: schema shapes (server-free)', () => {
|
|
73
|
+
it('a conforming section validates; a non-canonical or underscore locale key, a missing status, or an unknown status is rejected', () => {
|
|
74
|
+
expect(section(goodSection).ok, req(ID_SECTION, DOC, `a conforming section MUST validate: ${section(goodSection).errors}`)).toBe(true);
|
|
75
|
+
for (const key of ['zh-Hans', 'zh-Hant-TW', 'es-419', 'fil']) {
|
|
76
|
+
expect(section({ ...goodSection, localizations: { [key]: { heading: 'x' } } }).ok, req(ID_SECTION, 'RFC 0206 §A.1', `${key} MUST validate as a localizations key`)).toBe(true);
|
|
77
|
+
}
|
|
78
|
+
for (const key of ['EN', 'en_US', 'en-us', 'zh-hans', 'de-CH-1996']) {
|
|
79
|
+
expect(section({ ...goodSection, localizations: { [key]: { heading: 'x' } } }).ok, req(ID_SECTION, 'RFC 0206 §A.1', `${key} MUST be rejected as a localizations key`)).toBe(false);
|
|
80
|
+
}
|
|
81
|
+
const { status: _omit, ...noStatus } = goodSection;
|
|
82
|
+
expect(section(noStatus).ok, req(ID_SECTION, DOC, 'status is REQUIRED')).toBe(false);
|
|
83
|
+
expect(section({ ...goodSection, status: 'archived' }).ok, req(ID_SECTION, DOC, 'status MUST be draft or published')).toBe(false);
|
|
84
|
+
expect(section({ ...goodSection, extra: true }).ok, req(ID_SECTION, DOC, 'a section is a closed record')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('a conforming page validates and a bad slug is rejected', () => {
|
|
88
|
+
const good = { pageId: 'home', slug: 'home', name: 'Home', status: 'published', sectionOrder: ['hero'] };
|
|
89
|
+
expect(page(good).ok, req(ID_PAGE, DOC, `a conforming page MUST validate: ${page(good).errors}`)).toBe(true);
|
|
90
|
+
expect(page({ ...good, slug: 'Home Page' }).ok, req(ID_PAGE, DOC, 'slug MUST match ^[a-z][a-z0-9-]*$')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('a conforming language-settings document validates', () => {
|
|
94
|
+
const good = { baseLocale: 'en', supportedLocales: ['es', 'pt-BR', 'fr'], autoTranslateOnPublish: false };
|
|
95
|
+
expect(settings(good).ok, req(ID_SETTINGS, DOC, `settings MUST validate: ${settings(good).errors}`)).toBe(true);
|
|
96
|
+
expect(settings({ ...good, baseLocale: 'en_US' }).ok, req(ID_SETTINGS, DOC, 'baseLocale MUST be a canonical BCP 47 key')).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('a conforming page response validates', () => {
|
|
100
|
+
const good = {
|
|
101
|
+
version: '1', generatedAt: '2026-06-17T00:00:00Z', locale: 'pt-BR', slug: 'home',
|
|
102
|
+
page: { pageId: 'home', slug: 'home', name: 'Home' },
|
|
103
|
+
sections: [{ sectionId: 'hero', sectionType: 'hero', data: { heading: 'Bem-vindo', cta: 'Get started' } }],
|
|
104
|
+
};
|
|
105
|
+
expect(response(good).ok, req(ID_RESPONSE, DOC, `a resolved response MUST validate: ${response(good).errors}`)).toBe(true);
|
|
106
|
+
expect(response({ ...good, sections: [{ ...good.sections[0], localizations: {} }] }).ok, req(ID_RESPONSE, DOC, 'a delivered section carries resolved data, never its localizations')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('v2 localized content: the section merge every host resolves identically (server-free)', () => {
|
|
111
|
+
it('exact tag, then script family, then language, then data — a shallow overlay', () => {
|
|
112
|
+
const s: Section = { data: { heading: 'Welcome', cta: 'Get started' }, localizations: { es: { heading: 'Bienvenido', cta: 'Empezar' }, 'pt-BR': { heading: 'Bem-vindo' } } };
|
|
113
|
+
expect(resolveSection(s, 'es', 'en'), req(ID_MERGE, DOC, 'an exact hit MUST overlay its fields onto data')).toEqual({ heading: 'Bienvenido', cta: 'Empezar' });
|
|
114
|
+
expect(resolveSection(s, 'pt-BR', 'en'), req(ID_MERGE, DOC, 'a field the overlay lacks MUST fall through to data')).toEqual({ heading: 'Bem-vindo', cta: 'Get started' });
|
|
115
|
+
expect(resolveSection({ data: { h: 'Hi' }, localizations: { pt: { h: 'Oi' } } }, 'pt-BR', 'en'), req(ID_MERGE, DOC, 'pt-BR MUST fall back to the pt language overlay')).toEqual({ h: 'Oi' });
|
|
116
|
+
expect(resolveSection({ data: { h: 'x' }, localizations: { 'zh-Hant': { h: '繁' }, zh: { h: '简' } } }, 'zh-Hant-TW', 'en'), req(ID_MERGE, DOC, 'zh-Hant-TW MUST take the zh-Hant script family before zh')).toEqual({ h: '繁' });
|
|
117
|
+
expect(resolveSection(s, 'de', 'en'), req(ID_MERGE, DOC, 'no overlay MUST resolve to data alone')).toEqual(s.data);
|
|
118
|
+
expect(resolveSection(s, 'en', 'en'), req(ID_MERGE, DOC, 'baseLocale MUST resolve to data')).toEqual(s.data);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('v2 localized content: live advertisement coherence (i18n.md §Localized content "Advertisement")', () => {
|
|
123
|
+
it('the advertised content record is coherent with the advertised i18n record', async () => {
|
|
124
|
+
if (HTTP_SKIP) return softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
|
|
125
|
+
let doc: Record<string, unknown> | null;
|
|
126
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
127
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
|
|
128
|
+
const content = await familyAdvertised('content');
|
|
129
|
+
if (content === null) return softSkip('inapplicable', 'the host does not advertise content');
|
|
130
|
+
const out = coherenceLeg(PROFILE, doc);
|
|
131
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
132
|
+
const v = contentRecord(content);
|
|
133
|
+
expect(v.ok, req(ID_COHERENT, 'schemas/v2/capabilities.schema.json §content', `the content record MUST validate: ${v.errors}`)).toBe(true);
|
|
134
|
+
for (const x of out.findings) expect(x.ok, req(ID_COHERENT, x.doc, x.message)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `media.*` payload shapes, statically (`spec/v2/core/host-services.md`
|
|
3
|
+
* §aiProviders: "A `media.*` envelope inlines base64 only up to
|
|
4
|
+
* `maxInlineMediaBytes` (default 256 KiB); above that the host MUST use a
|
|
5
|
+
* `url`"; RFC 0055 §C). The v1 twin is the static half of
|
|
6
|
+
* `media-url-inline-cap`. Its advertisement leg (`aiProviders.maxInlineMediaBytes`)
|
|
7
|
+
* is in `v2-ai-providers-advertisement`; its seam legs (the media store and the
|
|
8
|
+
* debug-bundle walk) are not ported.
|
|
9
|
+
*
|
|
10
|
+
* compile the three `media.{image,audio,file}` schemas compile;
|
|
11
|
+
* url a URL-reference payload validates;
|
|
12
|
+
* inline an inline-base64 payload validates;
|
|
13
|
+
* bytes a payload without `bytes` is rejected (`bytes` is what a cap is
|
|
14
|
+
* judged against);
|
|
15
|
+
* closed an unknown property is rejected.
|
|
16
|
+
*
|
|
17
|
+
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/v2/core/host-services.md §aiProviders
|
|
20
|
+
* @see schemas/v2/envelopes/media.image.schema.json
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { v2Validator } from '../lib/v2.js';
|
|
25
|
+
import { req } from '../lib/requirement-ids.js';
|
|
26
|
+
|
|
27
|
+
const DOC = 'host-services.md §aiProviders';
|
|
28
|
+
const MEDIA_KINDS = ['media.image', 'media.audio', 'media.file'] as const;
|
|
29
|
+
const ID_COMPILE = 'openwop.requirement.media.payload-schemas-compile';
|
|
30
|
+
const ID_URL = 'openwop.requirement.media.url-reference-accepted';
|
|
31
|
+
const ID_INLINE = 'openwop.requirement.media.inline-base64-accepted';
|
|
32
|
+
const ID_BYTES = 'openwop.requirement.media.bytes-required';
|
|
33
|
+
const ID_CLOSED = 'openwop.requirement.media.payload-closed';
|
|
34
|
+
|
|
35
|
+
describe('v2 media payload shapes (host-services.md §aiProviders)', () => {
|
|
36
|
+
for (const kind of MEDIA_KINDS) {
|
|
37
|
+
it(`envelopes/${kind}.schema.json compiles under Ajv 2020`, () => {
|
|
38
|
+
expect(() => v2Validator(`envelopes/${kind}`), req(ID_COMPILE, DOC, `schemas/v2/envelopes/${kind}.schema.json MUST compile`)).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it('accepts a URL-reference image payload', () => {
|
|
43
|
+
const r = v2Validator('envelopes/media.image')({ url: 'https://host.example/runs/run_1/assets/img_9.png', bytes: 184320, mimeType: 'image/png' });
|
|
44
|
+
expect(r.ok, req(ID_URL, DOC, `a URL-reference media payload MUST validate (${r.errors})`)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('accepts an inline-base64 audio payload', () => {
|
|
48
|
+
const r = v2Validator('envelopes/media.audio')({ base64: 'AAAA', bytes: 3, mimeType: 'audio/ogg', durationSeconds: 1.2 });
|
|
49
|
+
expect(r.ok, req(ID_INLINE, DOC, `an inline-base64 media payload MUST validate (${r.errors})`)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects a media payload missing bytes', () => {
|
|
53
|
+
expect(v2Validator('envelopes/media.file')({ url: 'https://host.example/runs/run_1/assets/report.pdf' }).ok, req(ID_BYTES, DOC, 'bytes is required on a media payload — it is what the inline cap is judged against')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects a media payload with an unknown property', () => {
|
|
57
|
+
expect(v2Validator('envelopes/media.image')({ bytes: 1, wat: true }).ok, req(ID_CLOSED, DOC, 'a media payload is closed: an unknown property MUST be rejected')).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `modelCapabilities` advertisement (`spec/v2/core/host-services.md`
|
|
3
|
+
* §modelCapabilities; RFC 0031 §C/§E, RFC 0055). The v1 twin is the
|
|
4
|
+
* advertisement half of `model-capability-substituted`; the legs live in
|
|
5
|
+
* `lib/family-advert-witness.ts`.
|
|
6
|
+
*
|
|
7
|
+
* record the record validates against its seat in
|
|
8
|
+
* `schemas/v2/capabilities.schema.json` — at v2 there is no
|
|
9
|
+
* `supported` seat, so a v1-shaped record fails here;
|
|
10
|
+
* advertised every identifier is spec-reserved or host-private, prefixed
|
|
11
|
+
* `x-host-<host>-` ("a host-private identifier MUST be prefixed");
|
|
12
|
+
* substitution `substitutionSupported` is a boolean.
|
|
13
|
+
*
|
|
14
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `modelCapabilities` absent,
|
|
15
|
+
* or the facet a leg reads absent ⇒ `inapplicable`.
|
|
16
|
+
*
|
|
17
|
+
* @see spec/v2/core/host-services.md §modelCapabilities
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
22
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
23
|
+
import { req } from '../lib/requirement-ids.js';
|
|
24
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
25
|
+
import { modelCapabilitiesAdvertisedLeg, modelCapabilitiesSubstitutionLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
26
|
+
|
|
27
|
+
const PROFILE = majorProfile(2);
|
|
28
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
29
|
+
const ID_RECORD = 'openwop.requirement.model-capabilities.advert-record-schema';
|
|
30
|
+
const ID_ADVERTISED = 'openwop.requirement.model-capabilities.advertised-identifiers';
|
|
31
|
+
const ID_SUBSTITUTION = 'openwop.requirement.model-capabilities.substitution-supported-boolean';
|
|
32
|
+
|
|
33
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
34
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('v2 modelCapabilities advertisement (host-services.md §modelCapabilities)', () => {
|
|
38
|
+
it('the modelCapabilities record validates against its capabilities-schema seat', async () => {
|
|
39
|
+
const doc = await discovery();
|
|
40
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
41
|
+
const out = recordSchemaLeg(PROFILE, doc, 'modelCapabilities', 'host-services.md §modelCapabilities');
|
|
42
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
43
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('modelCapabilities.advertised names only spec-reserved or x-host- prefixed identifiers', async () => {
|
|
47
|
+
const doc = await discovery();
|
|
48
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
49
|
+
const out = modelCapabilitiesAdvertisedLeg(PROFILE, doc);
|
|
50
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
51
|
+
for (const x of out.findings) expect(x.ok, req(ID_ADVERTISED, x.doc, x.message)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('modelCapabilities.substitutionSupported is a boolean', async () => {
|
|
55
|
+
const doc = await discovery();
|
|
56
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
57
|
+
const out = modelCapabilitiesSubstitutionLeg(PROFILE, doc);
|
|
58
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
59
|
+
for (const x of out.findings) expect(x.ok, req(ID_SUBSTITUTION, x.doc, x.message)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the model-capability refusal, end to end (`spec/v2/core/host-services.md`
|
|
3
|
+
* §modelCapabilities; RFC 0031 §B step 4 + §D). The v1 twin is the two fixture
|
|
4
|
+
* legs of `model-capability-insufficient`; its four `evaluate-model-capability-gate`
|
|
5
|
+
* seam legs are not ported. The legs live in
|
|
6
|
+
* `lib/model-capability-insufficient-witness.ts`; what differs between majors
|
|
7
|
+
* (the run path, the event name `model.capability-insufficient`) is the profile row.
|
|
8
|
+
*
|
|
9
|
+
* One run of the `conformance-model-capability-insufficient` fixture, whose one
|
|
10
|
+
* node requires a capability no model offers and declares no fallback:
|
|
11
|
+
* refusal the run ends `failed` / `capability_not_provided`; the log
|
|
12
|
+
* carries `model.capability-insufficient` before `node.failed`,
|
|
13
|
+
* its payload valid against
|
|
14
|
+
* `run-event-payloads#/$defs/modelCapabilityInsufficient`, with
|
|
15
|
+
* `fallbackAttempted` not true;
|
|
16
|
+
* no-dispatch the log carries no `node.completed`, `provider.usage` or
|
|
17
|
+
* envelope-reliability event.
|
|
18
|
+
*
|
|
19
|
+
* Dispositions: `modelCapabilities` not advertised, or the fixture not
|
|
20
|
+
* advertised ⇒ `inapplicable` (the fixture is the opt-in).
|
|
21
|
+
*
|
|
22
|
+
* Proven against a scratch double in `lib/model-capability-insufficient-witness.test.ts`.
|
|
23
|
+
*
|
|
24
|
+
* @see spec/v2/core/host-services.md §modelCapabilities
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect } from 'vitest';
|
|
28
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
29
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
30
|
+
import { req } from '../lib/requirement-ids.js';
|
|
31
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
32
|
+
import { drive, judgeNoDispatch, judgeRefusal, type InsufficientObservation, type InsufficientSkip } from '../lib/model-capability-insufficient-witness.js';
|
|
33
|
+
|
|
34
|
+
const PROFILE = majorProfile(2);
|
|
35
|
+
const ID_REFUSAL = 'openwop.requirement.model-capabilities.insufficient-refusal';
|
|
36
|
+
const ID_NO_DISPATCH = 'openwop.requirement.model-capabilities.insufficient-no-dispatch';
|
|
37
|
+
|
|
38
|
+
let run: Promise<InsufficientSkip | InsufficientObservation | null> | undefined;
|
|
39
|
+
/** One fixture run serves both legs. `null`: discovery unreadable. */
|
|
40
|
+
function observe(): Promise<InsufficientSkip | InsufficientObservation | null> {
|
|
41
|
+
run ??= (async () => {
|
|
42
|
+
let doc: Record<string, unknown> | null;
|
|
43
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
44
|
+
return doc === null ? null : drive(PROFILE, doc);
|
|
45
|
+
})();
|
|
46
|
+
return run;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('v2 model-capability-insufficient (host-services.md §modelCapabilities)', () => {
|
|
50
|
+
it('a node requiring a capability no model offers fails capability_not_provided after model.capability-insufficient', async () => {
|
|
51
|
+
const o = await observe();
|
|
52
|
+
if (o === null) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
|
|
53
|
+
if ('kind' in o) return softSkip(o.disposition, o.reason);
|
|
54
|
+
for (const x of judgeRefusal(PROFILE, o)) expect(x.ok, req(ID_REFUSAL, x.doc, x.message)).toBe(true);
|
|
55
|
+
}, 60_000);
|
|
56
|
+
|
|
57
|
+
it('a refused dispatch emits no node completion, provider usage or envelope-reliability event', async () => {
|
|
58
|
+
const o = await observe();
|
|
59
|
+
if (o === null) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
|
|
60
|
+
if ('kind' in o) return softSkip(o.disposition, o.reason);
|
|
61
|
+
for (const x of judgeNoDispatch(PROFILE, o)) expect(x.ok, req(ID_NO_DISPATCH, x.doc, x.message)).toBe(true);
|
|
62
|
+
}, 60_000);
|
|
63
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — every PromptKind resolves and composes on real dispatch
|
|
3
|
+
* (`spec/v2/core/host-services.md` §`prompts` → §Resolution, §Composition;
|
|
4
|
+
* RFC 0027 §A). The v1 twin is `prompt-all-four-kinds-events`; the judges live
|
|
5
|
+
* in `lib/prompt-events-witness.ts`.
|
|
6
|
+
*
|
|
7
|
+
* A run of `conformance-prompt-all-four-kinds` (one node: system, user,
|
|
8
|
+
* schema-hint, and two few-shot refs) is created with `POST /runs`, and its
|
|
9
|
+
* log read with `GET /runs/{runId}/events/poll`:
|
|
10
|
+
* kinds the run completes; `agent.prompt-resolved` (v1
|
|
11
|
+
* `agent.promptResolved`) is emitted for each kind, and surfaces
|
|
12
|
+
* each of the five refs (`fewShotPromptRefs[1]` included); unless
|
|
13
|
+
* observability is `off`, every ref appears in some
|
|
14
|
+
* `prompt.composed.refs` — and under `off`, no `prompt.composed`
|
|
15
|
+
* at all; every payload validates against the v2 payload defs;
|
|
16
|
+
* ordering every `prompt.composed` follows an `agent.prompt-resolved` for
|
|
17
|
+
* its node.
|
|
18
|
+
*
|
|
19
|
+
* Dispositions: `prompts` not advertised or the fixture not in `fixtures[]` ⇒
|
|
20
|
+
* `inapplicable` (the fixture is the opt-in); discovery unreadable ⇒
|
|
21
|
+
* `blocked`; the event codemap unreadable ⇒ `blocked`.
|
|
22
|
+
*
|
|
23
|
+
* Proven against a scratch double in `lib/prompt-events-witness.test.ts`.
|
|
24
|
+
*
|
|
25
|
+
* @see spec/v2/core/host-services.md §Resolution
|
|
26
|
+
* @see conformance/fixtures/conformance-prompt-all-four-kinds.json
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { v2Discovery, v2RefValidator } from '../lib/v2.js';
|
|
31
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
32
|
+
import { req } from '../lib/requirement-ids.js';
|
|
33
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
34
|
+
import { ALL_FOUR_FIXTURE, driveFixture, judgeAllFourKinds, judgeOrdering, type FixtureObservation, type Skip } from '../lib/prompt-events-witness.js';
|
|
35
|
+
|
|
36
|
+
const PROFILE = majorProfile(2);
|
|
37
|
+
const ID_KINDS = 'openwop.requirement.prompts.all-four-kinds-events';
|
|
38
|
+
const ID_ORDER = 'openwop.requirement.prompts.resolution-precedes-composition';
|
|
39
|
+
const validators = {
|
|
40
|
+
resolved: v2RefValidator('run-event-payloads.schema.json#/$defs/agentPromptResolved'),
|
|
41
|
+
composed: v2RefValidator('run-event-payloads.schema.json#/$defs/promptComposed'),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let run: Promise<Skip | FixtureObservation> | undefined;
|
|
45
|
+
type Observed = { ok: true; o: FixtureObservation } | { ok: false; skip: () => undefined };
|
|
46
|
+
async function observed(): Promise<Observed> {
|
|
47
|
+
if (!process.env['OPENWOP_BASE_URL']) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
|
|
48
|
+
let doc: Record<string, unknown> | null;
|
|
49
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
50
|
+
if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
|
|
51
|
+
run ??= driveFixture(PROFILE, doc, ALL_FOUR_FIXTURE);
|
|
52
|
+
const o = await run;
|
|
53
|
+
if ('kind' in o) return { ok: false, skip: () => softSkip(o.disposition, o.reason) };
|
|
54
|
+
return { ok: true, o };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('v2 prompt events: all four kinds (host-services.md §Resolution, §Composition)', () => {
|
|
58
|
+
it('each kind and each configured ref is resolved and composed', async () => {
|
|
59
|
+
const r = await observed();
|
|
60
|
+
if (!r.ok) return r.skip();
|
|
61
|
+
for (const x of judgeAllFourKinds(r.o, validators)) expect(x.ok, req(ID_KINDS, x.doc, x.message)).toBe(true);
|
|
62
|
+
}, 60_000);
|
|
63
|
+
|
|
64
|
+
it('every prompt.composed follows an agent.prompt-resolved for its node', async () => {
|
|
65
|
+
const r = await observed();
|
|
66
|
+
if (!r.ok) return r.skip();
|
|
67
|
+
for (const x of judgeOrdering(r.o)) expect(x.ok, req(ID_ORDER, x.doc, x.message)).toBe(true);
|
|
68
|
+
}, 60_000);
|
|
69
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — a node's systemPromptRef resolves and composes on real dispatch
|
|
3
|
+
* (`spec/v2/core/host-services.md` §`prompts` → §Resolution, §Composition;
|
|
4
|
+
* RFCs 0027 §E, 0029 §A). The v1 twin is `prompt-end-to-end-events`; the
|
|
5
|
+
* judges live in `lib/prompt-events-witness.ts`.
|
|
6
|
+
*
|
|
7
|
+
* A run of `conformance-prompt-end-to-end` (node `writer`, `systemPromptRef:
|
|
8
|
+
* prompt:conformance.prompt.writer-system@1.0.0`) is created with
|
|
9
|
+
* `POST /runs`, and its log read with `GET /runs/{runId}/events/poll`:
|
|
10
|
+
* resolution the run completes; `agent.prompt-resolved` (kind `system`,
|
|
11
|
+
* node `writer`) names the node layer as the applied entry and
|
|
12
|
+
* resolves the configured ref;
|
|
13
|
+
* composition unless observability is `off`, `prompt.composed` carries a
|
|
14
|
+
* `sha256:<hex64>` hash and kind `system-only`; bodies
|
|
15
|
+
* (`composed`, `systemPrompt`) only under `full` — NEW at v2:
|
|
16
|
+
* under `hashed` (the default) no body field appears (v1
|
|
17
|
+
* required a body unconditionally); under `off`, no
|
|
18
|
+
* `prompt.composed` at all;
|
|
19
|
+
* ordering every `prompt.composed` follows an `agent.prompt-resolved`
|
|
20
|
+
* for its node.
|
|
21
|
+
*
|
|
22
|
+
* Dispositions: `prompts` not advertised or the fixture not in `fixtures[]` ⇒
|
|
23
|
+
* `inapplicable`; discovery unreadable ⇒ `blocked`.
|
|
24
|
+
*
|
|
25
|
+
* Proven against a scratch double in `lib/prompt-events-witness.test.ts`.
|
|
26
|
+
*
|
|
27
|
+
* @see spec/v2/core/host-services.md §Resolution, §Composition
|
|
28
|
+
* @see conformance/fixtures/conformance-prompt-end-to-end.json
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { v2Discovery, v2RefValidator } from '../lib/v2.js';
|
|
33
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
34
|
+
import { req } from '../lib/requirement-ids.js';
|
|
35
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
36
|
+
import { END_TO_END_FIXTURE, driveFixture, judgeEndToEnd, judgeOrdering, type FixtureObservation, type Skip } from '../lib/prompt-events-witness.js';
|
|
37
|
+
|
|
38
|
+
const PROFILE = majorProfile(2);
|
|
39
|
+
const ID_E2E = 'openwop.requirement.prompts.end-to-end-resolution-and-composition';
|
|
40
|
+
const ID_ORDER = 'openwop.requirement.prompts.resolution-precedes-composition';
|
|
41
|
+
const validators = {
|
|
42
|
+
resolved: v2RefValidator('run-event-payloads.schema.json#/$defs/agentPromptResolved'),
|
|
43
|
+
composed: v2RefValidator('run-event-payloads.schema.json#/$defs/promptComposed'),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let run: Promise<Skip | FixtureObservation> | undefined;
|
|
47
|
+
type Observed = { ok: true; o: FixtureObservation } | { ok: false; skip: () => undefined };
|
|
48
|
+
async function observed(): Promise<Observed> {
|
|
49
|
+
if (!process.env['OPENWOP_BASE_URL']) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
|
|
50
|
+
let doc: Record<string, unknown> | null;
|
|
51
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
52
|
+
if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
|
|
53
|
+
run ??= driveFixture(PROFILE, doc, END_TO_END_FIXTURE);
|
|
54
|
+
const o = await run;
|
|
55
|
+
if ('kind' in o) return { ok: false, skip: () => softSkip(o.disposition, o.reason) };
|
|
56
|
+
return { ok: true, o };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('v2 prompt events: end to end (host-services.md §Resolution, §Composition)', () => {
|
|
60
|
+
it('the node layer wins, and prompt.composed carries the hash with bodies only under full', async () => {
|
|
61
|
+
const r = await observed();
|
|
62
|
+
if (!r.ok) return r.skip();
|
|
63
|
+
for (const x of judgeEndToEnd(r.o, validators)) expect(x.ok, req(ID_E2E, x.doc, x.message)).toBe(true);
|
|
64
|
+
}, 60_000);
|
|
65
|
+
|
|
66
|
+
it('agent.prompt-resolved precedes prompt.composed for the node', async () => {
|
|
67
|
+
const r = await observed();
|
|
68
|
+
if (!r.ok) return r.skip();
|
|
69
|
+
for (const x of judgeOrdering(r.o)) expect(x.ok, req(ID_ORDER, x.doc, x.message)).toBe(true);
|
|
70
|
+
}, 60_000);
|
|
71
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the prompt library read surface (`spec/v2/core/host-services.md`
|
|
3
|
+
* §`prompts` + §Library; `api/v2/openapi.yaml` `listPromptTemplates`,
|
|
4
|
+
* `getPromptTemplate`; RFC 0028 §A). The v1 twin is `prompt-list-and-fetch`;
|
|
5
|
+
* the legs live in `lib/prompt-library-witness.ts`.
|
|
6
|
+
*
|
|
7
|
+
* list `GET /prompts` answers `{ items, nextCursor? }`; every item
|
|
8
|
+
* validates against the closed v2 PromptTemplate;
|
|
9
|
+
* filters `?kind=system` and `?source=host` narrow;
|
|
10
|
+
* fetch a listed template is fetched by id;
|
|
11
|
+
* etag an ETag, when sent (a SHOULD), revalidates to `304`;
|
|
12
|
+
* unknown an unknown id is `404` with the v2 error envelope;
|
|
13
|
+
* gate off NEW at v2, unaided: with `prompts.endpointsSupported` not
|
|
14
|
+
* advertised (the record absent, or the facet not `true`),
|
|
15
|
+
* `GET /prompts` answers `404 not_found` (errors.md
|
|
16
|
+
* §Unadvertised operations).
|
|
17
|
+
*
|
|
18
|
+
* Dispositions: no target ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`;
|
|
19
|
+
* `prompts` absent or `endpointsSupported` not `true` ⇒ the read legs are
|
|
20
|
+
* `inapplicable` (gated by presence, never a strict-mode failure) and the
|
|
21
|
+
* gate-off leg binds; an empty library ⇒ fetch/etag `inapplicable`.
|
|
22
|
+
*
|
|
23
|
+
* Proven against a scratch double in `lib/prompt-library-witness.test.ts`.
|
|
24
|
+
*
|
|
25
|
+
* @see spec/v2/core/host-services.md §prompts
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { describe, it, expect } from 'vitest';
|
|
29
|
+
import { v2Discovery, v2Validator } from '../lib/v2.js';
|
|
30
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
31
|
+
import { req } from '../lib/requirement-ids.js';
|
|
32
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
33
|
+
import { etagLeg, fetchLeg, filterLeg, gateOffReadLeg, listLeg, promptAdverts, unknownLeg, type PromptAdverts } from '../lib/prompt-library-witness.js';
|
|
34
|
+
|
|
35
|
+
const PROFILE = majorProfile(2);
|
|
36
|
+
const ID_LIST = 'openwop.requirement.prompts.list-shape';
|
|
37
|
+
const ID_FILTER = 'openwop.requirement.prompts.list-filters';
|
|
38
|
+
const ID_FETCH = 'openwop.requirement.prompts.fetch-by-id';
|
|
39
|
+
const ID_ETAG = 'openwop.requirement.prompts.etag-revalidation';
|
|
40
|
+
const ID_UNKNOWN = 'openwop.requirement.prompts.unknown-template-404';
|
|
41
|
+
const ID_GATE = 'openwop.requirement.prompts.endpoints-gate-off-404';
|
|
42
|
+
const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
|
|
43
|
+
const template = v2Validator('prompt-template');
|
|
44
|
+
const envelope = v2Validator('error-envelope');
|
|
45
|
+
|
|
46
|
+
type Ready = { ok: true; a: PromptAdverts } | { ok: false; skip: () => undefined };
|
|
47
|
+
async function ready(): Promise<Ready> {
|
|
48
|
+
if (HTTP_SKIP) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
|
|
49
|
+
let doc: Record<string, unknown> | null;
|
|
50
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
51
|
+
if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
|
|
52
|
+
return { ok: true, a: promptAdverts(PROFILE, doc) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('v2 prompt library: read surface (host-services.md §prompts)', () => {
|
|
56
|
+
it('GET /prompts answers items whose every entry validates', async () => {
|
|
57
|
+
const r = await ready();
|
|
58
|
+
if (!r.ok) return r.skip();
|
|
59
|
+
const out = await listLeg(r.a, template);
|
|
60
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
61
|
+
for (const x of out.findings) expect(x.ok, req(ID_LIST, x.doc, x.message)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('the kind and source filters narrow the list', async () => {
|
|
65
|
+
const r = await ready();
|
|
66
|
+
if (!r.ok) return r.skip();
|
|
67
|
+
const out = await filterLeg(r.a);
|
|
68
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
69
|
+
for (const x of out.findings) expect(x.ok, req(ID_FILTER, x.doc, x.message)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('a listed template is fetched by id', async () => {
|
|
73
|
+
const r = await ready();
|
|
74
|
+
if (!r.ok) return r.skip();
|
|
75
|
+
const out = await fetchLeg(PROFILE, r.a, template);
|
|
76
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
77
|
+
for (const x of out.findings) expect(x.ok, req(ID_FETCH, x.doc, x.message)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('an ETag, when sent, revalidates to 304', async () => {
|
|
81
|
+
const r = await ready();
|
|
82
|
+
if (!r.ok) return r.skip();
|
|
83
|
+
const out = await etagLeg(PROFILE, r.a);
|
|
84
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
85
|
+
for (const x of out.findings) expect(x.ok, req(ID_ETAG, x.doc, x.message)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('an unknown templateId answers 404 with the error envelope', async () => {
|
|
89
|
+
const r = await ready();
|
|
90
|
+
if (!r.ok) return r.skip();
|
|
91
|
+
const out = await unknownLeg(PROFILE, r.a, envelope);
|
|
92
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
93
|
+
for (const x of out.findings) expect(x.ok, req(ID_UNKNOWN, x.doc, x.message)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('with endpointsSupported unadvertised, GET /prompts answers 404 not_found', async () => {
|
|
97
|
+
const r = await ready();
|
|
98
|
+
if (!r.ok) return r.skip();
|
|
99
|
+
const out = await gateOffReadLeg(r.a);
|
|
100
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
101
|
+
for (const x of out.findings) expect(x.ok, req(ID_GATE, x.doc, x.message)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|