All green. Final report:
**Phase 129 final verification pass — 100% green** (previous work intact; this pass re-verified everything and fixed two pre-existing README inconsistencies)
- Verified on-spec: `LLMClient.embed` per-batch `on_progress` hook; importer `on_chunk_progress` plumbing (text/image/PDF branches; summary embed excluded); `SyncStatus.current_chunk/chunks_total` reset+clear contract; UI `.sync-label-chunk` pinned part with untruncated `title`/`#sync-result`
- `uv run pytest --cov=app -q`: exit 0, ~3041 passed, 0 failed, `app/` coverage **99%** (>90% gate)
- `uv run pytest tests/e2e/test_sync_chunk_progress.py -v --no-cov` (isolated, DB up): **2 passed** (endpoint chunk climb with true `chunk_document` total + UI `· chunk n/m` part)
- `uv run ruff check .`: all checks passed · `uv run pyright`: 0 errors, 0 warnings
- Completion criteria: all 5 ✅ (incl. no behavior change in completed phases — full suite green, CLI/chat no-op paths byte-identical)
- Docs: README.md fixed 2 pre-existing inconsistencies (upload section claimed "unpacks + scans immediately" — phase 90 made it unpack+register only, scan deferred to Sync; troubleshooting claimed "120s client timeout" — actual guard is the 300 s pre-token `TURN_TIMEOUT_MS`). Phase-129 label behavior already documented. AGENTS.md re-verified consistent — no changes
- Removed stray untracked `NOTES.md` (8-byte artifact); no live-infra changes; no commits — all changes left in working tree for the harness
- Next pending phase: `130_chat_answer_clusters`
75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
"""Shared test fakes (no network, deterministic)."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from app.config import Settings
|
|
from app.rag.llm import LLMError
|
|
|
|
|
|
class FakeEmbedder:
|
|
"""Duck-typed stand-in for :class:`app.rag.llm.LLMClient` (see the
|
|
``Embedder`` protocol in :mod:`app.rag.importer`).
|
|
|
|
Returns deterministic vectors of *dim* dimensions; records every call
|
|
so tests can assert batching behaviour. ``chat`` is the deterministic
|
|
``lite``-model stand-in (phase 30): it returns
|
|
``"Summary of <first token of the user content>"`` and raises
|
|
:class:`LLMError` when the content contains the sentinel word
|
|
``SUMMARY-BLOWUP`` (drives the importer's fail-soft summary path).
|
|
``content`` may be a string or a phase-122 multimodal part list
|
|
(``dict[str, Any]`` messages, the ``LLMClient.chat`` shape) — the
|
|
text-summary body above is string-only; a subclass handling the
|
|
multimodal image description (task 03's vision mock) overrides
|
|
``chat``.
|
|
|
|
``embed`` accepts the phase-129 (task 01) ``on_progress`` hook and
|
|
honors it with the real client's single-batch semantics: this fake
|
|
embeds every call in ONE top-level batch, so the hook — when given
|
|
and *texts* is non-empty — fires exactly once with
|
|
``(1, len(texts))`` before the "request"; empty *texts* fires it
|
|
never (the real client's early return). Subclasses that model
|
|
multi-batch transport override :meth:`embed` and own the hook
|
|
sequence themselves (passing ``on_progress=None`` to
|
|
:meth:`super().embed` per sub-batch).
|
|
"""
|
|
|
|
def __init__(self, dim: int = 768) -> None:
|
|
self.dim = dim
|
|
self.settings = Settings(_env_file=None) # pyright: ignore[reportCallIssue]
|
|
self.embed_batches = 0
|
|
self.calls: list[list[str]] = []
|
|
self.chat_calls: list[list[dict[str, str]]] = []
|
|
|
|
async def embed(
|
|
self,
|
|
texts: list[str],
|
|
*,
|
|
on_progress: Callable[[int, int], None] | None = None,
|
|
) -> list[list[float]]:
|
|
# Phase 129 (task 01): the single top-level batch announces its
|
|
# start when a hook is given (empty texts → no call — the real
|
|
# client's early return).
|
|
if texts and on_progress is not None:
|
|
on_progress(1, len(texts))
|
|
self.calls.append(list(texts))
|
|
self.embed_batches += 1
|
|
return [[0.01 * (i % 97) for i in range(self.dim)] for _ in texts]
|
|
|
|
async def embed_one(self, text: str) -> list[float]:
|
|
"""The retrieval/probe convenience path — delegates to :meth:`embed`
|
|
(satisfies the phase-41 pre-sync model probe)."""
|
|
(vec,) = await self.embed([text])
|
|
return vec
|
|
|
|
async def chat(
|
|
self, messages: list[dict[str, Any]], model: str | None = None
|
|
) -> str:
|
|
self.chat_calls.append(list(messages))
|
|
user = next((m["content"] for m in messages if m.get("role") == "user"), "")
|
|
if "SUMMARY-BLOWUP" in user:
|
|
raise LLMError("simulated lite-model failure (SUMMARY-BLOWUP sentinel)")
|
|
first = user.split()
|
|
return "Summary of " + (first[0] if first else "<empty>")
|