diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-07-04 14:54:45 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-07-04 14:54:45 +0900 |
| commit | 1efecaef1f867cb218366e2b4ab4b901fdd2835f (patch) | |
| tree | 3080a3d80a1a0671e8e82819d3f785465aace162 /tests | |
| parent | 34e00404e9561dcaf8d2d99df7470757a4fee915 (diff) | |
途中経過コミット
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/mcp_protocol_test.py | 172 | ||||
| -rw-r--r-- | tests/smoke.cmake | 3 | ||||
| -rw-r--r-- | tests/storage_test.c | 36 |
3 files changed, 205 insertions, 6 deletions
diff --git a/tests/mcp_protocol_test.py b/tests/mcp_protocol_test.py index 2dc70ff..1873c52 100644 --- a/tests/mcp_protocol_test.py +++ b/tests/mcp_protocol_test.py @@ -85,10 +85,16 @@ def tool_payload(response: Mapping[str, Any], expect_error: bool = False) -> dic def assert_stable_recency_order(messages: list[dict[str, Any]]) -> None: for left, right in zip(messages, messages[1:]): - left_time = int(left["timestamp"]) - right_time = int(right["timestamp"]) + left_time = int(left.get("updated_at", left["timestamp"])) + right_time = int(right.get("updated_at", right["timestamp"])) + left_created = int(left["timestamp"]) + right_created = int(right["timestamp"]) assert left_time > right_time or ( - left_time == right_time and left["message_id"] <= right["message_id"] + left_time == right_time + and ( + left_created > right_created + or (left_created == right_created and left["message_id"] <= right["message_id"]) + ) ), (left, right) @@ -124,6 +130,7 @@ def main() -> None: "write_message", "read_messages", "search_messages", + "update_message", "delete_message", "rebuild_index", "get_system_health", @@ -132,6 +139,7 @@ def main() -> None: "write_message": "Append and durably persist a conversation message. Use it to store information that should remain available after server restarts.", "read_messages": "Read persisted messages from one known session in chronological order. Returns a paginated history for reconstructing that conversation.", "search_messages": "Search persisted message content and tags by text relevance. Use it to recall matching information globally or within one session.", + "update_message": "Update message content or metadata while preserving identity, session, role, and creation time.", "delete_message": "Delete a persisted message so it is immediately excluded from reads and searches. Physical removal occurs after rebuild_index compacts the store.", "rebuild_index": "Compact the store and rebuild its snapshot and in-memory indexes. Use it for maintenance or to physically remove logically deleted records; it may block other requests while running.", "get_system_health": "Inspect storage health, recovery state, message counts, and file sizes. Use it to diagnose persistence or recovery problems.", @@ -142,22 +150,36 @@ def main() -> None: "session_id": "Conversation session that owns the message.", "role": "Message author role: user, assistant, or system.", "content": "UTF-8 message text to persist.", - "metadata_json": "Optional JSON object string containing tool_call_id, tags, and/or memory_tier; defaults to {}.", + "metadata_json": "Optional JSON object string containing tool_call_id, tags, memory_tier, memory_kind, importance, summary, relations, and/or source_message_ids; defaults to {}.", }, "read_messages": { + "view": "Read view: history (default), context, sessions, or export.", "session_id": "Conversation session whose chronological history to read.", + "message_id": "Message ULID required by the context view.", + "before": "Context messages before the target, from 0 to 10.", + "after": "Context messages after the target, from 0 to 10.", "limit": "Maximum messages to return in this page, from 1 to 1000; defaults to 100.", "cursor": "Opaque nextCursor returned by a previous read_messages call; omit for the first page.", }, "search_messages": { "query": "UTF-8 text to match against persisted message content and tags.", "session_id": "Conversation session to search; omit to search across all sessions.", + "memory_kind": "Optional exact memory kind filter.", + "memory_tier": "Optional exact memory tier filter.", + "tag": "Optional exact tag filter.", + "min_importance": "Minimum importance from 0 to 100; defaults to 0.", "limit": "Maximum ranked matches to return, from 1 to 100; defaults to 20.", }, "delete_message": { "message_id": "Canonical 26-character ULID of the message to delete.", "session_id": "Optional session guard; when provided, delete only if the message belongs to this session.", }, + "update_message": { + "message_id": "ULID of the live message to update.", + "content": "Replacement UTF-8 content; omit to retain content.", + "metadata_json": "Replacement metadata JSON; omit to retain metadata.", + "expected_revision": "Optional positive revision for optimistic concurrency.", + }, } for tool_name, descriptions in expected_property_descriptions.items(): properties = tools[tool_name]["inputSchema"]["properties"] @@ -167,12 +189,13 @@ def main() -> None: "role", "content", ] - assert tools["read_messages"]["inputSchema"]["required"] == ["session_id"] + assert tools["read_messages"]["inputSchema"].get("required", []) == [] 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"] + assert tools["update_message"]["inputSchema"]["required"] == ["message_id"] message_ids: list[str] = [] for index in range(20): @@ -186,6 +209,145 @@ def main() -> None: ) next_id += 1 message_ids.append(payload["message"]["message_id"]) + updated = tool_payload( + tool_call( + server, + next_id, + "update_message", + { + "message_id": message_ids[0], + "content": "needle target revised", + "expected_revision": 1, + }, + ) + ) + next_id += 1 + assert updated["changed"] is True + assert updated["message"]["revision"] == 2 + assert updated["message"]["content"] == "needle target revised" + unchanged = tool_payload( + tool_call( + server, + next_id, + "update_message", + { + "message_id": message_ids[0], + "content": "needle target revised", + "expected_revision": 2, + }, + ) + ) + next_id += 1 + assert unchanged["changed"] is False + assert unchanged["message"]["revision"] == 2 + io_before = tool_payload(tool_call(server, next_id, "get_system_health", {}))["io"] + next_id += 1 + context = tool_payload( + tool_call( + server, + next_id, + "read_messages", + {"view": "context", "message_id": message_ids[1], "before": 1, "after": 1}, + ) + ) + next_id += 1 + assert context["message"]["message_id"] == message_ids[1] + assert [item["message_id"] for item in context["chronological_context"]["before"]] == [ + message_ids[0] + ] + assert [item["message_id"] for item in context["chronological_context"]["after"]] == [ + message_ids[2] + ] + io_after = tool_payload(tool_call(server, next_id, "get_system_health", {}))["io"] + next_id += 1 + assert io_after == io_before + sessions_view = tool_payload( + tool_call(server, next_id, "read_messages", {"view": "sessions", "limit": 10}) + ) + next_id += 1 + assert any(item["session_id"] == "target" for item in sessions_view["sessions"]) + exported = tool_payload( + tool_call( + server, + next_id, + "read_messages", + {"view": "export", "session_id": "target", "limit": 100}, + ) + ) + next_id += 1 + export_lines = exported["jsonl"].splitlines() + assert json.loads(export_lines[0])["format_version"] == 2 + assert len(export_lines) == 21 + assert all(json.loads(line)["session_id"] == "target" for line in export_lines[1:]) + io_after_views = tool_payload(tool_call(server, next_id, "get_system_health", {}))["io"] + next_id += 1 + assert io_after_views == io_before + relation_message = tool_payload( + tool_call( + server, + next_id, + "write_message", + { + "session_id": "relations", + "role": "assistant", + "content": "summary of the revised record", + "metadata_json": json.dumps( + { + "memory_kind": "summary", + "importance": 90, + "summary": "record summary", + "relations": [ + {"type": "derived_from", "message_id": message_ids[0]} + ], + "source_message_ids": [message_ids[0]], + } + ), + }, + ) + )["message"] + next_id += 1 + relation_context = tool_payload( + tool_call( + server, + next_id, + "read_messages", + {"view": "context", "message_id": relation_message["message_id"]}, + ) + ) + next_id += 1 + assert relation_context["relations"][0]["type"] == "derived_from" + assert relation_context["relations"][0]["message"]["message_id"] == message_ids[0] + incoming_context = tool_payload( + tool_call( + server, + next_id, + "read_messages", + {"view": "context", "message_id": message_ids[0]}, + ) + ) + next_id += 1 + assert any( + relation["direction"] == "incoming" + and relation["message"]["message_id"] == relation_message["message_id"] + for relation in incoming_context["relations"] + ) + explained = tool_payload( + tool_call( + server, + next_id, + "search_messages", + { + "query": "record summary", + "memory_kind": "summary", + "min_importance": 80, + "limit": 5, + }, + ) + )["results"] + next_id += 1 + assert explained[0]["message_id"] == relation_message["message_id"] + assert "summary" in explained[0]["matched_fields"] + assert explained[0]["score_breakdown"]["summary"] > 0 for index in range(10): tool_payload( tool_call( diff --git a/tests/smoke.cmake b/tests/smoke.cmake index 3992b6b..609f794 100644 --- a/tests/smoke.cmake +++ b/tests/smoke.cmake @@ -32,6 +32,7 @@ foreach(expected "\"name\":\"write_message\"" "\"name\":\"read_messages\"" "\"name\":\"search_messages\"" + "\"name\":\"update_message\"" "\"name\":\"delete_message\"" "\"name\":\"rebuild_index\"" "\"name\":\"get_system_health\"" @@ -49,4 +50,4 @@ foreach(expected endif() endforeach() -message(STATUS "MCP initialize, six-tool listing, write, read, search, rebuild, and health succeeded") +message(STATUS "MCP initialize, seven-tool listing, write, read, search, update, rebuild, and health succeeded") diff --git a/tests/storage_test.c b/tests/storage_test.c index 0e93ad9..0343f46 100644 --- a/tests/storage_test.c +++ b/tests/storage_test.c @@ -98,6 +98,40 @@ main(void) input.content = duplicate_string("second"); CHECK(tiny_store_write(store, &input, &second_message) == TINY_STORE_OK); CHECK(strcmp(first.message_id, second_message.message_id) < 0); + { + tiny_message_patch_t patch; + tiny_message_t updated; + tiny_metadata_t metadata = first.metadata; + tiny_sys_io_counters_t counters; + bool changed = false; + metadata.importance = 80; + memset(&patch, 0, sizeof(patch)); + patch.metadata = &metadata; + patch.metadata_mask = TINY_METADATA_IMPORTANCE; + patch.expected_revision = 1; + patch.has_expected_revision = true; + tiny_message_init(&updated); + tiny_sys_io_counters_reset(); + CHECK(tiny_store_update(store, first.message_id, &patch, &updated, &changed) == TINY_STORE_OK); + tiny_sys_io_counters_get(&counters); + CHECK(changed && updated.revision == 2 && updated.metadata.importance == 80); + CHECK(counters.flush_calls == 1 && counters.write_calls == 2); + CHECK(counters.write_bytes < 8192); + tiny_message_free(&updated); + + patch.expected_revision = 2; + changed = true; + tiny_sys_io_counters_reset(); + CHECK(tiny_store_update(store, first.message_id, &patch, &updated, &changed) == TINY_STORE_OK); + tiny_sys_io_counters_get(&counters); + CHECK(!changed && counters.write_bytes == 0 && counters.write_calls == 0 + && counters.flush_calls == 0); + tiny_message_free(&updated); + + patch.expected_revision = 1; + CHECK(tiny_store_update(store, first.message_id, &patch, &updated, &changed) + == TINY_STORE_CONFLICT); + } tiny_message_free(&input); tiny_message_free(&first); tiny_message_free(&second_message); @@ -106,6 +140,8 @@ main(void) CHECK(tiny_store_open(&store, directory) == TINY_STORE_OK); CHECK(tiny_store_entry_count(store) == 2); + CHECK(tiny_store_entry(store, 0)->message.revision == 2); + CHECK(tiny_store_entry(store, 0)->message.metadata.importance == 80); { size_t failure_index; bool built = false; |
