diff --git a/src/chat/merkle_sync_doc.md b/src/chat/merkle_sync_doc.md index 2d06b40f..9c2dab0d 100644 --- a/src/chat/merkle_sync_doc.md +++ b/src/chat/merkle_sync_doc.md @@ -202,7 +202,9 @@ single разрешает один вариант либо собственны Изменение ответа заменяет его целиком с ver+1; нулевой ответ отменяет голос. message_id сохраняется в msg_ с UNIQUE-индексом: -SHA256(`utun-chat-message-v1` || BE64(channel_id) || BE64(timestamp) || author_signature). +SHA256(`utun-chat-message-v2` || BE64(channel_id) || BE64(timestamp) || BE64(author) || nonce[16]). +ID не меняется при правке текста; MESSAGES заменяет запись только при большем ver. +Конверт и подпись версии описаны в [db_sync_arch.md](db_sync_arch.md). Этот ID одинаков на всех узлах и возвращается API сообщений и результатом публикации опроса. Получение исходного сообщения повторно проверяет отложенные ответы. Пользовательские результаты вычисляются по последним допустимым непустым состояниям; проценты — от числа ответивших. diff --git a/tools/chatgui/tests/test_group_edits.cpp b/tools/chatgui/tests/test_group_edits.cpp index b3e0f4b7..d75a1fc3 100644 --- a/tools/chatgui/tests/test_group_edits.cpp +++ b/tools/chatgui/tests/test_group_edits.cpp @@ -54,50 +54,59 @@ private slots: QTRY_VERIFY_WITH_TIMEOUT(db.isOpen(), 3000); QCOMPARE(inCore([](UTUN_INSTANCE* inst) { chat_core_create_channel_auto(inst, "Editable"); return 0; }), 0); QTRY_COMPARE(db.getChannels().size(), 1); const auto channel = db.getChannels().first().channelId; - QByteArray id; + QByteArray id, replyId; QCOMPARE(inCore([&](UTUN_INSTANCE* inst) { chat_msg_submit req{}; req.inst = inst; strcpy(req.content_type, "text/plain"); snprintf(req.channel_id, sizeof(req.channel_id), "%s", qPrintable(channel)); req.data = (uint8_t*)"Original"; req.data_len = 8; if (chat_core_submit_message(inst, &req)) return -1; id = QByteArray((const char*)req.message_id, 32); memcpy(req.reply_to, req.message_id, 32); - req.data = (uint8_t*)"Reply"; req.data_len = 5; return chat_core_submit_message(inst, &req); + req.data = (uint8_t*)"Reply"; req.data_len = 5; + const int rc = chat_core_submit_message(inst, &req); replyId = QByteArray((const char*)req.message_id, 32); return rc; }), 0); auto* history = window.findChild(); QVERIFY(history); history->loadChannel(channel); auto* view = history->findChild(); auto* input = history->findChild(); QVERIFY(view && input); auto* editor = input->findChild(); QVERIFY(editor); QTRY_COMPARE(view->model()->rowCount(), 2); - const QModelIndex original = view->model()->index(0, 0), reply = view->model()->index(1, 0); - QCOMPARE(original.data(MsgTextRole).toString(), QString("Original")); + auto indexFor = [&](const QByteArray& key) { + for (int row = 0; row < view->model()->rowCount(); row++) { + const auto index = view->model()->index(row, 0); + if (index.data(MsgMessageKeyRole).toString() == QString::fromLatin1(key.toHex())) return index; + } + return QModelIndex(); + }; + QCOMPARE(indexFor(id).data(MsgTextRole).toString(), QString("Original")); editor->setPlainText("Unsent draft"); auto openEditor = [&] { + QCoreApplication::processEvents(); const auto original = indexFor(id); QVERIFY(original.isValid()); + view->scrollTo(original); + const auto pos = view->visualRect(original).center(); + QCOMPARE(view->indexAt(pos), original); QTimer::singleShot(0, [] { auto* menu = qobject_cast(QApplication::activePopupWidget()); if (!menu) qFatal("Edit menu missing"); for (auto* action : menu->actions()) if (action->text() == "Edit message") { action->trigger(); menu->close(); return; } qFatal("Edit action missing"); }); - view->scrollTo(original); QCoreApplication::processEvents(); - const auto pos = view->visualRect(original).center(); QMetaObject::invokeMethod(view, "customContextMenuRequested", Qt::DirectConnection, Q_ARG(QPoint, pos)); }; openEditor(); QCOMPARE(editor->toPlainText(), QString("Original")); gui_bridge_set_msg_received_cb([](const char*, int, uint64_t, int) { ++newMessages; }); newMessages = 0; editor->setPlainText(QString::fromUtf8("Изменено 👍")); QTest::keyClick(editor, Qt::Key_Return); - QTRY_COMPARE_WITH_TIMEOUT(original.data(MsgVersionRole).toULongLong(), quint64(2), 3000); - QCOMPARE(view->model()->rowCount(), 2); QCOMPARE(original.data(MsgTextRole).toString(), QString::fromUtf8("Изменено 👍")); - QVERIFY(original.data(MsgTimeRole).toString().contains("edited ")); - QCOMPARE(reply.data(MsgQuoteTextRole).toString(), QString::fromUtf8("Изменено 👍")); + QTRY_COMPARE_WITH_TIMEOUT(indexFor(id).data(MsgVersionRole).toULongLong(), quint64(2), 3000); + QCOMPARE(view->model()->rowCount(), 2); QCOMPARE(indexFor(id).data(MsgTextRole).toString(), QString::fromUtf8("Изменено 👍")); + QVERIFY(indexFor(id).data(MsgTimeRole).toString().contains("edited ")); + QCOMPARE(indexFor(replyId).data(MsgQuoteTextRole).toString(), QString::fromUtf8("Изменено 👍")); QTRY_COMPARE(editor->toPlainText(), QString("Unsent draft")); QCOMPARE(newMessages, 0); openEditor(); editor->setPlainText("Stale editor draft"); QCOMPARE(inCore([&](UTUN_INSTANCE* inst) { return chat_core_edit_message(inst, qPrintable(channel), (const uint8_t*)id.constData(), 2, "Remote edit", 11); }), 0); - QTRY_COMPARE(original.data(MsgVersionRole).toULongLong(), quint64(3)); + QTRY_COMPARE(indexFor(id).data(MsgVersionRole).toULongLong(), quint64(3)); QCOMPARE(editor->toPlainText(), QString("Stale editor draft")); QSignalSpy failures(input, &InputBar::attachmentRejected); QTest::keyClick(editor, Qt::Key_Return); QTRY_COMPARE(failures.size(), 1); QCOMPARE(editor->toPlainText(), QString("Stale editor draft")); - QCOMPARE(original.data(MsgTextRole).toString(), QString("Remote edit")); QCOMPARE(newMessages, 0); + QCOMPARE(indexFor(id).data(MsgTextRole).toString(), QString("Remote edit")); QCOMPARE(newMessages, 0); QTest::keyClick(editor, Qt::Key_Escape); QCOMPARE(editor->toPlainText(), QString("Unsent draft")); gui_bridge_set_msg_received_cb(nullptr); }