@openwop/openwop-conformance 2.45.15 → 2.45.16
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 +26 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +1281 -4
- package/scenario-majors.json +101 -2
- package/schemas/CORPUS-STAMP.json +13 -13
- package/src/lib/artifact-schema-url-witness.ts +83 -0
- package/src/lib/dispatch-witness.ts +212 -0
- package/src/lib/family-advert-witness.ts +204 -5
- package/src/lib/fixture-run-observer.ts +97 -0
- package/src/lib/provider-usage-witness.ts +63 -0
- package/src/lib/run-graph-double.ts +144 -0
- package/src/lib/subworkflow-witness.ts +192 -0
- package/src/scenarios/v2-agent-runtime-advertisement.test.ts +51 -0
- package/src/scenarios/v2-ai-envelope-advertisement.test.ts +43 -0
- package/src/scenarios/v2-artifact-created-static.test.ts +92 -0
- package/src/scenarios/v2-artifact-type-schema-url.test.ts +44 -0
- package/src/scenarios/v2-artifact-types-advertisement.test.ts +64 -0
- package/src/scenarios/v2-authorization-advertisement.test.ts +66 -0
- package/src/scenarios/v2-channel-presence-advertisement.test.ts +41 -0
- package/src/scenarios/v2-conversation-turn-model-provenance-advertisement.test.ts +41 -0
- package/src/scenarios/v2-conversation-turn-model-provenance.test.ts +110 -0
- package/src/scenarios/v2-credentials-advertisement.test.ts +66 -0
- package/src/scenarios/v2-data-residency-admission.test.ts +98 -0
- package/src/scenarios/v2-data-residency-advertisement.test.ts +52 -0
- package/src/scenarios/v2-dead-letter-advertisement.test.ts +54 -0
- package/src/scenarios/v2-dispatch-input-mapping.test.ts +70 -0
- package/src/scenarios/v2-dispatch-loop.test.ts +80 -0
- package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +40 -0
- package/src/scenarios/v2-export-bundle-static.test.ts +108 -0
- package/src/scenarios/v2-multi-party-conversation-advertisement.test.ts +52 -0
- package/src/scenarios/v2-node-pack-runtimes-advertisement.test.ts +52 -0
- package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +41 -0
- package/src/scenarios/v2-portability-advertisement.test.ts +63 -0
- package/src/scenarios/v2-prompt-library-advertisement.test.ts +40 -0
- package/src/scenarios/v2-provider-usage-advertisement.test.ts +61 -0
- package/src/scenarios/v2-provider-usage-emission.test.ts +50 -0
- package/src/scenarios/v2-provider-usage-payload-static.test.ts +92 -0
- package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +52 -0
- package/src/scenarios/v2-scheduling-advertisement.test.ts +55 -0
- package/src/scenarios/v2-self-hosted-runner-advertisement.test.ts +52 -0
- package/src/scenarios/v2-self-hosted-runner-frames-static.test.ts +121 -0
- package/src/scenarios/v2-sub-workflow-advertisement.test.ts +53 -0
- package/src/scenarios/v2-subworkflow-input-mapping.test.ts +74 -0
- package/src/scenarios/v2-subworkflow-linkage.test.ts +76 -0
- package/src/scenarios/v2-ui-plugins-advertisement.test.ts +85 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `core.subWorkflow` witness (`execution.md` §`subWorkflow`), a fixture
|
|
3
|
+
* port through `POST /runs` and `events/poll`. No seam.
|
|
4
|
+
*
|
|
5
|
+
* Linkage, on `conformance-subworkflow-parent` → `conformance-subworkflow-child`:
|
|
6
|
+
* output the parent's `node.completed` for `subwf-call` carries
|
|
7
|
+
* `outputs.childRunId` and `outputs.childStatus`
|
|
8
|
+
* (`completed | failed | cancelled`), and the status names the
|
|
9
|
+
* child's real terminal status;
|
|
10
|
+
* outputMapping after the child completes, the mapped variable is copied
|
|
11
|
+
* into the parent (`childOutcome` ← child `childResult`);
|
|
12
|
+
* ancestry where `getRunAncestry` is served (advertised by
|
|
13
|
+
* `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`),
|
|
14
|
+
* the child's `parent` is the parent run with
|
|
15
|
+
* `cause: "core.subWorkflow"`.
|
|
16
|
+
* The v1 `parentRunId`/`parentNodeId` child-linkage leg is RETIRED at v2
|
|
17
|
+
* (`spec/v1/migrations.json` C4.18): `parentRunId` is fork lineage, not this link.
|
|
18
|
+
*
|
|
19
|
+
* Input mapping, on `conformance-subworkflow-input-mapping[-no-default]` →
|
|
20
|
+
* `conformance-subworkflow-input-mapping-child` (child default `receivedPrdId: "baked-in"`):
|
|
21
|
+
* seed the child's variable bag is seeded: `receivedPrdId` is present;
|
|
22
|
+
* override the mapping lands after and over the default: `"prd-1"`;
|
|
23
|
+
* unset an unset parent variable arrives undefined, never an error or
|
|
24
|
+
* `null` (the run completes; the child's value is absent or its
|
|
25
|
+
* own default).
|
|
26
|
+
*
|
|
27
|
+
* The drivers observe and assert nothing; the judges are pure, proven in
|
|
28
|
+
* `subworkflow-witness.test.ts`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { driver } from './driver.js';
|
|
32
|
+
import { isFixtureAdvertised } from './fixtures.js';
|
|
33
|
+
import type { MajorProfile } from './major-profile.js';
|
|
34
|
+
import {
|
|
35
|
+
completionsOf, finding, isRecord, observed, observeRun, readRun, runCompleted, skip,
|
|
36
|
+
type Finding, type ObservedRun, type ObserveOpts, type Outcome, type RunRead, type Skip, type Validate,
|
|
37
|
+
} from './fixture-run-observer.js';
|
|
38
|
+
|
|
39
|
+
export const PARENT_FIXTURE = 'conformance-subworkflow-parent';
|
|
40
|
+
export const CHILD_FIXTURE = 'conformance-subworkflow-child';
|
|
41
|
+
export const IM_PARENT_FIXTURE = 'conformance-subworkflow-input-mapping';
|
|
42
|
+
export const IM_NO_DEFAULT_FIXTURE = 'conformance-subworkflow-input-mapping-no-default';
|
|
43
|
+
export const IM_CHILD_FIXTURE = 'conformance-subworkflow-input-mapping-child';
|
|
44
|
+
export const SUBWF_NODE = 'subwf-call';
|
|
45
|
+
|
|
46
|
+
const DOC = 'execution.md §subWorkflow';
|
|
47
|
+
const CHILD_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
48
|
+
|
|
49
|
+
/** The child a `core.subWorkflow` node started, as its `node.completed` names it. */
|
|
50
|
+
export interface ChildLink {
|
|
51
|
+
readonly outputs: Record<string, unknown> | undefined;
|
|
52
|
+
readonly childRunId: string | undefined;
|
|
53
|
+
readonly child: RunRead | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LinkageObservation extends ChildLink {
|
|
57
|
+
readonly parent: ObservedRun;
|
|
58
|
+
/** `undefined`: the host does not advertise `getRunAncestry`, so the leg does not bind. */
|
|
59
|
+
readonly ancestry: { readonly status: number; readonly json: unknown } | undefined;
|
|
60
|
+
}
|
|
61
|
+
export interface MappingObservation extends ChildLink { readonly parent: ObservedRun }
|
|
62
|
+
|
|
63
|
+
/** Whether the host advertises `getRunAncestry` (`runs.md` §Surface gate). */
|
|
64
|
+
export function ancestryAdvertised(profile: MajorProfile, doc: unknown): boolean {
|
|
65
|
+
const ma = profile.family(doc, 'multiAgent');
|
|
66
|
+
const em = isRecord(ma?.['executionModel']) ? ma['executionModel'] : undefined;
|
|
67
|
+
const chc = isRecord(em?.['crossHostCausation']) ? em['crossHostCausation'] : undefined;
|
|
68
|
+
return chc?.['ancestryEndpointSupported'] === true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fixturesGate(...ids: string[]): Skip | null {
|
|
72
|
+
for (const id of ids) if (!isFixtureAdvertised(id)) return skip('inapplicable', `fixture ${id} is not advertised — the fixture is the opt-in`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function followChild(profile: MajorProfile, parent: ObservedRun): Promise<ChildLink> {
|
|
77
|
+
const done = completionsOf(parent.events, SUBWF_NODE)[0];
|
|
78
|
+
const outputs = isRecord(done?.payload['outputs']) ? done.payload['outputs'] : undefined;
|
|
79
|
+
const childRunId = typeof outputs?.['childRunId'] === 'string' && outputs['childRunId'].length > 0 ? outputs['childRunId'] : undefined;
|
|
80
|
+
return { outputs, childRunId, child: childRunId === undefined ? undefined : await readRun(profile, childRunId) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Run the linkage fixture and follow it to the child. Asserts nothing. */
|
|
84
|
+
export async function driveLinkage(profile: MajorProfile, doc: unknown, opts: ObserveOpts = {}): Promise<Skip | LinkageObservation> {
|
|
85
|
+
const g = fixturesGate(PARENT_FIXTURE, CHILD_FIXTURE);
|
|
86
|
+
if (g) return g;
|
|
87
|
+
const parent = await observeRun(profile, PARENT_FIXTURE, opts);
|
|
88
|
+
const link = await followChild(profile, parent);
|
|
89
|
+
let ancestry: LinkageObservation['ancestry'];
|
|
90
|
+
if (ancestryAdvertised(profile, doc) && link.childRunId !== undefined) {
|
|
91
|
+
const r = await driver.get(`${profile.runsPath}/${profile.idSegment(link.childRunId)}/ancestry`);
|
|
92
|
+
ancestry = { status: r.status, json: r.json };
|
|
93
|
+
}
|
|
94
|
+
return { parent, ...link, ancestry };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Run an input-mapping fixture and follow it to the child. Asserts nothing. */
|
|
98
|
+
export async function driveInputMapping(profile: MajorProfile, fixture: string, opts: ObserveOpts = {}): Promise<Skip | MappingObservation> {
|
|
99
|
+
const g = fixturesGate(fixture, IM_CHILD_FIXTURE);
|
|
100
|
+
if (g) return g;
|
|
101
|
+
const parent = await observeRun(profile, fixture, opts);
|
|
102
|
+
return { parent, ...(await followChild(profile, parent)) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Leg: `node.completed` carries `outputs.childRunId` and `outputs.childStatus`. Pure. */
|
|
106
|
+
export function judgeOutput(o: LinkageObservation): Outcome {
|
|
107
|
+
const out: Finding[] = runCompleted(o.parent);
|
|
108
|
+
const status = o.outputs?.['childStatus'];
|
|
109
|
+
const childStatus = o.child?.snapshot['status'];
|
|
110
|
+
out.push(
|
|
111
|
+
finding(completionsOf(o.parent.events, SUBWF_NODE).length > 0, `${DOC} (Output)`, `the parent log MUST carry node.completed for the core.subWorkflow node ${SUBWF_NODE}`),
|
|
112
|
+
finding(o.childRunId !== undefined, `${DOC} (Output)`, `node.completed MUST carry outputs.childRunId (got ${JSON.stringify(o.outputs?.['childRunId'])})`),
|
|
113
|
+
finding(typeof status === 'string' && CHILD_STATUSES.has(status), `${DOC} (Output)`, `node.completed MUST carry outputs.childStatus, one of completed | failed | cancelled (got ${JSON.stringify(status)})`),
|
|
114
|
+
);
|
|
115
|
+
if (o.child !== undefined) {
|
|
116
|
+
out.push(
|
|
117
|
+
finding(o.child.status === 200, `${DOC} (Output)`, `outputs.childRunId MUST name a readable run (GET answered ${o.child.status})`),
|
|
118
|
+
finding(o.child.status !== 200 || status === childStatus, `${DOC} (Output)`, `outputs.childStatus MUST be the child's terminal status (got ${JSON.stringify(status)}, child is ${JSON.stringify(childStatus)})`),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return observed(out);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Leg: the mapped child variable is copied into the parent. Pure. */
|
|
125
|
+
export function judgeOutputMapping(o: LinkageObservation): Outcome {
|
|
126
|
+
const vars = isRecord(o.parent.snapshot['variables']) ? o.parent.snapshot['variables'] : {};
|
|
127
|
+
return observed([
|
|
128
|
+
...runCompleted(o.parent),
|
|
129
|
+
finding(vars['childOutcome'] === 'child-completed', `${DOC} (outputMapping)`, `after the child completes, the host MUST copy childResult into the parent's childOutcome ("child-completed"; got ${JSON.stringify(vars['childOutcome'])})`),
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Leg: the child's ancestry names the parent with `cause: "core.subWorkflow"`. Pure. */
|
|
134
|
+
export function judgeAncestry(o: LinkageObservation, validate: Validate): Outcome {
|
|
135
|
+
if (o.childRunId === undefined) return skip('blocked', 'the child run id is unobserved (node.completed carried no outputs.childRunId — the output leg convicts that), so its ancestry cannot be read');
|
|
136
|
+
if (o.ancestry === undefined) return skip('inapplicable', 'the host does not advertise multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported, so getRunAncestry is not served');
|
|
137
|
+
const body = o.ancestry.json;
|
|
138
|
+
const parent = isRecord(body) && isRecord(body['parent']) ? body['parent'] : undefined;
|
|
139
|
+
const v = validate(body);
|
|
140
|
+
return observed([
|
|
141
|
+
finding(o.ancestry.status === 200, `${DOC} (Parent link); runs.md §Diff and ancestry`, `getRunAncestry of the child MUST answer 200 (got ${o.ancestry.status})`),
|
|
142
|
+
finding(v.ok, 'schemas run-ancestry-response', `the ancestry body MUST validate: ${v.errors}`),
|
|
143
|
+
finding(parent !== undefined && parent['runId'] === o.parent.runId, `${DOC} (Parent link)`, `the child's parent MUST be the parent run ${String(o.parent.runId)} (got ${JSON.stringify(parent?.['runId'] ?? (isRecord(body) ? body['parent'] : undefined))})`),
|
|
144
|
+
finding(parent?.['cause'] === 'core.subWorkflow', `${DOC} (Parent link)`, `the child's parent.cause MUST be core.subWorkflow (got ${JSON.stringify(parent?.['cause'])})`),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const childVars = (o: MappingObservation): Record<string, unknown> | undefined =>
|
|
149
|
+
o.child !== undefined && o.child.status === 200 ? (isRecord(o.child.snapshot['variables']) ? o.child.snapshot['variables'] : {}) : undefined;
|
|
150
|
+
|
|
151
|
+
function childReached(o: MappingObservation): Finding[] {
|
|
152
|
+
return [
|
|
153
|
+
...runCompleted(o.parent),
|
|
154
|
+
finding(o.childRunId !== undefined, `${DOC} (Output)`, `the parent's node.completed for ${SUBWF_NODE} MUST carry outputs.childRunId`),
|
|
155
|
+
finding(o.child?.status === 200, `${DOC} (Output)`, `outputs.childRunId MUST name a readable run (GET answered ${String(o.child?.status)})`),
|
|
156
|
+
finding(o.child?.snapshot['status'] === 'completed', `fixtures.md ${IM_CHILD_FIXTURE}`, `the child run MUST end completed (got ${JSON.stringify(o.child?.snapshot['status'])})`),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Leg: the child's variable bag is seeded at creation — `receivedPrdId` is present. Pure. */
|
|
161
|
+
export function judgeSeed(o: MappingObservation): Outcome {
|
|
162
|
+
const v = childVars(o)?.['receivedPrdId'];
|
|
163
|
+
return observed([
|
|
164
|
+
...childReached(o),
|
|
165
|
+
finding(v !== undefined && v !== null, `${DOC} (inputMapping: defaultValue MUST seed first)`, `the child's receivedPrdId MUST be seeded at creation (got ${JSON.stringify(v)})`),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Leg: the mapping lands after and over the child's default. Pure. */
|
|
170
|
+
export function judgeOverride(o: MappingObservation): Outcome {
|
|
171
|
+
const vars = childVars(o);
|
|
172
|
+
if (vars === undefined) return skip('blocked', 'the child run is unread (no outputs.childRunId, or GET failed — the seed leg convicts that)');
|
|
173
|
+
const v = vars['receivedPrdId'];
|
|
174
|
+
return observed([
|
|
175
|
+
finding(v !== 'baked-in', `${DOC} (inputMapping: after and over defaultValue)`, 'the child\'s defaultValue "baked-in" MUST NOT win over the mapped parent value'),
|
|
176
|
+
finding(v === 'prd-1', `${DOC} (inputMapping)`, `the child's receivedPrdId MUST be the parent's currentPrdId projection "prd-1" (got ${JSON.stringify(v)})`),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Leg: an unset parent variable arrives undefined — no error, no `null`. Pure. */
|
|
181
|
+
export function judgeUnset(o: MappingObservation): Outcome {
|
|
182
|
+
const vars = childVars(o);
|
|
183
|
+
const out = childReached(o);
|
|
184
|
+
if (vars !== undefined) {
|
|
185
|
+
const v = vars['receivedPrdId'];
|
|
186
|
+
out.push(
|
|
187
|
+
finding(v !== null, `${DOC} (inputMapping: never null)`, 'an unset parent variable MUST NOT arrive as null'),
|
|
188
|
+
finding(!('receivedPrdId' in vars) || v === 'baked-in', `${DOC} (inputMapping: arrives undefined)`, `an unset parent variable MUST arrive undefined: the child's value is absent or its own default (got ${JSON.stringify(v)})`),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return observed(out);
|
|
192
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `agentRuntime` advertisement (`spec/v2/core/host-services.md` §`agentRuntime`): the agent runtime claim. It had no v1 witness;
|
|
3
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
4
|
+
*
|
|
5
|
+
* record the record validates against its capabilities-schema seat;
|
|
6
|
+
* implies-manifest-runtime advertising agentRuntime also advertises agents.manifestRuntime;
|
|
7
|
+
*
|
|
8
|
+
* Not here: the `ctx` surface (`spawn`, `delegate`, `consensus`, `messageSend`) cannot be observed from outside a node.
|
|
9
|
+
*
|
|
10
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `agentRuntime` absent, or the
|
|
11
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
12
|
+
*
|
|
13
|
+
* @see spec/v2/core/host-services.md §`agentRuntime`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
18
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
19
|
+
import { req } from '../lib/requirement-ids.js';
|
|
20
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
21
|
+
import { agentRuntimeImpliesManifestRuntimeLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
22
|
+
|
|
23
|
+
const PROFILE = majorProfile(2);
|
|
24
|
+
const DOC = 'host-services.md §agentRuntime';
|
|
25
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
26
|
+
const ID_RECORD = 'openwop.requirement.agent-runtime.advert-record-schema';
|
|
27
|
+
const ID_IMPLIES_MANIFEST_RUNTIME = 'openwop.requirement.agent-runtime.implies-manifest-runtime';
|
|
28
|
+
|
|
29
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
30
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('v2 agentRuntime advertisement (host-services.md §agentRuntime)', () => {
|
|
34
|
+
it('the agentRuntime record validates against its capabilities-schema seat', async () => {
|
|
35
|
+
const doc = await discovery();
|
|
36
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
37
|
+
if (!(await familyAdvertised('agentRuntime'))) return softSkip('inapplicable', 'the host does not advertise agentRuntime');
|
|
38
|
+
const out = recordSchemaLeg(PROFILE, doc, 'agentRuntime', DOC);
|
|
39
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
40
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('advertising agentRuntime also advertises agents.manifestRuntime', async () => {
|
|
44
|
+
const doc = await discovery();
|
|
45
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
46
|
+
if (!(await familyAdvertised('agentRuntime'))) return softSkip('inapplicable', 'the host does not advertise agentRuntime');
|
|
47
|
+
const out = agentRuntimeImpliesManifestRuntimeLeg(PROFILE, doc);
|
|
48
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
49
|
+
for (const x of out.findings) expect(x.ok, req(ID_IMPLIES_MANIFEST_RUNTIME, x.doc, x.message)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `aiEnvelope` advertisement (`spec/v2/core/host-services.md`
|
|
3
|
+
* §aiEnvelope; RFC 0144). The v1 twin is the record shape `aiEnvelope` carried
|
|
4
|
+
* at v1; the legs live in `lib/family-advert-witness.ts`.
|
|
5
|
+
*
|
|
6
|
+
* record the record (with its optional `await` boolean) validates against
|
|
7
|
+
* its capabilities-schema seat.
|
|
8
|
+
*
|
|
9
|
+
* Not here: exposing `ctx.aiEnvelope.generate` / `await` to pack code is a
|
|
10
|
+
* pack-runtime surface with no wire witness.
|
|
11
|
+
*
|
|
12
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `aiEnvelope` absent, or the
|
|
13
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
14
|
+
*
|
|
15
|
+
* @see spec/v2/core/host-services.md §aiEnvelope
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
20
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
21
|
+
import { req } from '../lib/requirement-ids.js';
|
|
22
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
23
|
+
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
24
|
+
|
|
25
|
+
const PROFILE = majorProfile(2);
|
|
26
|
+
const DOC = 'host-services.md §aiEnvelope';
|
|
27
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
28
|
+
const ID_RECORD = 'openwop.requirement.ai-envelope.advert-record-schema';
|
|
29
|
+
|
|
30
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
31
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('v2 aiEnvelope advertisement (host-services.md §aiEnvelope)', () => {
|
|
35
|
+
it('the aiEnvelope record validates against its capabilities-schema seat', async () => {
|
|
36
|
+
const doc = await discovery();
|
|
37
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
38
|
+
if (!(await familyAdvertised('aiEnvelope'))) return softSkip('inapplicable', 'the host does not advertise aiEnvelope');
|
|
39
|
+
const out = recordSchemaLeg(PROFILE, doc, 'aiEnvelope', DOC);
|
|
40
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
41
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `artifact.created` payload, statically
|
|
3
|
+
* (`spec/v2/core/artifact-type-packs.md` §Registration;
|
|
4
|
+
* `schemas/v2/run-event-payloads.schema.json#/$defs/artifactCreated`; RFCs
|
|
5
|
+
* 0071, 0075, 0138, 0145, 0185). The v1 twins are the schema halves of
|
|
6
|
+
* `artifact-type-store-emission` and `artifact-type-registration-source`;
|
|
7
|
+
* their fixture-run legs are not ported here.
|
|
8
|
+
*
|
|
9
|
+
* type `artifact.created` is a registered v2 event type;
|
|
10
|
+
* required `artifactId` and `artifactType` are REQUIRED (the RFC 0138
|
|
11
|
+
* replay correction reads the type from the event);
|
|
12
|
+
* provenance `registrationSource` is `pack` or `host` ("pack-registered …
|
|
13
|
+
* `registrationSource: "pack"`, host-registered … `"host"`"),
|
|
14
|
+
* and the discovery per-type seat carries the same vocabulary,
|
|
15
|
+
* so the two surfaces cannot disagree on a value;
|
|
16
|
+
* registered `registered` is a boolean;
|
|
17
|
+
* closed an unknown key is rejected; a vendor key under the RFC 0185
|
|
18
|
+
* hatch (`x-`, `vendor.`, `openwop-`) is carried.
|
|
19
|
+
*
|
|
20
|
+
* Host-free: every leg runs with no OPENWOP_BASE_URL.
|
|
21
|
+
*
|
|
22
|
+
* @see spec/v2/core/artifact-type-packs.md §Registration
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
29
|
+
import { v2RefValidator } from '../lib/v2.js';
|
|
30
|
+
import { req } from '../lib/requirement-ids.js';
|
|
31
|
+
|
|
32
|
+
const DOC = 'artifact-type-packs.md §Registration';
|
|
33
|
+
const ID_TYPE = 'openwop.requirement.artifact-types.created-event-type-registered';
|
|
34
|
+
const ID_REQUIRED = 'openwop.requirement.artifact-types.created-requires-artifact-type';
|
|
35
|
+
const ID_PROVENANCE = 'openwop.requirement.artifact-types.created-registration-source-enum';
|
|
36
|
+
const ID_REGISTERED = 'openwop.requirement.artifact-types.created-registered-boolean';
|
|
37
|
+
const ID_CLOSED = 'openwop.requirement.artifact-types.created-payload-closed';
|
|
38
|
+
|
|
39
|
+
const GOOD = { artifactId: 'art_1', artifactType: 'vendor.acme.prd', nodeId: 'n1', registered: true, registrationSource: 'host' } as const;
|
|
40
|
+
|
|
41
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
42
|
+
function enumAt(root: unknown, path: readonly string[]): unknown {
|
|
43
|
+
let cur: unknown = root;
|
|
44
|
+
for (const k of path) cur = isRecord(cur) ? cur[k] : undefined;
|
|
45
|
+
return isRecord(cur) ? cur['enum'] : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('v2 artifact.created payload (artifact-type-packs.md §Registration)', () => {
|
|
49
|
+
const validate = v2RefValidator('run-event-payloads.schema.json#/$defs/artifactCreated');
|
|
50
|
+
|
|
51
|
+
it('artifact.created is a registered v2 run-event type', () => {
|
|
52
|
+
const r = v2RefValidator('run-event.schema.json#/properties/type')('artifact.created');
|
|
53
|
+
expect(r.ok, req(ID_TYPE, DOC, `artifact.created MUST be accepted by the v2 run-event type union (${r.errors})`)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('accepts a well-formed payload, with either provenance or none', () => {
|
|
57
|
+
const { registrationSource: _src, ...noSource } = GOOD;
|
|
58
|
+
for (const p of [GOOD, { ...GOOD, registrationSource: 'pack' }, noSource]) {
|
|
59
|
+
const r = validate(p);
|
|
60
|
+
expect(r.ok, req(ID_PROVENANCE, DOC, `artifact.created ${JSON.stringify(p)} MUST validate (${r.errors})`)).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each(['artifactId', 'artifactType'])('rejects a payload without %s', (field) => {
|
|
65
|
+
const p: Record<string, unknown> = { ...GOOD };
|
|
66
|
+
delete p[field];
|
|
67
|
+
expect(validate(p).ok, req(ID_REQUIRED, DOC, `artifact.created without ${field} MUST be rejected`)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rejects a registrationSource outside pack | host', () => {
|
|
71
|
+
expect(validate({ ...GOOD, registrationSource: 'vendor' }).ok, req(ID_PROVENANCE, DOC, 'registrationSource MUST be pack or host')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('the discovery per-type registrationSource carries the same vocabulary as the event', () => {
|
|
75
|
+
const payloads: unknown = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'run-event-payloads.schema.json'), 'utf8'));
|
|
76
|
+
const caps: unknown = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'capabilities.schema.json'), 'utf8'));
|
|
77
|
+
const event = enumAt(payloads, ['$defs', 'artifactCreated', 'properties', 'registrationSource']);
|
|
78
|
+
const advert = enumAt(caps, ['properties', 'artifactTypes', 'properties', 'types', 'additionalProperties', 'properties', 'registrationSource']);
|
|
79
|
+
expect(event, req(ID_PROVENANCE, DOC, 'artifact.created.registrationSource MUST be the pack | host enum')).toEqual(['pack', 'host']);
|
|
80
|
+
expect(advert, req(ID_PROVENANCE, 'capabilities.schema.json §artifactTypes.types', 'the advertised per-type registrationSource MUST use the vocabulary artifact.created carries')).toEqual(event);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects a non-boolean registered', () => {
|
|
84
|
+
expect(validate({ ...GOOD, registered: 'true' }).ok, req(ID_REGISTERED, DOC, 'registered MUST be a boolean')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rejects an unknown key, and carries a vendor key under the RFC 0185 hatch', () => {
|
|
88
|
+
expect(validate({ ...GOOD, payload: { secret: 1 } }).ok, req(ID_CLOSED, DOC, 'an unknown key on artifact.created MUST be rejected')).toBe(false);
|
|
89
|
+
const r = validate({ ...GOOD, 'x-acme-trace': 'abc', 'vendor.acme.note': 'n' });
|
|
90
|
+
expect(r.ok, req(ID_CLOSED, 'run-event-payloads.schema.json §artifactCreated (RFC 0185)', `a vendor-prefixed key MUST be carried (${r.errors})`)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the canonical schema URL of a host-registered artifact type
|
|
3
|
+
* (`spec/v2/core/artifact-type-packs.md` §Schema distribution; RFCs 0071, 0075,
|
|
4
|
+
* 0145). NEW at v2, unaided: no seam and no fixture.
|
|
5
|
+
*
|
|
6
|
+
* "A host advertising `artifactTypes` SHOULD serve each installed type's schema
|
|
7
|
+
* there, and MUST for a host-registered type whose `schemaVersion` it
|
|
8
|
+
* advertises." The MUST set is read from discovery: each
|
|
9
|
+
* `artifactTypes.types[id]` with `registrationSource: "host"` and a
|
|
10
|
+
* `schemaVersion`. For each, `GET /schemas/artifacts/{id}.schema.json` answers
|
|
11
|
+
* `200`, `Content-Type: application/schema+json`, and a schema whose `$id` is
|
|
12
|
+
* that canonical URL. Pack-backed types are a SHOULD and are not asserted.
|
|
13
|
+
*
|
|
14
|
+
* Dispositions: `artifactTypes` not advertised, or no host-registered type with
|
|
15
|
+
* a `schemaVersion` ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`.
|
|
16
|
+
*
|
|
17
|
+
* Proven against a double in `lib/artifact-schema-url-witness.test.ts`.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/v2/core/artifact-type-packs.md §Schema distribution
|
|
20
|
+
* @see schemas/v2/capabilities.schema.json §artifactTypes.types.registrationSource
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { familyAdvertised, v2Discovery } from '../lib/v2.js';
|
|
25
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
26
|
+
import { req } from '../lib/requirement-ids.js';
|
|
27
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
28
|
+
import { driveSchemaUrls, judgeSchemaUrl } from '../lib/artifact-schema-url-witness.js';
|
|
29
|
+
|
|
30
|
+
const PROFILE = majorProfile(2);
|
|
31
|
+
const ID_SERVED = 'openwop.requirement.artifact-types.host-registered-schema-served';
|
|
32
|
+
|
|
33
|
+
describe('v2 artifact types: the canonical schema URL (artifact-type-packs.md §Schema distribution)', () => {
|
|
34
|
+
it('every host-registered type with an advertised schemaVersion has its schema served at {HostBase}/schemas/artifacts/{id}.schema.json', async () => {
|
|
35
|
+
if (!process.env['OPENWOP_BASE_URL']) return softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
|
|
36
|
+
let doc: Record<string, unknown> | null;
|
|
37
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
38
|
+
if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
|
|
39
|
+
if (!(await familyAdvertised('artifactTypes'))) return softSkip('inapplicable', 'the host does not advertise artifactTypes');
|
|
40
|
+
const o = await driveSchemaUrls(PROFILE, doc);
|
|
41
|
+
if (!Array.isArray(o)) return softSkip(o.disposition, o.reason);
|
|
42
|
+
for (const x of o.flatMap(judgeSchemaUrl)) expect(x.ok, req(ID_SERVED, x.doc, x.message)).toBe(true);
|
|
43
|
+
}, 120_000);
|
|
44
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `artifactTypes` advertisement
|
|
3
|
+
* (`spec/v2/core/artifact-type-packs.md`; RFCs 0071, 0075, 0145). The v1 twin
|
|
4
|
+
* is the advertisement legs of `artifact-type-registration-source` and
|
|
5
|
+
* `artifact-type-store-emission`; the legs live in
|
|
6
|
+
* `lib/family-advert-witness.ts`.
|
|
7
|
+
*
|
|
8
|
+
* record the record validates against its capabilities-schema seat;
|
|
9
|
+
* global `store` and `render` are booleans, `export` a list of format ids;
|
|
10
|
+
* per-type each `types` entry is closed, with `validation` open | closed,
|
|
11
|
+
* `schemaVersion` ≥ 0 and `registrationSource` pack | host.
|
|
12
|
+
*
|
|
13
|
+
* Not here: store-implies-emit and the emitted-vs-advertised `registrationSource`
|
|
14
|
+
* match need a fixture run; the event shape is `v2-artifact-created-static`.
|
|
15
|
+
*
|
|
16
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `artifactTypes` absent, or the
|
|
17
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/v2/core/artifact-type-packs.md §The capability
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from 'vitest';
|
|
23
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
24
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
25
|
+
import { req } from '../lib/requirement-ids.js';
|
|
26
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
27
|
+
import { artifactTypesGlobalFacetsLeg, artifactTypesPerTypeLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
28
|
+
|
|
29
|
+
const PROFILE = majorProfile(2);
|
|
30
|
+
const DOC = 'artifact-type-packs.md §The capability';
|
|
31
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
32
|
+
const ID_RECORD = 'openwop.requirement.artifact-types.advert-record-schema';
|
|
33
|
+
const ID_GLOBAL_FACETS_SHAPE = 'openwop.requirement.artifact-types.global-facets-shape';
|
|
34
|
+
const ID_PER_TYPE_ENTRY_SHAPE = 'openwop.requirement.artifact-types.per-type-entry-shape';
|
|
35
|
+
|
|
36
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
37
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('v2 artifactTypes advertisement (artifact-type-packs.md §The capability)', () => {
|
|
41
|
+
it('the artifactTypes record validates against its capabilities-schema seat', async () => {
|
|
42
|
+
const doc = await discovery();
|
|
43
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
44
|
+
const out = recordSchemaLeg(PROFILE, doc, 'artifactTypes', DOC);
|
|
45
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
46
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('artifactTypes.store, render and export are well-formed', async () => {
|
|
50
|
+
const doc = await discovery();
|
|
51
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
52
|
+
const out = artifactTypesGlobalFacetsLeg(PROFILE, doc);
|
|
53
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
54
|
+
for (const x of out.findings) expect(x.ok, req(ID_GLOBAL_FACETS_SHAPE, x.doc, x.message)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('every artifactTypes.types entry validates against its seat', async () => {
|
|
58
|
+
const doc = await discovery();
|
|
59
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
60
|
+
const out = artifactTypesPerTypeLeg(PROFILE, doc);
|
|
61
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
62
|
+
for (const x of out.findings) expect(x.ok, req(ID_PER_TYPE_ENTRY_SHAPE, x.doc, x.message)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `authorization` advertisement (`spec/v2/core/identity.md` §2.1; RFC
|
|
3
|
+
* 0049; invariant `authorization-fail-closed`). The v1 twin is the
|
|
4
|
+
* advertisement legs of `authorization-roles-shape` and
|
|
5
|
+
* `authorization-fail-closed`; the legs live in `lib/family-advert-witness.ts`.
|
|
6
|
+
*
|
|
7
|
+
* record the record validates against its capabilities-schema seat;
|
|
8
|
+
* failClosed `failClosed` MUST be `true` when present;
|
|
9
|
+
* roles each `roles[]` entry names a non-empty `role` and a unique
|
|
10
|
+
* `scopes[]` of non-empty strings, and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* Not here: the unseeded-role decision leg of `authorization-fail-closed` needs
|
|
13
|
+
* a decision seam with no v2 counterpart.
|
|
14
|
+
*
|
|
15
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `authorization` absent, or the
|
|
16
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
17
|
+
*
|
|
18
|
+
* @see spec/v2/core/identity.md §2.1
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
23
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
24
|
+
import { req } from '../lib/requirement-ids.js';
|
|
25
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
26
|
+
import { authorizationFailClosedLeg, authorizationRolesLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
27
|
+
|
|
28
|
+
const PROFILE = majorProfile(2);
|
|
29
|
+
const DOC = 'identity.md §2.1';
|
|
30
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
31
|
+
const ID_RECORD = 'openwop.requirement.authorization.advert-record-schema';
|
|
32
|
+
const ID_FAIL_CLOSED_TRUE = 'openwop.requirement.authorization.fail-closed-true';
|
|
33
|
+
const ID_ROLES_SHAPE = 'openwop.requirement.authorization.roles-shape';
|
|
34
|
+
|
|
35
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
36
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('v2 authorization advertisement (identity.md §2.1)', () => {
|
|
40
|
+
it('the authorization record validates against its capabilities-schema seat', async () => {
|
|
41
|
+
const doc = await discovery();
|
|
42
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
43
|
+
if (!(await familyAdvertised('authorization'))) return softSkip('inapplicable', 'the host does not advertise authorization');
|
|
44
|
+
const out = recordSchemaLeg(PROFILE, doc, 'authorization', DOC);
|
|
45
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
46
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('authorization.failClosed is true when present', async () => {
|
|
50
|
+
const doc = await discovery();
|
|
51
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
52
|
+
if (!(await familyAdvertised('authorization'))) return softSkip('inapplicable', 'the host does not advertise authorization');
|
|
53
|
+
const out = authorizationFailClosedLeg(PROFILE, doc);
|
|
54
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
55
|
+
for (const x of out.findings) expect(x.ok, req(ID_FAIL_CLOSED_TRUE, x.doc, x.message)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('every authorization.roles entry has a non-empty role and a scopes list', async () => {
|
|
59
|
+
const doc = await discovery();
|
|
60
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
61
|
+
if (!(await familyAdvertised('authorization'))) return softSkip('inapplicable', 'the host does not advertise authorization');
|
|
62
|
+
const out = authorizationRolesLeg(PROFILE, doc);
|
|
63
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
64
|
+
for (const x of out.findings) expect(x.ok, req(ID_ROLES_SHAPE, x.doc, x.message)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `channelPresence` advertisement (`spec/v2/core/conversation.md` §`channelPresence`): the channel presence claim. The v1 twin is
|
|
3
|
+
* `channel-presence-shape`;
|
|
4
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
*
|
|
8
|
+
* Not here: presence delivery needs a channel conversation the suite can create, and the non-persistence rule lives only in a schema description (TODO §8).
|
|
9
|
+
*
|
|
10
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `channelPresence` absent, or the
|
|
11
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
12
|
+
*
|
|
13
|
+
* @see spec/v2/core/conversation.md §`channelPresence`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
18
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
19
|
+
import { req } from '../lib/requirement-ids.js';
|
|
20
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
21
|
+
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
22
|
+
|
|
23
|
+
const PROFILE = majorProfile(2);
|
|
24
|
+
const DOC = 'conversation.md §channelPresence';
|
|
25
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
26
|
+
const ID_RECORD = 'openwop.requirement.channel-presence.advert-record-schema';
|
|
27
|
+
|
|
28
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
29
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('v2 channelPresence advertisement (conversation.md §channelPresence)', () => {
|
|
33
|
+
it('the channelPresence record validates against its capabilities-schema seat', async () => {
|
|
34
|
+
const doc = await discovery();
|
|
35
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
36
|
+
if (!(await familyAdvertised('channelPresence'))) return softSkip('inapplicable', 'the host does not advertise channelPresence');
|
|
37
|
+
const out = recordSchemaLeg(PROFILE, doc, 'channelPresence', DOC);
|
|
38
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
39
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — the `conversationTurnModelProvenance` advertisement (`spec/v2/core/conversation.md` §`conversationTurnModelProvenance`): the claim that a host stamps `agent.model` on agent turns. The v1 twin is
|
|
3
|
+
* `conversation-turn-model-provenance-shape`;
|
|
4
|
+
* the legs live in `lib/family-advert-witness.ts` (witness wave 3).
|
|
5
|
+
*
|
|
6
|
+
* record the record validates against its capabilities-schema seat;
|
|
7
|
+
*
|
|
8
|
+
* Not here: the stamp itself (only `provider` and `model`, read verbatim on fork) needs a conversation run; the record carries no facet a v2 rule gives meaning to.
|
|
9
|
+
*
|
|
10
|
+
* Dispositions: discovery unreadable ⇒ `blocked`; `conversationTurnModelProvenance` absent, or the
|
|
11
|
+
* facet a leg reads absent ⇒ `inapplicable`.
|
|
12
|
+
*
|
|
13
|
+
* @see spec/v2/core/conversation.md §`conversationTurnModelProvenance`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
18
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
19
|
+
import { req } from '../lib/requirement-ids.js';
|
|
20
|
+
import { majorProfile } from '../lib/major-profile.js';
|
|
21
|
+
import { recordSchemaLeg } from '../lib/family-advert-witness.js';
|
|
22
|
+
|
|
23
|
+
const PROFILE = majorProfile(2);
|
|
24
|
+
const DOC = 'conversation.md §conversationTurnModelProvenance';
|
|
25
|
+
const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
|
|
26
|
+
const ID_RECORD = 'openwop.requirement.conversation-turn-model-provenance.advert-record-schema';
|
|
27
|
+
|
|
28
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
29
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('v2 conversationTurnModelProvenance advertisement (conversation.md §conversationTurnModelProvenance)', () => {
|
|
33
|
+
it('the conversationTurnModelProvenance record validates against its capabilities-schema seat', async () => {
|
|
34
|
+
const doc = await discovery();
|
|
35
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
36
|
+
if (!(await familyAdvertised('conversationTurnModelProvenance'))) return softSkip('inapplicable', 'the host does not advertise conversationTurnModelProvenance');
|
|
37
|
+
const out = recordSchemaLeg(PROFILE, doc, 'conversationTurnModelProvenance', DOC);
|
|
38
|
+
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
39
|
+
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|