@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.
@@ -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
+ });