feat(pilot): [FIX_OUTCOME] marker parser + AI outcome proposal
The AI emits [FIX_OUTCOME] when the engineer indicates in chat that a prior suggested fix worked, didn't work, or was partially applied. The marker writes to session_suggested_fixes.ai_outcome_proposal (JSONB), which the frontend surfaces as a "confirm outcome?" banner. The status column is only updated when the engineer clicks confirm (via PATCH /outcome endpoint from Task 3). Placeholder-only system prompt wiring comes in Task 5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
backend/tests/test_fix_outcome_marker.py
Normal file
91
backend/tests/test_fix_outcome_marker.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Unit tests for the [FIX_OUTCOME] marker parser."""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.services.unified_chat_service import _parse_fix_outcome_marker
|
||||
|
||||
|
||||
def test_parses_success_outcome():
|
||||
ai = (
|
||||
"Great news — that confirms the root cause.\n\n"
|
||||
"[FIX_OUTCOME]\n"
|
||||
'{"fix_id":"11111111-1111-1111-1111-111111111111",'
|
||||
'"outcome":"success","reason":"user said the fix worked"}\n'
|
||||
"[/FIX_OUTCOME]\n"
|
||||
)
|
||||
cleaned, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert "[FIX_OUTCOME]" not in cleaned
|
||||
assert "confirms the root cause" in cleaned
|
||||
assert parsed == {
|
||||
"fix_id": "11111111-1111-1111-1111-111111111111",
|
||||
"outcome": "success",
|
||||
"reason": "user said the fix worked",
|
||||
}
|
||||
|
||||
|
||||
def test_parses_failure_outcome():
|
||||
ai = (
|
||||
"[FIX_OUTCOME]\n"
|
||||
'{"fix_id":"22222222-2222-2222-2222-222222222222",'
|
||||
'"outcome":"failure","reason":"user reports still broken"}\n'
|
||||
"[/FIX_OUTCOME]"
|
||||
)
|
||||
cleaned, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert "[FIX_OUTCOME]" not in cleaned
|
||||
assert parsed["outcome"] == "failure"
|
||||
|
||||
|
||||
def test_missing_marker_returns_none():
|
||||
ai = "no marker here"
|
||||
cleaned, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert cleaned == ai
|
||||
assert parsed is None
|
||||
|
||||
|
||||
def test_invalid_json_is_dropped():
|
||||
ai = "[FIX_OUTCOME]\nnot-json\n[/FIX_OUTCOME]"
|
||||
cleaned, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert "[FIX_OUTCOME]" not in cleaned
|
||||
assert parsed is None
|
||||
|
||||
|
||||
def test_unknown_outcome_rejected():
|
||||
ai = (
|
||||
"[FIX_OUTCOME]\n"
|
||||
'{"fix_id":"33333333-3333-3333-3333-333333333333",'
|
||||
'"outcome":"maybe","reason":"x"}\n'
|
||||
"[/FIX_OUTCOME]"
|
||||
)
|
||||
_, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert parsed is None
|
||||
|
||||
|
||||
def test_last_block_wins_when_multiple():
|
||||
ai = (
|
||||
"[FIX_OUTCOME]\n"
|
||||
'{"fix_id":"44444444-4444-4444-4444-444444444444",'
|
||||
'"outcome":"failure","reason":"first"}\n'
|
||||
"[/FIX_OUTCOME]\n"
|
||||
"[FIX_OUTCOME]\n"
|
||||
'{"fix_id":"55555555-5555-5555-5555-555555555555",'
|
||||
'"outcome":"success","reason":"second"}\n'
|
||||
"[/FIX_OUTCOME]"
|
||||
)
|
||||
cleaned, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert "[FIX_OUTCOME]" not in cleaned
|
||||
assert parsed["fix_id"] == "55555555-5555-5555-5555-555555555555"
|
||||
assert parsed["outcome"] == "success"
|
||||
|
||||
|
||||
def test_parses_partial_outcome():
|
||||
ai = (
|
||||
"[FIX_OUTCOME]\n"
|
||||
'{"fix_id":"66666666-6666-6666-6666-666666666666",'
|
||||
'"outcome":"partial","reason":"user ran cred clear only"}\n'
|
||||
"[/FIX_OUTCOME]"
|
||||
)
|
||||
_, parsed = _parse_fix_outcome_marker(ai)
|
||||
assert parsed == {
|
||||
"fix_id": "66666666-6666-6666-6666-666666666666",
|
||||
"outcome": "partial",
|
||||
"reason": "user ran cred clear only",
|
||||
}
|
||||
Reference in New Issue
Block a user