fix: stop the prepared demo failure from degrading n8n integration health

The demo seed plants exactly one failed delivery (BK-H-0020) to demonstrate
retry and audit. Because derive_n8n_status() counted any failure, every fresh
reset pinned the n8n integration to "degraded" -- the demo showed a warning
about a prop, which tells a viewer something untrue about the automation.

The seeded failure now carries its own error code, demoScenarioTimeout, rather
than the generic connectionError a real timeout produces. No column and no
migration: last_error_code already existed, is already surfaced to the UI and is
already localizable.

- integration status splits failed into unexpected_failed and
  demo_scenario_failed; only unexpected failures may move the state. A staged
  failure alone leaves n8n operational.
- latest_failure_at is a health signal and now ignores the staged failure;
  latest_demo_scenario_at reports it separately.
- /api/v1/workflows exposes is_demo_scenario. The Automation page labels the run
  as a prepared demo scenario, explains that it is a simulated temporary failure
  that does not affect automation health, and offers a distinct "retry demo
  scenario" action. Translated in nl-BE, en-GB and fr-BE.
- the carve-out stays narrow: a real failure still degrades n8n, and a genuine
  later failure of the same event overwrites the demo code with the real one.
- the retry itself is unchanged and real: the event goes back on the outbox and
  the dispatcher delivers it to n8n like any other, so 19+1 becomes 20+0 only on
  an actual round trip. The audit records which kind of failure was retried.

Tests that assert on the seeded scenario now reseed first, since earlier test
files legitimately mutate the outbox and the suite shares one database.

Verified locally against a real PostgreSQL 16: 181 passed, ruff clean, mypy
clean (50 files), tsc clean, frontend build clean. Not deployed and not
browser-verified.
This commit is contained in:
NuklearRabbit
2026-08-05 14:07:05 +00:00
parent e5307a7c0f
commit 6f77a30dce
17 changed files with 347 additions and 29 deletions
+59
View File
@@ -1,9 +1,28 @@
from app.core.db import SessionLocal
from app.seed_loader import reset_and_seed
def _reseed() -> None:
"""Restore the canonical demo dataset (19 succeeded + 1 prepared failure).
The suite shares one session-scoped database and earlier files legitimately mutate
the outbox, so any test that asserts on the *seeded* scenario has to re-establish it
rather than depend on file ordering.
"""
db = SessionLocal()
try:
reset_and_seed(db)
finally:
db.close()
def test_list_workflows_requires_operations_manager(employee_client):
response = employee_client.get("/api/v1/workflows")
assert response.status_code == 403
def test_list_workflows_includes_seeded_failed_run(ops_client):
_reseed()
response = ops_client.get("/api/v1/workflows", params={"status": "failed"})
assert response.status_code == 200
runs = response.json()
@@ -20,6 +39,7 @@ def test_retry_requires_failed_status(ops_client):
def test_retry_failed_run_moves_to_pending_and_audits(ops_client):
_reseed()
failed = ops_client.get("/api/v1/workflows", params={"status": "failed"}).json()
target = failed[0]["event_id"]
@@ -36,3 +56,42 @@ def test_retry_requires_operations_manager(employee_client):
"/api/v1/workflows/00000000-0000-4000-8000-000000000020/retry"
)
assert response.status_code == 403
def test_seeded_failure_is_labelled_as_a_prepared_demo_scenario(ops_client):
"""The one seeded failure must announce itself as staged. An unexplained red row in
a demo reads as a broken product; a labelled one reads as the retry story it is."""
_reseed()
runs = ops_client.get("/api/v1/workflows", params={"status": "failed"}).json()
demo_runs = [run for run in runs if run["is_demo_scenario"]]
assert len(demo_runs) == 1
assert demo_runs[0]["last_error_code"] == "demoScenarioTimeout"
assert demo_runs[0]["aggregate_ref"] == "BK-H-0020"
def test_succeeded_runs_are_never_marked_as_a_demo_scenario(ops_client):
runs = ops_client.get("/api/v1/workflows", params={"status": "succeeded"}).json()
assert runs
assert all(run["is_demo_scenario"] is False for run in runs)
def test_retry_of_the_demo_scenario_is_audited_as_a_demo_scenario(ops_client):
"""The retry is a real redelivery either way; the audit records which kind of
failure it resolved so a staged retry is never mistaken for a production fix."""
_reseed()
failed = ops_client.get("/api/v1/workflows", params={"status": "failed"}).json()
demo_run = next(run for run in failed if run["is_demo_scenario"])
response = ops_client.post(f"/api/v1/workflows/{demo_run['event_id']}/retry")
assert response.status_code == 200
assert response.json()["status"] == "pending"
audit = ops_client.get("/api/v1/audit", params={"action": "workflow_retry"}).json()
entry = next(
event
for event in audit
if (event.get("metadata") or event.get("metadata_json") or {}).get("event_id")
== demo_run["event_id"]
)
metadata = entry.get("metadata") or entry.get("metadata_json") or {}
assert metadata["demo_scenario"] is True