diff --git a/tests/test_db_sync.c b/tests/test_db_sync.c index 8f970f03..c007d84a 100644 --- a/tests/test_db_sync.c +++ b/tests/test_db_sync.c @@ -64,6 +64,7 @@ static char temp_dir[] = "/tmp/utun_dbsync_XXXXXX"; static char config_a[256], config_b[256], config_c[256]; static uint32_t ca_target, cb_target, cc_target; static uint64_t delivery_timestamp; +static uint64_t expected_revision; static int history_ready(struct DB_SYNC_INSTANCE* si); /* Оба сообщения одного автора в одной ms получают отдельные реальные ACK. */ @@ -259,6 +260,18 @@ static int same_roots(struct DB_SYNC_INSTANCE* a, struct DB_SYNC_INSTANCE* b, st return !c || (!db_sync_read_root(c, rc) && !memcmp(ra, rc, 32)); } +static int cond_revision(void) { + struct UTUN_INSTANCE* nodes[] = {inst_a, inst_b}; + for (unsigned i = 0; i < 2; i++) { + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(nodes[i]->topo_sqlite_db, "SELECT max(ver) FROM push_t", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + int matches = rc == SQLITE_ROW && (uint64_t)sqlite3_column_int64(st, 0) == expected_revision; + sqlite3_finalize(st); if (!matches) return 0; + } + return same_roots(si_a, si_b, NULL); +} + int main(void) { printf("=== test_db_sync ===\n"); debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); @@ -417,6 +430,35 @@ int main(void) { !wait_for("two distinct delivery ACKs", cond_delivery, PHASE_TIMEOUT_TB)) { test_phase = 2; goto done; } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS two same-timestamp PUSH records acknowledged separately"); + /* Отключённый пир пропускает две правки: Merkle принимает сразу v3 при том же количестве строк. */ + { + sqlite3_stmt* st = NULL; uint64_t ts; struct db_message_revision revision = {.ver = 1}; uint8_t signature[64]; + if (sqlite3_prepare_v2(inst_a->topo_sqlite_db, "SELECT timestamp,nonce FROM push_t ORDER BY timestamp LIMIT 1", -1, &st, NULL) != SQLITE_OK || + sqlite3_step(st) != SQLITE_ROW || sqlite3_column_bytes(st, 1) != 16) { + sqlite3_finalize(st); test_phase = 2; goto done; + } + ts = (uint64_t)sqlite3_column_int64(st, 0); memcpy(revision.nonce, sqlite3_column_blob(st, 1), 16); sqlite3_finalize(st); + db_sync_peer_set_paused(si_a, inst_b->node_id, 1); db_sync_peer_set_paused(si_b, inst_a->node_id, 1); + for (uint64_t ver = 2; ver <= 3; ver++) { + const char* text = ver == 2 ? "{\"ct\":\"text/plain\",\"d\":\"edit 2\"}" : "{\"ct\":\"text/plain\",\"d\":\"edit 3\"}"; + revision.ver = ver; revision.edited_at = db_sync_next_timestamp(si_a); + if (db_sync_sign_message(40, ts, inst_a->node_id, &revision, text, strlen(text), inst_a->my_ed25519_privkey, signature) < 0 || + db_sync_insert_signed(si_a, text, strlen(text), signature, 64, ts, NULL, &revision) != 0) { + test_phase = 2; goto done; + } + } + expected_revision = 3; reset_done_flags(); + db_sync_reinitiate(si_a, inst_b->node_id); db_sync_reinitiate(si_b, inst_a->node_id); + if (!wait_for("offline edits converge by Merkle", cond_revision, PHASE_TIMEOUT_TB) || + db_sync_count(si_a) != 6 || db_sync_count(si_b) != 6) { test_phase = 2; goto done; } + const char* text = "{\"ct\":\"text/plain\",\"d\":\"live edit 4\"}"; + revision.ver = 4; revision.edited_at = db_sync_next_timestamp(si_a); expected_revision = 4; + if (db_sync_sign_message(40, ts, inst_a->node_id, &revision, text, strlen(text), inst_a->my_ed25519_privkey, signature) < 0 || + db_sync_insert_signed(si_a, text, strlen(text), signature, 64, ts, NULL, &revision) != 0 || + !wait_for("live edit over PUSH", cond_revision, PHASE_TIMEOUT_TB)) { test_phase = 2; goto done; } + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS offline v1->v3 and live v4 converge with stable IDs and unchanged record count"); + } + /* Одинаковые count, отличие в первой записи длинной истории и разные локальные id. */ DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "Phase 5d: long history with equal counts and divergent first record"); remove_si(&si_a); remove_si(&si_b);