327 lines
12 KiB
Python
327 lines
12 KiB
Python
"""Opt-in boundary checks for the synthetic-check cleanup migration."""
|
|
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from uuid import UUID, uuid4
|
|
|
|
import psycopg
|
|
import pytest
|
|
from psycopg.rows import dict_row
|
|
|
|
from app.core.config import get_pgconn_string
|
|
from samples.peripheral.seed_identity import SEED_ACTOR_ID
|
|
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.environ.get("OPERATIONAL_TASKS_STORAGE_TEST") != "1",
|
|
reason="explicit PostgreSQL test opt-in required",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def anyio_backend():
|
|
return "asyncio"
|
|
|
|
|
|
def _migration_sql(*names: str) -> str:
|
|
root = Path(__file__).parents[2]
|
|
sql = "\n".join((root / f"resources/sql/{name}").read_text() for name in names)
|
|
return sql.replace("\nBEGIN;", "").replace("\nCOMMIT;", "")
|
|
|
|
|
|
async def _insert_task(
|
|
conn: psycopg.AsyncConnection,
|
|
*,
|
|
project_id: UUID,
|
|
external_id: str,
|
|
event_kind: str,
|
|
title: str,
|
|
task_type: str = "external_work_order",
|
|
) -> UUID:
|
|
task_id = uuid4()
|
|
request_id = uuid4()
|
|
execution_request_id = uuid4()
|
|
now = datetime.now(timezone.utc)
|
|
document = {
|
|
"project_id": str(project_id),
|
|
"task_id": str(task_id),
|
|
"task_type": task_type,
|
|
"workflow": "external-work-order" if task_type == "external_work_order" else event_kind,
|
|
"status": "completed",
|
|
"lifecycle_status": "completed",
|
|
"priority": "normal",
|
|
"revision": 1,
|
|
"source": {
|
|
"kind": "external_system",
|
|
"external_id": external_id,
|
|
"event_kind": event_kind,
|
|
},
|
|
"title": title,
|
|
}
|
|
await conn.execute(
|
|
"""INSERT INTO workbench.operational_tasks (
|
|
project_id,task_id,create_request_id,task_type,workflow,status,priority,
|
|
revision,execution_generation,execution_request_id,created_at,updated_at,
|
|
document,lifecycle_status
|
|
) VALUES (%s,%s,%s,%s,%s,'completed','normal',1,1,%s,%s,%s,%s::jsonb,'completed')""",
|
|
(
|
|
project_id,
|
|
task_id,
|
|
request_id,
|
|
task_type,
|
|
document["workflow"],
|
|
execution_request_id,
|
|
now,
|
|
now,
|
|
json.dumps(document),
|
|
),
|
|
)
|
|
return task_id
|
|
|
|
|
|
async def _insert_source_order(
|
|
conn: psycopg.AsyncConnection,
|
|
*,
|
|
project_id: UUID,
|
|
work_order_id: UUID,
|
|
title: str,
|
|
created_by: UUID = SEED_ACTOR_ID,
|
|
deleted: bool = True,
|
|
) -> None:
|
|
now = datetime.now(timezone.utc)
|
|
document = {
|
|
"project_id": str(project_id),
|
|
"work_order_id": str(work_order_id),
|
|
"revision": 1,
|
|
"title": title,
|
|
"created_by": str(created_by),
|
|
"origin": "synthetic",
|
|
}
|
|
await conn.execute(
|
|
"""INSERT INTO peripheral_sample.work_orders (
|
|
project_id,work_order_id,revision,document,deleted_at
|
|
) VALUES (%s,%s,1,%s::jsonb,%s)""",
|
|
(project_id, work_order_id, json.dumps(document), now if deleted else None),
|
|
)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_cleanup_deletes_only_unlinked_exact_namespace_and_is_idempotent():
|
|
base_migration = _migration_sql(
|
|
"014_operational_tasks.sql",
|
|
"015_workbench_events_and_task_runs.sql",
|
|
)
|
|
cleanup_migration = _migration_sql("018_remove_synthetic_check_task_projections.sql")
|
|
project_id = uuid4()
|
|
conn = await psycopg.AsyncConnection.connect(
|
|
get_pgconn_string(), row_factory=dict_row, connect_timeout=3
|
|
)
|
|
try:
|
|
async with conn.transaction(force_rollback=True):
|
|
await conn.execute(base_migration)
|
|
await conn.execute("CREATE SCHEMA IF NOT EXISTS peripheral_sample")
|
|
await conn.execute(
|
|
"""CREATE TABLE IF NOT EXISTS peripheral_sample.work_orders (
|
|
project_id uuid NOT NULL,
|
|
work_order_id uuid NOT NULL,
|
|
revision bigint NOT NULL,
|
|
document jsonb NOT NULL,
|
|
deleted_at timestamptz,
|
|
PRIMARY KEY (project_id,work_order_id)
|
|
)"""
|
|
)
|
|
removed_order_id = uuid4()
|
|
await _insert_source_order(
|
|
conn,
|
|
project_id=project_id,
|
|
work_order_id=removed_order_id,
|
|
title="合成通信核查",
|
|
deleted=False,
|
|
)
|
|
removed_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheral_sample:{removed_order_id}",
|
|
event_kind="general",
|
|
title=" 合成通信核查",
|
|
)
|
|
deleted_artifact_order_id = uuid4()
|
|
await _insert_source_order(
|
|
conn,
|
|
project_id=project_id,
|
|
work_order_id=deleted_artifact_order_id,
|
|
title="合成联调:预约工单编辑已对齐",
|
|
created_by=uuid4(),
|
|
)
|
|
deleted_user_order_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheral_sample:{deleted_artifact_order_id}",
|
|
event_kind="general",
|
|
title="合成联调:预约工单编辑已对齐",
|
|
)
|
|
suffix_alias_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheral_sample:{removed_order_id}:suffix",
|
|
event_kind="general",
|
|
title="合成但不是规范来源标识",
|
|
)
|
|
similar_namespace_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheralXsample:{uuid4()}",
|
|
event_kind="general",
|
|
title="合成但属于其他命名空间",
|
|
)
|
|
event_order_id = uuid4()
|
|
await _insert_source_order(
|
|
conn,
|
|
project_id=project_id,
|
|
work_order_id=event_order_id,
|
|
title="合成水质投诉",
|
|
)
|
|
event_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheral_sample:{event_order_id}",
|
|
event_kind="water_quality_complaint",
|
|
title="合成水质投诉",
|
|
)
|
|
linked_order_id = uuid4()
|
|
await _insert_source_order(
|
|
conn,
|
|
project_id=project_id,
|
|
work_order_id=linked_order_id,
|
|
title="合成现场采样",
|
|
)
|
|
linked_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheral_sample:{linked_order_id}",
|
|
event_kind="general",
|
|
title="合成现场采样",
|
|
)
|
|
normal_order_id = uuid4()
|
|
await _insert_source_order(
|
|
conn,
|
|
project_id=project_id,
|
|
work_order_id=normal_order_id,
|
|
title="常规预约",
|
|
created_by=uuid4(),
|
|
)
|
|
normal_id = await _insert_task(
|
|
conn,
|
|
project_id=project_id,
|
|
external_id=f"peripheral_sample:{normal_order_id}",
|
|
event_kind="general",
|
|
title="常规预约",
|
|
)
|
|
action_id = uuid4()
|
|
now = datetime.now(timezone.utc)
|
|
action_document = {
|
|
"action_id": str(action_id),
|
|
"version": 1,
|
|
"kind": "create_work_order",
|
|
"approval_status": "approved",
|
|
"execution_status": "succeeded",
|
|
"receipt": {
|
|
"adapter": "peripheral_sample",
|
|
"receipt_id": str(linked_order_id),
|
|
},
|
|
}
|
|
await conn.execute(
|
|
"""INSERT INTO workbench.operational_task_actions (
|
|
project_id,task_id,action_id,version,approval_status,execution_status,
|
|
document,created_at,updated_at
|
|
) VALUES (%s,%s,%s,1,'approved','succeeded',%s::jsonb,%s,%s)""",
|
|
(project_id, linked_id, action_id, json.dumps(action_document), now, now),
|
|
)
|
|
await conn.execute(
|
|
"""INSERT INTO workbench.operational_task_requests (
|
|
project_id,scope,request_id,request_digest,response
|
|
) VALUES (%s,%s,%s,'test',%s::jsonb)""",
|
|
(
|
|
project_id,
|
|
f"task:{removed_id}:create",
|
|
uuid4(),
|
|
json.dumps({"task_id": str(removed_id)}),
|
|
),
|
|
)
|
|
await conn.execute(
|
|
"""INSERT INTO workbench.operational_task_requests (
|
|
project_id,scope,request_id,request_digest,response
|
|
) VALUES (%s,%s,%s,'test',%s::jsonb)""",
|
|
(
|
|
project_id,
|
|
f"notification:{uuid4()}:ack",
|
|
uuid4(),
|
|
json.dumps({"notification": {"task_id": str(removed_id)}}),
|
|
),
|
|
)
|
|
|
|
await conn.execute(cleanup_migration)
|
|
|
|
rows = await (
|
|
await conn.execute(
|
|
"""SELECT task_id FROM workbench.operational_tasks
|
|
WHERE project_id=%s AND task_id=ANY(%s)""",
|
|
(
|
|
project_id,
|
|
[
|
|
removed_id,
|
|
deleted_user_order_id,
|
|
suffix_alias_id,
|
|
similar_namespace_id,
|
|
event_id,
|
|
linked_id,
|
|
normal_id,
|
|
],
|
|
),
|
|
)
|
|
).fetchall()
|
|
assert {row["task_id"] for row in rows} == {
|
|
similar_namespace_id,
|
|
deleted_user_order_id,
|
|
suffix_alias_id,
|
|
event_id,
|
|
linked_id,
|
|
normal_id,
|
|
}
|
|
request_count = await (
|
|
await conn.execute(
|
|
"""SELECT count(*) AS count FROM workbench.operational_task_requests
|
|
WHERE project_id=%s AND (
|
|
response->>'task_id'=%s
|
|
OR response#>>'{notification,task_id}'=%s
|
|
)""",
|
|
(project_id, str(removed_id), str(removed_id)),
|
|
)
|
|
).fetchone()
|
|
assert request_count["count"] == 0
|
|
cleanup_event_count = await (
|
|
await conn.execute(
|
|
"""SELECT count(*) AS count FROM workbench.domain_event_log
|
|
WHERE project_id=%s
|
|
AND metadata->>'reason'='synthetic_check_projection_cleanup'""",
|
|
(project_id,),
|
|
)
|
|
).fetchone()
|
|
assert cleanup_event_count["count"] == 1
|
|
|
|
await conn.execute("DROP TABLE synthetic_check_task_cleanup")
|
|
await conn.execute(cleanup_migration)
|
|
repeated_event_count = await (
|
|
await conn.execute(
|
|
"""SELECT count(*) AS count FROM workbench.domain_event_log
|
|
WHERE project_id=%s
|
|
AND metadata->>'reason'='synthetic_check_projection_cleanup'""",
|
|
(project_id,),
|
|
)
|
|
).fetchone()
|
|
assert repeated_event_count["count"] == 1
|
|
finally:
|
|
await conn.close()
|