Skip to content

Commit f56098f

Browse files
andinuxclaude
andcommitted
test(e2e): assert send return values, not just data convergence
The send path's status/version reporting had no e2e coverage — both the spurious gap and the optimistic-version regression moved data correctly while reporting the wrong status, so data-only assertions missed them. Add two helpers: - db_send_ok(): run a send expected to succeed and assert it did not fail at the protocol level (status != "error", no send.lastFailure) — catches a server-reported apply/check failure that does not raise a SQL error, which a plain db_exec of the send ignored. Applied at the basic and chunked primary send sites (not the two failure-path tests, nor the best-effort in-loop sends). - db_send_await_converge(): send, then poll until send.serverVersion reaches send.localVersion (durably covered, no gap), robust to the server's async apply. test_send_gap_from_clock_hole now uses it instead of a single synchronous assert. Full e2e suite (incl. chunked) green against a live server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2479549 commit f56098f

1 file changed

Lines changed: 42 additions & 16 deletions

File tree

test/integration.c

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,35 @@ int db_expect_min (sqlite3 *db, const char *sql, int expect_min) {
236236
return SQLITE_OK;
237237
}
238238

239+
// Run a send that is expected to succeed and assert it didn't fail at the protocol
240+
// level: status is not "error" and the server reported no per-chunk failure
241+
// (send.lastFailure is omitted on success). Catches a server-reported apply/check
242+
// failure that doesn't raise a SQL error — invisible to a plain db_exec of the send.
243+
int db_send_ok (sqlite3 *db) {
244+
return db_expect_int(db,
245+
"SELECT (j ->> '$.send.status') <> 'error' AND (j ->> '$.send.lastFailure') IS NULL "
246+
"FROM (SELECT cloudsync_network_send_changes() AS j);", 1);
247+
}
248+
249+
// Send, then poll until the server's optimistic version (send.serverVersion) catches
250+
// up to send.localVersion — the change is durably covered with no gap. Robust to the
251+
// server's asynchronous apply: the first call sends, later calls are no-ops that
252+
// re-read status. Fails if it has not converged within max_attempts.
253+
int db_send_await_converge (sqlite3 *db, int max_attempts, int delay_ms) {
254+
const char *sql =
255+
"SELECT (j ->> '$.send.serverVersion') >= (j ->> '$.send.localVersion') "
256+
"FROM (SELECT cloudsync_network_send_changes() AS j);";
257+
for (int i = 0; i < max_attempts; i++) {
258+
int converged = 0;
259+
int rc = db_select_int(db, sql, &converged);
260+
if (rc != SQLITE_OK) return rc;
261+
if (converged) return SQLITE_OK;
262+
if (i + 1 < max_attempts) sqlite3_sleep(delay_ms);
263+
}
264+
printf("Error: send did not converge (serverVersion < localVersion) after %d attempts\n", max_attempts);
265+
return SQLITE_ERROR;
266+
}
267+
239268
int integration_network_init(sqlite3 *db, const char *database_id, char *network_init, size_t network_init_len) {
240269
if (!database_id) {
241270
fprintf(stderr, "Error: integration database ID not set.\n");
@@ -528,7 +557,7 @@ int test_enable_disable(const char *db_path) {
528557
rc = db_exec(db, set_apikey); RCHECK
529558
}
530559

531-
rc = db_exec(db, "SELECT cloudsync_network_send_changes();"); RCHECK
560+
rc = db_send_ok(db); RCHECK
532561
rc = db_exec(db, "SELECT cloudsync_cleanup('users');"); RCHECK
533562
rc = db_exec(db, "SELECT cloudsync_cleanup('activities');"); RCHECK
534563
rc = db_exec(db, "SELECT cloudsync_cleanup('workouts');"); RCHECK
@@ -624,14 +653,11 @@ int test_send_gap_from_clock_hole(const char *db_path) {
624653
rc = db_exec(db, set_apikey); RCHECK
625654
}
626655

627-
// Send once. The server applies the change and computes lastOptimisticVersion
628-
// (serverVersion) synchronously from its per-site applied ranges. With contiguous
629-
// coverage it reaches localVersion (10); with the gap bug it stays at 0 because
630-
// db_versions 1..9 are reported missing.
631-
rc = db_expect_int(db,
632-
"SELECT (j ->> '$.send.serverVersion') = (j ->> '$.send.localVersion') "
633-
" AND (j ->> '$.send.localVersion') = 10 "
634-
"FROM (SELECT cloudsync_network_send_changes() AS j);", 1); RCHECK
656+
// Send, then poll until the server's optimistic version (serverVersion) reaches
657+
// localVersion (10). With contiguous coverage it converges; with the gap bug it
658+
// never does — serverVersion stays at 0 because db_versions 1..9 are reported
659+
// missing. Polling absorbs the server's asynchronous apply.
660+
rc = db_send_await_converge(db, 8, 1000); RCHECK
635661

636662
rc = db_exec(db, "SELECT cloudsync_cleanup('users');"); RCHECK
637663
rc = db_exec(db, "SELECT cloudsync_cleanup('activities');"); RCHECK
@@ -665,7 +691,7 @@ int test_chunked_payload_paths(void) {
665691
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 2); if (rc != SQLITE_OK) goto cleanup;
666692
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks() WHERE hex(substr(payload,5,1))='03';", 2); if (rc != SQLITE_OK) goto cleanup;
667693

668-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
694+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
669695
cleanup_remote_row = true;
670696

671697
for (int attempt = 0; attempt < 30; ++attempt) {
@@ -738,7 +764,7 @@ int test_chunked_payload_rowset_path(void) {
738764
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 2); if (rc != SQLITE_OK) goto cleanup;
739765
rc = db_expect_int(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks() WHERE hex(substr(payload,5,1))='03';", 0); if (rc != SQLITE_OK) goto cleanup;
740766

741-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
767+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
742768
cleanup_remote_rows = true;
743769

744770
for (int attempt = 0; attempt < 30; ++attempt) {
@@ -822,7 +848,7 @@ int test_chunked_payload_single_sync_drain(void) {
822848
// Sender splits into multiple non-fragment chunks.
823849
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 2); if (rc != SQLITE_OK) goto cleanup;
824850

825-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
851+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
826852
cleanup_remote_rows = true;
827853

828854
for (int attempt = 0; attempt < 40; ++attempt) {
@@ -921,7 +947,7 @@ int test_chunked_payload_capped_receive(void) {
921947

922948
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 2); if (rc != SQLITE_OK) goto cleanup;
923949

924-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
950+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
925951
cleanup_remote_rows = true;
926952

927953
for (int attempt = 0; attempt < 80; ++attempt) {
@@ -1017,7 +1043,7 @@ int test_chunked_payload_batched_receive(void) {
10171043

10181044
rc = db_expect_min(sender, "SELECT COUNT(*) FROM cloudsync_payload_chunks();", 3); if (rc != SQLITE_OK) goto cleanup;
10191045

1020-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
1046+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
10211047
cleanup_remote_rows = true;
10221048

10231049
for (int attempt = 0; attempt < 80; ++attempt) {
@@ -1189,7 +1215,7 @@ int test_chunked_negative_cache_invalidation(void) {
11891215
"INSERT INTO chunked_payload_items (id, body) VALUES ('%s', 'negative-cache-sentinel');",
11901216
sentinel_id);
11911217
rc = db_exec(sender, sql); if (rc != SQLITE_OK) goto cleanup;
1192-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
1218+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
11931219
cleanup_sentinel = true;
11941220

11951221
// Phase 1: drain the receiver until it is provably caught up. Each bare
@@ -1267,7 +1293,7 @@ int test_chunked_negative_cache_invalidation(void) {
12671293
"INSERT INTO chunked_payload_items (id, body) VALUES ('%s', 'negative-cache');",
12681294
row_id);
12691295
rc = db_exec(sender, sql); if (rc != SQLITE_OK) goto cleanup;
1270-
rc = db_exec(sender, "SELECT cloudsync_network_send_changes();"); if (rc != SQLITE_OK) goto cleanup;
1296+
rc = db_send_ok(sender); if (rc != SQLITE_OK) goto cleanup;
12711297
cleanup_remote_row = true;
12721298

12731299
// Phase 4: the receiver must now pick up the change on a subsequent receive.

0 commit comments

Comments
 (0)