feat(l1): l1_session_service record_step + update_notes

record_step appends to walked_path JSONB and advances current_node_id
on flow/proposal walks; refuses adhoc sessions. update_notes replaces
walk_notes (used by adhoc walks for debounced autosave); 256KB size cap
to prevent unbounded JSONB growth. Both reject non-active sessions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 13:20:20 -04:00
parent 6e7c4afc7d
commit e803a78ded
2 changed files with 259 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
"""Tests for l1_session_service start_* functions (T12)."""
"""Tests for l1_session_service start_* functions (T12) and record_step/update_notes (T13)."""
import uuid
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,6 +13,8 @@ from app.services.l1_session_service import (
start_proposal_session,
start_adhoc_session,
_resolve_acting_as,
record_step,
update_notes,
)
@@ -219,3 +221,201 @@ async def test_engineer_with_coverage_gets_acting_as_tag(test_db: AsyncSession):
ticket_kind="internal",
)
assert session.acting_as == "l1_coverage"
# ---------------------------------------------------------------------------
# T13: record_step and update_notes tests
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_record_step_appends_to_walked_path(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
session = await start_flow_session(
test_db,
account_id=account.id,
user=l1,
flow_id=tree.id,
ticket_id="t1",
ticket_kind="internal",
)
updated = await record_step(
test_db,
session_id=session.id,
node_id="n1",
question="Is the device powered on?",
answer="yes",
)
assert len(updated.walked_path) == 1
assert updated.walked_path[0] == {
"node_id": "n1",
"question": "Is the device powered on?",
"answer": "yes",
"l1_note": None,
}
assert updated.current_node_id == "n1"
assert updated.last_step_at is not None
@pytest.mark.asyncio
async def test_record_step_two_sequential_steps_accumulate(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
session = await start_flow_session(
test_db,
account_id=account.id,
user=l1,
flow_id=tree.id,
ticket_id="t2",
ticket_kind="internal",
)
await record_step(
test_db,
session_id=session.id,
node_id="n1",
question="Step 1?",
answer="yes",
)
updated = await record_step(
test_db,
session_id=session.id,
node_id="n2",
question="Step 2?",
answer="no",
)
assert len(updated.walked_path) == 2
assert updated.walked_path[0]["node_id"] == "n1"
assert updated.walked_path[1]["node_id"] == "n2"
assert updated.current_node_id == "n2"
@pytest.mark.asyncio
async def test_record_step_blocks_adhoc(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
session = await start_adhoc_session(
test_db,
account_id=account.id,
user=l1,
ticket_id="t3",
ticket_kind="internal",
)
with pytest.raises(ValueError, match="adhoc"):
await record_step(
test_db,
session_id=session.id,
node_id="n1",
question="Q?",
answer="yes",
)
@pytest.mark.asyncio
async def test_record_step_blocks_inactive_session(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
session = await start_flow_session(
test_db,
account_id=account.id,
user=l1,
flow_id=tree.id,
ticket_id="t4",
ticket_kind="internal",
)
# Manually mark resolved to simulate inactive state
session.status = "resolved"
await test_db.flush()
with pytest.raises(ValueError, match="not active"):
await record_step(
test_db,
session_id=session.id,
node_id="n1",
question="Q?",
answer="yes",
)
@pytest.mark.asyncio
async def test_record_step_includes_note_when_provided(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
session = await start_flow_session(
test_db,
account_id=account.id,
user=l1,
flow_id=tree.id,
ticket_id="t5",
ticket_kind="internal",
)
updated = await record_step(
test_db,
session_id=session.id,
node_id="n1",
question="Q?",
answer="yes",
note="Customer mentioned it started yesterday",
)
assert updated.walked_path[0]["l1_note"] == "Customer mentioned it started yesterday"
@pytest.mark.asyncio
async def test_update_notes_replaces_walk_notes(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
session = await start_adhoc_session(
test_db,
account_id=account.id,
user=l1,
ticket_id="t6",
ticket_kind="internal",
)
new_notes = [{"timestamp": "2026-05-28T10:00:00Z", "content": "Customer rebooted"}]
updated = await update_notes(test_db, session_id=session.id, notes=new_notes)
assert updated.walk_notes == new_notes
assert updated.last_step_at is not None
@pytest.mark.asyncio
async def test_update_notes_raises_if_session_not_found(test_db: AsyncSession):
missing_id = uuid.uuid4()
with pytest.raises(ValueError, match="not found"):
await update_notes(test_db, session_id=missing_id, notes=[])
@pytest.mark.asyncio
async def test_update_notes_raises_if_session_not_active(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
session = await start_adhoc_session(
test_db,
account_id=account.id,
user=l1,
ticket_id="t7",
ticket_kind="internal",
)
session.status = "escalated"
await test_db.flush()
with pytest.raises(ValueError, match="not active"):
await update_notes(test_db, session_id=session.id, notes=[])
@pytest.mark.asyncio
async def test_update_notes_size_cap(test_db: AsyncSession):
account = await _make_account(test_db)
l1 = await _make_user(test_db, account_id=account.id)
session = await start_adhoc_session(
test_db,
account_id=account.id,
user=l1,
ticket_id="t8",
ticket_kind="internal",
)
# Create a notes payload larger than 256KB
big_content = "x" * (256 * 1024 + 100)
notes = [{"timestamp": "2026-05-28T10:00:00Z", "content": big_content}]
with pytest.raises(ValueError, match="256KB"):
await update_notes(test_db, session_id=session.id, notes=notes)