feat(investigation): preserve cumulative reference graphs

Accumulate investigation paths and preserve failed attempts across rounds. Keep graph annotations separate from execution state, synchronize the API contract, and consolidate documentation.
This commit is contained in:
2026-11-11 19:10:41 +08:00
parent e47c61c121
commit b853b61a0b
38 changed files with 2445 additions and 147 deletions
@@ -0,0 +1,84 @@
"""Opt-in read-only JSONB probes; no tables or business records are written."""
import asyncio
import os
from copy import deepcopy
import pytest
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from app.infra.db.postgresql import read_queries
@pytest.mark.parametrize("section", ["progress", "report"])
def test_reference_annotations_are_normalized_before_nested_paging(section):
dsn = os.environ.get("MCP_QUERY_TEST_DSN")
if not dsn:
pytest.skip("MCP_QUERY_TEST_DSN is required for a read-only PostgreSQL JSONB probe")
async def check():
statuses = ["running", "completed", "waiting", "waiting_human", "skipped", "cancelled", "failed", None]
original = {"section": section, "data": {
"revision": 3, "execution_generation": 2,
section: {"diagram": {"mermaid": "flowchart TD\nA[原图]", "node_refs": [
{"node_id": f"n{index}", "status": status, "detail": "保留原说明", "evidence_refs": ["e1"]}
for index, status in enumerate(statuses)
] + [{"node_id": "unset"}]}},
"status": "running", "action": {"status": "completed"},
}}
untouched = deepcopy(original)
async with await AsyncConnection.connect(
dsn, row_factory=dict_row, options="-c default_transaction_read_only=on",
) as conn:
async def page(path=(), *, limit=50, offset=0, full=False):
statement, params = read_queries._reference_diagram_projection(
"SELECT %s::jsonb AS snapshot", (Jsonb(original),), section,
)
return await read_queries._snapshot_page(
conn, stored_sql=statement, stored_params=params, data_path=list(path),
limit=limit, offset=offset, mcp_sources=[], full_snapshot=full, max_bytes=8000,
)
scalar = await page([section, "diagram", "node_refs", "0", "status"])
assert scalar["value"] == "reference"
second = await page([section, "diagram", "node_refs"], limit=1, offset=1)
assert second["value"][0]["status"] == "recorded"
assert second["total"] == 9 and second["nested"]
assert second["value"][0]["evidence_refs"]["data_path"] == [section, "diagram", "node_refs", "1", "evidence_refs"]
snapshot = await page(full=True)
nodes = snapshot["data"][section]["diagram"]["node_refs"]
assert [node.get("status") for node in nodes] == [
"reference", "recorded", "needs_input", "needs_input", "not_taken", "not_taken", "failed", None, None,
]
assert "status" not in nodes[-1]
assert nodes[6]["detail"] == "保留原说明" and nodes[6]["evidence_refs"] == ["e1"]
assert snapshot["data"]["status"] == "running"
assert snapshot["data"]["action"]["status"] == "completed"
assert snapshot["data"][section]["diagram"]["mermaid"] == original["data"][section]["diagram"]["mermaid"]
assert original == untouched
asyncio.run(check())
@pytest.mark.parametrize("diagram", [None, {}, {"node_refs": []}])
def test_reference_projection_preserves_empty_and_missing_legacy_diagrams(diagram):
dsn = os.environ.get("MCP_QUERY_TEST_DSN")
if not dsn:
pytest.skip("MCP_QUERY_TEST_DSN is required for a read-only PostgreSQL JSONB probe")
async def check():
original = {"section": "progress", "data": {"progress": {"diagram": diagram}, "status": "running"}}
async with await AsyncConnection.connect(
dsn, row_factory=dict_row, options="-c default_transaction_read_only=on",
) as conn:
statement, params = read_queries._reference_diagram_projection(
"SELECT %s::jsonb AS snapshot", (Jsonb(original),), "progress",
)
result = await read_queries._snapshot_page(
conn, stored_sql=statement, stored_params=params, data_path=[],
limit=20, offset=0, mcp_sources=[], full_snapshot=True,
)
assert result == original
asyncio.run(check())