@openwop/openwop-conformance 1.28.0 → 1.33.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.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +53 -0
- package/coverage.md +21 -0
- package/package.json +1 -1
- package/schemas/capabilities.schema.json +62 -1
- package/schemas/conversation-event.schema.json +50 -2
- package/schemas/conversation-turn.schema.json +35 -0
- package/schemas/registry-version-manifest.schema.json +49 -2
- package/schemas/run-event-payloads.schema.json +87 -2
- package/schemas/run-event.schema.json +8 -1
- package/src/lib/multi-agent-capabilities.ts +23 -4
- package/src/lib/multiPartyConversation.ts +121 -0
- package/src/scenarios/aiproviders-realtimevoice-shape.test.ts +120 -0
- package/src/scenarios/aiproviders-speechsynth-shape.test.ts +92 -0
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +137 -0
- package/src/scenarios/multi-party-conversation-shape.test.ts +206 -0
- package/src/scenarios/registry-declarative-kinds.test.ts +111 -0
- package/src/scenarios/speech-synthesis-roundtrip.test.ts +78 -0
- package/src/scenarios/speech-synthesis-unadvertised.test.ts +65 -0
- package/src/scenarios/voice-event-payloads-shape.test.ts +127 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice run-event payload shapes (RFC 0106 §B/§C/§D) — server-free.
|
|
3
|
+
*
|
|
4
|
+
* Always-on schema-shape probe for the `voice.*` run-event taxonomy — the single
|
|
5
|
+
* canonical record of a live voice turn (the `ctx.callTranscriber` Promise resolves
|
|
6
|
+
* at `turn_commit`; the events ARE the streaming representation). Verifies that:
|
|
7
|
+
* - all seven `voice.*` types are members of `run-event.schema.json#$defs.RunEventType`;
|
|
8
|
+
* - each has a payload `$def` in `run-event-payloads.schema.json` reachable via the
|
|
9
|
+
* `typeIndex` lookup map;
|
|
10
|
+
* - `voice.transcript` REQUIRES `contentTrust: "untrusted"` — the schema-enforced wire
|
|
11
|
+
* half of SECURITY invariant `voice-transcript-untrusted` (live transcript is
|
|
12
|
+
* untrusted ingress and MUST NOT be promoted to system/developer authority);
|
|
13
|
+
* - `voice.synthesis_chunk` is metadata-shaped (seq + mimeType required; bytes by
|
|
14
|
+
* `url`/`streamRef`, inline `base64` optional-only) — backing the G8 metadata-only
|
|
15
|
+
* event-log rule (`voice-streamref-tenant-bound` keeps the log bounded);
|
|
16
|
+
* - the content-free events (`speech_start`/`endpoint_candidate`/`turn_commit`/
|
|
17
|
+
* `barge_in`/`cancelled`) reject an unknown property (`additionalProperties:false`).
|
|
18
|
+
*
|
|
19
|
+
* The behavioral legs (interim-not-durable, barge-in no-partial-leak, streamRef
|
|
20
|
+
* tenant-binding, the live `callTranscriber` Promise + emission) are the gated
|
|
21
|
+
* reference-host scenarios that land at `Active → Accepted` — those invariants are
|
|
22
|
+
* reference-impl tier until a host proves them non-vacuously.
|
|
23
|
+
*
|
|
24
|
+
* Spec references:
|
|
25
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0106-realtime-voice-session-profile.md (§B, §C, §D, §F)
|
|
26
|
+
* - https://github.com/openwop/openwop/blob/main/schemas/run-event-payloads.schema.json
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { readFileSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
33
|
+
import addFormats from 'ajv-formats';
|
|
34
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
35
|
+
|
|
36
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
37
|
+
|
|
38
|
+
function loadSchema(name: string): Record<string, any> {
|
|
39
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, any>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const VOICE_TYPES = [
|
|
43
|
+
'voice.speech_start',
|
|
44
|
+
'voice.transcript',
|
|
45
|
+
'voice.endpoint_candidate',
|
|
46
|
+
'voice.turn_commit',
|
|
47
|
+
'voice.synthesis_chunk',
|
|
48
|
+
'voice.barge_in',
|
|
49
|
+
'voice.cancelled',
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
const DEF_KEY: Record<string, string> = {
|
|
53
|
+
'voice.speech_start': 'voiceSpeechStart',
|
|
54
|
+
'voice.transcript': 'voiceTranscript',
|
|
55
|
+
'voice.endpoint_candidate': 'voiceEndpointCandidate',
|
|
56
|
+
'voice.turn_commit': 'voiceTurnCommit',
|
|
57
|
+
'voice.synthesis_chunk': 'voiceSynthesisChunk',
|
|
58
|
+
'voice.barge_in': 'voiceBargeIn',
|
|
59
|
+
'voice.cancelled': 'voiceCancelled',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('voice-event-payloads-shape: the voice.* run-event taxonomy (RFC 0106 §B/§C/§D, server-free)', () => {
|
|
63
|
+
it('all seven voice.* types are in the RunEventType enum', () => {
|
|
64
|
+
const enumVals: string[] = loadSchema('run-event.schema.json').$defs.RunEventType.enum;
|
|
65
|
+
for (const t of VOICE_TYPES) {
|
|
66
|
+
expect(enumVals.includes(t), why('run-event.schema.json §RunEventType', `${t} MUST be a RunEventType`)).toBe(true);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('each voice.* type has a payload $def reachable via typeIndex', () => {
|
|
71
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
72
|
+
const typeIndex = payloads.$defs._typeIndex.properties as Record<string, { $ref: string }>;
|
|
73
|
+
for (const t of VOICE_TYPES) {
|
|
74
|
+
expect(typeIndex[t], why('run-event-payloads.schema.json §typeIndex', `${t} MUST map to a $def`)).toBeDefined();
|
|
75
|
+
expect(payloads.$defs[DEF_KEY[t]], why('run-event-payloads.schema.json', `$defs.${DEF_KEY[t]} MUST exist`)).toBeDefined();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('voice.transcript REQUIRES contentTrust:"untrusted" (SECURITY: voice-transcript-untrusted)', () => {
|
|
80
|
+
const def = loadSchema('run-event-payloads.schema.json').$defs.voiceTranscript;
|
|
81
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
82
|
+
addFormats(ajv);
|
|
83
|
+
const validate = ajv.compile(def);
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
validate({ text: 'book a table', isFinal: true, atMs: 1200, contentTrust: 'untrusted' }),
|
|
87
|
+
why('RFC 0106 §F INV-2', 'a transcript marked untrusted MUST validate'),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
expect(
|
|
90
|
+
validate({ text: 'book a table', isFinal: true, atMs: 1200 }),
|
|
91
|
+
why('RFC 0106 §F INV-2 (voice-transcript-untrusted)', 'a transcript WITHOUT contentTrust MUST be rejected'),
|
|
92
|
+
).toBe(false);
|
|
93
|
+
expect(
|
|
94
|
+
validate({ text: 'book a table', isFinal: true, atMs: 1200, contentTrust: 'trusted' }),
|
|
95
|
+
why('RFC 0106 §F INV-2 (voice-transcript-untrusted)', 'contentTrust MUST be the const "untrusted" — never promoted'),
|
|
96
|
+
).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('voice.synthesis_chunk is metadata-shaped (seq+mimeType required; bytes by reference, inline base64 optional)', () => {
|
|
100
|
+
const def = loadSchema('run-event-payloads.schema.json').$defs.voiceSynthesisChunk;
|
|
101
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
102
|
+
addFormats(ajv);
|
|
103
|
+
const validate = ajv.compile(def);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
validate({ seq: 0, mimeType: 'audio/mpeg', durationMs: 240, url: 'https://host/seg/0', final: false }),
|
|
107
|
+
why('RFC 0106 §C', 'a metadata chunk referencing bytes by url MUST validate'),
|
|
108
|
+
).toBe(true);
|
|
109
|
+
expect(
|
|
110
|
+
validate({ mimeType: 'audio/mpeg' }),
|
|
111
|
+
why('RFC 0106 §C', 'a chunk without seq MUST be rejected'),
|
|
112
|
+
).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('content-free voice events reject unknown properties (additionalProperties:false)', () => {
|
|
116
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
117
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
118
|
+
addFormats(ajv);
|
|
119
|
+
for (const key of ['voiceSpeechStart', 'voiceTurnCommit', 'voiceBargeIn', 'voiceCancelled', 'voiceEndpointCandidate']) {
|
|
120
|
+
const validate = ajv.compile(payloads.$defs[key]);
|
|
121
|
+
expect(
|
|
122
|
+
validate({ atMs: 100, transcriptBody: 'should not be here' }),
|
|
123
|
+
why('RFC 0106 §D', `${key} MUST reject an unknown (content-bearing) property`),
|
|
124
|
+
).toBe(false);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|