282 lines
14 KiB
C
282 lines
14 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_programs.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;
|
|
}
|
|
|
|
static bool completed_program_execution_deletion_is_atomic_and_idempotent(void) {
|
|
char path[] = "/tmp/trainlog-web-delete-program-execution-XXXXXX";
|
|
TrainlogDatabase *database = NULL;
|
|
char *response = NULL;
|
|
char *replayed = NULL;
|
|
char *program_detail = NULL;
|
|
size_t response_size = 0U;
|
|
size_t replayed_size = 0U;
|
|
size_t program_detail_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 programs(program_id,title,state,created_at,updated_at,"
|
|
"revision_id,source_format,source_version,source_payload_sha256) VALUES("
|
|
"'pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa','Disposable Program','active',"
|
|
"'2026-09-18T10:00:00Z','2026-09-18T10:00:00Z','pgr_fixture',"
|
|
"'trainlog-program',1,"
|
|
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');"
|
|
"INSERT INTO program_sessions(program_session_id,program_id,position,title,"
|
|
"session_type) VALUES("
|
|
"'pgs_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',"
|
|
"'pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',0,'Executed','training'),("
|
|
"'pgs_cccccccc-cccc-4ccc-8ccc-cccccccccccc',"
|
|
"'pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',1,'Untouched','training');"
|
|
"INSERT INTO sessions(session_id,started_at,ended_at,session_type) VALUES("
|
|
"'se_dddddddd-dddd-4ddd-8ddd-dddddddddddd','2026-09-18T10:00:00Z',"
|
|
"'2026-09-18T11:00:00Z','training'),("
|
|
"'se_eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee','2026-09-17T10:00:00Z',"
|
|
"'2026-09-17T11:00:00Z','training');"
|
|
"INSERT INTO sync_causal_state VALUES("
|
|
"'session','se_dddddddd-dddd-4ddd-8ddd-dddddddddddd','lv_fixture',0,NULL);"
|
|
"INSERT INTO program_session_executions VALUES("
|
|
"'pgs_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',"
|
|
"'pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',"
|
|
"'se_dddddddd-dddd-4ddd-8ddd-dddddddddddd','completed',"
|
|
"'2026-09-18T11:00:00Z');",
|
|
NULL,
|
|
NULL,
|
|
NULL) == SQLITE_OK);
|
|
CHECK(trainlog_web_session_delete_json(database,
|
|
"history",
|
|
"se_dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
"lv_fixture",
|
|
"request-delete-program-history",
|
|
&response,
|
|
&response_size) == TRAINLOG_STATUS_OK);
|
|
CHECK(response != NULL && strstr(response, "\"state\":\"deleted\"") != NULL);
|
|
CHECK(scalar(database,
|
|
"SELECT COUNT(*) FROM sessions WHERE "
|
|
"session_id='se_dddddddd-dddd-4ddd-8ddd-dddddddddddd'") == 0);
|
|
CHECK(scalar(database,
|
|
"SELECT COUNT(*) FROM sessions WHERE "
|
|
"session_id='se_eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'") == 1);
|
|
CHECK(scalar(database, "SELECT COUNT(*) FROM programs") == 1);
|
|
CHECK(scalar(database, "SELECT COUNT(*) FROM program_sessions") == 2);
|
|
CHECK(scalar(database,
|
|
"SELECT COUNT(*) FROM program_session_executions WHERE state='deleted' AND "
|
|
"session_id='se_dddddddd-dddd-4ddd-8ddd-dddddddddddd' AND "
|
|
"observed_at='2026-09-18T11:00:00Z'") == 1);
|
|
CHECK(scalar(database,
|
|
"SELECT COUNT(*) FROM sync_causal_operations WHERE target_kind='session' AND "
|
|
"target_id='se_dddddddd-dddd-4ddd-8ddd-dddddddddddd'") == 1);
|
|
CHECK(scalar(database,
|
|
"SELECT COUNT(*) FROM sync_causal_state WHERE target_kind='session' AND "
|
|
"target_id='se_dddddddd-dddd-4ddd-8ddd-dddddddddddd' AND deleted=1") == 1);
|
|
CHECK(trainlog_web_programs_detail_json(database,
|
|
"pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
&program_detail,
|
|
&program_detail_size) == TRAINLOG_STATUS_OK);
|
|
CHECK(program_detail != NULL && program_detail_size > 0U &&
|
|
strstr(program_detail, "\"execution_state\":\"deleted\"") != NULL &&
|
|
strstr(program_detail, "\"execution_session_id\":null") != NULL);
|
|
CHECK(trainlog_web_session_delete_json(database,
|
|
"history",
|
|
"se_dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
"lv_fixture",
|
|
"request-delete-program-history-replay",
|
|
&replayed,
|
|
&replayed_size) == TRAINLOG_STATUS_OK);
|
|
CHECK(replayed != NULL && response_size == replayed_size && strcmp(response, replayed) == 0);
|
|
CHECK(scalar(database,
|
|
"SELECT COUNT(*) FROM sync_causal_operations WHERE target_kind='session' AND "
|
|
"target_id='se_dddddddd-dddd-4ddd-8ddd-dddddddddddd'") == 1);
|
|
CHECK(scalar(database, "SELECT COUNT(*) FROM web_session_deletion_requests") == 1);
|
|
free(program_detail);
|
|
free(replayed);
|
|
free(response);
|
|
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() &&
|
|
completed_program_execution_deletion_is_atomic_and_idempotent()
|
|
? EXIT_SUCCESS
|
|
: EXIT_FAILURE;
|
|
}
|