trainlog/tui/tests/test_web_session_deletions.c

185 lines
8.7 KiB
C

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sqlite3.h>
#include "database_internal.h"
#include "trainlog/database.h"
#include "trainlog/status.h"
#include "trainlog/web_session_deletions.h"
#define CHECK(condition) \
do { \
if (!(condition)) { \
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \
return false; \
} \
} while (0)
static sqlite3_int64 scalar(TrainlogDatabase *database, const char *sql) {
sqlite3_stmt *statement = NULL;
sqlite3_int64 value = -1;
if (sqlite3_prepare_v2(database->connection, sql, -1, &statement, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW) {
value = sqlite3_column_int64(statement, 0);
}
(void)sqlite3_finalize(statement);
return value;
}
static bool proposal_deletion_is_durable_and_preserves_derivatives(void) {
char path[] = "/tmp/trainlog-web-delete-proposal-XXXXXX";
TrainlogDatabase *database = NULL;
char *response = NULL;
char *replayed = NULL;
size_t response_size = 0U;
size_t replayed_size = 0U;
int descriptor = mkstemp(path);
CHECK(descriptor >= 0);
CHECK(close(descriptor) == 0);
CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK);
CHECK(sqlite3_exec(database->connection,
"INSERT INTO ai_session_drafts("
"draft_id,created_at,session_type,title,archive_status,published_at) "
"VALUES('aid_11111111-1111-4111-8111-111111111111',"
"'2026-09-18T10:00:00Z','training','Proposal','archived',"
"'2026-09-18T11:00:00Z');"
"INSERT INTO ai_session_draft_imports VALUES("
"last_insert_rowid(),'aid_11111111-1111-4111-8111-111111111111',"
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',"
"'2026-09-18T10:00:00Z');"
"INSERT INTO session_preparations VALUES("
"'sp_22222222-2222-4222-8222-222222222222','spr_source',"
"'2026-09-18T10:00:00Z','2026-09-18T10:00:00Z','draft','local',"
"'aid_11111111-1111-4111-8111-111111111111',"
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',"
"NULL,NULL,NULL);",
NULL,
NULL,
NULL) == SQLITE_OK);
CHECK(trainlog_web_session_delete_json(
database,
"proposal",
"aid_11111111-1111-4111-8111-111111111111",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"request-delete-proposal",
&response,
&response_size) == TRAINLOG_STATUS_OK);
CHECK(response != NULL && strstr(response, "\"state\":\"deleted\"") != NULL);
CHECK(trainlog_web_session_delete_json(
database,
"proposal",
"aid_11111111-1111-4111-8111-111111111111",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"request-delete-proposal",
&replayed,
&replayed_size) == TRAINLOG_STATUS_OK);
CHECK(response_size == replayed_size && strcmp(response, replayed) == 0);
CHECK(scalar(database,
"SELECT COUNT(*) FROM ai_session_drafts WHERE withdrawn_at IS NOT NULL") == 1);
CHECK(scalar(database, "SELECT COUNT(*) FROM session_preparations") == 1);
CHECK(scalar(database, "SELECT COUNT(*) FROM web_session_deletion_requests") == 1);
free(replayed);
free(response);
trainlog_database_close(database);
CHECK(unlink(path) == 0);
return true;
}
static bool draft_and_history_use_causal_deletion(void) {
char path[] = "/tmp/trainlog-web-delete-causal-XXXXXX";
TrainlogDatabase *database = NULL;
char *response = NULL;
size_t response_size = 0U;
int descriptor = mkstemp(path);
CHECK(descriptor >= 0);
CHECK(close(descriptor) == 0);
CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK);
CHECK(sqlite3_exec(database->connection,
"INSERT INTO execution_drafts VALUES("
"'se_33333333-3333-4333-8333-333333333333','training',NULL,"
"'2026-09-18T10:00:00Z','mu_draft',NULL,'pending','{}');"
"INSERT INTO sessions(session_id,started_at,ended_at,session_type) VALUES("
"'se_44444444-4444-4444-8444-444444444444',"
"'2026-09-17T10:00:00Z','2026-09-17T11:00:00Z','training');"
"INSERT INTO sync_causal_state VALUES("
"'session','se_44444444-4444-4444-8444-444444444444',"
"'mu_history',0,NULL);",
NULL,
NULL,
NULL) == SQLITE_OK);
CHECK(trainlog_web_session_delete_json(database,
"draft",
"se_33333333-3333-4333-8333-333333333333",
"mu_draft",
"request-delete-draft",
&response,
&response_size) == TRAINLOG_STATUS_OK);
free(response);
response = NULL;
CHECK(trainlog_web_session_delete_json(database,
"history",
"se_44444444-4444-4444-8444-444444444444",
"mu_history",
"request-delete-history",
&response,
&response_size) == TRAINLOG_STATUS_OK);
CHECK(scalar(database, "SELECT COUNT(*) FROM execution_drafts") == 0);
CHECK(scalar(database, "SELECT COUNT(*) FROM sessions") == 0);
CHECK(scalar(database, "SELECT COUNT(*) FROM sync_causal_operations") == 2);
CHECK(scalar(database, "SELECT COUNT(*) FROM sync_causal_state WHERE deleted=1") == 2);
CHECK(scalar(database,
"SELECT COUNT(*) FROM execution_draft_finalizations "
"WHERE session_id='se_44444444-4444-4444-8444-444444444444' "
"AND final_revision_id LIKE 'deleted:del_%'") == 1);
free(response);
trainlog_database_close(database);
CHECK(unlink(path) == 0);
return true;
}
static bool active_draft_conflicts_without_mutation(void) {
char path[] = "/tmp/trainlog-web-delete-active-XXXXXX";
TrainlogDatabase *database = NULL;
char *response = NULL;
size_t response_size = 0U;
int descriptor = mkstemp(path);
CHECK(descriptor >= 0);
CHECK(close(descriptor) == 0);
CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK);
CHECK(sqlite3_exec(database->connection,
"INSERT INTO execution_drafts VALUES("
"'se_55555555-5555-4555-8555-555555555555','training',NULL,"
"'2026-09-18T10:00:00Z','mu_active',NULL,'active','{}');",
NULL,
NULL,
NULL) == SQLITE_OK);
CHECK(trainlog_web_session_delete_json(database,
"draft",
"se_55555555-5555-4555-8555-555555555555",
"mu_active",
"request-delete-active",
&response,
&response_size) == TRAINLOG_STATUS_CONFLICT);
CHECK(response == NULL && response_size == 0U);
CHECK(scalar(database, "SELECT COUNT(*) FROM execution_drafts") == 1);
CHECK(scalar(database, "SELECT COUNT(*) FROM sync_causal_operations") == 0);
trainlog_database_close(database);
CHECK(unlink(path) == 0);
return true;
}
int main(void) {
return proposal_deletion_is_durable_and_preserves_derivatives() &&
draft_and_history_use_causal_deletion() &&
active_draft_conflicts_without_mutation()
? EXIT_SUCCESS
: EXIT_FAILURE;
}