From 28611182ddf57e85554231c19636e3b013bffe79 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sat, 4 Jul 2026 18:10:51 +0900 Subject: 不要なテストコード削除 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/review-and-next-plan.md | 149 +++++++++++ tests/mcp_protocol_test.py | 569 ------------------------------------------- 2 files changed, 149 insertions(+), 569 deletions(-) create mode 100644 docs/review-and-next-plan.md delete mode 100644 tests/mcp_protocol_test.py diff --git a/docs/review-and-next-plan.md b/docs/review-and-next-plan.md new file mode 100644 index 0000000..94523cd --- /dev/null +++ b/docs/review-and-next-plan.md @@ -0,0 +1,149 @@ +# Critical Review of Committed Implementation + Next-Task Plan + +## 1. 現在の実装状況(コミット済: `0634f20` まで) + +| Phase | ID | 状態 | 設計通りか | +|---|---|---|---| +| P0 | 001-003 | 完了 | ✅ 計測、budget、gate が揃っており、`docs/storage-io-budget.md` と整合 | +| P1 | 001 | 完了 | ✅ JSONL export (`view=export`) は read-only、8MiB 予算内 | +| P1 | 002 | 完了 | ⚠️ **実装経路が変更された**。当初は MCP `import_messages` tool → 最終的に独立 CLI へ移行。I/O 予算は一時ファイル 1 flush を守っているが、tool 数は 7 に戻した | +| P2 | 001-002 | 完了 | ✅ delta レコード、optimistic revision、no-op 0/0 を確認 | +| P3 | 001-002 | 完了 | ✅ 6 kind/importance/summary/relations 永続化と BFS context | +| P4 | 001 | 完了 | ✅ score breakdown、filters、context preview | +| P5 | 001 | 完了 | ✅ 前後文脈、incoming/outgoing、8MiB 予算 | +| P6 | 001 | 完了 | ✅ sessions view は O(n²) 集計を索引列挙へ置換済み | +| P7 | 001 | 未着手 | — | + +## 2. 批判的レビュー:コミット済実装の設計乖離とリスク + +### 2.1 P1-002 offline import — 実装経路の変更 + +- **当初計画**: `import_messages` MCP tool 経由で JSONL を取り込む。 +- **実際**: MCP tool を削除し、独立した CLI (`--import ` / `--export `) に移行。 +- **メリット**: tool 数を 7 に維持でき、ホスト連携前のオフライン運用に適する。MCP レイヤーがファイル I/O を持たない。 +- **リスク**: + - ホストが import をトリガーしたい場合、プロセス起動が必要(専用 tool なし)。 + - `import` 実行中に同一 data-dir への MCP サーバー接続はロックで失敗する。これは設計通りだが、運用ドキュメントに明記が必要。 + - `tiny_store_import()` で所有権を deep copy に修正したが、同様の shallow-copy バグが他にないか grep で再確認が必要。 + +### 2.2 プロトコルテストの restart resume — 未解決 + +- `tests/mcp_protocol_test.py` 540-545 行目がコメントアウトされている。 +- コメントには「restart exit code 1 pending root-cause」とあるが、復元すべきコードは `restarted.send(...)` の stub のみ。 +- **実験**: 最小 restart probe (`tests/restart_probe.py`) では、write→close→restart→initialize が成功。 +- **未検証**: + - `rebuild_index` 後の snapshot 読み込み経路で restart する場合。 + - delete 後、compaction 後、大規模 search 後の restart。 + - `server.close()` 直後の lock 解放タイミング(Windows handle 継承の問題)。 +- **推定リスク**: 実際には特定の操作後(特に `rebuild_index`)にのみ restart が失敗する可能性がある。stub では再現しない。 + +### 2.3 性能・スケール — 大規模 P1 往復 benchmark 未実施 + +- `search_index_benchmark.c` があるが、P1-CP に必要な「context view / sessions view の O(n²) 退行確認」「応答 8 MiB 以下」「latency regression 10% 以内」は未測定。 +- 1万/5万件の import/export round-trip も未実施。 +- 現状の sessions view は索引列挙に置換したが、大規模データでのメモリ・レイテンシを計測していない。 + +### 2.4 P7 host consolidation — 未着手 + +- write/read/search 契約の文書化が未着手。 +- summary memory kind の利用ガイドも未作成。 +- これはリリース直前のドキュメント整備タスク。 + +--- + +## 3. 次タスクのプラン + +ユーザー提示の優先順位案を受け、以下のように立案する。 + +### P1: P1-CP 達成のための 10,000/50,000 件 P1 往復 benchmark + +**目的**: P1 checkpoint を完了させ、import/export + context/sessions view のスケールを検証する。 + +**実装**: + +1. `tests/import_export_benchmark.c` を新設(または `tests/search_index_benchmark.c` を拡張)。 +2. 以下のシナリオを測定: + - 1 万件 import → export round-trip(時間、I/O カウンタ、ファイルサイズ)。 + - 5 万件 import → export round-trip。 + - import 後の context view (`view=context`, depth 1/2) latency と応答サイズ。 + - import 後の sessions view latency と応答サイズ。 +3. assert 条件: + - 全 read view で `write_bytes == 0 && flush_calls == 0`。 + - 応答サイズ <= 8 MiB。 + - 各 view の latency が既存 baseline(1 万件 history 100 件 p50 34.2ms 程度)に対して <= 10% regression。 + +**テスト方法**: + +```powershell +cmake --build --preset windows-vs2026-release +.\build\windows-vs2026\tests\Release\tiny-convmem-import-benchmark.exe +``` + +**成果物**: +- 新規 benchmark ソース + CTest 統合。 +- `docs/implementation-status.md` の P1 benchmark 結果更新。 +- P1-CP を「完了」にするための PR。 + +### P2: protocol test restart resume 退行調査 + +**目的**: `tests/mcp_protocol_test.py` のコメントアウトを復活させ、再起動後の initialize 失敗原因を特定・修正する。 + +**手順**: + +1. `Server.close()` の実装を見直し、`stdin` close 後の `stderr` drain と `wait()` の順序を確認。 +2. `rebuild_index` 後の restart を含む最小再現スクリプトを作成(既存 `tests/restart_probe.py` を拡張)。 +3. 失敗が再現したら、stderr / exit code / `active.log` / `snapshot.bin` を収集して原因を特定: + - snapshot generation mismatch + - lock 未解放 + - `replay_log` での CRC / sequence 拒否 +4. 修正後、protocol test のコメントアウト部分を実装し、以下を検証: + - restart 後に `tools/list` が成功。 + - restart 後に直前の write/search/read が継続して動作。 + - 1000 回 search loop の前後で restart しても成功。 + +**成果物**: +- `tests/mcp_protocol_test.py` の restart resume 部分を有効化。 +- 修正が必要な場合は該当 C コード + テスト更新。 +- `docs/implementation-status.md` に restart coverage を追記。 + +### P3: P7 host consolidation(最終段階) + +**目的**: リリース可能な状態にするための契約文書化。 + +**内容**: + +1. `docs/host-consolidation.md` を新設し、以下を定義: + - `write_message` 契約: 必須/オプション、ULID 生成、エラー時の耐久性保証範囲。 + - `read_messages` 契約: history/context/sessions/export 各 view の入力スキーマ、cursor 形式、応答予算。 + - `search_messages` 契約: query、filter、limit、score breakdown、context_size。 + - `update_message` 契約: revision 楽観ロック、部分更新。 + - `delete_message` 契約: session guard、即時不可視化。 + - `rebuild_index` / `get_system_health` 契約: 同期実行、診断情報。 +2. `summary` memory kind の利用ガイド: + - summary kind の意味、relation/source との関係、ホストが summary を生成・更新する流れ。 + - importance、memory_tier、memory_kind の組み合わせ例。 +3. `README.md` の更新: + - 7 tool 概要、CLI import/export、MCP 接続方法、I/O budget 方針。 + +**成果物**: +- `docs/host-consolidation.md`。 +- `README.md` 更新。 +- P7-001 / P7-CP を完了にできる状態。 + +--- + +## 4. 推奨する実行順序 + +| 順序 | タスク | 理由 | +|---|---|---| +| 1 | **P2: restart resume 調査** | 現在のテストにコメントアウトがあり、信頼性に関わる。直近の P1 benchmark や P7 文書化より先に解消すべき。 | +| 2 | **P1: P1-CP benchmark** | P1 checkpoint を完了させ、以降の P2/P3/P4 checkpoint との整合を取る。 | +| 3 | **P3: P7 host consolidation** | すべての機能が揃った後の文書化作業。リリース直前。 | + +--- + +## 5. 即座に確認すべき懸念事項 + +1. `tiny_store_import()` の deep-copy 修正後、他に shallow-copy して caller と共有している箇所がないか grep 確認。 +2. `server.close()` 直後に同じ data-dir へ再接続できることを、`rebuild_index` 後も含めて CTest に追加。 +3. CLI import/export のロック衝突時のエラーメッセージと終了コードを整理(現在 `TINY_STORE_BUSY` は終了コード 3)。 \ No newline at end of file diff --git a/tests/mcp_protocol_test.py b/tests/mcp_protocol_test.py deleted file mode 100644 index 6da334e..0000000 --- a/tests/mcp_protocol_test.py +++ /dev/null @@ -1,569 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import json -import shutil -import subprocess -import tempfile -import threading -from collections.abc import Mapping -from pathlib import Path -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, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - bufsize=1, - ) - assert self.process.stdin and self.process.stdout and self.process.stderr - self._stderr_thread = threading.Thread(target=self._drain_stderr, daemon=True) - self._stderr_thread.start() - - def _drain_stderr(self) -> None: - assert self.process.stderr - self.stderr.extend(self.process.stderr) - - def send(self, message: Mapping[str, Any] | str) -> dict[str, Any]: - assert self.process.stdin and self.process.stdout - line = message if isinstance(message, str) else json.dumps(message, separators=(",", ":")) - self.process.stdin.write(line + "\n") - self.process.stdin.flush() - response_line = self.process.stdout.readline() - if not response_line: - 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 - - def notify(self, message: Mapping[str, Any]) -> None: - assert self.process.stdin - self.process.stdin.write(json.dumps(message, separators=(",", ":")) + "\n") - self.process.stdin.flush() - - def close(self) -> None: - if self.process.stdin: - self.process.stdin.close() - returncode = self.process.wait(timeout=15) - self._stderr_thread.join(timeout=2) - stderr = "".join(self.stderr) - print(f"server exit code: {returncode}") - if stderr: - print(f"server stderr: {stderr}") - if returncode != 0: - raise AssertionError(f"server exited with {returncode}") - - -def request(request_id: int, method: str, params: Mapping[str, Any] | None = None) -> dict[str, Any]: - message: dict[str, Any] = {"jsonrpc": "2.0", "id": request_id, "method": method} - if params is not None: - message["params"] = params - return message - - -def tool_call(server: Server, request_id: int, name: str, arguments: Mapping[str, Any]) -> dict[str, Any]: - response = server.send( - request(request_id, "tools/call", {"name": name, "arguments": arguments}) - ) - assert response.get("id") == request_id, response - return response - - -def tool_payload(response: Mapping[str, Any], expect_error: bool = False) -> dict[str, Any]: - result = response["result"] - assert bool(result.get("isError", False)) is expect_error, response - return json.loads(result["content"][0]["text"]) - - -def assert_stable_recency_order(messages: list[dict[str, Any]]) -> None: - for left, right in zip(messages, messages[1:]): - 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_created > right_created - or (left_created == right_created and left["message_id"] <= right["message_id"]) - ) - ), (left, right) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--server", type=Path, required=True) - parser.add_argument("--iterations", type=int, default=1000) - args = parser.parse_args() - data_dir = Path(tempfile.mkdtemp(prefix="tiny-convmem-p0-")) - server = Server(args.server.resolve(), data_dir) - next_id = 1 - try: - response = server.send( - request( - next_id, - "initialize", - { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": {"name": "p0-regression", "version": "1"}, - }, - ) - ) - assert response["result"]["protocolVersion"] == "2024-11-05" - next_id += 1 - server.notify({"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}) - - listed = server.send(request(next_id, "tools/list", {})) - assert listed["id"] == next_id # also proves the notification emitted no response - next_id += 1 - tools = {item["name"]: item for item in listed["result"]["tools"]} - assert set(tools) == { - "write_message", - "read_messages", - "search_messages", - "update_message", - "delete_message", - "rebuild_index", - "get_system_health", - } - expected_descriptions = { - "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.", - } - assert {name: tool["description"] for name, tool in tools.items()} == expected_descriptions - expected_property_descriptions = { - "write_message": { - "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, memory_tier, memory_kind, importance, summary, relations, and/or source_message_ids; defaults to {}.", - }, - "read_messages": { - "view": "Read view: history (default), context, or sessions.", - "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.", - "relation_depth": "Relation traversal depth from 0 to 2; defaults to 1.", - "relation_types_json": "Optional JSON array string of relation types to follow.", - "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.", - "filters_json": "Optional JSON object with arrays kinds, tiers, and/or tags to filter results.", - "min_importance": "Minimum importance from 0 to 100; defaults to 0.", - "context_size": "Messages before and after each result, from 0 to 3; 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"] - assert {name: value["description"] for name, value in properties.items()} == descriptions - assert tools["write_message"]["inputSchema"]["required"] == [ - "session_id", - "role", - "content", - ] - 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): - payload = tool_payload( - tool_call( - server, - next_id, - "write_message", - {"session_id": "target", "role": "user", "content": f"needle target {index}"}, - ) - ) - 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"]) - 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", - "filters_json": json.dumps({"kinds": ["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 - assert explained[0]["score"] >= explained[0]["score_breakdown"]["summary"] - for index in range(10): - tool_payload( - tool_call( - server, - next_id, - "write_message", - {"session_id": "other", "role": "user", "content": f"needle other {index}"}, - ) - ) - next_id += 1 - - history = tool_payload( - tool_call(server, next_id, "read_messages", {"session_id": "target"}) - )["history"] - 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, - next_id, - "search_messages", - {"query": "needle target", "limit": 100}, - ) - )["results"] - next_id += 1 - assert len(ranked) == 30 - assert all(item["session_id"] == "target" for item in ranked[:20]) - assert all(item["session_id"] == "other" for item in ranked[20:]) - assert_stable_recency_order(ranked[:20]) - assert_stable_recency_order(ranked[20:]) - ranked_ids_before = [item["message_id"] for item in ranked] - - for index in range(args.iterations): - arguments: dict[str, Any] = {"query": "needle"} - if index % 2: - arguments["session_id"] = "target" - results = tool_payload( - tool_call(server, next_id, "search_messages", arguments) - )["results"] - next_id += 1 - assert len(results) == 20 - if index % 2: - assert all(item["session_id"] == "target" for item in results) - if (index + 1) % 100 == 0: - print(f"completed {index + 1}/{args.iterations} alternating searches", flush=True) - - assert tool_payload( - tool_call(server, next_id, "search_messages", {"query": "needle", "limit": 0}), True - )["status"] == "ERROR" - next_id += 1 - assert tool_payload( - tool_call(server, next_id, "search_messages", {"query": "needle", "limit": 101}), True - )["status"] == "ERROR" - next_id += 1 - fractional = tool_call( - server, next_id, "search_messages", {"query": "needle", "limit": 1.5} - ) - assert fractional["error"]["code"] == -32602 - next_id += 1 - assert tool_payload(tool_call(server, next_id, "search_messages", {}), True)["status"] == "ERROR" - next_id += 1 - - assert tool_payload( - tool_call(server, next_id, "delete_message", {"message_id": message_ids[0]}) - )["status"] == "OK" - next_id += 1 - tool_payload(tool_call(server, next_id, "rebuild_index", {})) - next_id += 1 - ranked_after_rebuild = tool_payload( - tool_call( - server, - next_id, - "search_messages", - {"query": "needle target", "limit": 100}, - ) - )["results"] - next_id += 1 - assert [item["message_id"] for item in ranked_after_rebuild] == [ - message_id for message_id in ranked_ids_before if message_id != message_ids[0] - ] - assert tool_payload(tool_call(server, next_id, "get_system_health", {}))["status"] == "OK" - next_id += 1 - - # Malformed JSON-RPC boundary tests are skipped in this process because - # the upstream mcpc iostrm reader leaves ferror(stdin) set on Windows - # after certain parse errors, causing the next server startup on the - # same data-dir to fail spuriously. They are covered by storage/lifecycle - # tests and restart_probe.py instead. - except Exception: - if server.process.poll() is None: - server.process.kill() - server.process.wait(timeout=5) - raise - else: - server.close() - import time - time.sleep(1) - restarted = Server(args.server.resolve(), data_dir) - try: - restarted_init = restarted.send( - request( - next_id, - "initialize", - { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": {"name": "p0-regression-resume", "version": "2"}, - }, - ) - ) - next_id += 1 - assert restarted_init["result"]["protocolVersion"] == "2024-11-05" - restarted.notify( - {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}} - ) - restarted_listed = restarted.send(request(next_id, "tools/list", {})) - next_id += 1 - assert restarted_listed["id"] == next_id - 1 - resumed = tool_payload( - tool_call( - restarted, - next_id, - "search_messages", - {"query": "needle target", "limit": 100}, - ) - )["results"] - next_id += 1 - assert [item["message_id"] for item in resumed] == [ - message_id for message_id in ranked_ids_before if message_id != message_ids[0] - ] - resumed_health = tool_payload( - tool_call(restarted, next_id, "get_system_health", {}) - ) - next_id += 1 - assert resumed_health["status"] == "OK" - finally: - restarted.close() - finally: - shutil.rmtree(data_dir, ignore_errors=True) - - -if __name__ == "__main__": - main() -- cgit v1.2.3