summaryrefslogtreecommitdiff
path: root/tests/mcp_protocol_test.py
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-04 14:54:45 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-04 14:54:45 +0900
commit1efecaef1f867cb218366e2b4ab4b901fdd2835f (patch)
tree3080a3d80a1a0671e8e82819d3f785465aace162 /tests/mcp_protocol_test.py
parent34e00404e9561dcaf8d2d99df7470757a4fee915 (diff)
途中経過コミット
Diffstat (limited to 'tests/mcp_protocol_test.py')
-rw-r--r--tests/mcp_protocol_test.py172
1 files changed, 167 insertions, 5 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(