fix: six P2 correctness/robustness cleanups from the SDK review

- legacy call_llm: open AsyncOpenAI via `async with` so the client (and
  its HTTP connection pool) is closed instead of leaked.
- LocalBackend.add_document: fail fast with CollectionNotFoundError when
  the collection doesn't exist, before the expensive parse + LLM index
  (previously the missing FK only tripped at save time, after paying for
  the LLM work). Also raise builtin FileNotFoundError for a missing path
  instead of FileTypeError (which now means only "unsupported extension").
- Collection.query(doc_ids=None): the empty-collection guard now always
  runs — previously it was skipped once PAGEINDEX_EXPERIMENTAL_MULTIDOC
  was set. A single list_documents call serves both the guard and the
  multi-doc warning (no separate call just to decide whether to warn).
- CloudBackend.query_stream: on early consumer break / GeneratorExit,
  signal the background SSE thread to stop and force-close the response,
  so it no longer drains the whole stream in the background.

Adds regression tests for each (client closed, fail-fast on unknown
collection, FileNotFoundError, empty-check under the multidoc env flag,
single list call, early-break thread stop).

Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
This commit is contained in:
mountain 2026-07-07 11:34:13 +08:00
parent 6c948a332b
commit fe36e25773
8 changed files with 128 additions and 19 deletions

View file

@ -219,3 +219,32 @@ def test_query_uses_long_timeout_and_single_attempt(monkeypatch):
backend.query("col", "q", doc_ids=["d1"])
assert len(calls) == 1
assert calls[0]["timeout"] == 300
def test_query_stream_early_break_stops_background_thread(monkeypatch):
"""Consumer breaking early must signal the SSE thread to stop, not let it
drain the whole stream in the background."""
import threading
import time as _real_time # autouse fixture stubs cloud_mod.time.sleep, not this
backend = CloudBackend(api_key="pi-test")
drained_all = threading.Event()
class SlowResponse:
status_code = 200
text = ""
def iter_lines(self, decode_unicode=True):
for i in range(1000):
yield _sse("text", f"chunk{i} ")
_real_time.sleep(0.002) # pace so the consumer reliably breaks first
drained_all.set() # only reached if the thread was NOT stopped
def close(self):
pass
monkeypatch.setattr(cloud_mod.requests, "post", lambda *a, **k: SlowResponse())
async def _run():
async for _ in backend.query_stream("col", "q", doc_ids=["d1"]):
break # consume one event, then abandon
asyncio.run(_run())
assert not drained_all.is_set()