런타임 계약과 학습자 흐름 보강

This commit is contained in:
Yun Chan 2026-06-29 08:12:14 +09:00
parent f456b8997a
commit 206018b088
56 changed files with 4306 additions and 1008 deletions

View file

@ -39,6 +39,9 @@ interface VoiceUiProbeState {
getUserMediaCalls: number;
recorderStarts: number;
recorderStops: number;
workletModuleLoads: number;
workletNodes: number;
workletChunks: number;
trackStops: number;
audioPlays: number;
audioContextResumes: number;
@ -375,6 +378,9 @@ async function installSyntheticVoiceCapture(
getUserMediaCalls: number;
recorderStarts: number;
recorderStops: number;
workletModuleLoads: number;
workletNodes: number;
workletChunks: number;
trackStops: number;
audioPlays: number;
audioContextResumes: number;
@ -388,6 +394,9 @@ async function installSyntheticVoiceCapture(
getUserMediaCalls: 0,
recorderStarts: 0,
recorderStops: 0,
workletModuleLoads: 0,
workletNodes: 0,
workletChunks: 0,
trackStops: 0,
audioPlays: 0,
audioContextResumes: 0,
@ -560,9 +569,68 @@ async function installSyntheticVoiceCapture(
}
}
class FakeWorkletPort {
onmessage: ((event: MessageEvent) => void) | null = null;
private closed = false;
postMessage(message: unknown) {
if ((message as { type?: string })?.type === "flush") {
this.emitChunk();
}
}
close() {
this.closed = true;
}
emitChunk() {
if (this.closed) return;
probe.workletChunks += 1;
const pcm = new Int16Array([0, 1024, -1024, 0]);
this.onmessage?.({ data: { type: "chunk", pcm: pcm.buffer } } as MessageEvent);
}
}
class FakeAudioWorkletNode {
port = new FakeWorkletPort();
constructor(_ctx: unknown, _name: string, _options?: unknown) {
probe.workletNodes += 1;
}
connect() {
return this;
}
disconnect() {
return undefined;
}
__start() {
window.setTimeout(() => this.port.emitChunk(), 25);
}
}
class FakeMediaStreamSource {
connect(node: { __start?: () => void }) {
node.__start?.();
return node;
}
disconnect() {
return undefined;
}
}
class FakeAudioContext {
state = "running";
destination = {};
sampleRate = 16000;
audioWorklet = {
addModule: async (_url: string) => {
probe.workletModuleLoads += 1;
},
};
async resume() {
probe.audioContextResumes += 1;
@ -584,8 +652,16 @@ async function installSyntheticVoiceCapture(
createBufferSource() {
return new FakeBufferSource();
}
createMediaStreamSource(_stream: unknown) {
return new FakeMediaStreamSource();
}
}
Object.defineProperty(window, "AudioWorkletNode", {
configurable: true,
value: FakeAudioWorkletNode,
});
Object.defineProperty(window, "AudioContext", {
configurable: true,
value: FakeAudioContext,
@ -883,7 +959,13 @@ test.describe("voice cascade success path", () => {
.poll(async () => (await readVoiceUiProbe(page)).getUserMediaCalls, { timeout: 10_000 })
.toBeGreaterThan(0);
await expect
.poll(async () => (await readVoiceUiProbe(page)).recorderStarts, { timeout: 10_000 })
.poll(async () => (await readVoiceUiProbe(page)).workletModuleLoads, { timeout: 10_000 })
.toBeGreaterThan(0);
await expect
.poll(async () => (await readVoiceUiProbe(page)).workletNodes, { timeout: 10_000 })
.toBeGreaterThan(0);
await expect
.poll(async () => (await readVoiceUiProbe(page)).workletChunks, { timeout: 10_000 })
.toBeGreaterThan(0);
await expect
.poll(async () => {
@ -892,7 +974,9 @@ test.describe("voice cascade success path", () => {
(message) =>
message.direction === "sent" &&
message.kind === "text" &&
message.data?.includes('"audio_start"'),
message.data?.includes('"audio_start"') &&
message.data?.includes('"format":"pcm"') &&
message.data?.includes('"sample_rate":16000'),
);
}, { timeout: 10_000 })
.toBeTruthy();