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.""" """Typed WebSocket event contracts and serialization helpers."""
import json import json
import logging
from typing import Any, Literal from typing import Any, Literal
from pydantic import TypeAdapter 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.models import Channel, Contact, Message, MessagePath, RawPacketBroadcast
from app.routers.health import HealthResponse from app.routers.health import HealthResponse
logger = logging.getLogger(__name__)
WsEventType = Literal[ WsEventType = Literal[
"health", "health",
"message", "message",
@@ -82,9 +85,16 @@ def dump_ws_event(event_type: str, data: Any) -> str:
if adapter is None: if adapter is None:
return json.dumps({"type": event_type, "data": data}) return json.dumps({"type": event_type, "data": data})
validated = adapter.validate_python(data) try:
payload = adapter.dump_python(validated, mode="json") validated = adapter.validate_python(data)
return json.dumps({"type": event_type, "data": payload}) 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: def dump_ws_event_payload(event_type: str, data: Any) -> Any:

View File

@@ -188,7 +188,9 @@ export function useAppShellProps({
key_type: 'channel', key_type: 'channel',
channel_key: created.key, channel_key: created.key,
}); });
await fetchUndecryptedCount(); void fetchUndecryptedCount().catch((error) => {
console.error('Failed to refresh undecrypted count after cracked channel create:', error);
});
}, },
[fetchUndecryptedCount, setChannels] [fetchUndecryptedCount, setChannels]
); );

View File

@@ -186,4 +186,40 @@ describe('useAppShellProps', () => {
}); });
expect(args.fetchUndecryptedCount).toHaveBeenCalledTimes(1); 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 from unittest.mock import AsyncMock, patch
import pytest import pytest
from pydantic import ValidationError
from app.websocket import SEND_TIMEOUT_SECONDS, WebSocketManager from app.websocket import SEND_TIMEOUT_SECONDS, WebSocketManager
@@ -262,8 +261,12 @@ class TestTypedEventSerialization:
"data": {"message_id": 7, "ack_count": 2}, "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 from app.events import dump_ws_event
with pytest.raises(ValidationError): serialized = dump_ws_event("message_acked", {"ack_count": 2})
dump_ws_event("message_acked", {"ack_count": 2})
assert json.loads(serialized) == {
"type": "message_acked",
"data": {"ack_count": 2},
}