Be more gentle with frontend typing + go back to fire-and-forget for cracked room creation

This commit is contained in:
Jack Kingsman
2026-03-09 21:51:07 -07:00
parent 3316f00271
commit c3f1a43a80
4 changed files with 59 additions and 8 deletions

View File

@@ -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})
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:

View File

@@ -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]
);

View File

@@ -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();
});
});

View File

@@ -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},
}