feat: prove repository rename continuity
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
parent
037c8360e2
commit
9f0a104b56
11 changed files with 1186 additions and 96 deletions
|
|
@ -1,16 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from api.config import settings
|
||||
from api.main import app
|
||||
from api.models.managed_repo import ManagedRepo
|
||||
from api.models.repository_rename import RepositoryRenameOperation, RepositorySlug
|
||||
from api.models.agent_message import AgentMessage
|
||||
from api.models.decision import Decision
|
||||
from api.models.sbom_entry import Ecosystem, SBOMEntry
|
||||
from api.models.sbom_snapshot import SBOMSnapshot
|
||||
from api.models.token_event import TokenEvent
|
||||
from api.models.workplan import Workplan
|
||||
from api.routers.repository_renames import repository_rename_events
|
||||
from api.services.forge_repository import (
|
||||
ForgeRepositorySnapshot,
|
||||
get_forge_repository_gateway,
|
||||
|
|
@ -251,7 +259,17 @@ async def test_interrupt_resume_every_phase_and_preserve_uuid(client, rename_set
|
|||
|
||||
sequence = [
|
||||
("statehub-rebound", "forge-renamed", {}),
|
||||
("source-synced", "statehub-rebound", {"evidence": {"clone": "fresh"}}),
|
||||
(
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
{
|
||||
"evidence": {
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
}
|
||||
},
|
||||
),
|
||||
("consumers-verified", "source-synced", {"checks": {"routes": True, "security-stack": True}}),
|
||||
("completed", "consumers-verified", {}),
|
||||
]
|
||||
|
|
@ -383,7 +401,17 @@ async def test_rollback_restores_old_canonical_and_remains_auditable(client, ren
|
|||
for requested, expected, extra in [
|
||||
("forge-renamed", "preflighted", {}),
|
||||
("statehub-rebound", "forge-renamed", {}),
|
||||
("source-synced", "statehub-rebound", {"evidence": {"clone": "fresh"}}),
|
||||
(
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
{
|
||||
"evidence": {
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
}
|
||||
},
|
||||
),
|
||||
]:
|
||||
await _phase(client, repo["id"], operation_id, requested, expected, confirmation, **extra)
|
||||
|
||||
|
|
@ -441,3 +469,463 @@ async def test_rollback_recovers_unrecorded_forge_rename(client, rename_setup):
|
|||
assert rollback.status_code == 200, rollback.text
|
||||
assert rollback.json()["phase"] == "rolled-back"
|
||||
assert forge.name == "flex-auth"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_continuity_evidence_covers_relationships_totals_routes_and_dispatch(
|
||||
client, test_engine, rename_setup
|
||||
):
|
||||
repo, _forge = rename_setup
|
||||
workplan = await create_test_workplan(
|
||||
client, repo_id=repo["id"], slug="FLEX-WP-0042", status="active"
|
||||
)
|
||||
task = await client.post(
|
||||
"/tasks/",
|
||||
json={
|
||||
"workplan_id": workplan["id"],
|
||||
"title": "Preserve security-stack lineage",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
},
|
||||
)
|
||||
assert task.status_code == 201, task.text
|
||||
now = datetime.now(timezone.utc)
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with factory() as session:
|
||||
bound = await session.get(Workplan, uuid.UUID(workplan["id"]))
|
||||
bound.backing_filename = "FLEX-WP-0042-lineage.md"
|
||||
bound.backing_relative_path = "workplans/FLEX-WP-0042-lineage.md"
|
||||
decision = Decision(
|
||||
workplan_id=bound.id,
|
||||
title="Retain immutable repository identity",
|
||||
)
|
||||
token = TokenEvent(
|
||||
repo_id=uuid.UUID(repo["id"]),
|
||||
workplan_id=bound.id,
|
||||
task_id=uuid.UUID(task.json()["id"]),
|
||||
tokens_in=120,
|
||||
tokens_out=30,
|
||||
cached_input_tokens=10,
|
||||
reasoning_output_tokens=5,
|
||||
raw_total_tokens=165,
|
||||
)
|
||||
snapshot = SBOMSnapshot(
|
||||
repo_id=uuid.UUID(repo["id"]),
|
||||
snapshot_at=now,
|
||||
source="pytest",
|
||||
entry_count=1,
|
||||
created_at=now,
|
||||
)
|
||||
message = AgentMessage(
|
||||
from_agent="security-review",
|
||||
to_agent="flex-auth",
|
||||
subject="Historical route",
|
||||
body="Keep the recorded coordinate",
|
||||
)
|
||||
session.add_all([decision, token, snapshot, message])
|
||||
await session.flush()
|
||||
session.add(
|
||||
SBOMEntry(
|
||||
repo_id=uuid.UUID(repo["id"]),
|
||||
snapshot_id=snapshot.id,
|
||||
package_name="policy-engine",
|
||||
package_version="1.0",
|
||||
ecosystem=Ecosystem.python,
|
||||
snapshot_at=now,
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
preflight = await _preflight(client, repo["id"])
|
||||
baseline = preflight["baselines"]
|
||||
assert set(baseline) >= {
|
||||
"repository",
|
||||
"workplans",
|
||||
"tasks",
|
||||
"progress_events",
|
||||
"decisions",
|
||||
"token_events",
|
||||
"sbom_snapshots",
|
||||
"active_dispatch",
|
||||
"aliases",
|
||||
"messages",
|
||||
"workplan_bindings",
|
||||
"continuity_checksum",
|
||||
}
|
||||
assert baseline["token_events"]["totals"] == {
|
||||
"tokens_in": 120,
|
||||
"tokens_out": 30,
|
||||
"cached_input_tokens": 10,
|
||||
"reasoning_output_tokens": 5,
|
||||
"raw_total_tokens": 165,
|
||||
}
|
||||
assert baseline["workplan_bindings"]["relationships"][0]["repo_id"] == repo["id"]
|
||||
|
||||
operation, confirmation = await _operation(client, repo["id"], preflight)
|
||||
for requested, expected, extra in [
|
||||
("forge-renamed", "preflighted", {}),
|
||||
("statehub-rebound", "forge-renamed", {}),
|
||||
(
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
{
|
||||
"evidence": {
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
}
|
||||
},
|
||||
),
|
||||
("consumers-verified", "source-synced", {"checks": {"dispatch": True}}),
|
||||
("completed", "consumers-verified", {}),
|
||||
]:
|
||||
await _phase(
|
||||
client,
|
||||
repo["id"],
|
||||
operation["id"],
|
||||
requested,
|
||||
expected,
|
||||
confirmation,
|
||||
**extra,
|
||||
)
|
||||
|
||||
verification = await client.get(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation['id']}/verify"
|
||||
)
|
||||
assert verification.status_code == 200, verification.text
|
||||
evidence = verification.json()
|
||||
assert evidence["ok"] is True
|
||||
for name, expected_checksum in evidence["relationship_checksums"]["baseline"].items():
|
||||
assert evidence["relationship_checksums"]["preserved"][name] == expected_checksum
|
||||
assert (await client.get("/repos/flex-auth")).json()["slug_status"] == "alias"
|
||||
dispatch = await client.get("/repos/flex-auth/dispatch")
|
||||
assert dispatch.status_code == 200
|
||||
assert dispatch.json()["canonical_slug"] == "access-engine"
|
||||
assert dispatch.json()["active_workplans"][0]["id"] == workplan["id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_equal_counts_do_not_hide_detached_work_and_telemetry(
|
||||
client, test_engine, rename_setup
|
||||
):
|
||||
repo, _forge = rename_setup
|
||||
workplan = await create_test_workplan(
|
||||
client, repo_id=repo["id"], slug="FLEX-WP-0050", status="active"
|
||||
)
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with factory() as session:
|
||||
original_token = TokenEvent(
|
||||
repo_id=uuid.UUID(repo["id"]), tokens_in=11, tokens_out=7
|
||||
)
|
||||
session.add(original_token)
|
||||
await session.commit()
|
||||
preflight = await _preflight(client, repo["id"])
|
||||
operation, _confirmation = await _operation(client, repo["id"], preflight)
|
||||
|
||||
other_domain = await create_test_domain(client, slug="other-domain")
|
||||
other_repo = await create_test_repo(
|
||||
client, domain_slug=other_domain["slug"], slug="other-repo"
|
||||
)
|
||||
async with factory() as session:
|
||||
detached = await session.get(Workplan, uuid.UUID(workplan["id"]))
|
||||
detached.repo_id = uuid.UUID(other_repo["id"])
|
||||
token = await session.get(TokenEvent, original_token.id)
|
||||
token.repo_id = uuid.UUID(other_repo["id"])
|
||||
session.add(
|
||||
Workplan(
|
||||
repo_id=uuid.UUID(repo["id"]),
|
||||
slug="FLEX-WP-0050-RECREATED",
|
||||
title="Count-preserving replacement",
|
||||
status="active",
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
TokenEvent(repo_id=uuid.UUID(repo["id"]), tokens_in=11, tokens_out=7)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
verification = await client.get(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation['id']}/verify"
|
||||
)
|
||||
assert verification.status_code == 200, verification.text
|
||||
result = verification.json()
|
||||
assert result["ok"] is False
|
||||
relationship = next(
|
||||
check for check in result["checks"] if check["name"] == "relationship_continuity"
|
||||
)
|
||||
assert relationship["ok"] is False
|
||||
assert set(relationship["actual"]["missing"]) >= {"workplans", "token_events"}
|
||||
counts = next(
|
||||
check for check in result["checks"] if check["name"] == "record_counts_non_decreasing"
|
||||
)
|
||||
assert counts["actual"]["workplans"] == counts["expected"]["workplans"]
|
||||
assert counts["actual"]["token_events"] == counts["expected"]["token_events"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirected_forge_and_wrong_fresh_clone_identity_fail_closed_then_resume(
|
||||
client, rename_setup
|
||||
):
|
||||
repo, forge = rename_setup
|
||||
original_inspect = forge.inspect
|
||||
|
||||
async def redirected(*, instance: str, owner: str, name: str):
|
||||
if name == "flex-auth":
|
||||
return forge.snapshot("access-engine")
|
||||
if name == "access-engine":
|
||||
return None
|
||||
return await original_inspect(instance=instance, owner=owner, name=name)
|
||||
|
||||
forge.inspect = redirected
|
||||
redirected_report = await _preflight(client, repo["id"])
|
||||
assert redirected_report["safe_to_apply"] is False
|
||||
assert {item["code"] for item in redirected_report["blockers"]} >= {"forge_redirected"}
|
||||
forge.inspect = original_inspect
|
||||
|
||||
preflight = await _preflight(client, repo["id"])
|
||||
operation, confirmation = await _operation(client, repo["id"], preflight)
|
||||
await _phase(client, repo["id"], operation["id"], "forge-renamed", "preflighted", confirmation)
|
||||
await _phase(client, repo["id"], operation["id"], "statehub-rebound", "forge-renamed", confirmation)
|
||||
wrong = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation['id']}/phases/source-synced",
|
||||
json={
|
||||
"expected_phase": "statehub-rebound",
|
||||
"confirmation": confirmation,
|
||||
"evidence": {
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 999,
|
||||
"head_commit": "a" * 40,
|
||||
"password": "must-never-reach-an-event",
|
||||
},
|
||||
},
|
||||
)
|
||||
assert wrong.status_code == 412
|
||||
status = (await client.get(f"/repository-renames/operations/{operation['id']}")).json()
|
||||
assert status["phase"] == "statehub-rebound"
|
||||
assert status["evidence"]["telemetry"]["failures"] == 1
|
||||
resumed = await _phase(
|
||||
client,
|
||||
repo["id"],
|
||||
operation["id"],
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
confirmation,
|
||||
evidence={
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
},
|
||||
)
|
||||
replay = await _phase(
|
||||
client,
|
||||
repo["id"],
|
||||
operation["id"],
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
confirmation,
|
||||
evidence={
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
},
|
||||
)
|
||||
assert resumed["phase"] == "source-synced"
|
||||
assert replay["no_op"] is True
|
||||
assert replay["evidence"]["telemetry"]["retries"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rollback_recovers_when_old_statehub_slug_temporarily_unavailable(
|
||||
client, test_engine, rename_setup
|
||||
):
|
||||
repo, forge = rename_setup
|
||||
preflight = await _preflight(client, repo["id"])
|
||||
operation, confirmation = await _operation(client, repo["id"], preflight)
|
||||
for requested, expected, extra in [
|
||||
("forge-renamed", "preflighted", {}),
|
||||
("statehub-rebound", "forge-renamed", {}),
|
||||
(
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
{
|
||||
"evidence": {
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
}
|
||||
},
|
||||
),
|
||||
]:
|
||||
await _phase(client, repo["id"], operation["id"], requested, expected, confirmation, **extra)
|
||||
rollback_confirmation = f"rollback:{operation['id']}"
|
||||
report = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation['id']}/rollback-preflight",
|
||||
json={"expected_phase": "source-synced", "confirmation": rollback_confirmation},
|
||||
)
|
||||
assert report.status_code == 200, report.text
|
||||
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with factory() as session:
|
||||
await session.execute(delete(RepositorySlug).where(RepositorySlug.slug == "flex-auth"))
|
||||
await session.commit()
|
||||
failed = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation['id']}/rollback",
|
||||
json={"expected_phase": "rollback-preflight", "confirmation": rollback_confirmation},
|
||||
)
|
||||
assert failed.status_code == 412
|
||||
assert forge.name == "flex-auth"
|
||||
async with factory() as session:
|
||||
session.add(
|
||||
RepositorySlug(
|
||||
repo_id=uuid.UUID(repo["id"]),
|
||||
slug="flex-auth",
|
||||
kind="alias",
|
||||
protected=True,
|
||||
source_operation_id=uuid.UUID(operation["id"]),
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
resumed = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation['id']}/rollback",
|
||||
json={"expected_phase": "rollback-preflight", "confirmation": rollback_confirmation},
|
||||
)
|
||||
assert resumed.status_code == 200, resumed.text
|
||||
assert resumed.json()["phase"] == "rolled-back"
|
||||
assert resumed.json()["evidence"]["telemetry"]["failures"] == 1
|
||||
assert resumed.json()["evidence"]["telemetry"]["rollback_outcome"] == "completed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_events_expose_metrics_without_operator_evidence_or_secrets(
|
||||
client, test_engine, rename_setup
|
||||
):
|
||||
repo, _forge = rename_setup
|
||||
preflight = await _preflight(client, repo["id"])
|
||||
operation, confirmation = await _operation(client, repo["id"], preflight)
|
||||
await _phase(client, repo["id"], operation["id"], "forge-renamed", "preflighted", confirmation)
|
||||
replay = await _phase(client, repo["id"], operation["id"], "forge-renamed", "preflighted", confirmation)
|
||||
assert replay["evidence"]["telemetry"]["retries"] == 1
|
||||
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with factory() as session:
|
||||
persisted = await session.get(RepositoryRenameOperation, uuid.UUID(operation["id"]))
|
||||
events = repository_rename_events(persisted, outcome="succeeded", no_op=True)
|
||||
assert [subject for subject, _event in events] == ["org.statehub.repo.rename.phase"]
|
||||
attributes = events[0][1].attributes
|
||||
assert attributes["repo_id"] == repo["id"]
|
||||
assert attributes["phase"] == "forge-renamed"
|
||||
assert attributes["phase_duration_ms"] >= 0
|
||||
assert attributes["retries"] == 1
|
||||
assert set(attributes) >= {
|
||||
"failures",
|
||||
"rollback_attempts",
|
||||
"verification_outcome",
|
||||
"evidence_ref",
|
||||
}
|
||||
serialized = events[0][1].model_dump_json().lower()
|
||||
assert "preflight_token" not in serialized
|
||||
assert "operator_evidence" not in serialized
|
||||
assert "password" not in serialized
|
||||
persisted.phase = "completed"
|
||||
completion = repository_rename_events(persisted, outcome="succeeded")
|
||||
assert [subject for subject, _event in completion] == [
|
||||
"org.statehub.repo.rename.phase",
|
||||
"org.statehub.repo.renamed",
|
||||
]
|
||||
persisted.phase = "forge-renamed"
|
||||
persisted.error_code = "repository_rename_precondition_failed"
|
||||
persisted.error_details = {"authorization": "Bearer must-not-publish"}
|
||||
failure = repository_rename_events(persisted, outcome="failed")
|
||||
failure_json = failure[0][1].model_dump_json()
|
||||
assert failure[0][0] == "org.statehub.repo.rename.failed"
|
||||
assert "must-not-publish" not in failure_json
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_every_forward_phase_failure_is_retry_safe(client, rename_setup):
|
||||
repo, forge = rename_setup
|
||||
preflight = await _preflight(client, repo["id"])
|
||||
operation, confirmation = await _operation(client, repo["id"], preflight)
|
||||
operation_id = operation["id"]
|
||||
|
||||
async def fail_unreadable(requested: str, expected: str) -> None:
|
||||
forge.unreadable = True
|
||||
response = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation_id}/phases/{requested}",
|
||||
json={"expected_phase": expected, "confirmation": confirmation},
|
||||
)
|
||||
forge.unreadable = False
|
||||
assert response.status_code == 412
|
||||
status = await client.get(f"/repository-renames/operations/{operation_id}")
|
||||
assert status.json()["phase"] == expected
|
||||
|
||||
await fail_unreadable("forge-renamed", "preflighted")
|
||||
await _phase(client, repo["id"], operation_id, "forge-renamed", "preflighted", confirmation)
|
||||
await fail_unreadable("statehub-rebound", "forge-renamed")
|
||||
await _phase(client, repo["id"], operation_id, "statehub-rebound", "forge-renamed", confirmation)
|
||||
|
||||
wrong_clone = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation_id}/phases/source-synced",
|
||||
json={
|
||||
"expected_phase": "statehub-rebound",
|
||||
"confirmation": confirmation,
|
||||
"evidence": {
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 999,
|
||||
"head_commit": "a" * 40,
|
||||
},
|
||||
},
|
||||
)
|
||||
assert wrong_clone.status_code == 412
|
||||
await _phase(
|
||||
client,
|
||||
repo["id"],
|
||||
operation_id,
|
||||
"source-synced",
|
||||
"statehub-rebound",
|
||||
confirmation,
|
||||
evidence={
|
||||
"fresh_clone": True,
|
||||
"forge_repository_id": 417,
|
||||
"head_commit": "a" * 40,
|
||||
},
|
||||
)
|
||||
|
||||
bad_consumers = await client.post(
|
||||
f"/repos/{repo['id']}/rename/operations/{operation_id}/phases/consumers-verified",
|
||||
json={
|
||||
"expected_phase": "source-synced",
|
||||
"confirmation": confirmation,
|
||||
"checks": {"security-stack": False},
|
||||
},
|
||||
)
|
||||
assert bad_consumers.status_code == 412
|
||||
await _phase(
|
||||
client,
|
||||
repo["id"],
|
||||
operation_id,
|
||||
"consumers-verified",
|
||||
"source-synced",
|
||||
confirmation,
|
||||
checks={"security-stack": True},
|
||||
)
|
||||
|
||||
await fail_unreadable("completed", "consumers-verified")
|
||||
completed = await _phase(
|
||||
client,
|
||||
repo["id"],
|
||||
operation_id,
|
||||
"completed",
|
||||
"consumers-verified",
|
||||
confirmation,
|
||||
)
|
||||
assert completed["evidence"]["telemetry"]["failures"] == 5
|
||||
assert completed["evidence"]["telemetry"]["verification_outcome"] == "passed"
|
||||
assert set(completed["evidence"]["telemetry"]["failures_by_phase"]) == {
|
||||
"forge-renamed",
|
||||
"statehub-rebound",
|
||||
"source-synced",
|
||||
"consumers-verified",
|
||||
"completed",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import json
|
||||
import stat
|
||||
import sys
|
||||
import urllib.error
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
|
@ -240,6 +241,27 @@ def test_rollback_requires_separate_execute_flag(monkeypatch, capsys):
|
|||
assert [method for method, _path, _body in calls] == ["GET", "POST"]
|
||||
|
||||
|
||||
def test_state_hub_outage_fails_without_losing_operation_identity_or_leaking_secrets(
|
||||
monkeypatch,
|
||||
):
|
||||
def unavailable(*_args, **_kwargs):
|
||||
raise urllib.error.URLError(
|
||||
"connection refused token=super-secret authorization=Bearer-secret"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(rename_cli.urllib.request, "urlopen", unavailable)
|
||||
with pytest.raises(rename_cli.RenameCLIError) as raised:
|
||||
rename_cli._api_request(
|
||||
"http://127.0.0.1:8000",
|
||||
"GET",
|
||||
f"/repository-renames/operations/{OPERATION_ID}",
|
||||
)
|
||||
assert raised.value.code == "state_hub_unavailable"
|
||||
assert str(raised.value) == "State Hub is unavailable"
|
||||
assert "super-secret" not in json.dumps(raised.value.details)
|
||||
assert OPERATION_ID not in raised.value.details.get("reason", "")
|
||||
|
||||
|
||||
def test_rollback_execute_reaches_terminal_state(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue