sidecar = $theirs; $supervisor = new SidecarSupervisor(new BinaryManager([]), ['auto_spawn' => false]); $this->engine = new RustMediaEngine($supervisor, ControlClient::fromStream($ours)); } private function sidecarSays(array $line): void { fwrite($this->sidecar, json_encode($line)."\n"); } private function sidecarHeard(): array { return json_decode((string) fgets($this->sidecar), true); } public function test_add_peer_speaks_the_control_protocol(): void { $this->sidecarSays(['id' => 1, 'ok' => true, 'answer' => 'v=answer']); $answer = $this->engine->addPeer('lobby', 'alice', 'v=offer', '/rec/alice.ogg'); $this->assertSame('v=answer', $answer); $this->assertSame( [ 'id' => 1, 'cmd' => 'add_peer', 'room' => 'lobby', 'peer' => 'alice', 'offer' => 'v=offer', 'record' => '/rec/alice.ogg', ], $this->sidecarHeard(), ); } public function test_media_engine_contract_splits_composite_peer_ids(): void { $this->sidecarSays(['id' => 1, 'ok' => true, 'answer' => 'v=a']); $this->engine->offer('lobby/alice', 'v=offer'); $heard = $this->sidecarHeard(); $this->assertSame(['lobby', 'alice'], [$heard['room'], $heard['peer']]); $this->assertArrayNotHasKey('record', $heard); $this->sidecarSays(['id' => 2, 'ok' => true]); $this->engine->startRecording('lobby/alice', '/rec/alice.ogg'); $heard = $this->sidecarHeard(); $this->assertSame('record_start', $heard['cmd']); $this->assertSame('/rec/alice.ogg', $heard['path']); $this->sidecarSays(['id' => 3, 'ok' => true]); $this->engine->close('lobby/alice'); $this->assertSame('remove_peer', $this->sidecarHeard()['cmd']); } public function test_bare_peer_ids_fall_back_to_the_default_room(): void { $this->sidecarSays(['id' => 1, 'ok' => true, 'answer' => 'v=a']); $this->engine->offer('alice', 'v=offer'); $heard = $this->sidecarHeard(); $this->assertSame(['default', 'alice'], [$heard['room'], $heard['peer']]); } public function test_mute_stats_and_events(): void { $this->sidecarSays(['id' => 1, 'ok' => true]); $this->engine->mutePeer('lobby', 'alice', true); $this->assertTrue($this->sidecarHeard()['muted']); $this->sidecarSays([ 'id' => 2, 'ok' => true, 'rooms' => [['room' => 'lobby', 'peers' => [['peer' => 'alice', 'connected' => true, 'muted' => true, 'seconds' => 4.2]]]], ]); $stats = $this->engine->stats(); $this->assertSame('lobby', $stats[0]['room']); fwrite($this->sidecar, json_encode(['event' => 'peer_connected', 'room' => 'lobby', 'peer' => 'bob'])."\n"); $this->sidecarSays(['id' => 3, 'ok' => true]); $this->engine->removePeer('lobby', 'bob'); $this->assertSame('peer_connected', $this->engine->drainEvents()[0]['event']); } public function test_ice_candidates_and_connect_peers_are_deliberate_noops(): void { // Neither may touch the socket: str0m learns remote candidates from // STUN, and room membership already routes media. $this->engine->addIceCandidate('lobby/alice', ['candidate' => 'x']); $this->engine->connectPeers('lobby/alice', 'lobby/bob'); stream_set_blocking($this->sidecar, false); $this->assertFalse(fgets($this->sidecar), 'no bytes were written to the sidecar'); } public function test_bridge_is_not_implemented_in_the_data_plane_yet(): void { $this->expectException(SidecarException::class); $this->expectExceptionMessage('OpenAiRealtimeBridge'); $this->engine->bridge('lobby/alice', ['model' => 'gpt-realtime']); } public function test_engine_name(): void { $this->assertSame('rust-sfu', $this->engine->name()); } }