fix(2): F2-5 — dep teardown verify=True, errors propagate to run-fail (Adversary cold)

Per REVIEW-2 ## Q2 FAIL: runner/harness/deps.py::teardown_deps suppressed ALL exceptions via
contextlib.suppress(Exception), silently swallowing teardown failures. The 'DEPS teardown' print
fired even when undeploy actually raised — leaving leftover swarm services/volumes/secrets that
broke the NEXT run targeting the same deterministic dep domain (this is what caused the Q3.1 dep
flake I saw immediately after the Q2.4 acceptance run).

Fix:
- runner/harness/deps.py: teardown_deps now uses lifecycle.teardown_app(..., verify=True) so
  residuals raise TeardownError. Errors are LOGGED LOUDLY per-dep but we continue to other deps
  so one failure doesn't strand the rest. After all attempts: raise a combined TeardownError if
  any dep failed.
- runner/run_recipe_ci.py: orchestrator catches the dep TeardownError in finally, prints it,
  captures into dep_teardown_error; the run summary surfaces it and the exit code is non-zero.
  The run STILL prints the diagnosable summary so a leak doesn't hide other failures.

Per §9 teardown sacred / DG7: a green run that leaks state is not 'green'. F2-5 now correctly
fails the run instead of silently passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 09:00:37 +01:00
co-authored by Claude Opus 4.7
parent 9a857d9ef4
commit c6e94af766
2 changed files with 35 additions and 6 deletions
+14 -1
View File
@@ -360,6 +360,7 @@ def main() -> int:
results: dict[str, str] = {}
lifecycle.janitor()
dep_deploy_failed = False
dep_teardown_error: str | None = None
try:
# ---- deps deploy FIRST (sequentially), if declared (Q2.3) ----
if declared:
@@ -437,10 +438,18 @@ def main() -> int:
results[op] = "skip"
finally:
# Teardown the recipe under test FIRST, then deps in reverse declaration order.
# Parent verify=False (Phase 1d): keep as-is so a parent residual doesn't mask a tier
# failure. Dep teardown uses verify=True via teardown_deps (F2-5 fix); failures are
# captured into dep_teardown_error and surfaced in the run summary + exit code, but
# we still print the diagnosable summary first.
lifecycle.teardown_app(domain, verify=False)
if deps_state:
print("\n===== DEPS teardown =====", flush=True)
deps_mod.teardown_deps(deps_state)
try:
deps_mod.teardown_deps(deps_state)
except lifecycle.TeardownError as e:
dep_teardown_error = str(e)
print(f"!! {dep_teardown_error}", flush=True)
# ---- deploy-count assertion (DG4.1) ----
with open(countfile) as f:
@@ -470,6 +479,10 @@ def main() -> int:
file=sys.stderr,
)
overall = 1
if dep_teardown_error:
# F2-5: dep teardown leaks violate §9 (teardown sacred); fail the run loudly.
print(f"!! dep teardown leaked state: {dep_teardown_error}", file=sys.stderr)
overall = 1
if any(v == "fail" for v in results.values()):
overall = 1
if not results: