From 1f19720cb83402b2b505683d4e6729a79f8e5bc9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 03:32:32 +0100 Subject: [PATCH] Exercise feedback lookup failures, atomic retries and rollback ownership --- tests/test_chat_feedback.c | 89 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/test_chat_feedback.c b/tests/test_chat_feedback.c index e5fdeb17..2323a65d 100644 --- a/tests/test_chat_feedback.c +++ b/tests/test_chat_feedback.c @@ -313,6 +313,94 @@ static void membership_regressions(struct UASYNC* ua) { assert(sqlite3_close(inviter.topo_sqlite_db) == SQLITE_OK); } +/* Реальный file DB позволяет проверить commit другим соединением, в том числе из события. */ +static struct UTUN_INSTANCE* feedback_reader; +static struct chat_feedback_record event_record; +static void fault_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + if (type != CHAT_EVT_FEEDBACK_CHANGED) return; + assert(sqlite3_get_autocommit(inst->topo_sqlite_db) && len == 38 && data[0] == 4); + assert_stored(feedback_reader, &event_record); + uint8_t writer[32], reader[32]; root(inst, writer); + assert(!merkle_tree_get(feedback_reader->topo_sqlite_db, "1:1001", 0, 0, reader) && !memcmp(reader, writer, 32)); events++; +} + +static void sql_fault_regressions(struct UASYNC* ua) { + char path[] = "/tmp/utun-feedback-fault-XXXXXX"; int fd = mkstemp(path); assert(fd >= 0); close(fd); + struct UTUN_INSTANCE author, target, reader; memset(&reader, 0, sizeof(reader)); + setup(&author, ua, NULL); setup(&target, ua, path); + add_member(&author, &author, "1001"); add_member(&target, &author, "1001"); add_member(&target, &target, "1001"); + assert(sqlite3_open_v2(path, &reader.topo_sqlite_db, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK); + feedback_reader = &reader; chat_event_set_handler(&target, fault_event); + uint8_t message[32] = {61}, reaction[] = {1, 0}, before[32], after[32]; uint64_t version; + struct chat_feedback_record old, current, candidate; + assert(chat_feedback_set(&author, "1001", message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), NULL) == 1); + assert(chat_feedback_get(&author, "1001", message, author.node_id, CHAT_FEEDBACK_REACTION, &old) == 1); + assert(chat_feedback_set(&author, "1001", message, CHAT_FEEDBACK_REACTION, NULL, 0, NULL) == 1); + assert(chat_feedback_get(&author, "1001", message, author.node_id, CHAT_FEEDBACK_REACTION, ¤t) == 1); + event_record = current; assert(chat_feedback_apply(&target, "1001", ¤t) == 1); + int saved = events; root(&target, before); + /* Ошибка поиска не должна воскрешать старую реакцию поверх более новой отмены. */ + injected_operation = F_GET; injected_bind = 1; injected = F_BIND; + event_record = old; + assert(chat_feedback_apply(&target, "1001", &old) < 0 && injected == F_NONE && events == saved); + assert_stored(&reader, ¤t); root(&target, after); assert(!memcmp(before, after, 32)); + unsigned cases = 0; + for (enum sql_fault point = F_BEGIN; point <= F_COMMIT; point++) { + for (enum sql_operation operation = F_GET; operation <= F_PUT; operation++) { + if (operation == F_GET && (point == F_BEGIN || point == F_CHANGES || point == F_COMMIT)) continue; + int bindings = point == F_BIND ? (operation == F_GET ? 3 : 8) : 1; + for (int bind = 1; bind <= bindings; bind++) { + candidate = old; candidate.ver = current.ver + 1; sign_record(&author, 1001, &candidate); + saved = events; root(&target, before); event_record = candidate; + injected_operation = operation; injected_bind = bind; injected = point; + assert(chat_feedback_apply(&target, "1001", &candidate) < 0 && injected == F_NONE && events == saved); + assert(sqlite3_get_autocommit(target.topo_sqlite_db)); assert_stored(&reader, ¤t); + root(&target, after); assert(!memcmp(before, after, 32)); + assert(chat_feedback_apply(&target, "1001", &candidate) == 1 && events == saved + 1); + current = candidate; cases++; + } + } + } + const char* triggers[] = { + "CREATE TEMP TRIGGER reject_feedback BEFORE INSERT ON feedback_1001 BEGIN SELECT RAISE(ABORT,'feedback'); END", + "CREATE TEMP TRIGGER reject_feedback BEFORE INSERT ON feedback_1001 BEGIN SELECT RAISE(IGNORE); END" + }; + for (unsigned i = 0; i < 2; i++) { + candidate = current; candidate.ver++; sign_record(&author, 1001, &candidate); event_record = candidate; + assert(sqlite3_exec(target.topo_sqlite_db, triggers[i], NULL, NULL, NULL) == SQLITE_OK); saved = events; + assert(chat_feedback_apply(&target, "1001", &candidate) < 0 && events == saved); assert_stored(&reader, ¤t); + assert(sqlite3_exec(target.topo_sqlite_db, "DROP TRIGGER reject_feedback", NULL, NULL, NULL) == SQLITE_OK); + assert(chat_feedback_apply(&target, "1001", &candidate) == 1); current = candidate; + } + /* Настоящий veto откатывает запись и индекс, повтор успешно фиксирует оба. */ + candidate = current; candidate.ver++; sign_record(&author, 1001, &candidate); event_record = candidate; + int vetoes = 1; sqlite3_commit_hook(target.topo_sqlite_db, veto_commit, &vetoes); saved = events; + assert(chat_feedback_apply(&target, "1001", &candidate) < 0 && events == saved && vetoes == 0); + sqlite3_commit_hook(target.topo_sqlite_db, NULL, NULL); assert_stored(&reader, ¤t); + assert(chat_feedback_apply(&target, "1001", &candidate) == 1); current = candidate; + /* Даже локальный no-op не должен принимать внешнюю транзакцию. */ + chat_event_set_handler(&target, on_event); + assert(chat_feedback_set(&target, "1001", message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), &version) == 1); + saved = events; assert(sqlite3_exec(target.topo_sqlite_db, "BEGIN;UPDATE channels SET name='pending' WHERE channel_id='1002'", + NULL, NULL, NULL) == SQLITE_OK); + version = UINT64_MAX; + assert(chat_feedback_set(&target, "1001", message, CHAT_FEEDBACK_REACTION, reaction, sizeof(reaction), &version) < 0); + uint8_t page[2 + FB_WIRE_MAX]; page[0] = 0; page[1] = 1; + candidate = current; candidate.ver++; sign_record(&author, 1001, &candidate); + int bytes = fb_encode(&candidate, page + 2); assert(bytes > 0); + assert(fb_apply_page(&target, "1001", author.node_id, page, 2 + bytes) < 0); + assert(!sqlite3_get_autocommit(target.topo_sqlite_db) && version == UINT64_MAX && events == saved); + assert_stored(&reader, ¤t); assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK); + /* После неудачного rollback нельзя RELEASE и commit частичной страницы. */ + target.running = 1; injected_operation = F_PUT; injected = F_FINALIZE; failed_rollback = 1; + assert(chat_feedback_apply(&target, "1001", &candidate) < 0 && injected == F_NONE && !failed_rollback); + assert(!target.running && !sqlite3_get_autocommit(target.topo_sqlite_db) && events == saved); assert_stored(&reader, ¤t); + member_sync_destroy(&target); assert(sqlite3_close(target.topo_sqlite_db) == SQLITE_OK); assert_stored(&reader, ¤t); + member_sync_destroy(&author); assert(sqlite3_close(author.topo_sqlite_db) == SQLITE_OK); + assert(sqlite3_close(reader.topo_sqlite_db) == SQLITE_OK); assert(!remove(path)); feedback_reader = NULL; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS feedback:%u SQL faults/retries, stale-state lookup failure, ABORT/IGNORE/veto, foreign no-op/page, failed rollback stops owner; reader sees exact committed records/roots before events", cases); +} + /* Диагностика копии профиля: без изменения записей или доступа к приватным ключам. */ static int check_profile_authors(const char* path) { struct UTUN_INSTANCE inst = {0}; @@ -445,6 +533,7 @@ int main(int argc, char** argv) { uint8_t empty[32] = {0}; assert(!memcmp(received, empty, 32)); puts("PASS: deleting a channel removes its feedback table and derived root"); membership_regressions(ua); + sql_fault_regressions(ua); member_sync_destroy(&source); member_sync_destroy(&target); assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK); uasync_poll(ua, 0); uasync_destroy(ua, 0);