mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Be more gentle with frontend typing + go back to fire-and-forget for cracked room creation
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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},
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user