From f3a065c8ab12348da954e7b24d550ff20524c82c Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Sat, 24 Jan 2026 10:59:49 +0100 Subject: [PATCH] Add space doctor endpoint with deterministic checks and tests --- CMakeLists.txt | 27 +- README.md | 14 + src/amduatd.c | 74 +++ src/amduatd_caps.c | 61 +++ src/amduatd_caps.h | 5 + src/amduatd_space_doctor.c | 860 ++++++++++++++++++++++++++++++ src/amduatd_space_doctor.h | 60 +++ tests/test_amduatd_space_doctor.c | 465 ++++++++++++++++ 8 files changed, 1563 insertions(+), 3 deletions(-) create mode 100644 src/amduatd_space_doctor.c create mode 100644 src/amduatd_space_doctor.h create mode 100644 tests/test_amduatd_space_doctor.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 361f7ea..a57348b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ target_link_libraries(amduat_federation set(amduatd_sources src/amduatd.c src/amduatd_http.c src/amduatd_caps.c src/amduatd_space.c src/amduatd_concepts.c src/amduatd_store.c src/amduatd_derivation_index.c - src/amduatd_fed.c) + src/amduatd_fed.c src/amduatd_space_doctor.c) if(AMDUATD_ENABLE_UI) list(APPEND amduatd_sources src/amduatd_ui.c) endif() @@ -46,8 +46,8 @@ target_compile_definitions(amduatd target_link_libraries(amduatd PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs amduat_asl_store_index_fs amduat_asl_derivation_index_fs - amduat_asl amduat_enc amduat_hash_asl1 amduat_util - amduat_federation + amduat_asl_record amduat_asl amduat_enc amduat_hash_asl1 + amduat_util amduat_federation ) add_executable(amduat_pel_gc @@ -136,3 +136,24 @@ target_link_libraries(amduatd_test_fed_cfg ) add_test(NAME amduatd_fed_cfg COMMAND amduatd_test_fed_cfg) + +add_executable(amduatd_test_space_doctor + tests/test_amduatd_space_doctor.c + src/amduatd_space_doctor.c + src/amduatd_space.c + src/amduatd_store.c +) + +target_include_directories(amduatd_test_space_doctor + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include +) + +target_link_libraries(amduatd_test_space_doctor + PRIVATE amduat_asl_store_fs amduat_asl_pointer_fs amduat_asl_record + amduat_asl_store_index_fs amduat_asl amduat_enc amduat_util + amduat_hash_asl1 +) + +add_test(NAME amduatd_space_doctor COMMAND amduatd_test_space_doctor) diff --git a/README.md b/README.md index d8f4835..4a93091 100644 --- a/README.md +++ b/README.md @@ -208,11 +208,25 @@ Precedence rules: When capability tokens are used, the requested space must match the token's space (or the token must be unscoped), otherwise the request is rejected. +## Space doctor + +Check deterministic invariants for the effective space: + +```sh +curl --unix-socket amduatd.sock http://localhost/v1/space/doctor +curl --unix-socket amduatd.sock http://localhost/v1/space/doctor \ + -H 'X-Amduat-Space: demo' +``` + +When the daemon uses the `fs` store backend, index-only checks are reported as +`"skipped"`; the `index` backend runs them. + ## Current endpoints - `GET /v1/meta` → `{store_id, encoding_profile_id, hash_id, api_contract_ref}` - `GET /v1/contract` → contract bytes (JSON) (+ `X-Amduat-Contract-Ref` header) - `GET /v1/contract?format=ref` → `{ref}` +- `GET /v1/space/doctor` → deterministic space health checks - `GET /v1/ui` → browser UI for authoring/running programs - `GET /v1/fed/records?domain_id=...&from_logseq=...&limit=...` → `{domain_id, snapshot_id, log_prefix, next_logseq, records[]}` (published artifacts + tombstones + PER + TGK edges) - `GET /v1/fed/artifacts/{ref}` → raw bytes for federation resolve diff --git a/src/amduatd.c b/src/amduatd.c index 1269418..9211f6e 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -42,6 +42,7 @@ #include "amduatd_fed.h" #include "amduatd_store.h" #include "amduatd_derivation_index.h" +#include "amduatd_space_doctor.h" #include #include @@ -112,6 +113,7 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/meta\"}," "{\"method\":\"HEAD\",\"path\":\"/v1/meta\"}," "{\"method\":\"GET\",\"path\":\"/v1/contract\"}," + "{\"method\":\"GET\",\"path\":\"/v1/space/doctor\"}," "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," "{\"method\":\"GET\",\"path\":\"/v1/cap/resolve\"}," "{\"method\":\"GET\",\"path\":\"/v1/fed/records\"}," @@ -786,6 +788,67 @@ static bool amduatd_handle_get_contract(int fd, } } +static bool amduatd_handle_get_space_doctor(int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req, + const amduatd_cfg_t *dcfg, + const amduatd_fed_cfg_t *fed_cfg, + const amduatd_caps_t *caps, + const char *root_path) { + amduat_asl_pointer_store_t pointer_store; + amduatd_space_doctor_report_t report; + char *json = NULL; + bool ok = false; + int status = 200; + + if (store == NULL || req == NULL || dcfg == NULL || root_path == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + + if (caps != NULL && caps->enabled && req->x_capability[0] != '\0') { + const char *reason = NULL; + if (!amduatd_caps_check_space(caps, dcfg, req, &reason)) { + if (reason != NULL && strcmp(reason, "wrong-space") == 0) { + return amduatd_send_json_error(fd, 403, "Forbidden", + "space not permitted by capability"); + } + return amduatd_send_json_error(fd, 403, "Forbidden", + "invalid capability"); + } + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root_path)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer store error"); + } + if (!amduatd_space_doctor_run(store, + &pointer_store, + req->effective_space, + dcfg, + fed_cfg, + &report)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "doctor failed"); + } + if (!amduatd_space_doctor_report_json(&report, &json)) { + amduatd_space_doctor_report_free(&report); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + if (report.fail_count != 0u) { + status = 500; + } + ok = amduatd_http_send_json(fd, + status, + status == 200 ? "OK" : "Internal Server Error", + json, + false); + free(json); + amduatd_space_doctor_report_free(&report); + return ok; +} + static bool amduatd_seed_api_contract(amduat_asl_store_t *store, const amduat_asl_store_fs_config_t *cfg, amduat_reference_t *out_ref) { @@ -4015,6 +4078,17 @@ static bool amduatd_handle_conn(int fd, ok = amduatd_handle_get_contract(fd, store, &req, api_contract_ref); goto conn_cleanup; } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/space/doctor") == 0) { + ok = amduatd_handle_get_space_doctor(fd, + store, + &req, + effective_cfg, + fed_cfg, + caps, + root_path); + goto conn_cleanup; + } if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/fed/records") == 0) { ok = amduatd_handle_get_fed_records(fd, store, fed_cfg, &req); diff --git a/src/amduatd_caps.c b/src/amduatd_caps.c index 529c03b..fb7bc61 100644 --- a/src/amduatd_caps.c +++ b/src/amduatd_caps.c @@ -1114,6 +1114,67 @@ static bool amduatd_cap_verify_token(const amduatd_caps_t *cap, return true; } +bool amduatd_caps_check_space(const amduatd_caps_t *cap, + const amduatd_cfg_t *dcfg, + const amduatd_http_req_t *req, + const char **out_reason) { + const char *cap_header = NULL; + uint8_t *token_bytes = NULL; + size_t token_len = 0u; + amduatd_cap_token_t token; + const char *reason = "invalid"; + bool ok = false; + + if (out_reason != NULL) { + *out_reason = NULL; + } + if (cap == NULL || dcfg == NULL || req == NULL) { + if (out_reason != NULL) { + *out_reason = "internal"; + } + return false; + } + if (!cap->enabled) { + return true; + } + cap_header = req->x_capability; + if (cap_header == NULL || cap_header[0] == '\0') { + return true; + } + if (strncmp(cap_header, "cap_", 4u) != 0) { + if (out_reason != NULL) { + *out_reason = "invalid"; + } + return false; + } + if (!amduatd_b64url_decode(cap_header + 4u, &token_bytes, &token_len)) { + if (out_reason != NULL) { + *out_reason = "invalid"; + } + return false; + } + + memset(&token, 0, sizeof(token)); + if (!amduatd_cap_verify_token(cap, + dcfg, + token_bytes, + token_len, + &token, + &reason)) { + if (out_reason != NULL) { + *out_reason = reason != NULL ? reason : "invalid"; + } + free(token_bytes); + amduatd_cap_token_free(&token); + return false; + } + ok = true; + + free(token_bytes); + amduatd_cap_token_free(&token); + return ok; +} + static bool amduatd_handle_post_capabilities( int fd, amduat_asl_store_t *store, diff --git a/src/amduatd_caps.h b/src/amduatd_caps.h index 8f27729..9283236 100644 --- a/src/amduatd_caps.h +++ b/src/amduatd_caps.h @@ -44,6 +44,11 @@ bool amduatd_caps_handle(amduatd_ctx_t *ctx, const amduatd_http_req_t *req, amduatd_http_resp_t *resp); +bool amduatd_caps_check_space(const amduatd_caps_t *caps, + const amduatd_cfg_t *dcfg, + const amduatd_http_req_t *req, + const char **out_reason); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/src/amduatd_space_doctor.c b/src/amduatd_space_doctor.c new file mode 100644 index 0000000..5a8e8c3 --- /dev/null +++ b/src/amduatd_space_doctor.c @@ -0,0 +1,860 @@ +#include "amduatd_space_doctor.h" + +#include "amduat/asl/record.h" +#include "amduat/enc/asl_log.h" +#include "amduat/enc/asl1_core_codec.h" +#include +#include +#include + +typedef struct { + char *data; + size_t len; + size_t cap; +} amduatd_doctor_strbuf_t; + +static void amduatd_doctor_strbuf_free(amduatd_doctor_strbuf_t *b) { + if (b == NULL) { + return; + } + free(b->data); + b->data = NULL; + b->len = 0u; + b->cap = 0u; +} + +static bool amduatd_doctor_strbuf_reserve(amduatd_doctor_strbuf_t *b, + size_t extra) { + size_t need; + size_t next_cap; + char *next; + + if (b == NULL) { + return false; + } + if (extra == 0u) { + return true; + } + if (b->len > SIZE_MAX - extra) { + return false; + } + need = b->len + extra; + if (need <= b->cap) { + return true; + } + next_cap = b->cap != 0u ? b->cap : 256u; + while (next_cap < need) { + if (next_cap > (SIZE_MAX / 2u)) { + next_cap = need; + break; + } + next_cap *= 2u; + } + next = (char *)realloc(b->data, next_cap); + if (next == NULL) { + return false; + } + b->data = next; + b->cap = next_cap; + return true; +} + +static bool amduatd_doctor_strbuf_append(amduatd_doctor_strbuf_t *b, + const char *data, + size_t len) { + if (b == NULL) { + return false; + } + if (data == NULL) { + len = 0u; + } + if (!amduatd_doctor_strbuf_reserve(b, len + 1u)) { + return false; + } + if (len != 0u) { + memcpy(b->data + b->len, data, len); + } + b->len += len; + b->data[b->len] = '\0'; + return true; +} + +static bool amduatd_doctor_strbuf_append_cstr(amduatd_doctor_strbuf_t *b, + const char *s) { + return amduatd_doctor_strbuf_append(b, + s != NULL ? s : "", + s != NULL ? strlen(s) : 0u); +} + +static bool amduatd_doctor_strbuf_append_char(amduatd_doctor_strbuf_t *b, + char c) { + return amduatd_doctor_strbuf_append(b, &c, 1u); +} + +static char *amduatd_doctor_strdup(const char *s) { + size_t len; + char *copy; + if (s == NULL) { + return NULL; + } + len = strlen(s); + copy = (char *)malloc(len + 1u); + if (copy == NULL) { + return NULL; + } + memcpy(copy, s, len); + copy[len] = '\0'; + return copy; +} + +static const char *amduatd_space_doctor_status_name( + amduatd_space_doctor_status_t status) { + switch (status) { + case AMDUATD_DOCTOR_OK: + return "ok"; + case AMDUATD_DOCTOR_WARN: + return "warn"; + case AMDUATD_DOCTOR_FAIL: + return "fail"; + case AMDUATD_DOCTOR_SKIPPED: + return "skipped"; + default: + return "unknown"; + } +} + +static void amduatd_space_doctor_report_init( + amduatd_space_doctor_report_t *report) { + if (report == NULL) { + return; + } + memset(report, 0, sizeof(*report)); + report->backend = AMDUATD_STORE_BACKEND_FS; +} + +static void amduatd_space_doctor_report_clear( + amduatd_space_doctor_report_t *report) { + if (report == NULL) { + return; + } + for (size_t i = 0u; i < report->checks_len; ++i) { + free(report->checks[i].name); + free(report->checks[i].detail); + } + free(report->checks); + amduatd_space_doctor_report_init(report); +} + +void amduatd_space_doctor_report_free(amduatd_space_doctor_report_t *report) { + amduatd_space_doctor_report_clear(report); +} + +static bool amduatd_space_doctor_report_add( + amduatd_space_doctor_report_t *report, + const char *name, + amduatd_space_doctor_status_t status, + const char *detail) { + size_t next_len; + amduatd_space_doctor_check_t *next; + amduatd_space_doctor_check_t *entry; + + if (report == NULL || name == NULL) { + return false; + } + if (report->checks_len > SIZE_MAX - 1u) { + return false; + } + next_len = report->checks_len + 1u; + next = (amduatd_space_doctor_check_t *)realloc( + report->checks, next_len * sizeof(*next)); + if (next == NULL) { + return false; + } + report->checks = next; + entry = &report->checks[report->checks_len]; + memset(entry, 0, sizeof(*entry)); + entry->name = amduatd_doctor_strdup(name); + entry->detail = amduatd_doctor_strdup(detail != NULL ? detail : ""); + if (entry->name == NULL || entry->detail == NULL) { + free(entry->name); + free(entry->detail); + return false; + } + entry->status = status; + report->checks_len = next_len; + switch (status) { + case AMDUATD_DOCTOR_OK: + report->ok_count++; + break; + case AMDUATD_DOCTOR_WARN: + report->warn_count++; + break; + case AMDUATD_DOCTOR_FAIL: + report->fail_count++; + break; + case AMDUATD_DOCTOR_SKIPPED: + report->skipped_count++; + break; + default: + break; + } + return true; +} + +static bool amduatd_space_doctor_build_collection_head_name( + const char *name, + char **out_name) { + size_t name_len; + size_t total_len; + char *buffer; + size_t offset = 0u; + + if (name == NULL || out_name == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(name)) { + return false; + } + name_len = strlen(name); + total_len = 11u + name_len + 5u + 1u; + buffer = (char *)malloc(total_len); + if (buffer == NULL) { + return false; + } + memcpy(buffer + offset, "collection/", 11u); + offset += 11u; + memcpy(buffer + offset, name, name_len); + offset += name_len; + memcpy(buffer + offset, "/head", 5u); + offset += 5u; + buffer[offset] = '\0'; + *out_name = buffer; + return true; +} + +static bool amduatd_space_doctor_build_collection_log_head_name( + const char *name, + char **out_name) { + size_t name_len; + size_t log_name_len; + size_t total_len; + char *buffer; + size_t offset = 0u; + + if (name == NULL || out_name == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(name)) { + return false; + } + name_len = strlen(name); + log_name_len = 11u + name_len + 4u; + total_len = 4u + log_name_len + 5u + 1u; + buffer = (char *)malloc(total_len); + if (buffer == NULL) { + return false; + } + memcpy(buffer + offset, "log/", 4u); + offset += 4u; + memcpy(buffer + offset, "collection/", 11u); + offset += 11u; + memcpy(buffer + offset, name, name_len); + offset += name_len; + memcpy(buffer + offset, "/log", 4u); + offset += 4u; + memcpy(buffer + offset, "/head", 5u); + offset += 5u; + buffer[offset] = '\0'; + *out_name = buffer; + return true; +} + +enum { + AMDUATD_EDGE_INDEX_MAGIC_LEN = 8, + AMDUATD_EDGE_INDEX_VERSION = 1 +}; + +static const uint8_t k_amduatd_edge_index_magic[AMDUATD_EDGE_INDEX_MAGIC_LEN] = { + 'A', 'S', 'L', 'E', 'I', 'X', '1', '\0' +}; + +typedef struct { + bool has_graph_ref; + amduat_reference_t graph_ref; +} amduatd_edge_index_state_view_t; + +static void amduatd_edge_index_state_view_free( + amduatd_edge_index_state_view_t *state) { + if (state == NULL) { + return; + } + if (state->has_graph_ref) { + amduat_reference_free(&state->graph_ref); + } + memset(state, 0, sizeof(*state)); +} + +static bool amduatd_doctor_read_u32_le(const uint8_t *data, + size_t len, + size_t *offset, + uint32_t *out) { + if (len - *offset < 4u) { + return false; + } + *out = (uint32_t)data[*offset] | + ((uint32_t)data[*offset + 1u] << 8) | + ((uint32_t)data[*offset + 2u] << 16) | + ((uint32_t)data[*offset + 3u] << 24); + *offset += 4u; + return true; +} + +static bool amduatd_doctor_read_u64_le(const uint8_t *data, + size_t len, + size_t *offset, + uint64_t *out) { + if (len - *offset < 8u) { + return false; + } + *out = (uint64_t)data[*offset] | + ((uint64_t)data[*offset + 1u] << 8) | + ((uint64_t)data[*offset + 2u] << 16) | + ((uint64_t)data[*offset + 3u] << 24) | + ((uint64_t)data[*offset + 4u] << 32) | + ((uint64_t)data[*offset + 5u] << 40) | + ((uint64_t)data[*offset + 6u] << 48) | + ((uint64_t)data[*offset + 7u] << 56); + *offset += 8u; + return true; +} + +static bool amduatd_edge_index_state_view_decode( + amduat_octets_t payload, + amduatd_edge_index_state_view_t *out_state) { + size_t offset = 0u; + uint32_t version = 0u; + uint32_t ref_len = 0u; + amduat_octets_t ref_bytes; + uint64_t ignored_offset = 0u; + + if (out_state == NULL) { + return false; + } + memset(out_state, 0, sizeof(*out_state)); + if (payload.data == NULL || + payload.len < AMDUATD_EDGE_INDEX_MAGIC_LEN + 4u + 8u + 4u) { + return false; + } + if (memcmp(payload.data, k_amduatd_edge_index_magic, + AMDUATD_EDGE_INDEX_MAGIC_LEN) != 0) { + return false; + } + offset += AMDUATD_EDGE_INDEX_MAGIC_LEN; + if (!amduatd_doctor_read_u32_le(payload.data, payload.len, &offset, &version) || + version != AMDUATD_EDGE_INDEX_VERSION) { + return false; + } + if (!amduatd_doctor_read_u64_le(payload.data, payload.len, &offset, + &ignored_offset) || + !amduatd_doctor_read_u32_le(payload.data, payload.len, &offset, &ref_len)) { + return false; + } + (void)ignored_offset; + if (payload.len - offset < ref_len) { + return false; + } + if (ref_len != 0u) { + ref_bytes = amduat_octets(payload.data + offset, ref_len); + if (!amduat_enc_asl1_core_decode_reference_v1(ref_bytes, + &out_state->graph_ref)) { + amduatd_edge_index_state_view_free(out_state); + return false; + } + out_state->has_graph_ref = true; + offset += ref_len; + } + return offset == payload.len; +} + +static amduatd_store_backend_t amduatd_space_doctor_backend_from_store( + const amduat_asl_store_t *store) { + if (store == NULL) { + return AMDUATD_STORE_BACKEND_FS; + } + if (store->ops.log_scan != NULL && store->ops.current_state != NULL) { + return AMDUATD_STORE_BACKEND_INDEX; + } + return AMDUATD_STORE_BACKEND_FS; +} + +typedef struct { + const char *label; + char *pointer_name; + bool pointer_exists; + amduat_reference_t pointer_ref; +} amduatd_doctor_pointer_t; + +static void amduatd_space_doctor_pointer_free(amduatd_doctor_pointer_t *ptr) { + if (ptr == NULL) { + return; + } + free(ptr->pointer_name); + ptr->pointer_name = NULL; + ptr->pointer_exists = false; + amduat_reference_free(&ptr->pointer_ref); +} + +bool amduatd_space_doctor_run(amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const amduatd_cfg_t *cfg, + const amduatd_fed_cfg_t *fed_cfg, + amduatd_space_doctor_report_t *out_report) { + amduatd_doctor_pointer_t pointers[3]; + amduat_octets_t edges_collection = amduat_octets(NULL, 0u); + amduat_octets_t edges_index_head = amduat_octets(NULL, 0u); + char *collection_head = NULL; + char *collection_log_head = NULL; + bool names_ok = true; + bool ok = false; + size_t i; + + if (out_report == NULL) { + return false; + } + amduatd_space_doctor_report_init(out_report); + if (store == NULL || pointer_store == NULL || cfg == NULL) { + return false; + } + (void)cfg; + + out_report->backend = amduatd_space_doctor_backend_from_store(store); + if (effective_space != NULL && + effective_space->enabled && + effective_space->space_id.data != NULL) { + out_report->scoped = true; + snprintf(out_report->space_id, sizeof(out_report->space_id), "%s", + (const char *)effective_space->space_id.data); + } + + memset(pointers, 0, sizeof(pointers)); + pointers[0].label = "edges_index_head"; + pointers[1].label = "edges_collection_snapshot_head"; + pointers[2].label = "edges_collection_log_head"; + for (i = 0u; i < 3u; ++i) { + memset(&pointers[i].pointer_ref, 0, sizeof(pointers[i].pointer_ref)); + } + + if (!amduatd_space_edges_collection_name(effective_space, &edges_collection) || + !amduatd_space_edges_index_head_name(effective_space, &edges_index_head)) { + (void)amduatd_space_doctor_report_add(out_report, + "pointer_name_validation", + AMDUATD_DOCTOR_FAIL, + "failed to scope pointer names"); + names_ok = false; + goto doctor_after_names; + } + if (!amduatd_space_doctor_build_collection_head_name( + (const char *)edges_collection.data, &collection_head) || + !amduatd_space_doctor_build_collection_log_head_name( + (const char *)edges_collection.data, &collection_log_head)) { + (void)amduatd_space_doctor_report_add(out_report, + "pointer_name_validation", + AMDUATD_DOCTOR_FAIL, + "failed to build collection heads"); + names_ok = false; + goto doctor_after_names; + } + if (!amduat_asl_pointer_name_is_valid( + (const char *)edges_collection.data) || + !amduat_asl_pointer_name_is_valid( + (const char *)edges_index_head.data) || + !amduat_asl_pointer_name_is_valid(collection_head) || + !amduat_asl_pointer_name_is_valid(collection_log_head)) { + (void)amduatd_space_doctor_report_add(out_report, + "pointer_name_validation", + AMDUATD_DOCTOR_FAIL, + "invalid pointer name encoding"); + names_ok = false; + goto doctor_after_names; + } + (void)amduatd_space_doctor_report_add(out_report, + "pointer_name_validation", + AMDUATD_DOCTOR_OK, + "scoped pointer names valid"); + +doctor_after_names: + if (!names_ok) { + (void)amduatd_space_doctor_report_add(out_report, + "edges_index_head", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "edges_collection_snapshot_head", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "edges_collection_log_head", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "cas_edges_index_head", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "cas_edges_collection_snapshot_head", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "cas_edges_collection_log_head", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_state_parse", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_SKIPPED, + "invalid pointer names"); + goto doctor_index_checks; + } + + pointers[0].pointer_name = amduatd_doctor_strdup( + (const char *)edges_index_head.data); + pointers[1].pointer_name = amduatd_doctor_strdup(collection_head); + pointers[2].pointer_name = amduatd_doctor_strdup(collection_log_head); + if (pointers[0].pointer_name == NULL || + pointers[1].pointer_name == NULL || + pointers[2].pointer_name == NULL) { + (void)amduatd_space_doctor_report_add(out_report, + "pointer_store", + AMDUATD_DOCTOR_FAIL, + "oom"); + goto doctor_cleanup; + } + + for (i = 0u; i < 3u; ++i) { + bool exists = false; + amduat_asl_pointer_error_t perr = + amduat_asl_pointer_get(pointer_store, + pointers[i].pointer_name, + &exists, + &pointers[i].pointer_ref); + if (perr != AMDUAT_ASL_POINTER_OK) { + char detail[256]; + snprintf(detail, sizeof(detail), "pointer error: %s", + pointers[i].pointer_name); + (void)amduatd_space_doctor_report_add(out_report, + pointers[i].label, + AMDUATD_DOCTOR_FAIL, + detail); + continue; + } + pointers[i].pointer_exists = exists; + if (exists) { + char detail[256]; + snprintf(detail, sizeof(detail), "present: %s", + pointers[i].pointer_name); + (void)amduatd_space_doctor_report_add(out_report, + pointers[i].label, + AMDUATD_DOCTOR_OK, + detail); + } else { + char detail[256]; + snprintf(detail, sizeof(detail), "missing: %s", + pointers[i].pointer_name); + (void)amduatd_space_doctor_report_add(out_report, + pointers[i].label, + AMDUATD_DOCTOR_WARN, + detail); + } + } + + for (i = 0u; i < 3u; ++i) { + const char *check_name = NULL; + switch (i) { + case 0u: + check_name = "cas_edges_index_head"; + break; + case 1u: + check_name = "cas_edges_collection_snapshot_head"; + break; + case 2u: + check_name = "cas_edges_collection_log_head"; + break; + default: + check_name = "cas_pointer_ref"; + break; + } + if (!pointers[i].pointer_exists) { + (void)amduatd_space_doctor_report_add(out_report, + check_name, + AMDUATD_DOCTOR_SKIPPED, + "pointer missing"); + continue; + } + { + amduat_artifact_t artifact; + memset(&artifact, 0, sizeof(artifact)); + if (amduat_asl_store_get(store, pointers[i].pointer_ref, &artifact) != + AMDUAT_ASL_STORE_OK) { + (void)amduatd_space_doctor_report_add(out_report, + check_name, + AMDUATD_DOCTOR_FAIL, + "store get failed"); + } else { + (void)amduatd_space_doctor_report_add(out_report, + check_name, + AMDUATD_DOCTOR_OK, + "ref readable"); + } + amduat_artifact_free(&artifact); + } + } + + if (!pointers[0].pointer_exists) { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_state_parse", + AMDUATD_DOCTOR_SKIPPED, + "pointer missing"); + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_SKIPPED, + "pointer missing"); + } else { + amduat_asl_record_t record; + amduatd_edge_index_state_view_t state; + const char *schema = "tgk/edge_index_state"; + bool parsed = false; + memset(&record, 0, sizeof(record)); + memset(&state, 0, sizeof(state)); + if (amduat_asl_record_store_get(store, + pointers[0].pointer_ref, + &record) != AMDUAT_ASL_STORE_OK) { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_state_parse", + AMDUATD_DOCTOR_FAIL, + "record load failed"); + } else if (record.schema.len != strlen(schema) || + memcmp(record.schema.data, schema, record.schema.len) != 0) { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_state_parse", + AMDUATD_DOCTOR_FAIL, + "unexpected schema"); + } else if (!amduatd_edge_index_state_view_decode(record.payload, &state)) { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_state_parse", + AMDUATD_DOCTOR_FAIL, + "payload decode failed"); + } else { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_state_parse", + AMDUATD_DOCTOR_OK, + "edge index state decoded"); + parsed = true; + } + + if (parsed && state.has_graph_ref) { + amduat_artifact_t graph_artifact; + memset(&graph_artifact, 0, sizeof(graph_artifact)); + if (amduat_asl_store_get(store, state.graph_ref, &graph_artifact) != + AMDUAT_ASL_STORE_OK) { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_FAIL, + "graph ref missing"); + } else { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_OK, + "graph ref readable"); + } + amduat_artifact_free(&graph_artifact); + } else if (parsed) { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_SKIPPED, + "no graph ref"); + } else { + (void)amduatd_space_doctor_report_add(out_report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_SKIPPED, + "edge index state unreadable"); + } + + amduatd_edge_index_state_view_free(&state); + amduat_asl_record_free(&record); + } + +doctor_index_checks: + if (out_report->backend == AMDUATD_STORE_BACKEND_INDEX) { + amduat_asl_index_state_t state; + amduat_asl_log_record_t *records = NULL; + size_t record_count = 0u; + if (!amduat_asl_index_current_state(store, &state)) { + (void)amduatd_space_doctor_report_add(out_report, + "index_current_state", + AMDUATD_DOCTOR_FAIL, + "current_state failed"); + } else { + (void)amduatd_space_doctor_report_add(out_report, + "index_current_state", + AMDUATD_DOCTOR_OK, + "current_state ok"); + } + { + amduat_asl_store_error_t scan_err = + amduat_asl_log_scan(store, &records, &record_count); + if (scan_err == AMDUAT_ASL_STORE_OK) { + (void)amduatd_space_doctor_report_add(out_report, + "index_log_scan", + AMDUATD_DOCTOR_OK, + "log_scan ok"); + } else { + (void)amduatd_space_doctor_report_add(out_report, + "index_log_scan", + AMDUATD_DOCTOR_FAIL, + "log_scan failed"); + } + if (records != NULL) { + amduat_enc_asl_log_free(records, record_count); + } + } + } else { + (void)amduatd_space_doctor_report_add(out_report, + "index_current_state", + AMDUATD_DOCTOR_SKIPPED, + "requires index backend"); + (void)amduatd_space_doctor_report_add(out_report, + "index_log_scan", + AMDUATD_DOCTOR_SKIPPED, + "requires index backend"); + } + + { + char detail[256]; + bool fed_enabled = fed_cfg != NULL && fed_cfg->enabled; + bool require_index_backend = fed_enabled; + snprintf(detail, sizeof(detail), + "enabled=%s require_index_backend=%s", + fed_enabled ? "true" : "false", + require_index_backend ? "true" : "false"); + (void)amduatd_space_doctor_report_add(out_report, + "federation", + AMDUATD_DOCTOR_OK, + detail); + } + + ok = true; + +doctor_cleanup: + for (i = 0u; i < 3u; ++i) { + amduatd_space_doctor_pointer_free(&pointers[i]); + } + free((void *)edges_collection.data); + free((void *)edges_index_head.data); + free(collection_head); + free(collection_log_head); + if (!ok) { + amduatd_space_doctor_report_clear(out_report); + } + return ok; +} + +bool amduatd_space_doctor_report_json( + const amduatd_space_doctor_report_t *report, + char **out_json) { + amduatd_doctor_strbuf_t b; + char header[256]; + + if (out_json != NULL) { + *out_json = NULL; + } + if (report == NULL || out_json == NULL) { + return false; + } + + memset(&b, 0, sizeof(b)); + if (!amduatd_doctor_strbuf_append_cstr(&b, "{")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + + if (!amduatd_doctor_strbuf_append_cstr(&b, "\"effective_space\":{")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + if (report->scoped) { + if (snprintf(header, sizeof(header), + "\"mode\":\"scoped\",\"space_id\":\"%s\"", + report->space_id) <= 0 || + !amduatd_doctor_strbuf_append_cstr(&b, header)) { + amduatd_doctor_strbuf_free(&b); + return false; + } + } else { + if (!amduatd_doctor_strbuf_append_cstr(&b, "\"mode\":\"unscoped\"")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + } + if (!amduatd_doctor_strbuf_append_cstr(&b, "},")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + + if (snprintf(header, sizeof(header), + "\"store_backend\":\"%s\",", + amduatd_store_backend_name(report->backend)) <= 0 || + !amduatd_doctor_strbuf_append_cstr(&b, header)) { + amduatd_doctor_strbuf_free(&b); + return false; + } + + if (!amduatd_doctor_strbuf_append_cstr(&b, "\"checks\":[")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + for (size_t i = 0u; i < report->checks_len; ++i) { + const amduatd_space_doctor_check_t *check = &report->checks[i]; + if (i != 0u) { + (void)amduatd_doctor_strbuf_append_char(&b, ','); + } + if (!amduatd_doctor_strbuf_append_cstr(&b, "{\"name\":\"") || + !amduatd_doctor_strbuf_append_cstr(&b, check->name) || + !amduatd_doctor_strbuf_append_cstr(&b, "\",\"status\":\"") || + !amduatd_doctor_strbuf_append_cstr( + &b, + amduatd_space_doctor_status_name(check->status)) || + !amduatd_doctor_strbuf_append_cstr(&b, "\",\"detail\":\"") || + !amduatd_doctor_strbuf_append_cstr(&b, check->detail) || + !amduatd_doctor_strbuf_append_cstr(&b, "\"}")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + } + if (!amduatd_doctor_strbuf_append_cstr(&b, "],")) { + amduatd_doctor_strbuf_free(&b); + return false; + } + + if (snprintf(header, sizeof(header), + "\"summary\":{" + "\"ok_count\":%llu," + "\"warn_count\":%llu," + "\"fail_count\":%llu," + "\"skipped_count\":%llu" + "}}\n", + (unsigned long long)report->ok_count, + (unsigned long long)report->warn_count, + (unsigned long long)report->fail_count, + (unsigned long long)report->skipped_count) <= 0 || + !amduatd_doctor_strbuf_append_cstr(&b, header)) { + amduatd_doctor_strbuf_free(&b); + return false; + } + + *out_json = b.data; + return true; +} diff --git a/src/amduatd_space_doctor.h b/src/amduatd_space_doctor.h new file mode 100644 index 0000000..62eda2f --- /dev/null +++ b/src/amduatd_space_doctor.h @@ -0,0 +1,60 @@ +#ifndef AMDUATD_SPACE_DOCTOR_H +#define AMDUATD_SPACE_DOCTOR_H + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/core.h" +#include "amduat/asl/store.h" +#include "amduatd_caps.h" +#include "amduatd_fed.h" +#include "amduatd_store.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + AMDUATD_DOCTOR_OK = 0, + AMDUATD_DOCTOR_WARN = 1, + AMDUATD_DOCTOR_FAIL = 2, + AMDUATD_DOCTOR_SKIPPED = 3 +} amduatd_space_doctor_status_t; + +typedef struct { + char *name; + amduatd_space_doctor_status_t status; + char *detail; +} amduatd_space_doctor_check_t; + +typedef struct { + bool scoped; + char space_id[AMDUAT_ASL_POINTER_NAME_MAX + 1u]; + amduatd_store_backend_t backend; + amduatd_space_doctor_check_t *checks; + size_t checks_len; + size_t ok_count; + size_t warn_count; + size_t fail_count; + size_t skipped_count; +} amduatd_space_doctor_report_t; + +bool amduatd_space_doctor_run(amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const amduatd_cfg_t *cfg, + const amduatd_fed_cfg_t *fed_cfg, + amduatd_space_doctor_report_t *out_report); + +bool amduatd_space_doctor_report_json( + const amduatd_space_doctor_report_t *report, + char **out_json); + +void amduatd_space_doctor_report_free(amduatd_space_doctor_report_t *report); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUATD_SPACE_DOCTOR_H */ diff --git a/tests/test_amduatd_space_doctor.c b/tests/test_amduatd_space_doctor.c new file mode 100644 index 0000000..b20c6a0 --- /dev/null +++ b/tests/test_amduatd_space_doctor.c @@ -0,0 +1,465 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "amduatd_space_doctor.h" + +#include "amduat/asl/asl_store_fs_meta.h" +#include "amduat/asl/core.h" +#include "amduat/asl/none.h" +#include "amduat/asl/record.h" +#include "amduat/enc/asl1_core_codec.h" +#include +#include +#include + +static char *amduatd_test_make_temp_dir(void) { + char tmpl[] = "/tmp/amduatd-doctor-XXXXXX"; + char *dir = mkdtemp(tmpl); + size_t len; + char *copy; + if (dir == NULL) { + perror("mkdtemp"); + return NULL; + } + len = strlen(dir); + copy = (char *)malloc(len + 1u); + if (copy == NULL) { + fprintf(stderr, "failed to allocate temp dir copy\n"); + return NULL; + } + memcpy(copy, dir, len + 1u); + return copy; +} + +static bool amduatd_check_status(const amduatd_space_doctor_report_t *report, + const char *name, + amduatd_space_doctor_status_t expected) { + if (report == NULL || name == NULL) { + return false; + } + for (size_t i = 0u; i < report->checks_len; ++i) { + if (strcmp(report->checks[i].name, name) == 0) { + return report->checks[i].status == expected; + } + } + return false; +} + +static bool amduatd_build_collection_head_name(const char *name, + char **out_name) { + size_t name_len; + size_t total_len; + char *buffer; + size_t offset = 0u; + + if (name == NULL || out_name == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(name)) { + return false; + } + name_len = strlen(name); + total_len = 11u + name_len + 5u + 1u; + buffer = (char *)malloc(total_len); + if (buffer == NULL) { + return false; + } + memcpy(buffer + offset, "collection/", 11u); + offset += 11u; + memcpy(buffer + offset, name, name_len); + offset += name_len; + memcpy(buffer + offset, "/head", 5u); + offset += 5u; + buffer[offset] = '\0'; + *out_name = buffer; + return true; +} + +static bool amduatd_build_collection_log_head_name(const char *name, + char **out_name) { + size_t name_len; + size_t log_name_len; + size_t total_len; + char *buffer; + size_t offset = 0u; + + if (name == NULL || out_name == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(name)) { + return false; + } + name_len = strlen(name); + log_name_len = 11u + name_len + 4u; + total_len = 4u + log_name_len + 5u + 1u; + buffer = (char *)malloc(total_len); + if (buffer == NULL) { + return false; + } + memcpy(buffer + offset, "log/", 4u); + offset += 4u; + memcpy(buffer + offset, "collection/", 11u); + offset += 11u; + memcpy(buffer + offset, name, name_len); + offset += name_len; + memcpy(buffer + offset, "/log", 4u); + offset += 4u; + memcpy(buffer + offset, "/head", 5u); + offset += 5u; + buffer[offset] = '\0'; + *out_name = buffer; + return true; +} + +static void amduatd_store_u32_le(uint8_t *out, uint32_t value) { + out[0] = (uint8_t)(value & 0xffu); + out[1] = (uint8_t)((value >> 8) & 0xffu); + out[2] = (uint8_t)((value >> 16) & 0xffu); + out[3] = (uint8_t)((value >> 24) & 0xffu); +} + +static void amduatd_store_u64_le(uint8_t *out, uint64_t value) { + out[0] = (uint8_t)(value & 0xffu); + out[1] = (uint8_t)((value >> 8) & 0xffu); + out[2] = (uint8_t)((value >> 16) & 0xffu); + out[3] = (uint8_t)((value >> 24) & 0xffu); + out[4] = (uint8_t)((value >> 32) & 0xffu); + out[5] = (uint8_t)((value >> 40) & 0xffu); + out[6] = (uint8_t)((value >> 48) & 0xffu); + out[7] = (uint8_t)((value >> 56) & 0xffu); +} + +static bool amduatd_build_edge_index_state_payload( + amduat_reference_t graph_ref, + amduat_octets_t *out_payload) { + static const uint8_t k_magic[8] = { + 'A', 'S', 'L', 'E', 'I', 'X', '1', '\0' + }; + amduat_octets_t ref_bytes = amduat_octets(NULL, 0u); + uint8_t *payload = NULL; + size_t payload_len = 0u; + size_t offset = 0u; + + if (out_payload == NULL) { + return false; + } + *out_payload = amduat_octets(NULL, 0u); + if (!amduat_enc_asl1_core_encode_reference_v1(graph_ref, &ref_bytes)) { + return false; + } + if (ref_bytes.len > UINT32_MAX) { + free((void *)ref_bytes.data); + return false; + } + payload_len = 8u + 4u + 8u + 4u + ref_bytes.len; + payload = (uint8_t *)malloc(payload_len); + if (payload == NULL) { + free((void *)ref_bytes.data); + return false; + } + memcpy(payload + offset, k_magic, 8u); + offset += 8u; + amduatd_store_u32_le(payload + offset, 1u); + offset += 4u; + amduatd_store_u64_le(payload + offset, 0u); + offset += 8u; + amduatd_store_u32_le(payload + offset, (uint32_t)ref_bytes.len); + offset += 4u; + memcpy(payload + offset, ref_bytes.data, ref_bytes.len); + offset += ref_bytes.len; + free((void *)ref_bytes.data); + *out_payload = amduat_octets(payload, payload_len); + return offset == payload_len; +} + +static int amduatd_test_empty_store(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduatd_store_ctx_t store_ctx; + amduat_asl_store_t store; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduatd_cfg_t dcfg; + amduatd_fed_cfg_t fed_cfg; + amduatd_space_doctor_report_t report; + + if (root == NULL) { + return 1; + } + + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + + memset(&store_ctx, 0, sizeof(store_ctx)); + memset(&store, 0, sizeof(store)); + if (!amduatd_store_init(&store, + &cfg, + &store_ctx, + root, + AMDUATD_STORE_BACKEND_FS)) { + fprintf(stderr, "failed to init fs backend\n"); + free(root); + return 1; + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + + if (!amduatd_space_init(&space, NULL, false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + memset(&dcfg, 0, sizeof(dcfg)); + dcfg.space = space; + memset(&fed_cfg, 0, sizeof(fed_cfg)); + + if (!amduatd_space_doctor_run(&store, + &pointer_store, + &dcfg.space, + &dcfg, + &fed_cfg, + &report)) { + fprintf(stderr, "doctor run failed\n"); + free(root); + return 1; + } + + if (report.backend != AMDUATD_STORE_BACKEND_FS) { + fprintf(stderr, "expected fs backend\n"); + amduatd_space_doctor_report_free(&report); + free(root); + return 1; + } + + if (!amduatd_check_status(&report, + "pointer_name_validation", + AMDUATD_DOCTOR_OK) || + !amduatd_check_status(&report, + "edges_index_head", + AMDUATD_DOCTOR_WARN) || + !amduatd_check_status(&report, + "edges_collection_snapshot_head", + AMDUATD_DOCTOR_WARN) || + !amduatd_check_status(&report, + "edges_collection_log_head", + AMDUATD_DOCTOR_WARN) || + !amduatd_check_status(&report, + "index_current_state", + AMDUATD_DOCTOR_SKIPPED) || + !amduatd_check_status(&report, + "index_log_scan", + AMDUATD_DOCTOR_SKIPPED)) { + fprintf(stderr, "unexpected doctor status for empty store\n"); + amduatd_space_doctor_report_free(&report); + free(root); + return 1; + } + + amduatd_space_doctor_report_free(&report); + free(root); + return 0; +} + +static int amduatd_test_minimal_fixture(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduatd_store_ctx_t store_ctx; + amduat_asl_store_t store; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduatd_cfg_t dcfg; + amduatd_fed_cfg_t fed_cfg; + amduatd_space_doctor_report_t report; + amduat_reference_t none_ref; + amduat_reference_t graph_ref; + amduat_reference_t edge_index_ref; + amduat_octets_t collection_name = amduat_octets(NULL, 0u); + amduat_octets_t index_head_name = amduat_octets(NULL, 0u); + char *collection_head = NULL; + char *collection_log_head = NULL; + amduat_octets_t payload = amduat_octets(NULL, 0u); + bool swapped = false; + + if (root == NULL) { + return 1; + } + + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + + memset(&store_ctx, 0, sizeof(store_ctx)); + memset(&store, 0, sizeof(store)); + if (!amduatd_store_init(&store, + &cfg, + &store_ctx, + root, + AMDUATD_STORE_BACKEND_FS)) { + fprintf(stderr, "failed to init fs backend\n"); + free(root); + return 1; + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + + if (!amduatd_space_init(&space, NULL, false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + memset(&dcfg, 0, sizeof(dcfg)); + dcfg.space = space; + memset(&fed_cfg, 0, sizeof(fed_cfg)); + + { + amduat_artifact_t none_artifact; + if (!amduat_asl_none_artifact(&none_artifact)) { + fprintf(stderr, "failed to build none artifact\n"); + free(root); + return 1; + } + if (amduat_asl_store_put(&store, none_artifact, &none_ref) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store none artifact\n"); + amduat_artifact_free(&none_artifact); + free(root); + return 1; + } + amduat_artifact_free(&none_artifact); + } + + { + amduat_artifact_t graph_artifact = + amduat_artifact(amduat_octets("graph", 5u)); + if (amduat_asl_store_put(&store, graph_artifact, &graph_ref) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store graph artifact\n"); + free(root); + return 1; + } + } + + if (!amduatd_build_edge_index_state_payload(graph_ref, &payload)) { + fprintf(stderr, "failed to encode edge index state\n"); + free(root); + return 1; + } + if (amduat_asl_record_store_put(&store, + amduat_octets("tgk/edge_index_state", 20u), + payload, + &edge_index_ref) != AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store edge index record\n"); + free((void *)payload.data); + free(root); + return 1; + } + free((void *)payload.data); + + if (!amduatd_space_edges_collection_name(&dcfg.space, &collection_name) || + !amduatd_space_edges_index_head_name(&dcfg.space, &index_head_name)) { + fprintf(stderr, "failed to build scoped names\n"); + free(root); + return 1; + } + + if (!amduatd_build_collection_head_name((const char *)collection_name.data, + &collection_head) || + !amduatd_build_collection_log_head_name( + (const char *)collection_name.data, &collection_log_head)) { + fprintf(stderr, "failed to build collection heads\n"); + free(root); + return 1; + } + + if (amduat_asl_pointer_cas(&pointer_store, + (const char *)index_head_name.data, + false, + NULL, + &edge_index_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped || + amduat_asl_pointer_cas(&pointer_store, + collection_head, + false, + NULL, + &none_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped || + amduat_asl_pointer_cas(&pointer_store, + collection_log_head, + false, + NULL, + &none_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped) { + fprintf(stderr, "failed to seed pointers\n"); + free(root); + return 1; + } + + if (!amduatd_space_doctor_run(&store, + &pointer_store, + &dcfg.space, + &dcfg, + &fed_cfg, + &report)) { + fprintf(stderr, "doctor run failed\n"); + free(root); + return 1; + } + + if (!amduatd_check_status(&report, + "edges_index_head", + AMDUATD_DOCTOR_OK) || + !amduatd_check_status(&report, + "edges_collection_snapshot_head", + AMDUATD_DOCTOR_OK) || + !amduatd_check_status(&report, + "edges_collection_log_head", + AMDUATD_DOCTOR_OK) || + !amduatd_check_status(&report, + "edge_index_state_parse", + AMDUATD_DOCTOR_OK) || + !amduatd_check_status(&report, + "edge_index_graph_ref", + AMDUATD_DOCTOR_OK) || + !amduatd_check_status(&report, + "index_current_state", + AMDUATD_DOCTOR_SKIPPED)) { + fprintf(stderr, "unexpected doctor status for fixture\n"); + amduatd_space_doctor_report_free(&report); + free(root); + return 1; + } + + amduatd_space_doctor_report_free(&report); + free((void *)collection_name.data); + free((void *)index_head_name.data); + free(collection_head); + free(collection_log_head); + free(root); + return 0; +} + +int main(void) { + if (amduatd_test_empty_store() != 0) { + return 1; + } + if (amduatd_test_minimal_fixture() != 0) { + return 1; + } + return 0; +}