From c3f1a43a8042283f99d794829941eb072da15dec Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Mon, 9 Mar 2026 21:51:07 -0700 Subject: [PATCH] Be more gentle with frontend typing + go back to fire-and-forget for cracked room creation --- app/events.py | 16 ++++++++-- frontend/src/hooks/useAppShellProps.ts | 4 ++- frontend/src/test/useAppShellProps.test.ts | 36 ++++++++++++++++++++++ tests/test_websocket.py | 11 ++++--- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/app/events.py b/app/events.py index 59fb163..b396786 100644 --- a/app/events.py +++ b/app/events.py @@ -1,6 +1,7 @@ """Typed WebSocket event contracts and serialization helpers.""" import json +import logging from typing import Any, Literal from pydantic import TypeAdapter @@ -9,6 +10,8 @@ from typing_extensions import NotRequired, TypedDict from app.models import Channel, Contact, Message, MessagePath, RawPacketBroadcast from app.routers.health import HealthResponse +logger = logging.getLogger(__name__) + WsEventType = Literal[ "health", "message", @@ -82,9 +85,16 @@ def dump_ws_event(event_type: str, data: Any) -> str: if adapter is None: return json.dumps({"type": event_type, "data": data}) - validated = adapter.validate_python(data) - payload = adapter.dump_python(validated, mode="json") - return json.dumps({"type": event_type, "data": payload}) + try: + validated = adapter.validate_python(data) + payload = adapter.dump_python(validated, mode="json") + return json.dumps({"type": event_type, "data": payload}) + except Exception: + logger.exception( + "Failed to validate WebSocket payload for event %s; falling back to raw JSON envelope", + event_type, + ) + return json.dumps({"type": event_type, "data": data}) def dump_ws_event_payload(event_type: str, data: Any) -> Any: diff --git a/frontend/src/hooks/useAppShellProps.ts b/frontend/src/hooks/useAppShellProps.ts index dd3eba5..46a4a9d 100644 --- a/frontend/src/hooks/useAppShellProps.ts +++ b/frontend/src/hooks/useAppShellProps.ts @@ -188,7 +188,9 @@ export function useAppShellProps({ key_type: 'channel', channel_key: created.key, }); - await fetchUndecryptedCount(); + void fetchUndecryptedCount().catch((error) => { + console.error('Failed to refresh undecrypted count after cracked channel create:', error); + }); }, [fetchUndecryptedCount, setChannels] ); diff --git a/frontend/src/test/useAppShellProps.test.ts b/frontend/src/test/useAppShellProps.test.ts index 692052e..c5d057e 100644 --- a/frontend/src/test/useAppShellProps.test.ts +++ b/frontend/src/test/useAppShellProps.test.ts @@ -186,4 +186,40 @@ describe('useAppShellProps', () => { }); expect(args.fetchUndecryptedCount).toHaveBeenCalledTimes(1); }); + + it('does not fail cracked channel creation when undecrypted count refresh rejects', async () => { + mocks.api.createChannel.mockResolvedValue({ + key: '22'.repeat(16), + name: 'Found', + is_hashtag: false, + }); + mocks.api.getChannels.mockResolvedValue([ + publicChannel, + { ...publicChannel, key: '22'.repeat(16), name: 'Found' }, + ]); + mocks.api.decryptHistoricalPackets.mockResolvedValue({ decrypted_count: 4 }); + + const args = createArgs({ + fetchUndecryptedCount: vi.fn(async () => { + throw new Error('refresh failed'); + }), + }); + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { result } = renderHook(() => useAppShellProps(args)); + + await act(async () => { + await result.current.crackerProps.onChannelCreate('Found', '22'.repeat(16)); + }); + + expect(mocks.api.decryptHistoricalPackets).toHaveBeenCalledWith({ + key_type: 'channel', + channel_key: '22'.repeat(16), + }); + expect(consoleError).toHaveBeenCalledWith( + 'Failed to refresh undecrypted count after cracked channel create:', + expect.any(Error) + ); + + consoleError.mockRestore(); + }); }); diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 49930ed..f18204f 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -5,7 +5,6 @@ import json from unittest.mock import AsyncMock, patch import pytest -from pydantic import ValidationError from app.websocket import SEND_TIMEOUT_SECONDS, WebSocketManager @@ -262,8 +261,12 @@ class TestTypedEventSerialization: "data": {"message_id": 7, "ack_count": 2}, } - def test_dump_ws_event_validates_supported_payloads(self): + def test_dump_ws_event_falls_back_to_raw_payload_when_validation_fails(self): from app.events import dump_ws_event - with pytest.raises(ValidationError): - dump_ws_event("message_acked", {"ack_count": 2}) + serialized = dump_ws_event("message_acked", {"ack_count": 2}) + + assert json.loads(serialized) == { + "type": "message_acked", + "data": {"ack_count": 2}, + }