mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-03-21 22:56:47 +00:00
fix: add tests that check some of the issues are already fixed (#905)
* Add tests that check some of the issues are already fixed * Fix test formatting
This commit is contained in:
17
server/tests/test_app.py
Normal file
17
server/tests/test_app.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Tests for app-level endpoints (root, not under /v1)."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint_returns_healthy():
|
||||
"""GET /health returns 200 and {"status": "healthy"} for probes and CI."""
|
||||
from httpx import AsyncClient
|
||||
|
||||
from reflector.app import app
|
||||
|
||||
# Health is at app root, not under /v1
|
||||
async with AsyncClient(app=app, base_url="http://test") as root_client:
|
||||
response = await root_client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "healthy"}
|
||||
@@ -5,6 +5,7 @@ This test verifies the complete file processing pipeline without mocking much,
|
||||
ensuring all processors are correctly invoked and the happy path works correctly.
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
@@ -651,3 +652,43 @@ async def test_pipeline_file_process_no_audio_file(
|
||||
# This should fail when trying to open the file with av
|
||||
with pytest.raises(Exception):
|
||||
await pipeline.process(non_existent_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_title_does_not_overwrite_user_set_title():
|
||||
"""When transcript already has a title, on_title does not call update."""
|
||||
from reflector.db.transcripts import Transcript, TranscriptFinalTitle
|
||||
from reflector.pipelines.main_file_pipeline import PipelineMainFile
|
||||
|
||||
transcript_id = str(uuid4())
|
||||
transcript_with_title = Transcript(
|
||||
id=transcript_id,
|
||||
name="test",
|
||||
source_kind="file",
|
||||
title="User set title",
|
||||
)
|
||||
|
||||
controller = "reflector.pipelines.main_live_pipeline.transcripts_controller"
|
||||
with patch(f"{controller}.get_by_id", new_callable=AsyncMock) as mock_get:
|
||||
with patch(f"{controller}.update", new_callable=AsyncMock) as mock_update:
|
||||
with patch(
|
||||
f"{controller}.append_event", new_callable=AsyncMock
|
||||
) as mock_append:
|
||||
with patch(f"{controller}.transaction") as mock_txn:
|
||||
mock_get.return_value = transcript_with_title
|
||||
mock_append.return_value = None
|
||||
|
||||
@asynccontextmanager
|
||||
async def noop_txn():
|
||||
yield
|
||||
|
||||
mock_txn.return_value = noop_txn()
|
||||
|
||||
pipeline = PipelineMainFile(transcript_id=transcript_id)
|
||||
await pipeline.on_title(
|
||||
TranscriptFinalTitle(title="Generated title")
|
||||
)
|
||||
|
||||
mock_get.assert_called_once()
|
||||
mock_update.assert_not_called()
|
||||
mock_append.assert_called_once()
|
||||
|
||||
@@ -110,6 +110,26 @@ async def test_transcript_get_update_title(authenticated_client, client):
|
||||
assert response.json()["title"] == "test_title"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_status_emits_status_event_and_updates_transcript(client):
|
||||
"""set_status adds a STATUS event and updates the transcript status (broadcast for WebSocket)."""
|
||||
response = await client.post("/transcripts", json={"name": "Status test"})
|
||||
assert response.status_code == 200
|
||||
transcript_id = response.json()["id"]
|
||||
|
||||
transcript = await transcripts_controller.get_by_id(transcript_id)
|
||||
assert transcript is not None
|
||||
assert transcript.status == "idle"
|
||||
|
||||
event = await transcripts_controller.set_status(transcript_id, "processing")
|
||||
assert event is not None
|
||||
assert event.event == "STATUS"
|
||||
assert event.data.get("value") == "processing"
|
||||
|
||||
updated = await transcripts_controller.get_by_id(transcript_id)
|
||||
assert updated.status == "processing"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transcripts_list_anonymous(client):
|
||||
# XXX this test is a bit fragile, as it depends on the storage which
|
||||
@@ -233,3 +253,43 @@ async def test_transcript_get_returns_null_room_name_when_no_room(
|
||||
assert response.status_code == 200
|
||||
assert response.json()["room_id"] is None
|
||||
assert response.json()["room_name"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transcripts_list_filtered_by_room_id(authenticated_client, client):
|
||||
"""GET /transcripts?room_id=X returns only transcripts for that room."""
|
||||
# Use same user as authenticated_client (conftest uses "randomuserid")
|
||||
user_id = "randomuserid"
|
||||
room = await rooms_controller.add(
|
||||
name="room-for-list-filter",
|
||||
user_id=user_id,
|
||||
zulip_auto_post=False,
|
||||
zulip_stream="",
|
||||
zulip_topic="",
|
||||
is_locked=False,
|
||||
room_mode="normal",
|
||||
recording_type="cloud",
|
||||
recording_trigger="automatic-2nd-participant",
|
||||
is_shared=False,
|
||||
webhook_url="",
|
||||
webhook_secret="",
|
||||
)
|
||||
in_room = await transcripts_controller.add(
|
||||
name="in-room",
|
||||
source_kind="file",
|
||||
room_id=room.id,
|
||||
user_id=user_id,
|
||||
)
|
||||
other = await transcripts_controller.add(
|
||||
name="no-room",
|
||||
source_kind="file",
|
||||
room_id=None,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
response = await client.get("/transcripts", params={"room_id": room.id})
|
||||
assert response.status_code == 200
|
||||
items = response.json()["items"]
|
||||
ids = [t["id"] for t in items]
|
||||
assert in_room.id in ids
|
||||
assert other.id not in ids
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Reconnection policy for WebSocket.
|
||||
* Ensures exponential backoff is applied and capped at 30s.
|
||||
*/
|
||||
import { getReconnectDelayMs, MAX_RETRIES } from "../webSocketReconnect";
|
||||
|
||||
describe("webSocketReconnect", () => {
|
||||
describe("getReconnectDelayMs", () => {
|
||||
it("returns exponential backoff: 1s, 2s, 4s, 8s, 16s, then cap 30s", () => {
|
||||
expect(getReconnectDelayMs(0)).toBe(1000);
|
||||
expect(getReconnectDelayMs(1)).toBe(2000);
|
||||
expect(getReconnectDelayMs(2)).toBe(4000);
|
||||
expect(getReconnectDelayMs(3)).toBe(8000);
|
||||
expect(getReconnectDelayMs(4)).toBe(16000);
|
||||
expect(getReconnectDelayMs(5)).toBe(30000); // 32s capped to 30s
|
||||
expect(getReconnectDelayMs(6)).toBe(30000);
|
||||
expect(getReconnectDelayMs(9)).toBe(30000);
|
||||
});
|
||||
|
||||
it("never exceeds 30s for any retry index", () => {
|
||||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||||
expect(getReconnectDelayMs(i)).toBeLessThanOrEqual(30000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../lib/apiHooks";
|
||||
import { useAuth } from "../../lib/AuthProvider";
|
||||
import { parseNonEmptyString } from "../../lib/utils";
|
||||
import { getReconnectDelayMs, MAX_RETRIES } from "./webSocketReconnect";
|
||||
|
||||
type TranscriptWsEvent =
|
||||
operations["v1_transcript_get_websocket_events"]["responses"][200]["content"]["application/json"];
|
||||
@@ -338,7 +339,6 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
||||
if (!transcriptId) return;
|
||||
const tsId = parseNonEmptyString(transcriptId);
|
||||
|
||||
const MAX_RETRIES = 10;
|
||||
const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
|
||||
let ws: WebSocket | null = null;
|
||||
let retryCount = 0;
|
||||
@@ -472,7 +472,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
||||
if (normalCodes.includes(event.code)) return;
|
||||
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
|
||||
const delay = getReconnectDelayMs(retryCount);
|
||||
console.log(
|
||||
`WebSocket reconnecting in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`,
|
||||
);
|
||||
|
||||
10
www/app/(app)/transcripts/webSocketReconnect.ts
Normal file
10
www/app/(app)/transcripts/webSocketReconnect.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/** Reconnection policy for WebSocket: exponential backoff, capped at 30s. */
|
||||
export const MAX_RETRIES = 10;
|
||||
|
||||
/**
|
||||
* Delay in ms before reconnecting. retryIndex is 0-based (0 = first retry).
|
||||
* Returns 1000, 2000, 4000, ... up to 30000 max.
|
||||
*/
|
||||
export function getReconnectDelayMs(retryIndex: number): number {
|
||||
return Math.min(1000 * Math.pow(2, retryIndex), 30000);
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write .",
|
||||
"openapi": "openapi-typescript http://127.0.0.1:1250/openapi.json -o ./app/reflector-api.d.ts",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.33.0",
|
||||
|
||||
Reference in New Issue
Block a user