summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/function-coverage.md1
-rw-r--r--tests/mcp_protocol_test.py114
-rw-r--r--tests/storage_test.c28
4 files changed, 144 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 9a02ee0..0bc5d89 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -94,6 +94,7 @@ add_executable(tiny-convmem-storage-test
"${PROJECT_SOURCE_DIR}/src/domain/message.c"
"${PROJECT_SOURCE_DIR}/src/storage/codec.c"
"${PROJECT_SOURCE_DIR}/src/storage/store.c"
+ "${PROJECT_SOURCE_DIR}/src/mcp/tools/common/history.c"
)
target_include_directories(tiny-convmem-storage-test PRIVATE "${PROJECT_SOURCE_DIR}/src")
target_compile_features(tiny-convmem-storage-test PRIVATE c_std_17)
diff --git a/tests/function-coverage.md b/tests/function-coverage.md
index 8787918..8976580 100644
--- a/tests/function-coverage.md
+++ b/tests/function-coverage.md
@@ -11,6 +11,7 @@ byte/encoding logic is exposed through internal headers and tested directly.
| `sys/system.c` | test-hook helpers; UTF adapters; `native_file`; argument, path, directory, file, clock and random APIs | `boundaries`, `storage`, `smoke`: NULL/invalid handle, Unicode paths, short I/O, operation failure, cleanup |
| `domain/message.c` | `duplicate_string`, `copy_failed`, all `tiny_message_*`, role/tier parse/name, `tiny_utf8_valid`, `tiny_message_id_valid` | `domain`: enum failure, UTF-8 invalidity, ULID alphabet/length, deep copy and ownership |
| `domain/search.c` | Unicode decode/score、open-addressing lookup/rehash、transactional delta merge、candidate union | `domain`, `search-index`: ASCII/日本語、8-bit hash衝突、複数rehash、allocation failure全位置、leak balance |
+| `mcp/tools/common/history.c` | session hash map、安定順序vector、startup/write/rebuild lifecycle | `storage`, `protocol`: allocation failure掃引、100/105件pagination、restart/rebuild cursor継続 |
| `domain/search.c` | `decode_codepoints`, `gram_hash`, `find_bucket`, `add_gram`, `index_text`, all `tiny_search_*` | `domain`: ASCII case folding, Unicode grams/tags, candidates, scoring and ownership |
| `storage/store.c` | error/I/O helpers, ULID, entry growth/search, record codec, replay/snapshot/recovery, all `tiny_store_*` | `storage`, `lifecycle`: lock, append/reopen, corruption, rebuild rollback, Unicode path |
| `mcp/json.c` | `reserve`, all `tiny_json_*` | `boundaries`: empty append, growth, control escaping, signed/unsigned integer limits, OOM |
diff --git a/tests/mcp_protocol_test.py b/tests/mcp_protocol_test.py
index f739168..4d1deb2 100644
--- a/tests/mcp_protocol_test.py
+++ b/tests/mcp_protocol_test.py
@@ -15,6 +15,7 @@ from typing import Any
class Server:
def __init__(self, executable: Path, data_dir: Path) -> None:
self.stderr: list[str] = []
+ self.last_response_size = 0
self.process = subprocess.Popen(
[str(executable), "--data-dir", str(data_dir)],
stdin=subprocess.PIPE,
@@ -41,6 +42,7 @@ class Server:
raise AssertionError(
f"server closed: returncode={self.process.poll()} stderr={''.join(self.stderr)[-4000:]}"
)
+ self.last_response_size = len(response_line.encode("utf-8"))
response = json.loads(response_line)
assert response.get("jsonrpc") == "2.0", response
return response
@@ -132,6 +134,8 @@ def main() -> None:
"content",
]
assert tools["read_messages"]["inputSchema"]["required"] == ["session_id"]
+ assert tools["read_messages"]["inputSchema"]["properties"]["limit"]["type"] == "integer"
+ assert tools["read_messages"]["inputSchema"]["properties"]["cursor"]["type"] == "string"
assert tools["search_messages"]["inputSchema"]["required"] == ["query"]
assert tools["search_messages"]["inputSchema"]["properties"]["limit"]["type"] == "integer"
assert tools["delete_message"]["inputSchema"]["required"] == ["message_id"]
@@ -165,6 +169,89 @@ def main() -> None:
next_id += 1
assert len(history) == 20
+ paged_ids: list[str] = []
+ for index in range(105):
+ payload = tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "write_message",
+ {"session_id": "paged", "role": "user", "content": f"page item {index}"},
+ )
+ )
+ next_id += 1
+ paged_ids.append(payload["message"]["message_id"])
+ first_page = tool_payload(
+ tool_call(server, next_id, "read_messages", {"session_id": "paged"})
+ )
+ next_id += 1
+ assert len(first_page["history"]) == 100 and first_page.get("nextCursor")
+ second_page = tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "read_messages",
+ {"session_id": "paged", "cursor": first_page["nextCursor"]},
+ )
+ )
+ next_id += 1
+ assert len(second_page["history"]) == 5 and "nextCursor" not in second_page
+ assert [item["message_id"] for item in first_page["history"] + second_page["history"]] == paged_ids
+ for invalid_limit in (0, 1001):
+ assert tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "read_messages",
+ {"session_id": "paged", "limit": invalid_limit},
+ ),
+ True,
+ )["status"] == "ERROR"
+ next_id += 1
+ assert tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "read_messages",
+ {"session_id": "other", "cursor": first_page["nextCursor"]},
+ ),
+ True,
+ )["status"] == "ERROR"
+ next_id += 1
+
+ large_content = "\\" * (512 * 1024)
+ for index in range(6):
+ tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "write_message",
+ {"session_id": "large-page", "role": "user", "content": large_content},
+ )
+ )
+ next_id += 1
+ large_page = tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "read_messages",
+ {"session_id": "large-page", "limit": 100},
+ )
+ )
+ next_id += 1
+ assert 0 < len(large_page["history"]) < 6 and large_page.get("nextCursor")
+ assert server.last_response_size <= 8 * 1024 * 1024
+ assert tool_payload(
+ tool_call(
+ server,
+ next_id,
+ "read_messages",
+ {"session_id": "paged", "cursor": "broken"},
+ ),
+ True,
+ )["status"] == "ERROR"
+ next_id += 1
+
ranked = tool_payload(
tool_call(
server,
@@ -265,6 +352,33 @@ def main() -> None:
raise
else:
server.close()
+ restarted = Server(args.server.resolve(), data_dir)
+ try:
+ restarted.send(
+ request(
+ 1,
+ "initialize",
+ {
+ "protocolVersion": "2024-11-05",
+ "capabilities": {},
+ "clientInfo": {"name": "pagination-restart", "version": "1"},
+ },
+ )
+ )
+ restarted.notify(
+ {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
+ )
+ resumed = tool_payload(
+ tool_call(
+ restarted,
+ 2,
+ "read_messages",
+ {"session_id": "paged", "cursor": first_page["nextCursor"]},
+ )
+ )
+ assert [item["message_id"] for item in resumed["history"]] == paged_ids[100:]
+ finally:
+ restarted.close()
finally:
shutil.rmtree(data_dir, ignore_errors=True)
diff --git a/tests/storage_test.c b/tests/storage_test.c
index 3217095..0e93ad9 100644
--- a/tests/storage_test.c
+++ b/tests/storage_test.c
@@ -1,10 +1,13 @@
#include "storage/store.h"
#include "sys/system.h"
+#include "mcp/tools/common/history.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
static int failures;
#define CHECK(value) \
do \
@@ -103,6 +106,31 @@ main(void)
CHECK(tiny_store_open(&store, directory) == TINY_STORE_OK);
CHECK(tiny_store_entry_count(store) == 2);
+ {
+ size_t failure_index;
+ bool built = false;
+ for (failure_index = 0; failure_index < 64; ++failure_index)
+ {
+ tiny_history_index_t *history;
+ size_t baseline = tiny_allocator_outstanding();
+ tiny_allocator_fail_after(failure_index);
+ history = tiny_history_index_build(store);
+ tiny_allocator_reset();
+ if (history)
+ {
+ const size_t *entries = NULL;
+ size_t entry_count = 0;
+ CHECK(tiny_history_index_session(history, "unit", &entries, &entry_count));
+ CHECK(entry_count == 2 && entries[0] == 0 && entries[1] == 1);
+ tiny_history_index_free(history);
+ CHECK(tiny_allocator_outstanding() == baseline);
+ built = true;
+ break;
+ }
+ CHECK(tiny_allocator_outstanding() == baseline);
+ }
+ CHECK(built);
+ }
for (operation = 0; operation < sizeof(rebuild_failures) / sizeof(rebuild_failures[0]);
++operation)
{