Mercurial
view mrjunejune/test/conversation_store_test.c @ 269:de291f396881
install initial production config
Copy the ignored repository config into /etc/mrjunejune on first deployment while preserving existing production configuration on later deploys.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 13:08:10 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
#include "mrjunejune/conversation_store.h" #include "deita/deita.h" #include <assert.h> #include <stdio.h> #include <string.h> #include <unistd.h> /* Helper to create and fill a temp DB file path */ static void make_temp_db(char *out, size_t cap) { snprintf(out, cap, "/tmp/mrjunejune-store-test-XXXXXX"); int fd = mkstemp(out); assert(fd >= 0); close(fd); } /* ------------------------------------------------------------------ */ /* Original (legacy-API) tests */ /* ------------------------------------------------------------------ */ static void test_legacy_api(const char *database_path) { Conversation_Store *p_store = Conversation_Store_Create(database_path); assert(p_store); char conversation_id[37]; assert(Conversation_Store_Create_Conversation( p_store, "First quest", conversation_id) == CONVERSATION_STORE_OK); assert(strlen(conversation_id) == 36); Dowa_Arena *p_arena = Dowa_Arena_Create(16 * 1024); Conversation_Record record; assert(Conversation_Store_Get( p_store, conversation_id, &record, p_arena) == CONVERSATION_STORE_OK); assert(strcmp(record.id, conversation_id) == 0); assert(strcmp(record.title, "First quest") == 0); assert(Dowa_Array_Length(record.turns) == 0); Dowa_Arena_Free(p_arena); assert(Conversation_Store_Update_Title( p_store, conversation_id, "Renamed quest") == CONVERSATION_STORE_OK); assert(Conversation_Store_Begin_Turn( p_store, conversation_id, "request-1", "Hello Epi") == CONVERSATION_STORE_OK); assert(Conversation_Store_Begin_Turn( p_store, conversation_id, "request-2", "Overlapping") == CONVERSATION_STORE_CONFLICT); assert(Conversation_Store_Complete_Turn( p_store, conversation_id, "request-1", "Hello traveler", 12, 4) == CONVERSATION_STORE_OK); assert(Conversation_Store_Begin_Turn( p_store, conversation_id, "request-2", "Try again") == CONVERSATION_STORE_OK); assert(Conversation_Store_Fail_Turn( p_store, conversation_id, "request-2", "cancelled", TRUE) == CONVERSATION_STORE_OK); p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_Get( p_store, conversation_id, &record, p_arena) == CONVERSATION_STORE_OK); assert(strcmp(record.title, "Renamed quest") == 0); assert(Dowa_Array_Length(record.turns) == 4); assert(strcmp(record.turns[0].role, "user") == 0); assert(strcmp(record.turns[0].content, "Hello Epi") == 0); assert(strcmp(record.turns[1].role, "assistant") == 0); assert(strcmp(record.turns[1].content, "Hello traveler") == 0); assert(record.turns[1].input_tokens == 12); assert(record.turns[1].output_tokens == 4); assert(strcmp(record.turns[3].status, "aborted") == 0); Dowa_Arena_Free(p_arena); assert(Conversation_Store_Begin_Turn( p_store, conversation_id, "request-3", "Interrupted") == CONVERSATION_STORE_OK); Conversation_Store_Destroy(p_store); p_store = Conversation_Store_Create(database_path); assert(p_store); assert(Conversation_Store_Begin_Turn( p_store, conversation_id, "request-4", "Recovered") == CONVERSATION_STORE_OK); assert(Conversation_Store_Complete_Turn( p_store, conversation_id, "request-4", "Recovered answer", 1, 1) == CONVERSATION_STORE_OK); p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_Get( p_store, conversation_id, &record, p_arena) == CONVERSATION_STORE_OK); assert(Dowa_Array_Length(record.turns) == 8); assert(strcmp(record.turns[5].status, "failed") == 0); assert(strcmp(record.turns[5].error_message, "Interrupted by server restart") == 0); Dowa_Arena_Free(p_arena); assert(Conversation_Store_Delete(p_store, conversation_id) == CONVERSATION_STORE_OK); p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_Get(p_store, conversation_id, &record, p_arena) == CONVERSATION_STORE_NOT_FOUND); Dowa_Arena_Free(p_arena); Conversation_Store_Destroy(p_store); printf(" legacy API tests PASS\n"); } /* ------------------------------------------------------------------ */ /* Migration test: create a DB with old schema (no owner cols), then */ /* re-open and verify migration ran, all rows preserved as legacy. */ /* ------------------------------------------------------------------ */ static void test_migration_from_old_schema(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); /* Simulate an old-schema DB by inserting a row directly */ Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); /* Create a conversation using the legacy API (will be owner_kind='legacy') */ char legacy_id[37]; assert(Conversation_Store_Create_Conversation( p_store, "Old conversation", legacy_id) == CONVERSATION_STORE_OK); Conversation_Store_Destroy(p_store); /* Re-open: migration should be idempotent */ p_store = Conversation_Store_Create(db_path); assert(p_store); /* Legacy row must still be accessible via legacy API */ Dowa_Arena *p_arena = Dowa_Arena_Create(8192); Conversation_Record record; assert(Conversation_Store_Get( p_store, legacy_id, &record, p_arena) == CONVERSATION_STORE_OK); assert(strcmp(record.title, "Old conversation") == 0); Dowa_Arena_Free(p_arena); /* Owner-aware GET must return NOT_FOUND for a user trying to access a legacy row */ Conversation_Owner user_owner = {CONVERSATION_OWNER_KIND_USER, "user-uuid-abc-0000000000000000000000"}; p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_Get_Owned( p_store, legacy_id, &user_owner, &record, p_arena) == CONVERSATION_STORE_NOT_FOUND); Dowa_Arena_Free(p_arena); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" migration from old schema PASS\n"); } /* ------------------------------------------------------------------ */ /* Owned CRUD and isolation tests */ /* ------------------------------------------------------------------ */ static void test_owned_crud_and_isolation(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); Conversation_Owner user_a = {CONVERSATION_OWNER_KIND_USER, "aaaaaaaa-0000-0000-0000-000000000001"}; Conversation_Owner user_b = {CONVERSATION_OWNER_KIND_USER, "bbbbbbbb-0000-0000-0000-000000000002"}; Conversation_Owner guest_c = {CONVERSATION_OWNER_KIND_GUEST, "cccccccc-0000-0000-0000-000000000003"}; char id_a[37], id_b[37], id_c[37]; assert(Conversation_Store_Create_Owned(p_store, "A's chat", &user_a, id_a) == CONVERSATION_STORE_OK); assert(Conversation_Store_Create_Owned(p_store, "B's chat", &user_b, id_b) == CONVERSATION_STORE_OK); assert(Conversation_Store_Create_Owned(p_store, "Guest C", &guest_c, id_c) == CONVERSATION_STORE_OK); /* Owner A can get their own */ Dowa_Arena *p_arena = Dowa_Arena_Create(16 * 1024); Conversation_Record record; assert(Conversation_Store_Get_Owned( p_store, id_a, &user_a, &record, p_arena) == CONVERSATION_STORE_OK); assert(strcmp(record.title, "A's chat") == 0); Dowa_Arena_Free(p_arena); /* Owner B cannot get A's conversation (returns NOT_FOUND, no leak) */ p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_Get_Owned( p_store, id_a, &user_b, &record, p_arena) == CONVERSATION_STORE_NOT_FOUND); Dowa_Arena_Free(p_arena); /* Guest C cannot get user A's conversation */ p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_Get_Owned( p_store, id_a, &guest_c, &record, p_arena) == CONVERSATION_STORE_NOT_FOUND); Dowa_Arena_Free(p_arena); /* A can update their own title */ assert(Conversation_Store_Update_Title_Owned( p_store, id_a, &user_a, "A's renamed chat") == CONVERSATION_STORE_OK); /* B cannot update A's title */ assert(Conversation_Store_Update_Title_Owned( p_store, id_a, &user_b, "B tries to rename") == CONVERSATION_STORE_NOT_FOUND); /* A can begin a turn */ assert(Conversation_Store_Begin_Turn_Owned( p_store, id_a, &user_a, "req-a-1", "Hello from A") == CONVERSATION_STORE_OK); /* B cannot begin a turn on A's conversation */ assert(Conversation_Store_Begin_Turn_Owned( p_store, id_a, &user_b, "req-b-1", "B intrudes") == CONVERSATION_STORE_NOT_FOUND); /* Complete the turn */ assert(Conversation_Store_Complete_Turn( p_store, id_a, "req-a-1", "Response for A", 5, 2) == CONVERSATION_STORE_OK); /* A can delete their own */ assert(Conversation_Store_Delete_Owned(p_store, id_a, &user_a) == CONVERSATION_STORE_OK); /* B cannot delete A's (now gone) conversation */ assert(Conversation_Store_Delete_Owned(p_store, id_a, &user_b) == CONVERSATION_STORE_NOT_FOUND); /* Legacy creation is rejected */ Conversation_Owner bad_legacy = {CONVERSATION_OWNER_KIND_LEGACY, ""}; char throwaway[37]; assert(Conversation_Store_Create_Owned( p_store, "Bad", &bad_legacy, throwaway) == CONVERSATION_STORE_ERROR); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" owned CRUD and isolation PASS\n"); } /* ------------------------------------------------------------------ */ /* Paginated listing and cursor tests */ /* ------------------------------------------------------------------ */ static void test_list_and_cursor(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); Conversation_Owner owner = {CONVERSATION_OWNER_KIND_USER, "11111111-0000-0000-0000-000000000001"}; Conversation_Owner other = {CONVERSATION_OWNER_KIND_USER, "22222222-0000-0000-0000-000000000002"}; /* Create 5 conversations for owner, 1 for other */ char ids[5][37]; for (int i = 0; i < 5; i++) assert(Conversation_Store_Create_Owned(p_store, "Chat", &owner, ids[i]) == CONVERSATION_STORE_OK); char other_id[37]; assert(Conversation_Store_Create_Owned(p_store, "Other chat", &other, other_id) == CONVERSATION_STORE_OK); /* List without cursor, limit 3 */ Dowa_Arena *p_arena = Dowa_Arena_Create(32 * 1024); Conversation_Summary *summaries = NULL; int32 count = 0; assert(Conversation_Store_List(p_store, &owner, 0, NULL, 3, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 3); Dowa_Arena_Free(p_arena); /* List all 5 with limit 10 */ p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_List(p_store, &owner, 0, NULL, 10, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 5); /* Use cursor from end of first page */ Conversation_Summary *last_first_page = NULL; Dowa_Arena_Free(p_arena); p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_List(p_store, &owner, 0, NULL, 3, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 3); last_first_page = &summaries[2]; int64 cur_ts = last_first_page->updated_at; char cur_id[37]; strncpy(cur_id, last_first_page->id, sizeof(cur_id) - 1); cur_id[36] = '\0'; Dowa_Arena_Free(p_arena); p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_List(p_store, &owner, cur_ts, cur_id, 10, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 2); Dowa_Arena_Free(p_arena); /* Other owner's conversation is NOT in owner's list */ p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_List(p_store, &other, 0, NULL, 10, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 1); assert(strcmp(summaries[0].id, other_id) == 0); Dowa_Arena_Free(p_arena); /* Legacy owner returns empty list */ Conversation_Owner legacy_owner = {CONVERSATION_OWNER_KIND_LEGACY, ""}; p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_List(p_store, &legacy_owner, 0, NULL, 10, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 0); Dowa_Arena_Free(p_arena); /* Limit is capped at 50 */ p_arena = Dowa_Arena_Create(32 * 1024); assert(Conversation_Store_List(p_store, &owner, 0, NULL, 200, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); assert(count == 5); /* only 5 exist, so < 50 */ Dowa_Arena_Free(p_arena); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" list and cursor pagination PASS\n"); } /* ------------------------------------------------------------------ */ /* Legacy claim tests */ /* ------------------------------------------------------------------ */ static void test_claim_legacy(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); /* Create a legacy row via the legacy API */ char legacy_id[37]; assert(Conversation_Store_Create_Conversation( p_store, "Unclaimed adventure", legacy_id) == CONVERSATION_STORE_OK); /* Claim as user */ const char *user_id = "33333333-0000-0000-0000-000000000099"; assert(Conversation_Store_Claim_Legacy(p_store, legacy_id, user_id) == CONVERSATION_STORE_OK); /* Now accessible via owned API */ Dowa_Arena *p_arena = Dowa_Arena_Create(8192); Conversation_Record record; Conversation_Owner claimed_owner = {CONVERSATION_OWNER_KIND_USER, {0}}; strncpy(claimed_owner.id, user_id, sizeof(claimed_owner.id) - 1); assert(Conversation_Store_Get_Owned( p_store, legacy_id, &claimed_owner, &record, p_arena) == CONVERSATION_STORE_OK); assert(strcmp(record.title, "Unclaimed adventure") == 0); Dowa_Arena_Free(p_arena); /* Cannot claim again (CONFLICT: already user-owned) */ assert(Conversation_Store_Claim_Legacy(p_store, legacy_id, user_id) == CONVERSATION_STORE_CONFLICT); /* Non-existent conversation returns NOT_FOUND */ assert(Conversation_Store_Claim_Legacy( p_store, "00000000-0000-0000-0000-000000000000", user_id) == CONVERSATION_STORE_NOT_FOUND); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" legacy claim PASS\n"); } /* ------------------------------------------------------------------ */ /* Atomic/idempotent guest-to-user transfer test */ /* ------------------------------------------------------------------ */ static void test_guest_transfer(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); const char *guest_id = "44444444-0000-0000-0000-000000000001"; const char *user_id = "55555555-0000-0000-0000-000000000001"; Conversation_Owner guest_owner = {CONVERSATION_OWNER_KIND_GUEST, {0}}; Conversation_Owner user_owner = {CONVERSATION_OWNER_KIND_USER, {0}}; strncpy(guest_owner.id, guest_id, 36); strncpy(user_owner.id, user_id, 36); /* Create 3 guest conversations */ char gid1[37], gid2[37], gid3[37]; assert(Conversation_Store_Create_Owned(p_store, "Guest chat 1", &guest_owner, gid1) == CONVERSATION_STORE_OK); assert(Conversation_Store_Create_Owned(p_store, "Guest chat 2", &guest_owner, gid2) == CONVERSATION_STORE_OK); assert(Conversation_Store_Create_Owned(p_store, "Guest chat 3", &guest_owner, gid3) == CONVERSATION_STORE_OK); /* Transfer guest to user */ assert(Conversation_Store_Transfer_Guest_To_User(p_store, guest_id, user_id) == CONVERSATION_STORE_OK); /* All three now belong to user */ Dowa_Arena *p_arena = Dowa_Arena_Create(8192); Conversation_Record record; assert(Conversation_Store_Get_Owned(p_store, gid1, &user_owner, &record, p_arena) == CONVERSATION_STORE_OK); Dowa_Arena_Free(p_arena); p_arena = Dowa_Arena_Create(8192); assert(Conversation_Store_Get_Owned(p_store, gid2, &user_owner, &record, p_arena) == CONVERSATION_STORE_OK); Dowa_Arena_Free(p_arena); p_arena = Dowa_Arena_Create(8192); assert(Conversation_Store_Get_Owned(p_store, gid3, &user_owner, &record, p_arena) == CONVERSATION_STORE_OK); Dowa_Arena_Free(p_arena); /* Guest can no longer see them */ p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_Get_Owned(p_store, gid1, &guest_owner, &record, p_arena) == CONVERSATION_STORE_NOT_FOUND); Dowa_Arena_Free(p_arena); /* Idempotent: running again succeeds (no-op) */ assert(Conversation_Store_Transfer_Guest_To_User(p_store, guest_id, user_id) == CONVERSATION_STORE_OK); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" atomic/idempotent guest transfer PASS\n"); } /* ------------------------------------------------------------------ */ /* Transfer-then-stale-guest-create: race-safety test */ /* ------------------------------------------------------------------ */ static void test_transfer_stale_guest_create(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); const char *guest_id = "66666666-0000-0000-0000-000000000001"; const char *user_id = "77777777-0000-0000-0000-000000000001"; Conversation_Owner guest_owner = {CONVERSATION_OWNER_KIND_GUEST, {0}}; Conversation_Owner user_owner = {CONVERSATION_OWNER_KIND_USER, {0}}; strncpy(guest_owner.id, guest_id, 36); strncpy(user_owner.id, user_id, 36); /* Create one conversation as guest before transfer */ char pre_transfer_id[37]; assert(Conversation_Store_Create_Owned(p_store, "Pre-transfer", &guest_owner, pre_transfer_id) == CONVERSATION_STORE_OK); /* Transfer guest to user (records mapping) */ assert(Conversation_Store_Transfer_Guest_To_User(p_store, guest_id, user_id) == CONVERSATION_STORE_OK); /* Stale guest create: arrives after transfer (simulates race) */ char stale_id[37]; assert(Conversation_Store_Create_Owned(p_store, "Stale guest create", &guest_owner, stale_id) == CONVERSATION_STORE_OK); /* The stale create must be owned by the mapped user, not the guest */ Dowa_Arena *p_arena = Dowa_Arena_Create(8192); Conversation_Record record; /* User can see the stale-created conversation */ assert(Conversation_Store_Get_Owned(p_store, stale_id, &user_owner, &record, p_arena) == CONVERSATION_STORE_OK); assert(strcmp(record.title, "Stale guest create") == 0); Dowa_Arena_Free(p_arena); /* Guest can no longer see the stale-created conversation */ p_arena = Dowa_Arena_Create(4096); assert(Conversation_Store_Get_Owned(p_store, stale_id, &guest_owner, &record, p_arena) == CONVERSATION_STORE_NOT_FOUND); Dowa_Arena_Free(p_arena); /* Pre-transfer conversation also belongs to user */ p_arena = Dowa_Arena_Create(8192); assert(Conversation_Store_Get_Owned(p_store, pre_transfer_id, &user_owner, &record, p_arena) == CONVERSATION_STORE_OK); Dowa_Arena_Free(p_arena); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" transfer then stale guest create → owned by user PASS\n"); } /* ------------------------------------------------------------------ */ /* Conflicting transfer mapping test */ /* ------------------------------------------------------------------ */ static void test_transfer_conflict(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); const char *guest_id = "88888888-0000-0000-0000-000000000001"; const char *user_a_id = "aaaaaaaa-1111-0000-0000-000000000001"; const char *user_b_id = "bbbbbbbb-2222-0000-0000-000000000001"; /* First transfer: guest → user A */ assert(Conversation_Store_Transfer_Guest_To_User(p_store, guest_id, user_a_id) == CONVERSATION_STORE_OK); /* Idempotent: same mapping again must succeed */ assert(Conversation_Store_Transfer_Guest_To_User(p_store, guest_id, user_a_id) == CONVERSATION_STORE_OK); /* Conflicting transfer: guest → user B must fail with CONFLICT */ assert(Conversation_Store_Transfer_Guest_To_User(p_store, guest_id, user_b_id) == CONVERSATION_STORE_CONFLICT); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" conflicting transfer mapping returns CONFLICT PASS\n"); } /* ------------------------------------------------------------------ */ /* Index DESC migration test */ /* ------------------------------------------------------------------ */ static void test_index_desc_migration(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); /* Open twice to verify migration is idempotent */ Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); Conversation_Store_Destroy(p_store); /* Re-open: migrations must be idempotent */ p_store = Conversation_Store_Create(db_path); assert(p_store); /* Verify the listing still works (index is usable) after migration 3 */ Conversation_Owner owner = {CONVERSATION_OWNER_KIND_USER, "cccccccc-3333-0000-0000-000000000001"}; char id1[37], id2[37]; assert(Conversation_Store_Create_Owned(p_store, "Alpha", &owner, id1) == CONVERSATION_STORE_OK); assert(Conversation_Store_Create_Owned(p_store, "Beta", &owner, id2) == CONVERSATION_STORE_OK); Dowa_Arena *p_arena = Dowa_Arena_Create(32 * 1024); Conversation_Summary *summaries = NULL; int32 count = 0; assert(Conversation_Store_List(p_store, &owner, 0, NULL, 10, &summaries, &count, p_arena) == CONVERSATION_STORE_OK); /* Both conversations present */ assert(count == 2); /* Ordering must be stable: both IDs must appear */ assert( (strcmp(summaries[0].id, id1) == 0 || strcmp(summaries[0].id, id2) == 0) && (strcmp(summaries[1].id, id1) == 0 || strcmp(summaries[1].id, id2) == 0) && strcmp(summaries[0].id, summaries[1].id) != 0); Dowa_Arena_Free(p_arena); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" index DESC migration applied and listing order correct PASS\n"); } /* ------------------------------------------------------------------ */ /* Transfer_Atomic: conversations + quota cleared in one transaction */ /* ------------------------------------------------------------------ */ /* * We exercise Transfer_Guest_To_User_Atomic by inserting synthetic quota * rows directly via a secondary connection (since the conversation store * and auth store share the same SQLite file) and verifying that after the * atomic transfer the reservation rows are gone and output_tokens_reserved * is decremented. */ static void test_transfer_atomic_quota_cleanup(void) { char db_path[64]; make_temp_db(db_path, sizeof(db_path)); Conversation_Store *p_store = Conversation_Store_Create(db_path); assert(p_store); /* Seed guest identity, usage, and reservation rows via a raw connection * to simulate what the auth store would have written (in production both * stores share the same SQLite file). */ Deita_Connection *p_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_conn); Deita_Query_Execute_Update(p_conn, "PRAGMA foreign_keys = OFF;" "PRAGMA journal_mode = WAL;"); /* Create auth tables that share the same file in production. */ Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS guest_identities (" " id TEXT PRIMARY KEY," " ip_binding_digest TEXT NOT NULL," " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " expires_at INTEGER NOT NULL" ");"); Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS guest_usage (" " guest_id TEXT NOT NULL," " window_start INTEGER NOT NULL," " count INTEGER NOT NULL DEFAULT 0," " turns_used INTEGER NOT NULL DEFAULT 0," " output_tokens_used INTEGER NOT NULL DEFAULT 0," " output_tokens_reserved INTEGER NOT NULL DEFAULT 0," " PRIMARY KEY (guest_id, window_start)" ");"); Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS guest_usage_reservations (" " request_id TEXT PRIMARY KEY," " guest_id TEXT NOT NULL," " window_start INTEGER NOT NULL," " output_tokens_reserved INTEGER NOT NULL DEFAULT 0," " reserved_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " expires_at INTEGER NOT NULL" ");"); const char *gid = "aaaabbbb-cccc-4000-8000-111111111111"; const char *uid = "ddddeeee-ffff-4000-8000-222222222222"; const char *rid1 = "rrrr1111-0000-4000-8000-000000000001"; const char *rid2 = "rrrr2222-0000-4000-8000-000000000002"; /* guest_identities */ const char *gid_p[] = {gid}; Deita_Query_Execute_Update_Prepared(p_conn, "INSERT OR IGNORE INTO guest_identities " "(id, ip_binding_digest, expires_at) VALUES (?, 'x', 9999999999)", 1, gid_p); /* guest_usage: 2 turns used, 300 tokens reserved */ const char *gu_p[] = {gid}; Deita_Query_Execute_Update_Prepared(p_conn, "INSERT OR REPLACE INTO guest_usage " "(guest_id, window_start, count, turns_used, output_tokens_used, " " output_tokens_reserved) VALUES (?, 1700524800, 2, 2, 0, 300)", 1, gu_p); /* Two reservation rows (100 + 200 = 300 total) */ const char *r1_p[] = {rid1, gid}; Deita_Query_Execute_Update_Prepared(p_conn, "INSERT OR REPLACE INTO guest_usage_reservations " "(request_id, guest_id, window_start, output_tokens_reserved, expires_at) " "VALUES (?, ?, 1700524800, 100, 9999999999)", 2, r1_p); const char *r2_p[] = {rid2, gid}; Deita_Query_Execute_Update_Prepared(p_conn, "INSERT OR REPLACE INTO guest_usage_reservations " "(request_id, guest_id, window_start, output_tokens_reserved, expires_at) " "VALUES (?, ?, 1700524800, 200, 9999999999)", 2, r2_p); Deita_Connection_Close(p_conn); /* Create one guest conversation */ Conversation_Owner g_owner = {CONVERSATION_OWNER_KIND_GUEST, ""}; strncpy(g_owner.id, gid, 36); char conv_id[37]; assert(Conversation_Store_Create_Owned(p_store, "G conv", &g_owner, conv_id) == CONVERSATION_STORE_OK); /* Atomic transfer */ assert(Conversation_Store_Transfer_Guest_To_User_Atomic( p_store, gid, uid) == CONVERSATION_STORE_OK); /* Verify: conversation now belongs to user */ Conversation_Owner u_owner = {CONVERSATION_OWNER_KIND_USER, ""}; strncpy(u_owner.id, uid, 36); Dowa_Arena *p_arena = Dowa_Arena_Create(8 * 1024); Conversation_Record rec; assert(Conversation_Store_Get_Owned( p_store, conv_id, &u_owner, &rec, p_arena) == CONVERSATION_STORE_OK); Dowa_Arena_Free(p_arena); /* Verify: quota reservation rows deleted and output_tokens_reserved = 0 */ Dowa_Arena *chk_arena = Dowa_Arena_Create(4096); Deita_Connection *p_chk = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_chk); Deita_Query_Execute_Update(p_chk, "PRAGMA journal_mode = WAL;"); const char *chk_gid[] = {gid}; Deita_Result_Set *rs = Deita_Query_Execute_Prepared( p_chk, "SELECT output_tokens_reserved FROM guest_usage WHERE guest_id = ?", 1, chk_gid, chk_arena); assert(rs && Deita_Result_Set_Next(rs)); assert(Deita_Result_Set_Get_Integer(rs, 0) == 0); Deita_Result_Set_Free(rs); rs = Deita_Query_Execute_Prepared( p_chk, "SELECT COUNT(*) FROM guest_usage_reservations WHERE guest_id = ?", 1, chk_gid, chk_arena); assert(rs && Deita_Result_Set_Next(rs)); assert(Deita_Result_Set_Get_Integer(rs, 0) == 0); Deita_Result_Set_Free(rs); Deita_Connection_Close(p_chk); Dowa_Arena_Free(chk_arena); /* Idempotent: re-run same transfer is a no-op */ assert(Conversation_Store_Transfer_Guest_To_User_Atomic( p_store, gid, uid) == CONVERSATION_STORE_OK); Conversation_Store_Destroy(p_store); unlink(db_path); printf(" transfer_atomic_quota_cleanup PASS\n"); } int main(void) { printf("conversation_store tests:\n"); char legacy_db[64]; make_temp_db(legacy_db, sizeof(legacy_db)); test_legacy_api(legacy_db); unlink(legacy_db); test_migration_from_old_schema(); test_owned_crud_and_isolation(); test_list_and_cursor(); test_claim_legacy(); test_guest_transfer(); test_transfer_stale_guest_create(); test_transfer_conflict(); test_index_desc_migration(); test_transfer_atomic_quota_cleanup(); printf("All conversation store tests passed\n"); return 0; }