Skip to content

Commit 90b3a4a

Browse files
earayuclaude
andauthored
test(w7-task#11): scaffold Wave 7 e2e narrative — 9-step skeleton, bodies pending task #8 wiring (#1760)
* test(w7-task#11): scaffold Wave 7 e2e narrative — 9-step skeleton, bodies pending task #8 wiring Per architect ratify msg=a0ba75da + huangheng CR rationale msg=0b48af2b, task #11 is the integration safety net for the three pieces of wiring that task #8 introduces: 1. ``LineageGraphStoreWithAliasRedirect`` decorator wrap in ``worker_factory._build_lineage_graph_store``. 2. REST ``POST /collections/{cid}/graphs/nodes/merge`` cutover from legacy ``GraphIndexService.merge_entities`` to the new ``GraphCurationService.merge_entities``. 3. ``retrieval/pipeline.py:_graph_search`` cutover to vector recall. Unit tests can prove each wiring CALL exists; only the end-to-end narrative validates the *behaviour*. Per huangheng's CR rationale: "grep 只能 catch wiring 是否存在, 不能 catch wiring 是否 produce 期望行为". This PR ships only the scaffold: * File ``tests/integration/test_w7_e2e_graph_recall_and_merge.py`` with 9 narrative test functions, each documenting its step shape. * Module-level ``pytestmark`` skips the file behind two gates: - ``_TASK8_WIRING_LANDED = False`` flips True alongside the task #8 PR (or in this PR's own follow-up commit) to enable bodies. - ``RUN_W7_E2E_NARRATIVE=1`` env gate so local-dev pytest stays fast; CI Wave 7 lane sets it once task #8 wiring is alive (mirrors the Wave 4 ``test_full_indexing_pipeline.py`` Layer 2 gate pattern). * ``pytest -v`` collects 9 tests, all skip cleanly. Sequence (per huangheng + architect): task #8 merge → flip ``_TASK8_WIRING_LANDED = True`` + fill bodies + run Layer 2 → push final PR → merge BEFORE task #10 close-out (so a regression introduced by deleting legacy is caught while rollback is still cheap). Step-9 fold-in (per architect msg=a0ba75da): failure-mode integration — simulate compactor LLM down → re-sync, verify Wave 6 graceful degrade preserved. Step-8 W8-3 trigger pin (per huangheng msg=0b48af2b): ``GET /collections/{cid}/graphs/nodes/{alicia_id}`` returns 404 today (read-side alias resolution deferred to Wave 8 W8-3). When W8-3 ships the assertion flips to "200 with Alice payload" — the trigger condition is physically pinned in the repo. * test(w7-task#11): flip _TASK8_WIRING_LANDED + concrete API pointers (+ post-#1762 lint sweep) Task #8 PR #1762 merged 2026-04-28 (commit 08d9d3b). Updates to this scaffold + a small repo-wide lint sweep that pre-commit caught. This PR's substantive change (single test file): * ``_TASK8_WIRING_LANDED`` flipped True; module-level wiring skip removed. The file now gates only on ``RUN_W7_E2E_NARRATIVE=1`` (Layer 2 env gate) so local-dev pytest stays fast while CI Wave 7 lane can run bodies. * Per-step skip messages rewritten as concrete API pointers — each step names the exact merged surface its body must call (REST path, service method, repository class), so the body-fill follow-up can grep straight to the right spot. Bodies still pending implementation — they need a running stack (Postgres + Qdrant + Redis + ES + LLM provider keys). PR remains draft until bodies are filled + Layer 2 has run green at least once. Bundled lint cleanup (ruff format only, zero behavior change): * aperag/mcp/server.py — import block re-sort (post #1759 squash ordering drift). * aperag/domains/knowledge_graph/service.py * aperag/graph_curation/lineage_merge.py * tests/unit_test/mcp/test_graph_tools.py * tests/unit_test/service/test_graph_search_service_layer.py The four ``aperag/`` + ``tests/unit_test/`` files above all came in via the PR #1762 squash merge moments ago and trip ``ruff format --check``. Bundling here so this PR can land cleanly through pre-commit; the alternative is a separate "format-only" PR for code that just merged, which is more PR overhead than value. * test(w7-task#11): fill 9-step e2e narrative bodies Per architect msg=fa045579 routing — chenyexuan owns body fill on 冬柏's branch. Bodies exercise the three task #8 wiring points behaviourally so a regression that survives unit-level grep is caught here while rollback is still cheap. Approach * Service-layer + DB-direct narrative (not HTTP through FastAPI). The HTTP shape is pinned by the task #7 / task #8 route unit tests; narrative-correctness lives in the service-layer behaviour the routes delegate to (``GraphService.merge_entities``, ``GraphService.get_entity_detail``, ``GraphSearchService.search_entities``, ``LineageGraphStoreWithAliasRedirect``, ``AliasMapRepository.resolve_canonical``). This keeps the test fixture-light: no live FastAPI, no auth bootstrap, no Collection ORM seeding — just module-scoped store / decorator / repo fixtures bound to a synthetic collection_id. * Pre-extract entities (Alice / Bob / Acme / Alicia) via direct ``upsert_entity_with_lineage`` so the narrative does not depend on a non-deterministic LLM extractor's output. The wiring under test is the storage + vector + alias paths, not the extractor prompt. * Module-scoped state via the lineage store + alias_map (production data plane) so step N+1 sees step N's side-effects without Python globals. Step coverage step 1 seed Alice / Bob / Acme via raw inner store; assert ``get_entity`` returns them post-write. step 2 build the per-collection ``GraphSearchService`` factory; re-assert step 1 entities survived (snapshot-diff invariant — Wave 4 §C.3). step 3 ``search_entities("Alice", top_k=5)`` shape contract: list of ``EntityWithLineage`` (graceful-degrade on empty per Wave 6 keyword-path convention). step 4 call ``GraphService.merge_entities(target="Alice", sources=["Alicia"])`` (the function the REST route now delegates to). Asserts backward-compat shape including ``edges_redirected/edges_collapsed == 0`` (Wave 7 §K.12 invariant #9 / cuiwenbo schema diff lock). step 5 ``AliasMapRepository.resolve_canonical("Alicia")`` → ``"Alice"``. Self-canonical case ``Alice → Alice`` also asserted. step 6 the critical inseparability gate (wiring #1). Re-extract ``Alicia`` through the *decorated* store. Assert ``get_entity("Alicia") is None`` (silent redirect) and the new lineage member shows up under ``"Alice"``. step 7 ``search_entities("Alice")`` round-trip post-merge — alias name MUST NOT leak into the recall result (the alias is a payload-redirected write, not a separate vector point). step 8 W8-3 trigger pin. ``GraphService.get_entity_detail ("Alicia")`` returns ``None`` today because read-side alias resolution is deferred to Wave 8. The assertion message documents the flip when W8-3 ships ("flip to ``assert result.name == 'Alice'``"), so the trigger condition is mechanically pinned in the repo (per architect msg=a0ba75da + huangheng msg=0b48af2b). step 9 failure-mode fold-in. Patch ``GraphIndexCompactor.compact_if_oversized`` to raise. Re-trigger the merge. Assert the merge still completes with a unified description (graceful degrade — compaction failure is non-fatal per Wave 6 ``test_w7_phase3_*_failure_non_fatal`` unit invariant). Layer 2 gating preserved — tests skip cleanly under default ``pytest`` (no ``RUN_W7_E2E_NARRATIVE=1``); CI Wave 7 lane flips it on once the e2e-http-compose stack provides Postgres / Redis / Qdrant / Elasticsearch + provider keys. Local verification * ``uv run pytest tests/integration/test_w7_e2e_graph_recall_and_merge.py --collect-only`` → 9 collected. * ``uv run pytest`` (default gate off) → 9 skipped. * ``uv run ruff check`` clean. Bodies use real production APIs (no mock-of-mock) so end-to-end verification on the e2e-http-compose lane is what 冬柏 retains for the un-draft toggle (per msg=fa045579). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(w7-task#11): apply 冬柏 review — assert-message W8-3 pin + drop self-canonical Per @冬柏 msg=8f488513 + PM msg=a769156c review items: * **Q1 step 8 W8-3 trigger pin**: lift the future-flip instruction from a docstring comment into the ``assert ... , ""`` message so the Wave 8 implementer's traceback names the exact line to flip (``flip to: assert result is not None and result.name == 'Alice'``). Comments are easy to grep-miss; assert messages travel with the failure. * **Q3 step 5 self-canonical case**: drop the ``Alice → Alice`` resolve_canonical assertion. It is degenerate corner-case coverage that already lives in unit-level ``test_alias_map_resolve_canonical*``; carrying it on the e2e narrative dilutes the merge-journey storyline without adding integration value. Q2 (step 9 monkeypatch target ``GraphIndexCompactor.compact_if_oversized``) acknowledged unchanged — already aligned with the unit-level ``test_w7_phase3_*_failure_non_fatal`` pattern. Local verify: 9 collected + 9 skipped under default gate; ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(w7-task#11): ruff format on chenyexuan iteration commit --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 985b7d5 commit 90b3a4a

6 files changed

Lines changed: 597 additions & 21 deletions

File tree

aperag/domains/knowledge_graph/service.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,7 @@ async def get_entity_detail(
343343

344344
db_collection = await self._get_and_validate_collection(user_id, collection_id)
345345
backend_type = _resolve_graph_backend_type(db_collection)
346-
store = await asyncio.to_thread(
347-
_build_lineage_graph_store, backend_type=backend_type, collection=db_collection
348-
)
346+
store = await asyncio.to_thread(_build_lineage_graph_store, backend_type=backend_type, collection=db_collection)
349347
entity = await store.get_entity(entity_name)
350348
if entity is None:
351349
return None

aperag/graph_curation/lineage_merge.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,9 +518,7 @@ def build_lineage_entity_merger_for(collection: Any) -> "LineageEntityMerger":
518518
try:
519519
llm = build_collection_llm_callable(collection)
520520
except Exception as exc: # noqa: BLE001 — surface as factory failure.
521-
raise WorkerFactoryError(
522-
f"merge_entities: LLM not configured for collection {collection.id!r}: {exc}"
523-
) from exc
521+
raise WorkerFactoryError(f"merge_entities: LLM not configured for collection {collection.id!r}: {exc}") from exc
524522

525523
# Compactor is best-effort — the merger still runs without it
526524
# (description stays uncompacted; embedding falls back to unified).

aperag/mcp/server.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -567,11 +567,6 @@ def get_api_key() -> str:
567567
# existing ``aperag.mcp.server.web_search`` access path for backward
568568
# compatibility with callers (e.g. ``tests/unit_test/test_mcp_server.py``)
569569
# that read attributes off the server module directly.
570-
from aperag.mcp.tools.search_fulltext import fulltext_search # noqa: E402, F401
571-
from aperag.mcp.tools.search_graph import graph_search # noqa: E402, F401
572-
from aperag.mcp.tools.search_vector import vector_search # noqa: E402, F401
573-
from aperag.mcp.tools.search_web import web_search # noqa: E402, F401
574-
575570
# Wave 7 §K.12.6 — graph entity search / subgraph expand / detail.
576571
# Importing the module is what registers the three ``@mcp_server.tool``
577572
# decorators with the FastMCP instance above.
@@ -580,6 +575,10 @@ def get_api_key() -> str:
580575
get_entity_detail,
581576
query_graph_entities,
582577
)
578+
from aperag.mcp.tools.search_fulltext import fulltext_search # noqa: E402, F401
579+
from aperag.mcp.tools.search_graph import graph_search # noqa: E402, F401
580+
from aperag.mcp.tools.search_vector import vector_search # noqa: E402, F401
581+
from aperag.mcp.tools.search_web import web_search # noqa: E402, F401
583582

584583
# Export the server instance
585584
__all__ = ["mcp_server"]

0 commit comments

Comments
 (0)