summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-06-29 02:36:40 +0900
committertanaka takashi <isazaka@gmail.com>2026-06-29 02:36:40 +0900
commit4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (patch)
tree074021381abf57cad434430f25027115b681b8a9 /tests
parent8d38b06f11c713e547d70468340242f23322152c (diff)
feat: 会話メモリのプロトタイプを実装
- 6種類のMCPツールと入力検証を追加 - バイナリログ、スナップショット、復旧、圧縮処理を実装 - Unicode n-gram検索とドメインモデルを追加 - 単体テストと統合テストを追加 - アーキテクチャと実装状況を文書化 - Cコードの記述スタイルを統一
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt39
-rw-r--r--tests/domain_test.c71
-rw-r--r--tests/fixtures/health.jsonl3
-rw-r--r--tests/fixtures/smoke-input.jsonl8
-rw-r--r--tests/fixtures/write-once.jsonl2
-rw-r--r--tests/lifecycle.cmake51
-rw-r--r--tests/smoke.cmake21
-rw-r--r--tests/storage_test.c97
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(&copy);
+ 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(&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(&copy);
+ 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;
+}