94 lines
4.0 KiB
Python
94 lines
4.0 KiB
Python
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.action_catalog import ACTION_CODES, FLOW_V2_BUSINESS_ACTION_CODES, TRIAGE_ACTION_CODES
|
|
from app.blif_flow_v2_projection_service import _projection_value, rebuild_blif_flow_v2_projection
|
|
|
|
|
|
def derived_row(**overrides):
|
|
row = {
|
|
"opportunity_id": "11111111-1111-1111-1111-111111111111",
|
|
"material_process_key": "odoo_sale_name:s00001",
|
|
"canonical_process_id": "11111111-1111-1111-1111-111111111111",
|
|
"raw_v2": {
|
|
"business_state": "PROFORMA_REQUIRED",
|
|
"business_next_action": "CREATE_PROFORMA",
|
|
"diagnostic_status": "clear",
|
|
"confidence": "high",
|
|
"precedence": "business_transition",
|
|
"reason": "Current order intent requires a proforma.",
|
|
},
|
|
"evidence": {
|
|
"latest_relevant_inbound": {"id": "m1", "at": "2026-08-15T00:00:00+00:00"},
|
|
"latest_relevant_outbound": None, "proforma": [], "invoice": [],
|
|
"payment": [], "odoo": [], "reconciliation": [],
|
|
},
|
|
}
|
|
row.update(overrides)
|
|
return row
|
|
|
|
|
|
def test_migration_011_is_additive_reversible_and_does_not_touch_stage():
|
|
up = Path("migrations/011_blif_flow_v2_persistence.sql").read_text()
|
|
down = Path("migrations/011_blif_flow_v2_persistence_down.sql").read_text()
|
|
assert "CREATE TABLE IF NOT EXISTS opportunity_flow_state_v2" in up
|
|
assert "CREATE TABLE IF NOT EXISTS opportunity_flow_transitions" in up
|
|
for column in ("resolution_code", "resolved_at", "resolved_by_event_id", "superseded_by_task_id"):
|
|
assert f"ADD COLUMN IF NOT EXISTS {column}" in up
|
|
assert f"DROP COLUMN IF EXISTS {column}" in down
|
|
assert "DROP TABLE IF EXISTS opportunity_flow_state_v2" in down
|
|
assert "UPDATE opportunities" not in up
|
|
assert "stage =" not in up
|
|
|
|
|
|
def test_flow_v2_actions_are_separate_from_existing_triage_and_runtime_actions():
|
|
assert FLOW_V2_BUSINESS_ACTION_CODES == {
|
|
"CREATE_PROFORMA", "CREATE_INVOICE", "VALIDATE_ODOO_ORDER", "COMPLETE_OPPORTUNITY",
|
|
}
|
|
assert "CREATE_JASMIN_QUOTE" not in FLOW_V2_BUSINESS_ACTION_CODES
|
|
assert FLOW_V2_BUSINESS_ACTION_CODES.isdisjoint(TRIAGE_ACTION_CODES)
|
|
assert FLOW_V2_BUSINESS_ACTION_CODES.isdisjoint(ACTION_CODES)
|
|
|
|
|
|
def test_projection_fingerprint_is_stable_and_excludes_derived_time():
|
|
first = _projection_value(derived_row(), datetime(2026, 8, 15, tzinfo=timezone.utc))
|
|
second = _projection_value(derived_row(), datetime(2026, 8, 16, tzinfo=timezone.utc))
|
|
assert first["source_fingerprint"] == second["source_fingerprint"]
|
|
assert first["business_next_action"] == "CREATE_PROFORMA"
|
|
assert first["evidence_refs"] == [{
|
|
"source": "message", "role": "latest_relevant_inbound", "id": "m1",
|
|
"at": "2026-08-15T00:00:00+00:00",
|
|
}]
|
|
|
|
|
|
def test_duplicate_projection_persists_canonical_material_identity():
|
|
row = derived_row(
|
|
opportunity_id="22222222-2222-2222-2222-222222222222",
|
|
canonical_process_id="11111111-1111-1111-1111-111111111111",
|
|
)
|
|
value = _projection_value(row, datetime.now(timezone.utc))
|
|
assert value["is_duplicate_representation"] is True
|
|
assert value["canonical_opportunity_id"] == "11111111-1111-1111-1111-111111111111"
|
|
assert value["material_process_key"] == "odoo_sale_name:s00001"
|
|
|
|
|
|
def test_off_mode_is_a_noop_without_deriving_or_connecting():
|
|
assert rebuild_blif_flow_v2_projection(mode="off") == {
|
|
"mode": "off", "projection_count": 0, "transitions_written": 0, "disabled": True,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["authoritative", "invalid"])
|
|
def test_unimplemented_modes_fail_closed(mode):
|
|
with pytest.raises(RuntimeError, match="disabled"):
|
|
rebuild_blif_flow_v2_projection(mode=mode, derived_rows=[])
|
|
|
|
|
|
def test_compare_mode_uses_the_same_additive_projection_path():
|
|
from inspect import getsource
|
|
source = getsource(rebuild_blif_flow_v2_projection)
|
|
assert 'selected_mode not in {"shadow", "compare"}' in source
|
|
assert '"mode": selected_mode' in source
|