From e5d8c3451458c7f0626660be68d6d90f8a607af2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 13:49:06 +0200 Subject: [PATCH] Fix desktop uasync readiness for radio member mute requests --- tools/chatgui/CMakeLists.txt | 6 + .../chatgui/tests/test_radio_mute_bridge.cpp | 122 ++++++++++++++++++ tools/chatgui/transport/gui_bridge_impl.cpp | 8 +- tools/chatgui/transport/utun_node.cpp | 8 +- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 tools/chatgui/tests/test_radio_mute_bridge.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 06db1fa5..3a52229b 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -354,6 +354,12 @@ add_executable(test_radio_member_mute tests/test_radio_member_mute.cpp) target_include_directories(test_radio_member_mute PRIVATE ${UTUN_INCLUDE_DIRS}) target_link_libraries(test_radio_member_mute PRIVATE ${QT_CORE} utun_voice utun pthread) add_test(NAME test_radio_member_mute COMMAND test_radio_member_mute) +add_executable(test_radio_mute_bridge tests/test_radio_mute_bridge.cpp transport/utun_node.cpp transport/gui_bridge_impl.cpp + src/accountdelegate.cpp src/flagpainter.cpp) +target_include_directories(test_radio_mute_bridge PRIVATE ${UTUN_INCLUDE_DIRS}) +target_link_libraries(test_radio_mute_bridge PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun_voice utun pthread) +add_test(NAME test_radio_mute_bridge COMMAND test_radio_mute_bridge) +set_tests_properties(test_radio_mute_bridge PROPERTIES TIMEOUT 15 ENVIRONMENT "QT_QPA_PLATFORM=offscreen") set_tests_properties(test_radio_member_mute PROPERTIES TIMEOUT 20) if(UNIX AND NOT APPLE) target_compile_definitions(test_radio_member_mute PRIVATE UTUN_TEST_WRAP_VAD) diff --git a/tools/chatgui/tests/test_radio_mute_bridge.cpp b/tools/chatgui/tests/test_radio_mute_bridge.cpp new file mode 100644 index 00000000..719f8c40 --- /dev/null +++ b/tools/chatgui/tests/test_radio_mute_bridge.cpp @@ -0,0 +1,122 @@ +/* Настоящий desktop worker и Qt bridge: нажатие иконки → uasync → SQLite → цвет и результат в GUI. */ +#include "../transport/utun_node.h" +#include "../transport/gui_bridge.h" +#include "../src/accountdelegate.h" +#include +#include +#include +#include +#include + +extern "C" { +#include "utun_instance.h" +#include "chat/chat_core.h" +#include "chat/chat_member.h" +#include "routing_layer/topo_group.h" +#include "routing_layer/topo_node_sqlite.h" +#include "u_async.h" +#include "debug_config.h" +} + +struct Probe { + QObject* receiver = nullptr; + QStandardItemModel* model = nullptr; + QString channel; + bool ready = false, setup_ok = false, polling_running = false; + int updates = 0; + QList results; +}; +static Probe* current; + +static void prepare_channel(void* arg) { + auto* probe = static_cast(arg); + debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_DEBUG); debug_enable_console(1); + UTUN_INSTANCE* inst = gui_bridge_get_inst(); + chat_core_create_channel_auto(inst, "radio mute bridge"); + TOPO_GROUP* channel = nullptr; + for (ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) { + auto* group = reinterpret_cast(e); + if (group->group_type == TOPO_GROUP_TYPE_CHAT) channel = group; + } + const uint8_t key[32] = {1}; + const bool ok = channel && topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, channel->channel_id, 42, key, key) == 0; + const QString id = channel ? QString::fromUtf8(channel->channel_id) : QString(); + const int running = uasync_is_running(inst->ua); + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: desktop bridge probe setup=%d poll_running=%d inst=%p ua=%p ch=%s", + ok, running, (void*)inst, (void*)inst->ua, qPrintable(id)); + QMetaObject::invokeMethod(probe->receiver, [probe, ok, running, id] { + probe->setup_ok = ok; probe->polling_running = running; probe->channel = id; probe->ready = true; + }, Qt::QueuedConnection); +} + +static void member_updated(const uint8_t* data, int len) { + if (!current || !data || len < 2 || len != data[0] + 2 + CHAT_MEMBER_DISPLAY_SIZE) return; + chat_member_display member{}; + if (chat_core_deserialize_member(data + data[0] + 2, &member) < 0 || member.node_id != 42) return; + ++current->updates; + current->model->setData(current->model->index(0, 0), member.flags, Qt::UserRole + 3); +} + +static void mute_result(const uint8_t* data, int len) { + if (current && len == 17) current->results.append(data[16] != 0); +} + +class RadioMuteBridgeTest : public QObject { + Q_OBJECT +private slots: + void cleanup() { + gui_bridge_set_member_updated_cb(nullptr); gui_bridge_set_radio_mute_result_cb(nullptr); + gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); current = nullptr; + debug_disable_file_output(); + } + + void desktopWorkerAcceptsIconToggleAndReturnsConfirmedState() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("node.cfg"); + QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); + const QByteArray text = "[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" + "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n[ntp]\nenabled=no\n"; + QCOMPARE(config.write(text), text.size()); config.close(); + gui_bridge_init(); + QStandardItemModel model; auto* member = new QStandardItem("other"); + member->setData(false, Qt::UserRole + 2); member->setData(0, Qt::UserRole + 3); model.appendRow(member); + Probe probe; probe.receiver = this; probe.model = &model; current = &probe; + gui_bridge_set_member_updated_cb(member_updated); gui_bridge_set_radio_mute_result_cb(mute_result); + UtunNode node; node.setDbPath(directory.filePath("db")); + node.setDebugFile(qEnvironmentVariable("UTUN_TEST_DIAGNOSTIC_LOG", directory.filePath("diagnostic.log"))); + node.setDebugLevel("warn"); node.setDebugCategories("radio=debug,general=debug"); + QVERIFY(node.start(path)); QTRY_VERIFY_WITH_TIMEOUT(gui_bridge_is_uasync_ready(), 3000); + gui_bridge_post_uasync_fn(prepare_channel, &probe); + QTRY_VERIFY_WITH_TIMEOUT(probe.ready, 3000); QVERIFY(probe.setup_ok); + QListView view; AccountDelegate delegate; + view.setModel(&model); view.setItemDelegate(&delegate); view.resize(280, 100); view.show(); QTest::qWait(30); + int accepted = -2; + connect(&delegate, &AccountDelegate::radioMuteRequested, &view, [&](const QModelIndex&) { + accepted = gui_bridge_toggle_member_radio_mute(probe.channel.toUtf8().constData(), 42); + }); + const QRect row = view.visualRect(model.index(0, 0)); const QPoint button(row.right() - 50, row.center().y()); + auto iconHasColor = [&](const QColor& color) { + const QImage image = view.viewport()->grab().toImage(); + for (int y = row.top(); y <= row.bottom(); ++y) + for (int x = row.right() - 63; x < row.right() - 35; ++x) if (image.pixelColor(x, y) == color) return true; + return false; + }; + QVERIFY(iconHasColor(QColor("#4CAF50"))); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, button); + QCOMPARE(accepted, 0); QVERIFY(probe.polling_running); + QTRY_COMPARE_WITH_TIMEOUT(probe.results.size(), 1, 3000); QVERIFY(probe.results[0]); + QCOMPARE(probe.updates, 1); QVERIFY(model.index(0, 0).data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED); + QVERIFY(iconHasColor(QColor("#EF5350"))); QVERIFY(!iconHasColor(QColor("#4CAF50"))); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, button); + QCOMPARE(accepted, 0); QTRY_COMPARE_WITH_TIMEOUT(probe.results.size(), 2, 3000); QVERIFY(probe.results[1]); + QCOMPARE(probe.updates, 2); QVERIFY(!(model.index(0, 0).data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_RADIO_MUTED)); + QVERIFY(iconHasColor(QColor("#4CAF50"))); QVERIFY(!iconHasColor(QColor("#EF5350"))); + node.stop(); QVERIFY(!gui_bridge_is_uasync_ready()); + QCOMPARE(gui_bridge_toggle_member_radio_mute(probe.channel.toUtf8().constData(), 42), -1); + gui_bridge_set_member_updated_cb(nullptr); gui_bridge_set_radio_mute_result_cb(nullptr); current = nullptr; + debug_disable_file_output(); + } +}; +QTEST_MAIN(RadioMuteBridgeTest) +#include "test_radio_mute_bridge.moc" diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index abd2f10b..be3ed5c9 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -599,8 +599,11 @@ void gui_bridge_set_feedback_result_cb(gui_dm_messages_fn cb) { g_feedback_resul void gui_bridge_set_radio_mute_result_cb(gui_dm_messages_fn cb) { g_radio_mute_result_cb = cb; } int gui_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id) { - if (!g_inst || !uasync_is_running(g_ua) || !channel_id || !channel_id[0] || strlen(channel_id) >= 64 || !node_id) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute request rejected: core unavailable or invalid member"); return -1; + const int running = g_inst && g_ua ? uasync_is_running(g_ua) : 0; + const bool valid_channel = channel_id && channel_id[0] && strlen(channel_id) < 64; + if (!g_inst || !running || !valid_channel || !node_id) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: GUI mute rejected inst=%p ua=%p running=%d valid_channel=%d node=%016llx", + (void*)g_inst, (void*)g_ua, running, valid_channel, (unsigned long long)node_id); return -1; } auto* req = static_cast(u_calloc(1, sizeof(chat_member_radio_mute_req))); auto* post = static_cast(u_calloc(1, sizeof(posted_task))); @@ -610,6 +613,7 @@ int gui_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id req->inst = g_inst; req->node_id = node_id; strcpy(req->ch_id, channel_id); post->callback = chat_core_toggle_member_radio_mute_trampoline; post->arg = req; uasync_post_reserved(g_ua, post); + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: GUI mute queued ch=%s node=%016llx", channel_id, (unsigned long long)node_id); return 0; } diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index c06b6812..d56bd19a 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -92,6 +92,7 @@ void UtunNode::stop() { void UtunNode::finalize() { gui_bridge_set_inst(nullptr); + gui_bridge_set_uasync(nullptr); if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; } m_instance = nullptr; m_running = false; @@ -248,13 +249,16 @@ void UtunNode::runLoop() { chat_core_sync_my_addresses(m_instance); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "utun_node: entering poll loop"); + uasync_mark_running(ua); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll started ua=%p running=%d", (void*)ua, uasync_is_running(ua)); while (!m_stop) { uasync_poll(ua, 100); } - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: poll exit m_stop=%d", (int)m_stop); + uasync_mark_stopped(ua); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll stopped ua=%p running=%d requested=%d", + (void*)ua, uasync_is_running(ua), (int)m_stop); if (m_instance) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread");