fix(rag): convert RAGChunk to dict in _rag_search_result to match type contract

The RAG engine's search() returns List[RAGChunk] (dataclass instances),
but _rag_search_result's return type is Result[list[Metadata]] (a list
of dicts). The previous code returned the RAGChunks as-is, then the
caller in _handle_request_event did chunk["metadata"] (dict access
on a dataclass) which raised TypeError. The exception was silently
swallowed by the submit_io worker, leaving ai_status stuck at
sending... for the full 50-second test poll before failing.

Two surgical changes:
1. _rag_search_result: convert RAGChunk to dict via to_dict() (with a
   hasattr guard for tests that return dicts directly). Matches the
   function's documented return type.
2. _handle_request_event: use isinstance guards + dict.get() on the
   chunk fields. Defensive against the type mismatch and matches the
   dict contract.

The test fix (unique collection name + workspace-targeted cleanup)
is the test-side complement that prevents the dim-mismatch path from
being hit in batched runs.

Verified: 4 consecutive PASS runs of test_rag_phase4_final_verify in
isolation (7-8s each). 25/26 RAG tests pass; the one remaining
failure (test_rag_collection_dim_mismatch_recreates_collection) is a
pre-existing regression from commit 24e93a75 which changed the dim
check from delete_collection to shutil.rmtree without updating the
test mock setup. Out of scope for this fix.
This commit is contained in:
ed
2026-06-27 20:58:36 -04:00
parent d26a2f9fce
commit 4d2a6666a4
2 changed files with 33 additions and 25 deletions
+22 -17
View File
@@ -17,23 +17,28 @@ def test_phase4_final_verify(live_gui, live_gui_workspace):
client = api_hook_client.ApiHookClient()
assert client.wait_for_server(timeout=15), "Hook server did not start"
# Clean the chroma cache BEFORE the test starts. In batched live_gui
# context, the live_gui subprocess is shared across many tests, and
# prior tests leave chroma state at the controller's project root
# (e.g. C:\projects\manual_slop\tests\artifacts\.slop_cache\chroma_test_*).
# The dim-mismatch rmtree in rag_engine._validate_collection_dim
# fails on Windows with WinError 32 (file in use), leaving a stale
# locked collection that PersistentClient can't open. Wipe the
# relevant cache dirs proactively so the test starts clean.
_workspace_root = str(live_gui_workspace.parent if live_gui_workspace else Path.cwd())
stale_path = Path(_workspace_root) / ".slop_cache"
if stale_path.exists():
for col_dir in stale_path.iterdir():
# Use a unique collection name per test invocation. The RAG engine
# stores its chromadb collection at
# <base_dir>/.slop_cache/chroma_<collection_name>. The live_gui
# subprocess holds a Windows file lock on the chroma sqlite file
# from the prior test (WinError 32), so the rmtree in
# _validate_collection_dim is a no-op on locked files, leaving the
# collection with a stale dim (e.g. 3072 from a prior Gemini
# embedding pass) that breaks subsequent searches (hangs on
# dim mismatch). A unique name avoids the collision entirely so the
# dim check is a no-op and the test gets a fresh collection.
_collection_name = f"test_final_verify_{int(time.time() * 1000)}"
# Best-effort cleanup of the workspace's .slop_cache (where the
# chroma collection actually lives). ignore_errors=True handles
# WinError 32 from the live_gui subprocess's file lock. This is
# belt-and-suspenders; the unique collection name above is the
# primary defense.
_slop_cache = Path(live_gui_workspace) / ".slop_cache"
if _slop_cache.exists():
for col_dir in _slop_cache.iterdir():
if col_dir.is_dir() and col_dir.name.startswith("chroma_"):
try:
shutil.rmtree(col_dir)
except Exception:
pass
shutil.rmtree(col_dir, ignore_errors=True)
# 1. Setup mock project data
workspace_dir = live_gui_workspace
@@ -45,7 +50,7 @@ def test_phase4_final_verify(live_gui, live_gui_workspace):
try:
# 2. Configure project through Hook API
client.set_value('rag_collection_name', 'test_final_verify')
client.set_value('rag_collection_name', _collection_name)
client.set_value('files', ['final_test_1.txt', 'final_test_2.py'])
client.set_value('rag_enabled', True)
client.set_value('rag_source', 'chroma')