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.
|
ensuring all processors are correctly invoked and the happy path works correctly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from uuid import uuid4
|
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
|
# This should fail when trying to open the file with av
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
await pipeline.process(non_existent_path)
|
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"
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_transcripts_list_anonymous(client):
|
async def test_transcripts_list_anonymous(client):
|
||||||
# XXX this test is a bit fragile, as it depends on the storage which
|
# 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.status_code == 200
|
||||||
assert response.json()["room_id"] is None
|
assert response.json()["room_id"] is None
|
||||||
assert response.json()["room_name"] 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";
|
} from "../../lib/apiHooks";
|
||||||
import { useAuth } from "../../lib/AuthProvider";
|
import { useAuth } from "../../lib/AuthProvider";
|
||||||
import { parseNonEmptyString } from "../../lib/utils";
|
import { parseNonEmptyString } from "../../lib/utils";
|
||||||
|
import { getReconnectDelayMs, MAX_RETRIES } from "./webSocketReconnect";
|
||||||
|
|
||||||
type TranscriptWsEvent =
|
type TranscriptWsEvent =
|
||||||
operations["v1_transcript_get_websocket_events"]["responses"][200]["content"]["application/json"];
|
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;
|
if (!transcriptId) return;
|
||||||
const tsId = parseNonEmptyString(transcriptId);
|
const tsId = parseNonEmptyString(transcriptId);
|
||||||
|
|
||||||
const MAX_RETRIES = 10;
|
|
||||||
const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
|
const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
|
||||||
let ws: WebSocket | null = null;
|
let ws: WebSocket | null = null;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
@@ -472,7 +472,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
|||||||
if (normalCodes.includes(event.code)) return;
|
if (normalCodes.includes(event.code)) return;
|
||||||
|
|
||||||
if (retryCount < MAX_RETRIES) {
|
if (retryCount < MAX_RETRIES) {
|
||||||
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
|
const delay = getReconnectDelayMs(retryCount);
|
||||||
console.log(
|
console.log(
|
||||||
`WebSocket reconnecting in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`,
|
`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",
|
"lint": "next lint",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"openapi": "openapi-typescript http://127.0.0.1:1250/openapi.json -o ./app/reflector-api.d.ts",
|
"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": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^3.33.0",
|
"@chakra-ui/react": "^3.33.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user