Setzt den in #84 skizzierten Workflow um: record.html nimmt im Handy-Browser eine Sprachnotiz per MediaRecorder auf und laedt sie unauthentifiziert zu POST /api/v1/agents/{id}/recordings hoch. Ein Klick auf die Personen-Figur jedes registrierten Agenten in der Buero-Visualisierung verlinkt dorthin, ein Zaehler am Schreibtisch zeigt offene Aufnahmen. Der Agent selbst holt seine Warteschlange ueber die bestehenden Api-Key-authentifizierten Endpunkte ab (GET .../recordings, GET /recordings/{id}/audio) und markiert eine Aufnahme nach der Transkription per PATCH als erledigt - die eigentliche Transkription (whisper.cpp/faster-whisper) bleibt agent-seitig, siehe fds2610/Agent#17. Schliesst #85
127 lines
4.1 KiB
PHP
127 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Monitor\Api\Tests\Controller;
|
|
|
|
use Monitor\Api\Controller\StreamController;
|
|
use Monitor\Api\Db\Database;
|
|
use Monitor\Api\Http\Request;
|
|
use Monitor\Api\Metrics\HookCounter;
|
|
use Monitor\Api\Realtime\ChangeFeed;
|
|
use Monitor\Api\Repository\AgentRepository;
|
|
use Monitor\Api\Repository\EventRepository;
|
|
use Monitor\Api\Repository\KpiRepository;
|
|
use Monitor\Api\Repository\SystemRepository;
|
|
use Monitor\Api\Repository\TicketRepository;
|
|
use Monitor\Api\Repository\VoiceRecordingRepository;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class StreamControllerTest extends TestCase
|
|
{
|
|
private TicketRepository $tickets;
|
|
private ChangeFeed $changeFeed;
|
|
private string $hooksPath;
|
|
private string $recordingsStorageDir;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$db = Database::inMemory();
|
|
$this->tickets = new TicketRepository($db);
|
|
$this->hooksPath = sys_get_temp_dir() . '/monitor-stream-hooks-' . uniqid() . '/hooks-count';
|
|
$this->recordingsStorageDir = sys_get_temp_dir() . '/monitor-stream-recordings-' . uniqid();
|
|
$this->changeFeed = new ChangeFeed(
|
|
$this->tickets,
|
|
new EventRepository($db),
|
|
new SystemRepository($db),
|
|
new KpiRepository($db),
|
|
new AgentRepository($db),
|
|
new HookCounter($this->hooksPath),
|
|
new VoiceRecordingRepository($db, $this->recordingsStorageDir)
|
|
);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (file_exists($this->hooksPath)) {
|
|
unlink($this->hooksPath);
|
|
}
|
|
if (is_dir(dirname($this->hooksPath))) {
|
|
rmdir(dirname($this->hooksPath));
|
|
}
|
|
if (is_dir($this->recordingsStorageDir)) {
|
|
foreach (glob($this->recordingsStorageDir . '/*') ?: [] as $file) {
|
|
unlink($file);
|
|
}
|
|
rmdir($this->recordingsStorageDir);
|
|
}
|
|
}
|
|
|
|
public function testStreamSendsHeartbeatsWhenNothingChanges(): void
|
|
{
|
|
$controller = new StreamController(
|
|
$this->changeFeed,
|
|
maxIterations: 2,
|
|
pollIntervalSeconds: 0,
|
|
sleep: function (int $seconds): void {
|
|
},
|
|
);
|
|
|
|
$response = $controller->stream(Request::create('GET', '/api/v1/stream'));
|
|
self::assertTrue($response->isStream());
|
|
|
|
$output = $this->captureWriterOutput($response->writer());
|
|
|
|
self::assertStringContainsString(': heartbeat', $output);
|
|
self::assertStringNotContainsString('event: update', $output);
|
|
}
|
|
|
|
public function testStreamEmitsUpdateEventWhenAResourceChangesBetweenPolls(): void
|
|
{
|
|
$tickets = $this->tickets;
|
|
$pollCount = 0;
|
|
$controller = new StreamController(
|
|
$this->changeFeed,
|
|
maxIterations: 2,
|
|
pollIntervalSeconds: 0,
|
|
sleep: function (int $seconds) use (&$pollCount, $tickets): void {
|
|
$pollCount++;
|
|
if ($pollCount === 1) {
|
|
$tickets->create(['title' => 'Neu waehrend des Wartens']);
|
|
}
|
|
},
|
|
);
|
|
|
|
$response = $controller->stream(Request::create('GET', '/api/v1/stream'));
|
|
$output = $this->captureWriterOutput($response->writer());
|
|
|
|
self::assertStringContainsString('event: update', $output);
|
|
self::assertStringContainsString('"tickets"', $output);
|
|
self::assertSame(2, $pollCount);
|
|
}
|
|
|
|
public function testStreamStopsAfterMaxIterations(): void
|
|
{
|
|
$pollCount = 0;
|
|
$controller = new StreamController(
|
|
$this->changeFeed,
|
|
maxIterations: 3,
|
|
pollIntervalSeconds: 0,
|
|
sleep: function (int $seconds) use (&$pollCount): void {
|
|
$pollCount++;
|
|
},
|
|
);
|
|
|
|
$this->captureWriterOutput($controller->stream(Request::create('GET', '/api/v1/stream'))->writer());
|
|
|
|
self::assertSame(3, $pollCount);
|
|
}
|
|
|
|
private function captureWriterOutput(\Closure $writer): string
|
|
{
|
|
ob_start();
|
|
$writer();
|
|
|
|
return (string) ob_get_clean();
|
|
}
|
|
}
|