fix(l1): resolve PR #193 backend review findings (1,4,5,6,7,8,9,10)
Server-assigns a uuid4 id to every AI-generated node (Finding 1 showstopper:
nodes had no id but the advance protocol keys on node_id, so ai_build walks
never advanced past question 1). Replaces the hidden {"node_type":"meta"}
walked_path convention with real category/problem_text/pending_node columns on
l1_walk_sessions (migration 61dda4f615c6) — fixes junk proposals + off-by-one
depth cap (Findings 8,9), and pending_node replays the served node on re-mount
(no duplicate paid LLM call). Intake honors explicit flow_id and adhoc=True
(Findings 4,5); flow_proposals.l1_session_id FK -> CASCADE (Finding 6 time
bomb); L1 category GET is owner+admin like PATCH and require_account_owner_or_admin
delegates to User.can_manage_account (Finding 7); escalate falls back to default
recipients + filters deleted_at + warns when empty (Finding 10). Cleanups: dead
ticket_ref removed, IntakeResponse per-outcome validator, unused acknowledged
dropped, escalations partial index, restored a deleted audit assertion.
Full Phase 2A backend set: 110 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,3 +155,73 @@ async def test_escalations_forbidden_for_l1_tech(client: AsyncClient, test_db: A
|
||||
info = await _make_user(client, test_db, email="aib_l1@example.com", account_role="l1_tech")
|
||||
r = await client.get("/api/v1/l1/escalations", headers=info["headers"])
|
||||
assert r.status_code == 403, r.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_intake_with_flow_id_starts_flow_directly(client: AsyncClient, test_db: AsyncSession):
|
||||
"""Finding 4: an explicit flow_id bypasses the matcher and starts that flow."""
|
||||
from app.models.tree import Tree
|
||||
info = await _make_user(client, test_db, email="aib_flowid@example.com", account_role="l1_tech")
|
||||
tree = Tree(
|
||||
id=uuid.uuid4(), name="VPN Flow", account_id=info["account_id"],
|
||||
author_id=info["user_id"], tree_type="troubleshooting",
|
||||
tree_structure={"nodes": [], "edges": []}, visibility="team", status="published",
|
||||
)
|
||||
test_db.add(tree)
|
||||
await test_db.commit()
|
||||
|
||||
# match_or_build must NOT be called when flow_id is supplied.
|
||||
with patch(
|
||||
"app.api.endpoints.l1.match_or_build.match_or_build",
|
||||
new=AsyncMock(side_effect=AssertionError("matcher should be bypassed")),
|
||||
):
|
||||
r = await client.post(
|
||||
"/api/v1/l1/intake",
|
||||
json={"problem_statement": "vpn down", "flow_id": str(tree.id)},
|
||||
headers=info["headers"],
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["outcome"] == "matched"
|
||||
assert body["session_kind"] == "flow"
|
||||
assert body["flow_id"] == str(tree.id)
|
||||
assert body["session_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_intake_adhoc_starts_adhoc_session(client: AsyncClient, test_db: AsyncSession):
|
||||
"""Finding 5: adhoc=True starts a free-form ad-hoc walk (out_of_scope fallback)."""
|
||||
info = await _make_user(client, test_db, email="aib_adhoc@example.com", account_role="l1_tech")
|
||||
with patch(
|
||||
"app.api.endpoints.l1.match_or_build.match_or_build",
|
||||
new=AsyncMock(side_effect=AssertionError("matcher should be bypassed")),
|
||||
):
|
||||
r = await client.post(
|
||||
"/api/v1/l1/intake",
|
||||
json={"problem_statement": "weird thing", "adhoc": True},
|
||||
headers=info["headers"],
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["outcome"] == "adhoc"
|
||||
assert body["session_kind"] == "adhoc"
|
||||
assert body["session_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_intake_build_persists_category_and_problem_text(client: AsyncClient, test_db: AsyncSession):
|
||||
"""Root cause B: build stores category + problem_text on the session (no meta entry)."""
|
||||
info = await _make_user(client, test_db, email="aib_cols@example.com", account_role="l1_tech")
|
||||
with patch(
|
||||
"app.api.endpoints.l1.match_or_build.match_or_build",
|
||||
new=AsyncMock(return_value={"outcome": "build", "session_kind": "ai_build",
|
||||
"category": "printer"}),
|
||||
):
|
||||
r = await client.post("/api/v1/l1/intake",
|
||||
json={"problem_statement": "printer jam"}, headers=info["headers"])
|
||||
sid = r.json()["session_id"]
|
||||
sess = await test_db.get(L1WalkSession, uuid.UUID(sid))
|
||||
assert sess.category == "printer"
|
||||
assert sess.problem_text == "printer jam"
|
||||
# No hidden meta entry smuggled into walked_path.
|
||||
assert sess.walked_path == []
|
||||
|
||||
Reference in New Issue
Block a user