#include "chat/delivery/chat_delivery_read_model.h" #include "chat/delivery/legacy_chat_delivery_bridge.h" #include "chat/domain/chat_model.h" #include "chat/infra/store/ram_store.h" #include "chat/ports/i_mesh_adapter.h" #include "chat/usecase/chat_service.h" #include "sys/clock.h" #include "ui/presentation_sources/chat_presentation_source.h" #include "ui/presentation_sources/legacy_chat_action_sink.h" #include #include #include #include #include namespace { class FakeMeshAdapter final : public ::chat::IMeshAdapter { public: bool sendText(::chat::ChannelId channel, const std::string& text, ::chat::MessageId* out_msg_id, ::chat::NodeId peer = 0) override { ++send_count; last_channel = channel; last_text = text; last_peer = peer; if (out_msg_id) { *out_msg_id = next_id++; } return send_ok; } ::chat::MeshSendResult sendTextDetailed(::chat::ChannelId channel, const std::string& text, ::chat::MessageId forced_msg_id = 0, ::chat::NodeId peer = 0) override { ::chat::MessageId msg_id = forced_msg_id; const bool ok = sendText(channel, text, &msg_id, peer); if (ok) { return ::chat::MeshSendResult::success(msg_id); } return ::chat::MeshSendResult::fail(send_failure, fail_returns_msg_id ? msg_id : 0); } bool pollIncomingText(::chat::MeshIncomingText* out) override { if (!out || incoming.empty()) { return false; } *out = incoming.front(); incoming.pop_front(); return true; } bool sendAppData(::chat::ChannelId, uint32_t, const uint8_t*, size_t, ::chat::NodeId = 0, bool = false, ::chat::MessageId = 0, bool = false) override { return false; } bool pollIncomingData(::chat::MeshIncomingData*) override { return false; } void applyConfig(const ::chat::MeshConfig&) override {} bool isReady() const override { return true; } bool pollIncomingRawPacket(uint8_t*, size_t&, size_t) override { return false; } int send_count = 0; bool send_ok = true; bool fail_returns_msg_id = true; ::chat::MeshOperationFailure send_failure = ::chat::MeshOperationFailure::Unknown; ::chat::MessageId next_id = 100; ::chat::ChannelId last_channel = ::chat::ChannelId::PRIMARY; std::string last_text; ::chat::NodeId last_peer = 0; std::deque<::chat::MeshIncomingText> incoming; }; ui::chat::ConversationId directPeer(uint32_t peer) { ui::chat::ConversationId id; id.kind = ui::chat::ConversationKind::DirectPeer; id.protocol = ui::chat::ChatProtocolKind::Meshtastic; id.primary = peer; id.secondary = static_cast(::chat::ChannelId::PRIMARY); return id; } ui::chat::ConversationId teamConversation() { ui::chat::ConversationId id; id.kind = ui::chat::ConversationKind::Team; id.protocol = ui::chat::ChatProtocolKind::TrailMate; id.primary = 7; return id; } ui::chat::ConversationId broadcastConversation() { ui::chat::ConversationId id; id.kind = ui::chat::ConversationKind::Channel; id.protocol = ui::chat::ChatProtocolKind::Meshtastic; id.primary = static_cast(::chat::ChannelId::PRIMARY); return id; } ui::chat::ConversationId meshCoreBroadcastConversation() { ui::chat::ConversationId id; id.kind = ui::chat::ConversationKind::Channel; id.protocol = ui::chat::ChatProtocolKind::MeshCore; id.primary = static_cast(::chat::ChannelId::PRIMARY); return id; } ui::chat::ConversationId systemConversation() { ui::chat::ConversationId id; id.kind = ui::chat::ConversationKind::System; id.protocol = ui::chat::ChatProtocolKind::TrailMate; id.primary = 1; return id; } } // namespace int main() { sys::set_epoch_seconds_provider([]() -> uint32_t { return 1700000000U; }); sys::set_millis_provider([]() -> uint32_t { return 5000U; }); ::chat::ChatModel model; FakeMeshAdapter mesh; ::chat::RamStore store; ::chat::ChatService service(model, mesh, store); ::chat::delivery::ChatDeliveryReadModel delivery_read_model; ui::presentation_sources::LegacyChatActionSink sink(service); ui::presentation_sources::ChatPresentationSource source( service, nullptr, &delivery_read_model); const ui::chat::ConversationId ada = directPeer(1234); ui::chat::SendMessageView send; send.conversation = ada; send.text = "hello"; send.text_len = 5; const auto send_result = sink.sendMessage(send); assert(send_result.ok); assert(mesh.send_count == 1); assert(mesh.last_peer == 1234); assert(mesh.last_text == "hello"); ::chat::delivery::ChatDeliveryRecord delivered{}; delivered.ref.protocol_id = 100; delivered.state = ::chat::delivery::DeliveryState::Delivered; delivered.failure = ::chat::delivery::DeliveryFailureKind::None; assert(delivery_read_model.upsert(delivered)); ui::chat::ChatWorkspaceRequest request; request.selected = ada; ui::chat::ChatWorkspaceSnapshot snapshot; assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.header.valid); assert(snapshot.conversation_count == 1); assert(snapshot.conversations[0].id == ada); assert(snapshot.conversations[0].selected); assert(snapshot.conversations[0].last_timestamp != 0); assert(std::strcmp(snapshot.conversations[0].title.c_str(), "04D2") == 0); assert(snapshot.message_count == 1); assert(snapshot.messages[0].conversation == ada); assert(snapshot.messages[0].outgoing); assert(std::strcmp(snapshot.messages[0].time_label.c_str(), "") != 0); assert(std::strtoul(snapshot.messages[0].time_label.c_str(), nullptr, 10) == snapshot.conversations[0].last_timestamp); assert(snapshot.messages[0].delivery == ui::chat::MessageDeliveryState::Delivered); assert(snapshot.messages[0].failure == ui::chat::MessageFailureKind::None); assert(std::strcmp(snapshot.messages[0].text.c_str(), "hello") == 0); assert(snapshot.can_send); assert(snapshot.composer_enabled); mesh.send_ok = false; mesh.send_failure = ::chat::MeshOperationFailure::PeerKeyMissing; const ui::chat::SendMessageView failed_send{ada, "fail", 4}; const auto rejected_send = sink.sendMessage(failed_send); assert(!rejected_send.ok); assert(rejected_send.failure == ui::UiActionFailure::PeerKeyMissing); mesh.fail_returns_msg_id = false; mesh.send_failure = ::chat::MeshOperationFailure::ChannelKeyMissing; const auto channel_key_send = sink.sendMessage(failed_send); assert(!channel_key_send.ok); assert(channel_key_send.failure == ui::UiActionFailure::ChannelKeyMissing); mesh.send_failure = ::chat::MeshOperationFailure::RadioOffline; const auto radio_offline_send = sink.sendMessage(failed_send); assert(!radio_offline_send.ok); assert(radio_offline_send.failure == ui::UiActionFailure::RadioOffline); mesh.fail_returns_msg_id = true; assert(delivery_read_model.upsert(::chat::delivery::toFailedDeliveryRecord( ::chat::delivery::ChatDeliveryRef{0, 101, 0}, ::chat::delivery::SendFailureKind::PeerKeyMissing))); assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.message_count == 2); assert(snapshot.messages[1].delivery == ui::chat::MessageDeliveryState::Failed); assert(snapshot.messages[1].failure == ui::chat::MessageFailureKind::PeerKeyMissing); assert(delivery_read_model.upsert(::chat::delivery::toFailedDeliveryRecord( ::chat::delivery::ChatDeliveryRef{0, 101, 0}, ::chat::delivery::SendFailureKind::ChannelKeyMissing))); assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.message_count == 2); assert(snapshot.messages[1].failure == ui::chat::MessageFailureKind::ChannelKeyMissing); assert(sink.markRead(ada).ok); ::chat::MeshIncomingText incoming{}; incoming.channel = ::chat::ChannelId::PRIMARY; incoming.from = 0x648144D4; incoming.to = 0xFFFFFFFFUL; incoming.msg_id = 900; incoming.text = "broadcast hello"; mesh.incoming.push_back(incoming); service.processIncoming(); const ui::chat::ConversationId broadcast = broadcastConversation(); request.selected = broadcast; assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.header.valid); assert(snapshot.message_count == 1); assert(snapshot.messages[0].conversation == broadcast); assert(!snapshot.messages[0].outgoing); assert(snapshot.messages[0].sender_node_id == 0x648144D4); assert(std::strcmp(snapshot.messages[0].sender_label.c_str(), "44D4") == 0); service.setActiveProtocol(::chat::MeshProtocol::MeshCore); ::chat::MeshIncomingText unknown_meshcore_incoming{}; unknown_meshcore_incoming.channel = ::chat::ChannelId::PRIMARY; unknown_meshcore_incoming.from = 0; unknown_meshcore_incoming.to = 0xFFFFFFFFUL; unknown_meshcore_incoming.msg_id = 901; unknown_meshcore_incoming.text = "mc sender unknown"; mesh.incoming.push_back(unknown_meshcore_incoming); service.processIncoming(); const ui::chat::ConversationId meshcore_broadcast = meshCoreBroadcastConversation(); request.selected = meshcore_broadcast; assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(snapshot.header.valid); assert(snapshot.message_count == 1); assert(snapshot.messages[0].conversation == meshcore_broadcast); assert(!snapshot.messages[0].outgoing); assert(snapshot.messages[0].sender_node_id == 0); assert(std::strcmp(snapshot.messages[0].sender_label.c_str(), "Unknown") == 0); const ui::chat::ConversationId team = teamConversation(); assert(!sink.selectConversation(team).ok); assert(!sink.markRead(team).ok); send.conversation = team; const auto team_send = sink.sendMessage(send); assert(!team_send.ok); assert(team_send.failure == ui::UiActionFailure::Unsupported); request.selected = team; assert(source.buildChatWorkspaceSnapshot(request, snapshot)); assert(!snapshot.can_send); assert(!snapshot.composer_enabled); send.conversation = broadcast; mesh.send_ok = true; const auto broadcast_send = sink.sendMessage(send); assert(broadcast_send.ok); const ui::chat::ConversationId system = systemConversation(); send.conversation = system; const auto system_send = sink.sendMessage(send); assert(!system_send.ok); assert(system_send.failure == ui::UiActionFailure::Unsupported); sys::set_epoch_seconds_provider(nullptr); sys::set_millis_provider(nullptr); return 0; }