fix(automation): bind backup approvals to full owner
This commit is contained in:
@@ -23,9 +23,7 @@ def _explicit_local_test_database_url() -> str:
|
||||
if not database_url:
|
||||
pytest.skip("BACKUP_RESTORE_TEST_DATABASE_URL is required for real PG tests")
|
||||
|
||||
parsed = urlsplit(
|
||||
database_url.replace("postgresql+asyncpg://", "postgresql://", 1)
|
||||
)
|
||||
parsed = urlsplit(database_url.replace("postgresql+asyncpg://", "postgresql://", 1))
|
||||
if parsed.hostname not in {None, "", "localhost", "127.0.0.1", "::1"}:
|
||||
pytest.fail("backup/restore PG regression must target a local database")
|
||||
database_name = parsed.path.rsplit("/", 1)[-1].strip().lower()
|
||||
@@ -45,7 +43,8 @@ async def test_backfill_run_inserts_satisfy_no_default_cost_column() -> None:
|
||||
async with engine.connect() as connection:
|
||||
transaction = await connection.begin()
|
||||
try:
|
||||
await connection.execute(text("""
|
||||
await connection.execute(
|
||||
text("""
|
||||
CREATE TEMP TABLE awooop_run_state (
|
||||
run_id UUID PRIMARY KEY,
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
@@ -69,7 +68,8 @@ async def test_backfill_run_inserts_satisfy_no_default_cost_column() -> None:
|
||||
timeout_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
) ON COMMIT DROP
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
await connection.execute(
|
||||
backfill._CURSOR_INSERT_SQL,
|
||||
{
|
||||
@@ -77,9 +77,7 @@ async def test_backfill_run_inserts_satisfy_no_default_cost_column() -> None:
|
||||
"project_id": "awoooi",
|
||||
"agent_id": backfill.BACKFILL_CURSOR_AGENT_ID,
|
||||
"input_sha256": "a" * 64,
|
||||
"error_detail": json.dumps(
|
||||
backfill._initial_cursor("awoooi")
|
||||
),
|
||||
"error_detail": json.dumps(backfill._initial_cursor("awoooi")),
|
||||
},
|
||||
)
|
||||
await connection.execute(
|
||||
@@ -127,19 +125,23 @@ async def test_backfill_run_inserts_satisfy_no_default_cost_column() -> None:
|
||||
assert finished.scalar_one() == item_run_id
|
||||
|
||||
rows = (
|
||||
await connection.execute(text("""
|
||||
(
|
||||
await connection.execute(
|
||||
text("""
|
||||
SELECT run_id, cost_usd
|
||||
FROM awooop_run_state
|
||||
ORDER BY run_id
|
||||
"""))
|
||||
).mappings().all()
|
||||
""")
|
||||
)
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
assert {row["run_id"] for row in rows} == {
|
||||
cursor_run_id,
|
||||
item_run_id,
|
||||
}
|
||||
assert {row["cost_usd"] for row in rows} == {
|
||||
Decimal("0.0000")
|
||||
}
|
||||
assert {row["cost_usd"] for row in rows} == {Decimal("0.0000")}
|
||||
finally:
|
||||
await transaction.rollback()
|
||||
finally:
|
||||
@@ -166,12 +168,14 @@ async def test_domain_separated_fingerprints_avoid_cross_domain_pg_collision() -
|
||||
async with engine.connect() as connection:
|
||||
transaction = await connection.begin()
|
||||
try:
|
||||
await connection.execute(text("""
|
||||
await connection.execute(
|
||||
text("""
|
||||
CREATE TEMP TABLE approval_records (
|
||||
source_identity TEXT NOT NULL,
|
||||
fingerprint VARCHAR(64) NOT NULL UNIQUE
|
||||
) ON COMMIT DROP
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
await connection.execute(
|
||||
text("""
|
||||
INSERT INTO approval_records (
|
||||
@@ -186,12 +190,18 @@ async def test_domain_separated_fingerprints_avoid_cross_domain_pg_collision() -
|
||||
},
|
||||
)
|
||||
persisted = (
|
||||
await connection.execute(text("""
|
||||
(
|
||||
await connection.execute(
|
||||
text("""
|
||||
SELECT source_identity, fingerprint
|
||||
FROM approval_records
|
||||
ORDER BY source_identity
|
||||
"""))
|
||||
).mappings().all()
|
||||
""")
|
||||
)
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
assert len(persisted) == 2
|
||||
assert {row["fingerprint"] for row in persisted} == {
|
||||
stored_long_fingerprint,
|
||||
@@ -204,6 +214,159 @@ async def test_domain_separated_fingerprints_avoid_cross_domain_pg_collision() -
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_owner_pg_replay_does_not_reuse_resolved_prior_occurrence() -> None:
|
||||
"""A source-only v2 row cannot bind a later startsAt to its Incident."""
|
||||
|
||||
engine = create_async_engine(_explicit_local_test_database_url(), echo=False)
|
||||
source_fingerprint = "legacy-backup-occurrence:" + ("a" * 64)
|
||||
common = {
|
||||
"project_id": "awoooi",
|
||||
"source_fingerprint": source_fingerprint,
|
||||
"source_event_fingerprint": "legacy-backup:message:hash",
|
||||
}
|
||||
previous_owner = (
|
||||
backup_restore_alertmanager_ingress.build_backup_restore_ingress_owner(
|
||||
**common,
|
||||
source_started_at="2026-07-11T10:00:00Z",
|
||||
)
|
||||
)
|
||||
previous_legacy = (
|
||||
backup_restore_alertmanager_ingress._legacy_backup_restore_ingress_owner(
|
||||
**common,
|
||||
source_started_at="2026-07-11T10:00:00Z",
|
||||
)
|
||||
)
|
||||
current_owner = (
|
||||
backup_restore_alertmanager_ingress.build_backup_restore_ingress_owner(
|
||||
**common,
|
||||
source_started_at="2026-07-12T10:00:00Z",
|
||||
)
|
||||
)
|
||||
current_legacy = (
|
||||
backup_restore_alertmanager_ingress._legacy_backup_restore_ingress_owner(
|
||||
**common,
|
||||
source_started_at="2026-07-12T10:00:00Z",
|
||||
)
|
||||
)
|
||||
source_only_v2 = backup_restore_alertmanager_ingress._approval_storage_fingerprint(
|
||||
source_fingerprint
|
||||
)
|
||||
current_v3 = (
|
||||
backup_restore_alertmanager_ingress._approval_owner_storage_fingerprint(
|
||||
current_owner
|
||||
)
|
||||
)
|
||||
assert previous_owner["approval_id"] == previous_legacy["approval_id"]
|
||||
|
||||
try:
|
||||
async with engine.connect() as connection:
|
||||
transaction = await connection.begin()
|
||||
try:
|
||||
await connection.execute(
|
||||
text("""
|
||||
CREATE TEMP TABLE approval_owner_replay (
|
||||
id UUID PRIMARY KEY,
|
||||
fingerprint VARCHAR(64) NOT NULL,
|
||||
incident_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
extra_metadata JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
) ON COMMIT DROP
|
||||
""")
|
||||
)
|
||||
await connection.execute(
|
||||
text("""
|
||||
INSERT INTO approval_owner_replay (
|
||||
id, fingerprint, incident_id, status, extra_metadata
|
||||
) VALUES (
|
||||
:id, :fingerprint, :incident_id, 'resolved',
|
||||
CAST(:extra_metadata AS JSONB)
|
||||
)
|
||||
"""),
|
||||
{
|
||||
"id": UUID(previous_owner["approval_id"]),
|
||||
"fingerprint": source_only_v2,
|
||||
"incident_id": previous_owner["incident_id"],
|
||||
"extra_metadata": json.dumps(previous_legacy),
|
||||
},
|
||||
)
|
||||
|
||||
async def find_by_fingerprint(
|
||||
*,
|
||||
fingerprint: str,
|
||||
debounce_minutes: int,
|
||||
):
|
||||
assert debounce_minutes == 30
|
||||
row = (
|
||||
(
|
||||
await connection.execute(
|
||||
text("""
|
||||
SELECT id, incident_id, status, extra_metadata
|
||||
FROM approval_owner_replay
|
||||
WHERE fingerprint = :fingerprint
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"fingerprint": fingerprint},
|
||||
)
|
||||
)
|
||||
.mappings()
|
||||
.one_or_none()
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
return SimpleNamespace(
|
||||
id=row["id"],
|
||||
incident_id=row["incident_id"],
|
||||
status=row["status"],
|
||||
metadata=dict(row["extra_metadata"]),
|
||||
)
|
||||
|
||||
service = SimpleNamespace(find_by_fingerprint=find_by_fingerprint)
|
||||
incompatible = await (
|
||||
backup_restore_alertmanager_ingress._find_compatible_owner_approval(
|
||||
service,
|
||||
owner=current_owner,
|
||||
legacy_owner=current_legacy,
|
||||
)
|
||||
)
|
||||
assert incompatible is None
|
||||
|
||||
await connection.execute(
|
||||
text("""
|
||||
INSERT INTO approval_owner_replay (
|
||||
id, fingerprint, incident_id, status, extra_metadata
|
||||
) VALUES (
|
||||
:id, :fingerprint, :incident_id, 'approved',
|
||||
CAST(:extra_metadata AS JSONB)
|
||||
)
|
||||
"""),
|
||||
{
|
||||
"id": UUID(current_owner["approval_id"]),
|
||||
"fingerprint": current_v3,
|
||||
"incident_id": current_owner["incident_id"],
|
||||
"extra_metadata": json.dumps(current_owner),
|
||||
},
|
||||
)
|
||||
replay = await (
|
||||
backup_restore_alertmanager_ingress._find_compatible_owner_approval(
|
||||
service,
|
||||
owner=current_owner,
|
||||
legacy_owner=current_legacy,
|
||||
)
|
||||
)
|
||||
assert replay is not None
|
||||
assert str(replay.id) == current_owner["approval_id"]
|
||||
assert replay.incident_id == current_owner["incident_id"]
|
||||
assert replay.incident_id != previous_owner["incident_id"]
|
||||
assert len(current_v3) == 64
|
||||
finally:
|
||||
await transaction.rollback()
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_direct_readback_rewrites_ssl_and_keeps_rls_context_in_transaction(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -217,9 +380,7 @@ async def test_direct_readback_rewrites_ssl_and_keeps_rls_context_in_transaction
|
||||
):
|
||||
separator = "&" if "?" in sqlalchemy_url else "?"
|
||||
sqlalchemy_url = f"{sqlalchemy_url}{separator}ssl=disable"
|
||||
direct_url = platform_operator_service._asyncpg_direct_database_url(
|
||||
sqlalchemy_url
|
||||
)
|
||||
direct_url = platform_operator_service._asyncpg_direct_database_url(sqlalchemy_url)
|
||||
direct_query = dict(parse_qsl(urlsplit(direct_url).query))
|
||||
assert "ssl" not in direct_query
|
||||
assert direct_query["sslmode"] == "disable"
|
||||
@@ -271,16 +432,18 @@ async def test_direct_readback_rewrites_ssl_and_keeps_rls_context_in_transaction
|
||||
)
|
||||
""",
|
||||
UUID("66cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92"),
|
||||
json.dumps({
|
||||
"ai_automation_alert_card": {
|
||||
"event_type": "backup_restore_escrow_signal",
|
||||
"lane": "backup_restore_escrow_triage",
|
||||
"target": "backup_restore",
|
||||
"delivery_receipt_readback_required": True,
|
||||
"runtime_write_gate_count": 0,
|
||||
},
|
||||
"source_refs": {"fingerprints": ["backup-fixture"]},
|
||||
}),
|
||||
json.dumps(
|
||||
{
|
||||
"ai_automation_alert_card": {
|
||||
"event_type": "backup_restore_escrow_signal",
|
||||
"lane": "backup_restore_escrow_triage",
|
||||
"target": "backup_restore",
|
||||
"delivery_receipt_readback_required": True,
|
||||
"runtime_write_gate_count": 0,
|
||||
},
|
||||
"source_refs": {"fingerprints": ["backup-fixture"]},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
original_connect = asyncpg.connect
|
||||
@@ -316,6 +479,4 @@ async def test_direct_readback_rewrites_ssl_and_keeps_rls_context_in_transaction
|
||||
assert summary["backup_restore_total"] == 1
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["project_id"] == "awoooi"
|
||||
assert rows[0]["alert_card"]["event_type"] == (
|
||||
"backup_restore_escrow_signal"
|
||||
)
|
||||
assert rows[0]["alert_card"]["event_type"] == ("backup_restore_escrow_signal")
|
||||
|
||||
Reference in New Issue
Block a user