fix(staging_check): B6 reads registry from running staging instance env
B6 false-FAILed because it built the project registry from the launcher process-env via a host-path hack (sys.path.insert + importlib.reload), not from the running staging instance. Run from the host, ORCH_PROJECTS_JSON is unset -> default ET+ORCH registry -> false FAIL -> spurious deploy-staging -> development rollback. Variant (v) per ADR-001: remove the host-path hack; canonically run the suite INSIDE orchestrator-staging via docker exec so src.projects resolves from /app (PYTHONPATH) with .env.staging. Verdict logic extracted into pure _evaluate_b6(known) -> (passed, detail) + _known_project_ids_from_registry() / _run_b6() with deterministic FAIL on source unavailability. deployer.md and STAGING_CHECK.md updated to the docker exec command. src/projects.py, .env* and checks A/B4/B5/C untouched. Refs: ORCH-048 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
151
tests/test_staging_check_b6.py
Normal file
151
tests/test_staging_check_b6.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""ORCH-048: unit tests for the B6 registry-isolation verdict in staging_check.py.
|
||||
|
||||
B6 «Registry: sandbox present, prod ET/ORCH absent» is the staging-isolation
|
||||
safety check. Its verdict logic is isolated into the pure function
|
||||
``_evaluate_b6(known) -> (passed, detail)`` so both outcomes (clean staging
|
||||
registry → PASS, polluted registry → FAIL) can be tested without standing up a
|
||||
live staging instance or docker (02-trz §9, ADR-001).
|
||||
|
||||
These tests target that pure function plus the deterministic-degradation path
|
||||
(``_run_b6``) and statically assert the host-path hack is gone (TR-6 / TC-06).
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load scripts/staging_check.py by path (scripts/ is not an importable package).
|
||||
# ---------------------------------------------------------------------------
|
||||
_SCRIPT_PATH = (
|
||||
pathlib.Path(__file__).resolve().parent.parent / "scripts" / "staging_check.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("staging_check", _SCRIPT_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
sc = _load_module()
|
||||
|
||||
SANDBOX = sc.SANDBOX_PROJECT_ID
|
||||
PROD_ET = sc.PROD_ET_PROJECT_ID
|
||||
PROD_ORCH = sc.PROD_ORCH_PROJECT_ID
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-01 — clean staging registry → PASS
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc01_clean_registry_passes():
|
||||
passed, detail = sc._evaluate_b6({SANDBOX})
|
||||
assert passed is True
|
||||
assert "sandbox=YES" in detail
|
||||
assert "prod-ET=NO(good)" in detail
|
||||
assert "prod-ORCH=NO(good)" in detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-02 — prod-ET leaked into registry → FAIL
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc02_prod_et_present_fails():
|
||||
passed, detail = sc._evaluate_b6({SANDBOX, PROD_ET})
|
||||
assert passed is False
|
||||
assert "sandbox=YES" in detail
|
||||
assert "prod-ET=YES(BAD!)" in detail
|
||||
assert "prod-ORCH=NO(good)" in detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-03 — prod-ORCH leaked into registry → FAIL
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc03_prod_orch_present_fails():
|
||||
passed, detail = sc._evaluate_b6({SANDBOX, PROD_ORCH})
|
||||
assert passed is False
|
||||
assert "sandbox=YES" in detail
|
||||
assert "prod-ET=NO(good)" in detail
|
||||
assert "prod-ORCH=YES(BAD!)" in detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-04 — sandbox absent (empty registry) → deterministic FAIL, no exception
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc04_empty_registry_fails_without_sandbox():
|
||||
passed, detail = sc._evaluate_b6(set())
|
||||
assert passed is False
|
||||
assert "sandbox=NO" in detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-05 — both prod projects leaked → FAIL
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc05_both_prod_present_fails():
|
||||
passed, detail = sc._evaluate_b6({SANDBOX, PROD_ET, PROD_ORCH})
|
||||
assert passed is False
|
||||
assert "prod-ET=YES(BAD!)" in detail
|
||||
assert "prod-ORCH=YES(BAD!)" in detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-06 — registry source no longer depends on the host-path hack (TR-6)
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc06_no_host_path_hack_in_source():
|
||||
source = _SCRIPT_PATH.read_text(encoding="utf-8")
|
||||
# The host-worktree path injection and the env-of-the-launcher reload that
|
||||
# caused the false FAIL must be gone from the B6 mechanics.
|
||||
assert 'sys.path.insert(0, "/repos/orchestrator")' not in source
|
||||
assert "importlib.reload" not in source
|
||||
|
||||
|
||||
def test_tc06_registry_loader_uses_src_projects():
|
||||
# The verdict input is built from src.projects.known_plane_project_ids()
|
||||
# resolved via the running instance's own PYTHONPATH/env — not from a
|
||||
# host-path-injected import. We verify the loader delegates to that function.
|
||||
import src.projects as projects_mod
|
||||
|
||||
sentinel = {"sentinel-id"}
|
||||
original = projects_mod.known_plane_project_ids
|
||||
projects_mod.known_plane_project_ids = lambda: sentinel
|
||||
try:
|
||||
known = sc._known_project_ids_from_registry()
|
||||
finally:
|
||||
projects_mod.known_plane_project_ids = original
|
||||
assert known == sentinel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-07 — degraded registry source → deterministic FAIL (not false PASS, not raise)
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc07_source_failure_is_deterministic_fail(monkeypatch):
|
||||
def _boom():
|
||||
raise RuntimeError("registry import blew up")
|
||||
|
||||
monkeypatch.setattr(sc, "_known_project_ids_from_registry", _boom)
|
||||
|
||||
results = sc.Results()
|
||||
# Must not raise.
|
||||
sc._run_b6(results)
|
||||
|
||||
assert len(results._items) == 1
|
||||
label, passed, detail = results._items[0]
|
||||
assert passed is False
|
||||
assert "registry source unavailable" in detail
|
||||
assert "registry import blew up" in detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _run_b6 happy path wiring (clean registry → PASS result recorded)
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_run_b6_records_pass_for_clean_registry(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
sc, "_known_project_ids_from_registry", lambda: {SANDBOX}
|
||||
)
|
||||
results = sc.Results()
|
||||
sc._run_b6(results)
|
||||
assert len(results._items) == 1
|
||||
_label, passed, detail = results._items[0]
|
||||
assert passed is True
|
||||
assert "sandbox=YES" in detail
|
||||
Reference in New Issue
Block a user