From b2a242f371df7d8a3d6d832586202adf60871118 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sat, 27 Jun 2026 17:27:49 +0800 Subject: [PATCH 1/2] test(memory): cover semantic index ADR gaps --- ...memory-semantic-index-and-enable-switch.md | 6 +- .../tests/agentkit/storage/test_protocols.py | 72 ++++++++++++++++++- .../tests/coding_agent/test_semantic_sync.py | 44 ++++++++++++ 3 files changed, 118 insertions(+), 4 deletions(-) diff --git a/coding-agent/docs/adr/0072-tape-memory-semantic-index-and-enable-switch.md b/coding-agent/docs/adr/0072-tape-memory-semantic-index-and-enable-switch.md index 66b7e36bc..7ecc7f81a 100644 --- a/coding-agent/docs/adr/0072-tape-memory-semantic-index-and-enable-switch.md +++ b/coding-agent/docs/adr/0072-tape-memory-semantic-index-and-enable-switch.md @@ -257,11 +257,11 @@ unchecked items remain required by the ADR. - [x] `tests/coding_agent/test_semantic_recall.py::test_semantic_refill_does_not_expand_beyond_configured_limit` - [x] `tests/coding_agent/test_semantic_tape_index.py::test_hybrid_merge_keeps_deterministic_topics_before_high_score_semantic_refill` - [x] `tests/coding_agent/test_memory_index_backend_contract.py::test_memory_hit_score_is_normalized_high_is_better_by_result_ordering` -- [ ] `tests/agentkit/storage/test_protocols.py::test_memory_index_protocol_stays_provider_agnostic` +- [x] `tests/agentkit/storage/test_protocols.py::test_memory_index_protocol_stays_provider_agnostic` - [x] `tests/coding_agent/test_semantic_sync.py::test_sync_indexes_only_finalized_topic_summaries` - [x] `tests/coding_agent/test_semantic_sync.py::test_sync_indexes_only_accepted_reviewed_memories` - [x] `tests/coding_agent/test_semantic_sync.py::test_sync_skips_candidate_rejected_and_archived_memories` -- [ ] `tests/coding_agent/test_semantic_sync.py::test_manifest_cache_is_not_sync_authority` +- [x] `tests/coding_agent/test_semantic_sync.py::test_manifest_cache_is_not_sync_authority` - [x] `tests/coding_agent/test_semantic_sync.py::test_full_rebuild_is_idempotent` - [x] `tests/coding_agent/test_semantic_sync.py::test_manual_rebuild_startup_and_event_triggers_share_sync_contract` - [x] `tests/coding_agent/test_semantic_maintenance.py::test_semantic_maintenance_rebuild_scans_topic_store_in_pages` @@ -290,7 +290,7 @@ unchecked items remain required by the ADR. - [x] `tests/ui/test_http_server.py::TestMemoryReviewTransitions::test_semantic_transition_race_value_error_returns_400` - [x] `tests/ui/test_http_server.py::TestMemoryReviewTransitions::test_semantic_sync_value_error_after_transition_returns_500` - [x] `tests/coding_agent/test_memory_index_backend_contract.py::test_fake_backend_satisfies_memory_index_contract` -- [ ] `uv run pytest -q` +- [x] `uv run pytest -q` ## References diff --git a/coding-agent/tests/agentkit/storage/test_protocols.py b/coding-agent/tests/agentkit/storage/test_protocols.py index 3709e296d..39ba2fe3e 100644 --- a/coding-agent/tests/agentkit/storage/test_protocols.py +++ b/coding-agent/tests/agentkit/storage/test_protocols.py @@ -1,8 +1,17 @@ +from dataclasses import fields +import inspect from typing import Any import pytest from agentkit.result.models import ArtifactRef -from agentkit.storage.protocols import ArtifactStore, TapeStore, DocIndex, SessionStore +from agentkit.storage.protocols import ( + ArtifactStore, + DocIndex, + MemoryHit, + MemoryIndex, + SessionStore, + TapeStore, +) class InMemoryTapeStore: @@ -40,6 +49,30 @@ async def delete(self, doc_id: str) -> None: self._docs = [d for d in self._docs if d["id"] != doc_id] +class InMemoryMemoryIndex: + def __init__(self): + self._memories: dict[str, MemoryHit] = {} + + async def upsert( + self, + memory_id: str, + text: str, + metadata: dict[str, Any], + ) -> None: + self._memories[memory_id] = MemoryHit( + memory_id=memory_id, + text=text, + score=1.0, + metadata=metadata, + ) + + async def search(self, query: str, limit: int = 10) -> list[MemoryHit]: + return list(self._memories.values())[:limit] + + async def delete(self, memory_id: str) -> None: + self._memories.pop(memory_id, None) + + class InMemorySessionStore: def __init__(self): self._sessions: dict[str, dict[str, Any]] = {} @@ -164,3 +197,40 @@ async def test_artifact_store_protocol_deletes_refs_for_session(self): assert await store.load_artifact_ref("artifact_patch_1") is None assert await store.list_artifact_refs("session_1") == [] assert await store.load_artifact_ref("artifact_patch_2") == other_ref + + +def test_memory_index_protocol_stays_provider_agnostic(): + idx = InMemoryMemoryIndex() + + assert isinstance(idx, MemoryIndex) + assert _public_protocol_methods(MemoryIndex) == {"upsert", "search", "delete"} + assert list(inspect.signature(MemoryIndex.upsert).parameters) == [ + "self", + "memory_id", + "text", + "metadata", + ] + assert list(inspect.signature(MemoryIndex.search).parameters) == [ + "self", + "query", + "limit", + ] + assert list(inspect.signature(MemoryIndex.delete).parameters) == [ + "self", + "memory_id", + ] + assert [field.name for field in fields(MemoryHit)] == [ + "memory_id", + "text", + "score", + "metadata", + "source_refs", + ] + + +def _public_protocol_methods(protocol: type) -> set[str]: + return { + name + for name, value in protocol.__dict__.items() + if not name.startswith("_") and callable(value) + } diff --git a/coding-agent/tests/coding_agent/test_semantic_sync.py b/coding-agent/tests/coding_agent/test_semantic_sync.py index 609082e3e..d22247c90 100644 --- a/coding-agent/tests/coding_agent/test_semantic_sync.py +++ b/coding-agent/tests/coding_agent/test_semantic_sync.py @@ -214,6 +214,50 @@ async def test_full_rebuild_is_idempotent() -> None: assert await backend.list_ids() == list(expected_ids) +@pytest.mark.asyncio +async def test_manifest_cache_is_not_sync_authority() -> None: + backend, syncer = _syncer() + stale_topic = _topic("topic-stale", title="Stale topic", summary="Delete me") + stale_memory = ReviewedMemoryRecord( + candidate=_candidate( + "memory-stale", + title="Stale memory", + summary="Delete me too", + ), + status="accepted", + ) + authoritative_topic = _topic( + "topic-auth", + title="Auth convention", + summary="Use JWT", + ) + authoritative_memory = ReviewedMemoryRecord( + candidate=_candidate( + "memory-auth", + title="Accepted auth", + summary="JWT rule", + ), + status="accepted", + ) + stale_memory_id = _reviewed_memory_doc_id(stale_memory) + authoritative_memory_id = _reviewed_memory_doc_id(authoritative_memory) + await syncer.sync_topic(stale_topic) + await syncer.sync_reviewed_memory(stale_memory) + + report = await syncer.rebuild([authoritative_topic], [authoritative_memory]) + + expected_indexed_ids = ( + authoritative_memory_id, + "topic-summary:topic-auth:2-9", + ) + assert report.deleted_ids == ( + stale_memory_id, + "topic-summary:topic-stale:2-9", + ) + assert report.indexed_ids == expected_indexed_ids + assert await backend.list_ids() == list(expected_indexed_ids) + + @pytest.mark.asyncio async def test_full_topic_scan_handles_more_than_100_topics() -> None: backend, syncer = _syncer() From faa20ab2fa9f7b9c8999e5ec2b61cfb1b3603ea0 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sat, 27 Jun 2026 17:35:21 +0800 Subject: [PATCH 2/2] test(memory): avoid semantic rebuild order coupling --- coding-agent/tests/coding_agent/test_semantic_sync.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coding-agent/tests/coding_agent/test_semantic_sync.py b/coding-agent/tests/coding_agent/test_semantic_sync.py index d22247c90..c7dbef536 100644 --- a/coding-agent/tests/coding_agent/test_semantic_sync.py +++ b/coding-agent/tests/coding_agent/test_semantic_sync.py @@ -250,12 +250,12 @@ async def test_manifest_cache_is_not_sync_authority() -> None: authoritative_memory_id, "topic-summary:topic-auth:2-9", ) - assert report.deleted_ids == ( + assert set(report.deleted_ids) == { stale_memory_id, "topic-summary:topic-stale:2-9", - ) - assert report.indexed_ids == expected_indexed_ids - assert await backend.list_ids() == list(expected_indexed_ids) + } + assert set(report.indexed_ids) == set(expected_indexed_ids) + assert set(await backend.list_ids()) == set(expected_indexed_ids) @pytest.mark.asyncio