1+ // Arkilian DLQ Tool Regression Tests — 5,000-business launch verification
2+ // (Checklist #5: "Confirm tools/arkilian-dlq compiles cleanly across target
3+ // platforms and that the recovery runbook is complete for tier-1 support".)
4+ //
5+ // Drives the `arkilian-dlq` CLI via system() against a crafted outbox and
6+ // asserts the on-disk state transitions. Regression-protects the
7+ // `--replay --id N` path which was broken at launch verification with
8+ // "replay prepare failed: near \"AND\": syntax error" (a `FROM ... AND id`
9+ // construction). Idempotency, single-row, and full-replay paths are all
10+ // covered so the recovery runbook's commands are provably correct.
11+ //
12+ // Cross-platform: uses only system() + sqlite3 (no BSD sockets), so it
13+ // runs on the MSYS2/MinGW CI leg alongside the rest of the suite.
14+ //
15+ // Compile (manual, macOS/Linux):
16+ // cc -O2 -Wall -Wextra tests/test_dlq.c src/deps/sqlite/sqlite3.c \
17+ // -Isrc/deps/sqlite -DARKILIAN_DLQ_BIN='"./arkilian-dlq"' -o test_dlq
18+ // Assumes ./arkilian-dlq is already built next to the test.
19+
20+ #include "sqlite3.h"
21+ #include <assert.h>
22+ #include <stdio.h>
23+ #include <stdlib.h>
24+ #include <string.h>
25+
26+ #ifndef ARKILIAN_DLQ_BIN
27+ #define ARKILIAN_DLQ_BIN "./arkilian-dlq"
28+ #endif
29+
30+ static const char * DB = "test_dlq_data.db" ;
31+
32+ static void cleanup_db (void ) {
33+ remove (DB );
34+ char s [64 ];
35+ snprintf (s , sizeof s , "%s-wal" , DB ); remove (s );
36+ snprintf (s , sizeof s , "%s-shm" , DB ); remove (s );
37+ snprintf (s , sizeof s , "%s-journal" , DB ); remove (s );
38+ }
39+
40+ // Invoke the dlq binary; returns the process exit status (0 = success).
41+ static int run_dlq (const char * args ) {
42+ char cmd [4096 ];
43+ snprintf (cmd , sizeof cmd , "\"%s\" \"%s\" %s" , ARKILIAN_DLQ_BIN , DB , args );
44+ // On POSIX system() returns a wait status whose exit code is WEXITSTATUS;
45+ // on Windows it returns the exit code directly. A successful tool run
46+ // exits 0 in both, which is all we assert on.
47+ return system (cmd );
48+ }
49+
50+ static sqlite3 * open_ro (void ) {
51+ sqlite3 * h = NULL ;
52+ sqlite3_open_v2 (DB , & h , SQLITE_OPEN_READONLY , NULL );
53+ return h ;
54+ }
55+
56+ static long long count_in (const char * table ) {
57+ sqlite3 * h = open_ro ();
58+ if (!h ) return -1 ;
59+ sqlite3_stmt * st = NULL ;
60+ long long n = -1 ;
61+ char sql [128 ];
62+ snprintf (sql , sizeof sql , "SELECT COUNT(*) FROM %s" , table );
63+ if (sqlite3_prepare_v2 (h , sql , -1 , & st , NULL ) == SQLITE_OK &&
64+ sqlite3_step (st ) == SQLITE_ROW ) n = sqlite3_column_int64 (st , 0 );
65+ sqlite3_finalize (st );
66+ sqlite3_close (h );
67+ return n ;
68+ }
69+
70+ static int attempts_of (sqlite3_int64 id ) {
71+ sqlite3 * h = open_ro ();
72+ if (!h ) return -1 ;
73+ sqlite3_stmt * st = NULL ;
74+ int n = -1 ;
75+ if (sqlite3_prepare_v2 (h , "SELECT attempts FROM _pending_backup WHERE id=?" ,
76+ -1 , & st , NULL ) == SQLITE_OK ) {
77+ sqlite3_bind_int64 (st , 1 , id );
78+ if (sqlite3_step (st ) == SQLITE_ROW ) n = sqlite3_column_int (st , 0 );
79+ }
80+ sqlite3_finalize (st );
81+ sqlite3_close (h );
82+ return n ;
83+ }
84+
85+ static void seed (void ) {
86+ cleanup_db ();
87+ sqlite3 * h = NULL ;
88+ int rc = sqlite3_open_v2 (DB , & h ,
89+ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX , NULL );
90+ assert (rc == SQLITE_OK && h );
91+ char * err = NULL ;
92+ sqlite3_exec (h ,
93+ "CREATE TABLE _pending_backup(id INTEGER PRIMARY KEY,payload TEXT,"
94+ "attempts INTEGER,created_at INTEGER,last_attempt_at INTEGER);"
95+ "CREATE TABLE _dead_backup(id INTEGER PRIMARY KEY,payload TEXT,"
96+ "attempts INTEGER,failed_reason TEXT,created_at INTEGER,dead_lettered_at INTEGER);" ,
97+ 0 , 0 , & err );
98+ assert (!err );
99+ // 4 dead rows (100,101,102,200). id 200 is ALSO already in _pending,
100+ // exercising the idempotency / "NOT IN (SELECT id FROM _pending_backup)"
101+ // branch of the replay query.
102+ sqlite3_exec (h ,
103+ "INSERT INTO _dead_backup(id,payload,attempts,failed_reason,created_at,dead_lettered_at) VALUES"
104+ "(100,'PA',10,'max attempts exceeded',0,0),"
105+ "(101,'PB',10,'max attempts exceeded',0,0),"
106+ "(102,'PC',10,'max attempts exceeded',0,0),"
107+ "(200,'PD',5,'max attempts exceeded',0,0);"
108+ "INSERT INTO _pending_backup(id,payload,attempts,created_at) VALUES(200,'PD',0,0);" ,
109+ 0 , 0 , & err );
110+ assert (!err );
111+ sqlite3_close (h );
112+ }
113+
114+ static int tests_run = 0 , tests_passed = 0 ;
115+ #define RUN (fn ) do { tests_run++; printf(" [%02d] %-52s ", tests_run, #fn); fflush(stdout); \
116+ fn(); tests_passed++; printf("PASS\n"); } while (0)
117+
118+ static void test_count_reports_dead (void ) {
119+ seed ();
120+ assert (run_dlq ("--count" ) == 0 );
121+ assert (count_in ("_dead_backup" ) == 4 );
122+ }
123+
124+ static void test_list_runs_clean (void ) {
125+ seed ();
126+ assert (run_dlq ("--list --limit 2" ) == 0 );
127+ assert (count_in ("_dead_backup" ) == 4 );
128+ assert (count_in ("_pending_backup" ) == 1 );
129+ }
130+
131+ static void test_dry_run_no_mutation (void ) {
132+ seed ();
133+ assert (run_dlq ("--replay --dry-run" ) == 0 );
134+ assert (count_in ("_dead_backup" ) == 4 ); // unchanged
135+ assert (count_in ("_pending_backup" ) == 1 ); // unchanged
136+ }
137+
138+ static void test_replay_single_id (void ) {
139+ // THE regression: `--replay --id N` used to fail with
140+ // "replay prepare failed: near \"AND\": syntax error".
141+ seed ();
142+ assert (run_dlq ("--replay --id 101" ) == 0 );
143+ assert (count_in ("_dead_backup" ) == 3 ); // 101 removed
144+ assert (count_in ("_pending_backup" ) == 2 ); // 101 added (200 still there)
145+ assert (attempts_of (101 ) == 0 ); // attempts reset on re-queue
146+ }
147+
148+ static void test_replay_id_already_pending (void ) {
149+ seed ();
150+ assert (run_dlq ("--replay --id 200" ) == 0 );
151+ assert (count_in ("_dead_backup" ) == 3 ); // 200 removed from dead
152+ assert (count_in ("_pending_backup" ) == 1 ); // not duplicated (INSERT OR IGNORE)
153+ assert (attempts_of (200 ) == 0 );
154+ }
155+
156+ static void test_replay_id_nonexistent (void ) {
157+ seed ();
158+ assert (run_dlq ("--replay --id 99999" ) == 0 );
159+ assert (count_in ("_dead_backup" ) == 4 );
160+ assert (count_in ("_pending_backup" ) == 1 );
161+ }
162+
163+ static void test_replay_all (void ) {
164+ seed ();
165+ assert (run_dlq ("--replay" ) == 0 );
166+ assert (count_in ("_dead_backup" ) == 0 ); // all drained
167+ assert (count_in ("_pending_backup" ) == 4 ); // 100,101,102,200
168+ assert (attempts_of (100 ) == 0 );
169+ assert (attempts_of (101 ) == 0 );
170+ assert (attempts_of (102 ) == 0 );
171+ }
172+
173+ static void test_replay_idempotent_second (void ) {
174+ seed ();
175+ assert (run_dlq ("--replay" ) == 0 );
176+ assert (run_dlq ("--replay" ) == 0 ); // second run: nothing left to replay
177+ assert (count_in ("_dead_backup" ) == 0 );
178+ assert (count_in ("_pending_backup" ) == 4 );
179+ }
180+
181+ int main (void ) {
182+ printf ("=== Arkilian DLQ Tool Tests ===\n\n" );
183+ RUN (test_count_reports_dead );
184+ RUN (test_list_runs_clean );
185+ RUN (test_dry_run_no_mutation );
186+ RUN (test_replay_single_id );
187+ RUN (test_replay_id_already_pending );
188+ RUN (test_replay_id_nonexistent );
189+ RUN (test_replay_all );
190+ RUN (test_replay_idempotent_second );
191+ cleanup_db ();
192+ printf ("\n=== Results: %d/%d passed ===\n" , tests_passed , tests_run );
193+ return (tests_passed == tests_run ) ? 0 : 1 ;
194+ }
0 commit comments