feat(approval): add safe risk disposition workflow

This commit is contained in:
caoxiaozhu
2026-07-16 15:34:58 +08:00
parent ee88a36baf
commit 4940ebc419
64 changed files with 6602 additions and 631 deletions

View File

@@ -28,7 +28,7 @@ from app.models.risk_observation import RiskObservation
MIGRATION_TEST_DATABASE_URL = os.getenv("MIGRATION_TEST_DATABASE_URL", "").strip()
LEGACY_PROBE_TABLE = "legacy_migration_probe_records"
HEAD_REVISION = "20260716_0009"
HEAD_REVISION = "20260716_0011"
SERVER_DIR = Path(__file__).resolve().parents[1]
ALEMBIC_INI_PATH = SERVER_DIR / "alembic.ini"
@@ -357,12 +357,150 @@ def _assert_head_schema(engine: Engine) -> None:
"uq_risk_observations_tenant_key",
("tenant_id", "observation_key"),
)
_assert_unique_constraint(
engine,
"risk_observations",
"uq_risk_observations_tenant_id",
("tenant_id", "id"),
)
_assert_unique_constraint(
engine,
"few_shot_samples",
"uq_few_shot_samples_tenant_key",
("tenant_id", "sample_key"),
)
_assert_unique_constraint(
engine,
"approval_action_ledgers",
"uq_approval_action_ledger_request",
("tenant_id", "actor_id", "request_id"),
)
_assert_indexes(
engine,
"approval_action_ledgers",
{
"ix_approval_action_ledger_claim_action": (
"tenant_id",
"claim_id",
"action",
)
},
)
_assert_check_constraint(
engine,
"approval_action_ledgers",
"ck_approval_action_ledger_action",
)
_assert_check_constraint(
engine,
"approval_action_ledgers",
"ck_approval_action_ledger_completion",
)
_assert_unique_constraint(
engine,
"risk_dispositions",
"uq_risk_dispositions_tenant_observation",
("tenant_id", "observation_id"),
)
_assert_unique_constraint(
engine,
"risk_dispositions",
"uq_risk_dispositions_tenant_id",
("tenant_id", "id"),
)
_assert_composite_foreign_key(
engine,
"risk_dispositions",
("tenant_id", "observation_id"),
"risk_observations",
)
_assert_indexes(
engine,
"risk_dispositions",
{
"ix_risk_dispositions_tenant_lifecycle_due": (
"tenant_id",
"lifecycle_status",
"due_at",
),
"ix_risk_dispositions_assignee": ("tenant_id", "assignee"),
},
)
_assert_check_constraint(
engine,
"risk_dispositions",
"ck_risk_dispositions_adjudication",
)
_assert_check_constraint(
engine,
"risk_dispositions",
"ck_risk_dispositions_lifecycle",
)
_assert_check_constraint(
engine,
"risk_dispositions",
"ck_risk_dispositions_version",
)
_assert_unique_constraint(
engine,
"risk_disposition_events",
"uq_risk_disposition_events_tenant_request",
("tenant_id", "request_id"),
)
_assert_unique_constraint(
engine,
"risk_disposition_events",
"uq_risk_disposition_events_version",
("disposition_id", "version"),
)
_assert_indexes(
engine,
"risk_disposition_events",
{
"ix_risk_disposition_events_disposition_id": ("disposition_id",),
"ix_risk_disposition_events_tenant_observation_time": (
"tenant_id",
"observation_id",
"created_at",
),
},
)
_assert_check_constraint(
engine,
"risk_disposition_events",
"ck_risk_disposition_events_action",
)
_assert_check_constraint(
engine,
"risk_disposition_events",
"ck_risk_disposition_events_version",
)
_assert_composite_foreign_key(
engine,
"risk_disposition_events",
("tenant_id", "disposition_id"),
"risk_dispositions",
)
_assert_composite_foreign_key(
engine,
"risk_disposition_events",
("tenant_id", "observation_id"),
"risk_observations",
)
with engine.connect() as connection:
append_only_trigger_count = int(
connection.scalar(
text(
"SELECT COUNT(*) FROM pg_trigger trigger "
"JOIN pg_class relation ON relation.oid = trigger.tgrelid "
"WHERE relation.relname = 'risk_disposition_events' "
"AND trigger.tgname = 'trg_risk_disposition_events_append_only' "
"AND NOT trigger.tgisinternal"
)
)
or 0
)
assert append_only_trigger_count == 1
_assert_check_constraint(
engine,
"memory_entries",
@@ -972,12 +1110,15 @@ def _create_hierarchical_memory_downgrade_probe(engine: Engine) -> None:
"""
)
)
assert connection.scalar(
text(
"SELECT COUNT(*) FROM memory_entries "
"WHERE id = 'hierarchical-memory-downgrade-probe'"
assert (
connection.scalar(
text(
"SELECT COUNT(*) FROM memory_entries "
"WHERE id = 'hierarchical-memory-downgrade-probe'"
)
)
) == 1
== 1
)
def _create_duplicate_active_organization_memory_probe(engine: Engine) -> None:
@@ -1056,10 +1197,7 @@ def _create_enriched_few_shot_downgrade_probe(engine: Engine) -> None:
def _delete_enriched_few_shot_downgrade_probe(engine: Engine) -> None:
with engine.begin() as connection:
connection.execute(
text(
"DELETE FROM few_shot_samples "
"WHERE id = 'enriched-few-shot-downgrade-probe'"
)
text("DELETE FROM few_shot_samples WHERE id = 'enriched-few-shot-downgrade-probe'")
)
@@ -1116,12 +1254,8 @@ def _create_historical_case_downgrade_probe(engine: Engine) -> None:
def _assert_historical_case_downgrade_probe(engine: Engine) -> None:
inspector = inspect(engine)
risk_columns = {
str(item["name"]) for item in inspector.get_columns("risk_observations")
}
sample_columns = {
str(item["name"]) for item in inspector.get_columns("few_shot_samples")
}
risk_columns = {str(item["name"]) for item in inspector.get_columns("risk_observations")}
sample_columns = {str(item["name"]) for item in inspector.get_columns("few_shot_samples")}
assert "tenant_id" not in risk_columns
assert {"tenant_id", "policy_ref", "rule_version"}.isdisjoint(sample_columns)
assert not any(
@@ -1131,24 +1265,32 @@ def _assert_historical_case_downgrade_probe(engine: Engine) -> None:
for item in inspector.get_foreign_keys("risk_observations")
)
with engine.connect() as connection:
assert connection.scalar(
text(
"SELECT COUNT(*) FROM risk_observations "
"WHERE id = 'historical-downgrade-observation'"
assert (
connection.scalar(
text(
"SELECT COUNT(*) FROM risk_observations "
"WHERE id = 'historical-downgrade-observation'"
)
)
) == 1
assert connection.scalar(
text(
"SELECT COUNT(*) FROM risk_observation_feedback "
"WHERE id = 'historical-downgrade-feedback'"
== 1
)
assert (
connection.scalar(
text(
"SELECT COUNT(*) FROM risk_observation_feedback "
"WHERE id = 'historical-downgrade-feedback'"
)
)
) == 1
assert connection.scalar(
text(
"SELECT COUNT(*) FROM few_shot_samples "
"WHERE id = 'historical-downgrade-sample'"
== 1
)
assert (
connection.scalar(
text(
"SELECT COUNT(*) FROM few_shot_samples WHERE id = 'historical-downgrade-sample'"
)
)
) == 1
== 1
)
def _assert_legacy_sentinel(engine: Engine) -> None:
@@ -1179,6 +1321,10 @@ def _assert_base_schema(engine: Engine) -> None:
("20260716_0008_tenant_safe_historical_cases.py", "downgrade"),
("20260716_0009_organization_memory_idempotency.py", "upgrade"),
("20260716_0009_organization_memory_idempotency.py", "downgrade"),
("20260716_0010_approval_action_protocol.py", "upgrade"),
("20260716_0010_approval_action_protocol.py", "downgrade"),
("20260716_0011_risk_disposition.py", "upgrade"),
("20260716_0011_risk_disposition.py", "downgrade"),
],
)
def test_postgresql_only_migrations_reject_other_dialects_before_mutation(