diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/CMakeLists.txt | 39 | ||||
| -rw-r--r-- | tests/domain_test.c | 71 | ||||
| -rw-r--r-- | tests/fixtures/health.jsonl | 3 | ||||
| -rw-r--r-- | tests/fixtures/smoke-input.jsonl | 8 | ||||
| -rw-r--r-- | tests/fixtures/write-once.jsonl | 2 | ||||
| -rw-r--r-- | tests/lifecycle.cmake | 51 | ||||
| -rw-r--r-- | tests/smoke.cmake | 21 | ||||
| -rw-r--r-- | tests/storage_test.c | 97 |
8 files changed, 286 insertions, 6 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8f79a91..1407fc7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,8 +3,47 @@ add_test( COMMAND "${CMAKE_COMMAND}" "-DSERVER=$<TARGET_FILE:tiny-convmem-mcp>" "-DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/fixtures/smoke-input.jsonl" + "-DDATA_DIR=${CMAKE_CURRENT_BINARY_DIR}/smoke-data" -P "${CMAKE_CURRENT_SOURCE_DIR}/smoke.cmake" ) set_tests_properties(tiny-convmem-mcp.smoke PROPERTIES TIMEOUT 15) +add_executable(tiny-convmem-domain-test + domain_test.c + "${PROJECT_SOURCE_DIR}/src/domain/message.c" + "${PROJECT_SOURCE_DIR}/src/domain/search.c" +) +target_include_directories(tiny-convmem-domain-test PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_compile_features(tiny-convmem-domain-test PRIVATE c_std_17) +if(MSVC) + target_compile_options(tiny-convmem-domain-test PRIVATE /utf-8 /W4) +endif() +add_test(NAME tiny-convmem-mcp.domain COMMAND tiny-convmem-domain-test) + +add_executable(tiny-convmem-storage-test + storage_test.c + "${PROJECT_SOURCE_DIR}/src/domain/message.c" + "${PROJECT_SOURCE_DIR}/src/storage/store.c" +) +target_include_directories(tiny-convmem-storage-test PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_compile_features(tiny-convmem-storage-test PRIVATE c_std_17) +target_link_libraries(tiny-convmem-storage-test PRIVATE bcrypt) +if(MSVC) + target_compile_options(tiny-convmem-storage-test PRIVATE /utf-8 /W4) +endif() +add_test(NAME tiny-convmem-mcp.storage COMMAND tiny-convmem-storage-test) + +add_test( + NAME tiny-convmem-mcp.lifecycle + COMMAND "${CMAKE_COMMAND}" + "-DSERVER=$<TARGET_FILE:tiny-convmem-mcp>" + "-DWRITE_INPUT=${CMAKE_CURRENT_SOURCE_DIR}/fixtures/write-once.jsonl" + "-DHEALTH_INPUT=${CMAKE_CURRENT_SOURCE_DIR}/fixtures/health.jsonl" + "-DDATA_DIR=${CMAKE_CURRENT_BINARY_DIR}/lifecycle-data" + -P "${CMAKE_CURRENT_SOURCE_DIR}/lifecycle.cmake" +) +set_tests_properties(tiny-convmem-mcp.lifecycle PROPERTIES TIMEOUT 15) + +add_test(NAME tiny-convmem-mcp.requires-data-dir COMMAND $<TARGET_FILE:tiny-convmem-mcp>) +set_tests_properties(tiny-convmem-mcp.requires-data-dir PROPERTIES WILL_FAIL TRUE) diff --git a/tests/domain_test.c b/tests/domain_test.c new file mode 100644 index 0000000..d50e4bd --- /dev/null +++ b/tests/domain_test.c @@ -0,0 +1,71 @@ +#include "domain/message.h" +#include "domain/search.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int failures; +#define CHECK(value) \ + do \ + { \ + if (!(value)) \ + { \ + fprintf(stderr, "CHECK failed at line %d: %s\n", __LINE__, #value); \ + failures++; \ + } \ + } \ + while (0) + +int +main(void) +{ + tiny_role_t role; + tiny_memory_tier_t tier; + const char valid_utf8[] = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; + const char invalid_utf8[] = "\xc0\xaf"; + tiny_message_t original, copy; + tiny_search_index_t *search; + size_t *candidates = NULL, candidate_count = 0; + + CHECK(tiny_role_parse("user", &role) && role == TINY_ROLE_USER); + CHECK(!tiny_role_parse("tool", &role)); + CHECK(tiny_tier_parse("discard", &tier) && tier == TINY_TIER_DISCARD); + CHECK(!tiny_tier_parse("archive", &tier)); + CHECK(tiny_utf8_valid(valid_utf8, strlen(valid_utf8))); + CHECK(!tiny_utf8_valid(invalid_utf8, sizeof(invalid_utf8) - 1)); + CHECK(tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAV")); + CHECK(!tiny_message_id_valid("81ARZ3NDEKTSV4RRFFQ69G5FAV")); + CHECK(!tiny_message_id_valid("01ARZ3NDEKTSV4RRFFQ69G5FAI")); + + tiny_message_init(&original); + tiny_message_init(©); + original.session_id = _strdup("session"); + original.content = _strdup("content"); + original.role = TINY_ROLE_ASSISTANT; + strcpy_s(original.message_id, sizeof(original.message_id), "01ARZ3NDEKTSV4RRFFQ69G5FAV"); + CHECK(tiny_message_copy(©, &original) == 0); + CHECK(copy.session_id != original.session_id); + CHECK(strcmp(copy.content, "content") == 0); + search = tiny_search_index_create(); + CHECK(search != NULL); + free(original.content); + original.content = _strdup("Curry and coffee"); + original.metadata.tags = calloc(1, sizeof(char *)); + original.metadata.tags[0] = _strdup("設計メモ"); + original.metadata.tag_count = 1; + CHECK(tiny_search_index_add(search, &original, 7)); + CHECK(tiny_search_index_candidates(search, "CURRY", &candidates, &candidate_count)); + CHECK(candidate_count == 1 && candidates[0] == 7); + CHECK(tiny_search_score("curry", &original) > 1.0); + free(candidates); + candidates = NULL; + CHECK(tiny_search_index_candidates(search, "設計", &candidates, &candidate_count)); + CHECK(candidate_count == 1 && candidates[0] == 7); + CHECK(tiny_search_score("設計", &original) > 0.0); + free(candidates); + tiny_search_index_free(search); + tiny_message_free(&original); + tiny_message_free(©); + return failures == 0 ? 0 : 1; +} diff --git a/tests/fixtures/health.jsonl b/tests/fixtures/health.jsonl new file mode 100644 index 0000000..8d1a19f --- /dev/null +++ b/tests/fixtures/health.jsonl @@ -0,0 +1,3 @@ +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lifecycle","version":"1"}}} +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_messages","arguments":{"session_id":"lifecycle"}}} +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_system_health","arguments":{}}} diff --git a/tests/fixtures/smoke-input.jsonl b/tests/fixtures/smoke-input.jsonl index 5c046a4..64bf18f 100644 --- a/tests/fixtures/smoke-input.jsonl +++ b/tests/fixtures/smoke-input.jsonl @@ -1,5 +1,9 @@ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"tiny-convmem-smoke","version":"0.1.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} -{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"health","arguments":{}}} - +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"hello durable memory","metadata":{"tags":["hello","memory"],"memory_tier":"short"}}}} +{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"read_messages","arguments":{"session_id":"smoke"}}} +{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"search_messages","arguments":{"query":"durable","limit":10}}} +{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"rebuild_index","arguments":{}}} +{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"get_system_health","arguments":{}}} +{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"smoke","role":"user","content":"must not be stored","unexpected":true}}} diff --git a/tests/fixtures/write-once.jsonl b/tests/fixtures/write-once.jsonl new file mode 100644 index 0000000..0378968 --- /dev/null +++ b/tests/fixtures/write-once.jsonl @@ -0,0 +1,2 @@ +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lifecycle","version":"1"}}} +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_message","arguments":{"session_id":"lifecycle","role":"user","content":"secret-to-remove","metadata":{"tags":["秘密","remove"],"memory_tier":"long"}}}} diff --git a/tests/lifecycle.cmake b/tests/lifecycle.cmake new file mode 100644 index 0000000..8d2f97d --- /dev/null +++ b/tests/lifecycle.cmake @@ -0,0 +1,51 @@ +foreach(variable SERVER WRITE_INPUT HEALTH_INPUT DATA_DIR) + if(NOT DEFINED ${variable}) + message(FATAL_ERROR "${variable} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${DATA_DIR}") +execute_process( + COMMAND "${SERVER}" --data-dir "${DATA_DIR}" + INPUT_FILE "${WRITE_INPUT}" + OUTPUT_VARIABLE write_output + ERROR_VARIABLE write_error + RESULT_VARIABLE write_result +) +if(NOT write_result EQUAL 0) + message(FATAL_ERROR "initial write failed: ${write_error}") +endif() +string(REGEX MATCH "message_id[^0-9A-Z]+([0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z])" match "${write_output}") +set(message_id "${CMAKE_MATCH_1}") +if(NOT message_id) + message(FATAL_ERROR "could not extract message ID from: ${write_output}") +endif() + +set(delete_input "${DATA_DIR}/delete.jsonl") +file(WRITE "${delete_input}" + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"lifecycle\",\"version\":\"1\"}}}\n" + "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"delete_message\",\"arguments\":{\"message_id\":\"${message_id}\",\"session_id\":\"lifecycle\"}}}\n" + "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"read_messages\",\"arguments\":{\"session_id\":\"lifecycle\"}}}\n" + "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"rebuild_index\",\"arguments\":{}}}\n" +) +execute_process(COMMAND "${SERVER}" --data-dir "${DATA_DIR}" INPUT_FILE "${delete_input}" + OUTPUT_VARIABLE delete_output ERROR_VARIABLE delete_error RESULT_VARIABLE delete_result) +if(NOT delete_result EQUAL 0 OR NOT delete_output MATCHES "records_removed[^0-9]+1") + message(FATAL_ERROR "delete/compaction failed: ${delete_output} ${delete_error}") +endif() + +file(READ "${DATA_DIR}/active.log" compacted_hex HEX) +string(FIND "${compacted_hex}" "7365637265742d746f2d72656d6f7665" secret_position) +if(NOT secret_position EQUAL -1) + message(FATAL_ERROR "compacted log still contains deleted content") +endif() + +file(APPEND "${DATA_DIR}/active.log" "TC") +file(WRITE "${DATA_DIR}/snapshot.bin" "invalid snapshot") +execute_process(COMMAND "${SERVER}" --data-dir "${DATA_DIR}" INPUT_FILE "${HEALTH_INPUT}" + OUTPUT_VARIABLE recovery_output ERROR_VARIABLE recovery_error RESULT_VARIABLE recovery_result) +string(FIND "${recovery_output}" "secret-to-remove" recovered_secret_position) +if(NOT recovery_result EQUAL 0 OR NOT recovery_output MATCHES "recovered_tail[^a-z]+true" OR + NOT recovery_output MATCHES "snapshot_valid[^a-z]+false" OR NOT recovered_secret_position EQUAL -1) + message(FATAL_ERROR "restart/tail recovery failed: ${recovery_output} ${recovery_error}") +endif() diff --git a/tests/smoke.cmake b/tests/smoke.cmake index b741d4b..2481297 100644 --- a/tests/smoke.cmake +++ b/tests/smoke.cmake @@ -4,9 +4,14 @@ endif() if(NOT DEFINED INPUT OR NOT EXISTS "${INPUT}") message(FATAL_ERROR "INPUT does not name the smoke-test fixture") endif() +if(NOT DEFINED DATA_DIR) + message(FATAL_ERROR "DATA_DIR is required") +endif() + +file(REMOVE_RECURSE "${DATA_DIR}") execute_process( - COMMAND "${SERVER}" + COMMAND "${SERVER}" --data-dir "${DATA_DIR}" INPUT_FILE "${INPUT}" OUTPUT_VARIABLE server_output ERROR_VARIABLE server_error @@ -24,9 +29,17 @@ foreach(expected "\"protocolVersion\":\"2024-11-05\"" "\"serverInfo\"" "\"capabilities\"" - "\"name\":\"health\"" + "\"name\":\"write_message\"" + "\"name\":\"read_messages\"" + "\"name\":\"search_messages\"" + "\"name\":\"delete_message\"" + "\"name\":\"rebuild_index\"" + "\"name\":\"get_system_health\"" + "\"required\":[\"session_id\",\"role\",\"content\"]" "\"type\":\"text\"" - "\"text\":\"ok\"" + "\\\"status\\\":\\\"OK\\\"" + "\\\"code\\\":\\\"VALIDATION_ERROR\\\"" + "hello durable memory" ) string(FIND "${server_output}" "${expected}" match_position) if(match_position EQUAL -1) @@ -36,4 +49,4 @@ foreach(expected endif() endforeach() -message(STATUS "MCP initialize, tools/list, and health tools/call succeeded") +message(STATUS "MCP initialize, six-tool listing, write, read, search, rebuild, and health succeeded") diff --git a/tests/storage_test.c b/tests/storage_test.c new file mode 100644 index 0000000..bac0f0b --- /dev/null +++ b/tests/storage_test.c @@ -0,0 +1,97 @@ +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +#include "storage/store.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int failures; +#define CHECK(value) \ + do \ + { \ + if (!(value)) \ + { \ + fprintf(stderr, "CHECK failed at line %d: %s\n", __LINE__, #value); \ + failures++; \ + } \ + } \ + while (0) + +int +wmain(void) +{ + wchar_t temporary[MAX_PATH], directory[MAX_PATH], log_path[MAX_PATH]; + tiny_store_t *store = NULL, *second = NULL; + tiny_message_t input, first, second_message; + tiny_store_status_t status; + HANDLE log; + HANDLE blocker; + tiny_rebuild_report_t report; + LARGE_INTEGER position; + unsigned char byte; + DWORD count; + + CHECK(GetTempPathW(MAX_PATH, temporary) > 0); + CHECK(GetTempFileNameW(temporary, L"tcm", 0, directory) > 0); + DeleteFileW(directory); + CHECK(CreateDirectoryW(directory, NULL) != 0); + status = tiny_store_open(&store, directory); + CHECK(status == TINY_STORE_OK); + CHECK(tiny_store_open(&second, directory) == TINY_STORE_BUSY); + + tiny_message_init(&input); + tiny_message_init(&first); + tiny_message_init(&second_message); + input.session_id = _strdup("unit"); + input.content = _strdup("first"); + input.role = TINY_ROLE_USER; + CHECK(tiny_store_write(store, &input, &first) == TINY_STORE_OK); + free(input.content); + input.content = _strdup("second"); + CHECK(tiny_store_write(store, &input, &second_message) == TINY_STORE_OK); + CHECK(strcmp(first.message_id, second_message.message_id) < 0); + tiny_message_free(&input); + tiny_message_free(&first); + tiny_message_free(&second_message); + tiny_store_close(store); + store = NULL; + + CHECK(tiny_store_open(&store, directory) == TINY_STORE_OK); + CHECK(tiny_store_entry_count(store) == 2); + swprintf_s(log_path, MAX_PATH, L"%s\\active.tmp", directory); + blocker = CreateFileW( + log_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + CHECK(blocker != INVALID_HANDLE_VALUE); + CHECK(tiny_store_rebuild(store, &report) == TINY_STORE_IO_ERROR); + CHECK(tiny_store_entry_count(store) == 2); + CloseHandle(blocker); + DeleteFileW(log_path); + tiny_store_close(store); + store = NULL; + + swprintf_s(log_path, MAX_PATH, L"%s\\active.log", directory); + log = CreateFileW( + log_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + CHECK(log != INVALID_HANDLE_VALUE); + position.QuadPart = -1; + CHECK(SetFilePointerEx(log, position, NULL, FILE_END) != 0); + CHECK(ReadFile(log, &byte, 1, &count, NULL) != 0 && count == 1); + byte ^= 0x5a; + position.QuadPart = -1; + CHECK(SetFilePointerEx(log, position, NULL, FILE_CURRENT) != 0); + CHECK(WriteFile(log, &byte, 1, &count, NULL) != 0 && count == 1); + CloseHandle(log); + CHECK(tiny_store_open(&store, directory) == TINY_STORE_CORRUPT); + + DeleteFileW(log_path); + swprintf_s(log_path, MAX_PATH, L"%s\\active.tmp", directory); + DeleteFileW(log_path); + swprintf_s(log_path, MAX_PATH, L"%s\\store.lock", directory); + DeleteFileW(log_path); + swprintf_s(log_path, MAX_PATH, L"%s\\snapshot.bin", directory); + DeleteFileW(log_path); + RemoveDirectoryW(directory); + return failures == 0 ? 0 : 1; +} |
