feat(suggested-fix): add applied_pending status for deferred verification
Some checks failed
Mirror to GitHub / mirror (push) Has been cancelled
CI / backend (pull_request) Successful in 10m43s
CI / frontend (pull_request) Successful in 5m42s
CI / e2e (pull_request) Successful in 11m13s

Engineer applies a fix but can't verify yet (waiting on client power-cycle,
AD replication, async sync). Today the verifying banner forces a synchronous
verdict (worked / didn't / partial) — anything else means leaving the banner
stale or guessing wrong. This adds a fourth outcome that parks the fix in a
non-terminal "Awaiting verification" state with a reason ("waiting on what?")
and exposes it on the chat-anchored banner so the engineer doesn't lose track.

Backend
- New non-terminal status `applied_pending` parallel to `applied_partial`.
- New `pending_reason` column (nullable Text) — the "what are you waiting on?"
  prose, mirrors `partial_notes`. Required when outcome=applied_pending.
- Outcome endpoint allows pending in/out transitions; pending stamps
  applied_at but NOT verified_at (it's parked, not verified).
- Resolution-note + escalation-package prompts handle the new status:
  resolution note frames the fix as provisional; escalation package surfaces
  pending verification as the leading hypothesis with reference to what's
  being waited on.
- Migration: add column + extend status CHECK constraint.

Frontend
- New `BannerMode = 'pending'` + `PendingBanner` component (info-tone,
  parallel to PartialBanner) with worked / didn't / update-reason actions.
- VerifyingBanner overflow menu adds "Waiting to verify…".
- Nudge banner's "Still checking" button now actually records pending with
  a reason, instead of just silencing for the session.
- AssistantChatPage banner-mode derivation maps applied_pending → 'pending'.

Tests: 4 new integration tests covering pending notes requirement, reason
storage + applied_at/verified_at semantics, pending→success transition,
and pending_reason update on re-PATCH.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 16:28:45 -04:00
parent ac42f971fc
commit 00663a4734
10 changed files with 285 additions and 13 deletions

View File

@@ -193,6 +193,95 @@ async def test_applied_at_auto_stamped_on_first_outcome(
assert body["verified_at"] is not None
@pytest.mark.asyncio
async def test_pending_requires_notes(
client: AsyncClient, test_user, auth_headers, test_db
):
"""applied_pending requires notes (the "what are you waiting on?" reason)."""
session_id, fix_id = await _make_session_with_fix(test_db, test_user)
r = await client.patch(
f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome",
headers=auth_headers,
json={"outcome": "applied_pending"},
)
assert r.status_code == 400
assert "notes" in r.text.lower()
@pytest.mark.asyncio
async def test_pending_stores_reason_and_stamps_applied_at(
client: AsyncClient, test_user, auth_headers, test_db
):
"""applied_pending stores notes under pending_reason and stamps applied_at
but NOT verified_at — the fix is parked, not verified."""
session_id, fix_id = await _make_session_with_fix(test_db, test_user)
r = await client.patch(
f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome",
headers=auth_headers,
json={"outcome": "applied_pending", "notes": "client power-cycling router"},
)
assert r.status_code == 200, r.text
body = r.json()
assert body["status"] == "applied_pending"
assert body["pending_reason"] == "client power-cycling router"
assert body["applied_at"] is not None
assert body["verified_at"] is None
assert body["partial_notes"] is None
assert body["failure_reason"] is None
@pytest.mark.asyncio
async def test_pending_to_success_allowed(
client: AsyncClient, test_user, auth_headers, test_db
):
"""pending is non-terminal — engineer can advance to success once verified."""
session_id, fix_id = await _make_session_with_fix(test_db, test_user)
r1 = await client.patch(
f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome",
headers=auth_headers,
json={"outcome": "applied_pending", "notes": "waiting on AD replication"},
)
assert r1.status_code == 200
r2 = await client.patch(
f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome",
headers=auth_headers,
json={"outcome": "applied_success"},
)
assert r2.status_code == 200
body = r2.json()
assert body["status"] == "applied_success"
assert body["verified_at"] is not None
# pending_reason is preserved as audit trail
assert body["pending_reason"] == "waiting on AD replication"
@pytest.mark.asyncio
async def test_pending_reason_can_be_updated(
client: AsyncClient, test_user, auth_headers, test_db
):
"""pending→pending with new notes updates the stored pending_reason."""
session_id, fix_id = await _make_session_with_fix(test_db, test_user)
r1 = await client.patch(
f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome",
json={"outcome": "applied_pending", "notes": "waiting on AD replication"},
headers=auth_headers,
)
assert r1.status_code == 200
assert r1.json()["pending_reason"] == "waiting on AD replication"
r2 = await client.patch(
f"/api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/outcome",
json={"outcome": "applied_pending", "notes": "now waiting on client to confirm login"},
headers=auth_headers,
)
assert r2.status_code == 200
assert r2.json()["pending_reason"] == "now waiting on client to confirm login"
@pytest.mark.asyncio
async def test_failed_outcome_stores_notes_as_failure_reason(
client: AsyncClient, test_user, auth_headers, test_db