Commit 90b3a4a
* 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
- graph_curation
- mcp
- tests
- integration
- unit_test
- mcp
- service
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
343 | 343 | | |
344 | 344 | | |
345 | 345 | | |
346 | | - | |
347 | | - | |
348 | | - | |
| 346 | + | |
349 | 347 | | |
350 | 348 | | |
351 | 349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
518 | 518 | | |
519 | 519 | | |
520 | 520 | | |
521 | | - | |
522 | | - | |
523 | | - | |
| 521 | + | |
524 | 522 | | |
525 | 523 | | |
526 | 524 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
567 | 567 | | |
568 | 568 | | |
569 | 569 | | |
570 | | - | |
571 | | - | |
572 | | - | |
573 | | - | |
574 | | - | |
575 | 570 | | |
576 | 571 | | |
577 | 572 | | |
| |||
580 | 575 | | |
581 | 576 | | |
582 | 577 | | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
583 | 582 | | |
584 | 583 | | |
585 | 584 | | |
0 commit comments