summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-04 18:10:51 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-04 18:10:51 +0900
commit28611182ddf57e85554231c19636e3b013bffe79 (patch)
treef8f25cbcd0a8f9a5800dac02809045bb49ede8fa
parent5ac1c7adb7eeefd68dbed1bf6cfbe77cd348f257 (diff)
不要なテストコード削除
-rw-r--r--docs/review-and-next-plan.md149
-rw-r--r--tests/mcp_protocol_test.py569
2 files changed, 149 insertions, 569 deletions
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 <file>` / `--export <file>`) に移行。
+- **メリット**: 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()