@openwop/openwop-conformance 1.29.0 → 1.34.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 +4 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +53 -0
- package/coverage.md +13 -0
- package/package.json +1 -1
- package/schemas/capabilities.schema.json +58 -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/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/voice-bargein-no-partial-leak.test.ts +61 -0
- package/src/scenarios/voice-event-payloads-shape.test.ts +127 -0
- package/src/scenarios/voice-interim-not-durable.test.ts +59 -0
- package/src/scenarios/voice-streamref-tenant-bound.test.ts +59 -0
- package/src/scenarios/voice-synthesis-streaming.test.ts +77 -0
- package/src/scenarios/voice-transcription-streaming.test.ts +64 -0
- package/src/scenarios/voice-transcription-unadvertised.test.ts +46 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming speech-synthesis arm (RFC 0106 §C) — behavioral.
|
|
3
|
+
*
|
|
4
|
+
* Gated on `capabilities.aiProviders.realtimeVoice.synthesis === 'streaming'`
|
|
5
|
+
* (root-first per RFC 0073). Soft-skips when unadvertised (default) / hard-fails
|
|
6
|
+
* under `OPENWOP_REQUIRE_BEHAVIOR=true`. Drives the documented host-sample seam
|
|
7
|
+
* `POST /v1/host/sample/ai/call-speech-synthesizer` with `stream: true`
|
|
8
|
+
* (soft-skips on 404 until a host wires it):
|
|
9
|
+
*
|
|
10
|
+
* - returns 200 with the finalized RFC 0105 `audio` asset (EXACTLY ONE of
|
|
11
|
+
* `url` / `base64`) — `stream: true` resolves the Promise at completion,
|
|
12
|
+
* unchanged whole-file result shape;
|
|
13
|
+
* - emits `voice.synthesis_chunk` run-events carrying METADATA ONLY
|
|
14
|
+
* (`seq` + `mimeType`; bytes by `url`/`streamRef`, NOT inline base64 past
|
|
15
|
+
* the host cap — the G8 event-log-bounded rule), with a terminal `final: true`.
|
|
16
|
+
*
|
|
17
|
+
* Spec references:
|
|
18
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0106-realtime-voice-session-profile.md (§C)
|
|
19
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/host-capabilities.md (§host.aiProviders)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from 'vitest';
|
|
23
|
+
import { driver } from '../lib/driver.js';
|
|
24
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
25
|
+
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
26
|
+
|
|
27
|
+
const SEAM = '/v1/host/sample/ai/call-speech-synthesizer';
|
|
28
|
+
|
|
29
|
+
function realtimeVoiceOf(ai: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
30
|
+
const rv = (ai as { realtimeVoice?: unknown })?.realtimeVoice;
|
|
31
|
+
return rv && typeof rv === 'object' ? (rv as Record<string, unknown>) : undefined;
|
|
32
|
+
}
|
|
33
|
+
function eventsOf(json: unknown): Array<{ type?: string; payload?: Record<string, unknown> }> {
|
|
34
|
+
const e = (json as { events?: unknown })?.events;
|
|
35
|
+
return Array.isArray(e) ? (e as Array<{ type?: string; payload?: Record<string, unknown> }>) : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('voice-synthesis-streaming (RFC 0106 §C)', () => {
|
|
39
|
+
it('stream:true resolves the finalized asset and emits metadata-only voice.synthesis_chunk events', async () => {
|
|
40
|
+
const ai = await readCapabilityFamily<Record<string, unknown>>('aiProviders');
|
|
41
|
+
const advertised = realtimeVoiceOf(ai)?.synthesis === 'streaming';
|
|
42
|
+
if (!behaviorGate('openwop-voice-synthesis', advertised)) return;
|
|
43
|
+
|
|
44
|
+
const res = await driver.post(SEAM, { text: 'Welcome to the weekly digest.', voiceId: 'host:narrator-test', stream: true });
|
|
45
|
+
if (res.status === 404) return; // seam unwired — soft-skip
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
res.status === 200,
|
|
49
|
+
driver.describe('RFC 0106 §C', 'an advertised host MUST resolve stream:true with 200'),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
|
|
52
|
+
const audio = (res.json as { audio?: Record<string, unknown> })?.audio;
|
|
53
|
+
expect(audio !== undefined, driver.describe('RFC 0106 §C', 'the response MUST carry the finalized `audio` asset')).toBe(true);
|
|
54
|
+
if (audio) {
|
|
55
|
+
const hasUrl = typeof audio.url === 'string' && (audio.url as string).length > 0;
|
|
56
|
+
const hasB64 = typeof audio.base64 === 'string' && (audio.base64 as string).length > 0;
|
|
57
|
+
expect(hasUrl !== hasB64, driver.describe('RFC 0106 §C', 'audio MUST carry EXACTLY ONE of url/base64')).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const chunks = eventsOf(res.json).filter((e) => e.type === 'voice.synthesis_chunk');
|
|
61
|
+
expect(chunks.length >= 1, driver.describe('RFC 0106 §C', 'MUST emit ≥1 voice.synthesis_chunk run-event')).toBe(true);
|
|
62
|
+
for (const c of chunks) {
|
|
63
|
+
const p = c.payload ?? {};
|
|
64
|
+
expect(typeof p.seq === 'number', driver.describe('RFC 0106 §C', 'each chunk carries a numeric seq')).toBe(true);
|
|
65
|
+
expect(typeof p.mimeType === 'string', driver.describe('RFC 0106 §C', 'each chunk carries a mimeType')).toBe(true);
|
|
66
|
+
// Metadata-only: bytes by reference. An inline base64 is permitted ONLY under the host cap;
|
|
67
|
+
// a chunk over the RFC 0055 256 KiB inline cap MUST be a url/streamRef reference.
|
|
68
|
+
if (typeof p.base64 === 'string') {
|
|
69
|
+
expect((p.base64 as string).length <= 262144, driver.describe('RFC 0106 §C (G8)', 'inline chunk base64 MUST stay under the 256 KiB cap; else url/streamRef')).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
expect(
|
|
73
|
+
chunks.some((c) => (c.payload ?? {}).final === true),
|
|
74
|
+
driver.describe('RFC 0106 §C', 'a terminal chunk MUST carry final:true'),
|
|
75
|
+
).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming transcription round-trip (RFC 0106 §B) — behavioral.
|
|
3
|
+
*
|
|
4
|
+
* Gated on `capabilities.aiProviders.realtimeVoice.transcription === 'streaming'`.
|
|
5
|
+
* Soft-skips when unadvertised (default) / hard-fails under
|
|
6
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`. Drives `POST /v1/host/sample/ai/call-transcriber`
|
|
7
|
+
* (soft-skips on 404 and on `transcription_unsupported` — a host whose live-stream
|
|
8
|
+
* transport is host-internal per §E honestly rejects a `streamRef`, so the
|
|
9
|
+
* non-vacuous turn requires the deterministic test-seam arm
|
|
10
|
+
* (`OPENWOP_TEST_SEAM_ENABLED`) or a finalized media asset):
|
|
11
|
+
*
|
|
12
|
+
* - resolves with the settled turn (`finalText`, non-empty) at `voice.turn_commit`;
|
|
13
|
+
* - any emitted `voice.transcript` carries `contentTrust: "untrusted"` (§F);
|
|
14
|
+
* - the turn terminates with `voice.turn_commit`.
|
|
15
|
+
*
|
|
16
|
+
* Spec references:
|
|
17
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0106-realtime-voice-session-profile.md (§B, §F)
|
|
18
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/host-capabilities.md (§host.aiProviders)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
24
|
+
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
25
|
+
|
|
26
|
+
const SEAM = '/v1/host/sample/ai/call-transcriber';
|
|
27
|
+
|
|
28
|
+
function realtimeVoiceOf(ai: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
29
|
+
const rv = (ai as { realtimeVoice?: unknown })?.realtimeVoice;
|
|
30
|
+
return rv && typeof rv === 'object' ? (rv as Record<string, unknown>) : undefined;
|
|
31
|
+
}
|
|
32
|
+
function errCode(json: unknown): string | undefined {
|
|
33
|
+
return (json as { error?: { code?: string } })?.error?.code;
|
|
34
|
+
}
|
|
35
|
+
function eventsOf(json: unknown): Array<{ type?: string; payload?: Record<string, unknown> }> {
|
|
36
|
+
const e = (json as { events?: unknown })?.events;
|
|
37
|
+
return Array.isArray(e) ? (e as Array<{ type?: string; payload?: Record<string, unknown> }>) : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('voice-transcription-streaming (RFC 0106 §B)', () => {
|
|
41
|
+
it('resolves a settled turn at voice.turn_commit with untrusted transcript events', async () => {
|
|
42
|
+
const ai = await readCapabilityFamily<Record<string, unknown>>('aiProviders');
|
|
43
|
+
const advertised = realtimeVoiceOf(ai)?.transcription === 'streaming';
|
|
44
|
+
if (!behaviorGate('openwop-voice-transcription', advertised)) return;
|
|
45
|
+
|
|
46
|
+
const res = await driver.post(SEAM, { audio: { streamRef: 'stream:conformance/mic' }, languageCode: 'en-US' });
|
|
47
|
+
if (res.status === 404) return; // seam unwired — soft-skip
|
|
48
|
+
// §E: a stateless host with no live transport honestly rejects a live streamRef.
|
|
49
|
+
if (errCode(res.json) === 'transcription_unsupported') return; // soft-skip (no live transport / no test-seam arm)
|
|
50
|
+
|
|
51
|
+
expect(res.status === 200, driver.describe('RFC 0106 §B', 'a produced turn MUST return 200')).toBe(true);
|
|
52
|
+
|
|
53
|
+
const finalText = (res.json as { finalText?: unknown })?.finalText;
|
|
54
|
+
expect(typeof finalText === 'string' && (finalText as string).length > 0, driver.describe('RFC 0106 §B', 'the turn MUST resolve a non-empty finalText at turn_commit')).toBe(true);
|
|
55
|
+
|
|
56
|
+
const events = eventsOf(res.json);
|
|
57
|
+
if (events.length > 0) {
|
|
58
|
+
for (const ev of events.filter((e) => e.type === 'voice.transcript')) {
|
|
59
|
+
expect((ev.payload ?? {}).contentTrust === 'untrusted', driver.describe('RFC 0106 §F INV-2', 'every voice.transcript MUST carry contentTrust:"untrusted"')).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
expect(events.some((e) => e.type === 'voice.turn_commit'), driver.describe('RFC 0106 §B', 'the turn MUST terminate with voice.turn_commit')).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming transcription on an unadvertising host (RFC 0106 §B) — behavioral.
|
|
3
|
+
*
|
|
4
|
+
* Gated by ABSENCE: active precisely when `realtimeVoice.transcription` is NOT
|
|
5
|
+
* advertised but the seam exists. Soft-skips otherwise (default) / hard-fails
|
|
6
|
+
* under `OPENWOP_REQUIRE_BEHAVIOR=true`. A host that does NOT advertise streaming
|
|
7
|
+
* transcription MUST reject `ctx.callTranscriber` with `transcription_unsupported`
|
|
8
|
+
* (never a 200 success, never a silent no-op) — paralleling RFC 0105's
|
|
9
|
+
* `speech_synthesis_unsupported` / RFC 0091's `unsupported_modality`.
|
|
10
|
+
*
|
|
11
|
+
* Spec references:
|
|
12
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0106-realtime-voice-session-profile.md (§B)
|
|
13
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/host-capabilities.md (§host.aiProviders Failure modes)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
19
|
+
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
20
|
+
|
|
21
|
+
const SEAM = '/v1/host/sample/ai/call-transcriber';
|
|
22
|
+
|
|
23
|
+
function realtimeVoiceOf(ai: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
24
|
+
const rv = (ai as { realtimeVoice?: unknown })?.realtimeVoice;
|
|
25
|
+
return rv && typeof rv === 'object' ? (rv as Record<string, unknown>) : undefined;
|
|
26
|
+
}
|
|
27
|
+
function errCode(json: unknown): string | undefined {
|
|
28
|
+
return (json as { error?: { code?: string } })?.error?.code;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('voice-transcription-unadvertised (RFC 0106 §B)', () => {
|
|
32
|
+
it('a host NOT advertising realtimeVoice.transcription MUST reject with transcription_unsupported', async () => {
|
|
33
|
+
const ai = await readCapabilityFamily<Record<string, unknown>>('aiProviders');
|
|
34
|
+
const advertised = realtimeVoiceOf(ai)?.transcription === 'streaming';
|
|
35
|
+
if (!behaviorGate('openwop-voice-transcription-unadvertised', !advertised)) return;
|
|
36
|
+
|
|
37
|
+
const res = await driver.post(SEAM, { audio: { streamRef: 'stream:conformance/mic' } });
|
|
38
|
+
if (res.status === 404) return; // seam unwired — soft-skip
|
|
39
|
+
|
|
40
|
+
expect(res.status !== 200, driver.describe('RFC 0106 §B', 'an unadvertising host MUST NOT return a 200 (never a no-op)')).toBe(true);
|
|
41
|
+
expect(
|
|
42
|
+
errCode(res.json) === 'transcription_unsupported',
|
|
43
|
+
driver.describe('RFC 0106 §B', 'the call MUST be rejected with `transcription_unsupported`'),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|