Migrate Amazon Connect TEI study to the Mercury Notebook Pattern
studies/202602_AmazonConnect -> studies/202602_TEI_Amazon_Connect, rebuilt as pattern Variant 4 (TEI composite reproduction): - teicalc/ self-contained engine (stdlib-only): Forrester's tables as the never-edited verbatim anchor, NPV/ROI/payback + risk adjustment transplanted from core/calculations, ClientDrivers overlay (contacts/ agents/fixed driver map, growth re-base, identity at composite scale), scenario stress with core-identical semantics - one deliverable notebook (business_case.ipynb): widget-pair sidebar drivers, published-vs-overlay KPI columns, cash-flow/waterfall/scenario charts, verification gate, backstage JSON data appendix - gate + tests reproduce the published totals within PDF rounding: NPV $78.7M / ROI 342% / payback <6 months (engine $78,713,492 / 342.48% / 0.7 months); 27 study tests, headless nbconvert green, stage simulation leak-free, exports carry the appendix - old Athena workflow (00_provision..04_export, config.py, seed_data.py) deleted; git history preserves it; root test fixture repointed to teicalc.anchor - docs: study README rewritten; root README points new studies at template/MercuryNotebook; pattern doc stale ctm-token-calculator paths now cite studies/202607_CTM_GenesysCX; Variant 4 cites this study as its realized reference Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
studies/202602_TEI_Amazon_Connect/tests/conftest.py
Normal file
7
studies/202602_TEI_Amazon_Connect/tests/conftest.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Make teicalc importable even without the study venv active (the normal
|
||||
setup is ``pip install -e ".[dev]"`` into the study-local ``.venv/``)."""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
95
studies/202602_TEI_Amazon_Connect/tests/test_anchor.py
Normal file
95
studies/202602_TEI_Amazon_Connect/tests/test_anchor.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""The verbatim anchor is Forrester's published record — pinned value by
|
||||
value, and proven immutable under every engine code path."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from teicalc import (
|
||||
ASSUMPTIONS,
|
||||
BENEFITS_VERBATIM,
|
||||
COMPOSITE,
|
||||
COSTS_VERBATIM,
|
||||
PUBLISHED,
|
||||
ClientDrivers,
|
||||
apply_scenario,
|
||||
compute_summary,
|
||||
overlay_rows,
|
||||
)
|
||||
|
||||
|
||||
def _row(rows, key):
|
||||
return next(r for r in rows if r["field_key"] == key)
|
||||
|
||||
|
||||
def test_benefit_rows_verbatim():
|
||||
assert [r["field_key"] for r in BENEFITS_VERBATIM] == [
|
||||
"ai_contact_resolution",
|
||||
"ai_content_sentiment",
|
||||
"ai_forecasting_supervision",
|
||||
"data_driven_profit_lift",
|
||||
"legacy_solution_savings",
|
||||
]
|
||||
expected = {
|
||||
"ai_contact_resolution": ({"1": 13_911_040, "2": 23_932_480, "3": 37_797_760}, 0.15),
|
||||
"ai_content_sentiment": ({"1": 4_586_620, "2": 5_358_412, "3": 6_291_680}, 0.15),
|
||||
"ai_forecasting_supervision": ({"1": 6_651_680, "2": 9_133_760, "3": 12_391_712}, 0.15),
|
||||
"data_driven_profit_lift": ({"1": 1_200_000, "2": 1_560_000, "3": 2_028_000}, 0.20),
|
||||
"legacy_solution_savings": ({"1": 6_177_600, "2": 8_030_880, "3": 10_440_144}, 0.20),
|
||||
}
|
||||
for key, (years, rf) in expected.items():
|
||||
row = _row(BENEFITS_VERBATIM, key)
|
||||
assert row["year_values"] == years
|
||||
assert row["risk_adjustment"] == rf
|
||||
assert row["table"] == "benefits"
|
||||
|
||||
|
||||
def test_cost_rows_verbatim():
|
||||
expected = {
|
||||
"amazon_connect_usage": ({"1": 6_456_448, "2": 7_951_164, "3": 9_832_961}, 0.05, 0),
|
||||
"implementation_migration": ({"1": 188_333, "2": 188_333, "3": 0}, 0.10, 1_087_500),
|
||||
"ongoing_management": ({"1": 256_200, "2": 187_200, "3": 187_200}, 0.15, 0),
|
||||
}
|
||||
for key, (years, rf, initial) in expected.items():
|
||||
row = _row(COSTS_VERBATIM, key)
|
||||
assert row["year_values"] == years
|
||||
assert row["risk_adjustment"] == rf
|
||||
assert row["initial"] == initial
|
||||
assert row["table"] == "costs"
|
||||
|
||||
|
||||
def test_assumptions_and_published():
|
||||
assert ASSUMPTIONS["agents_fte"] == 2_000
|
||||
assert ASSUMPTIONS["supervisors_fte"] == 200
|
||||
assert ASSUMPTIONS["annual_contacts_y1"] == 20_000_000
|
||||
assert ASSUMPTIONS["growth_rate"] == 0.30
|
||||
assert ASSUMPTIONS["discount_rate"] == 0.10
|
||||
assert ASSUMPTIONS["analysis_years"] == 3
|
||||
|
||||
assert PUBLISHED["benefits_pv"] == 101_696_791
|
||||
assert PUBLISHED["costs_pv"] == 22_983_076
|
||||
assert PUBLISHED["npv"] == 78_713_715
|
||||
assert PUBLISHED["roi_pct"] == 342
|
||||
assert PUBLISHED["payback_months_max"] == 6
|
||||
|
||||
# The composite drivers ARE the anchor assumptions.
|
||||
assert COMPOSITE.agents_fte == ASSUMPTIONS["agents_fte"]
|
||||
assert COMPOSITE.annual_contacts_y1 == ASSUMPTIONS["annual_contacts_y1"]
|
||||
assert COMPOSITE.growth_rate == ASSUMPTIONS["growth_rate"]
|
||||
assert COMPOSITE.discount_rate == ASSUMPTIONS["discount_rate"]
|
||||
|
||||
|
||||
def test_anchor_is_never_mutated():
|
||||
"""Exercise every engine code path, then prove the record unchanged."""
|
||||
ben_snap = deepcopy(BENEFITS_VERBATIM)
|
||||
cost_snap = deepcopy(COSTS_VERBATIM)
|
||||
|
||||
overlay_rows()
|
||||
overlay_rows(ClientDrivers(agents_fte=137, annual_contacts_y1=1_000_000,
|
||||
growth_rate=0.0))
|
||||
for scenario in ("conservative", "moderate", "aggressive"):
|
||||
apply_scenario(BENEFITS_VERBATIM, scenario)
|
||||
apply_scenario(COSTS_VERBATIM, scenario)
|
||||
compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
||||
compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.08)
|
||||
|
||||
assert BENEFITS_VERBATIM == ben_snap
|
||||
assert COSTS_VERBATIM == cost_snap
|
||||
123
studies/202602_TEI_Amazon_Connect/tests/test_model.py
Normal file
123
studies/202602_TEI_Amazon_Connect/tests/test_model.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Engine pins — every number hand-checked before pinning.
|
||||
|
||||
RA_benefit = v×(1−rf), RA_cost = v×(1+rf), PV = Σ RA_n/(1.1)^n, initial
|
||||
undiscounted. The composite reproduction lands within Forrester's own table
|
||||
rounding of the published Financial Summary (benefits PV $223 low, costs PV
|
||||
$0.22 low) — pinned both engine-exact (±$1) and against PUBLISHED (±$1,000,
|
||||
the convention the retired workflow notebooks used).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from teicalc import (
|
||||
BENEFITS_VERBATIM,
|
||||
COSTS_VERBATIM,
|
||||
PUBLISHED,
|
||||
X_LABELS,
|
||||
YEAR_INDEX,
|
||||
YEARS,
|
||||
by_calendar,
|
||||
compute_summary,
|
||||
discount_factor,
|
||||
money,
|
||||
npv,
|
||||
payback_label,
|
||||
payback_months,
|
||||
payback_years,
|
||||
roi_pct,
|
||||
)
|
||||
|
||||
# Hand-checked risk-adjusted PVs per row (see module docstring).
|
||||
ROW_PVS = {
|
||||
"ai_contact_resolution": 51_699_826.78,
|
||||
"ai_content_sentiment": 11_326_357.54,
|
||||
"ai_forecasting_supervision": 19_469_777.37,
|
||||
"data_driven_profit_lift": 3_123_065.36,
|
||||
"legacy_solution_savings": 16_077_540.50,
|
||||
"amazon_connect_usage": 20_819_775.10,
|
||||
"implementation_migration": 1_555_794.82,
|
||||
"ongoing_management": 607_505.86,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def composite():
|
||||
return compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
||||
|
||||
|
||||
def test_calendar_mapping():
|
||||
assert YEARS == [2026, 2027, 2028]
|
||||
assert YEAR_INDEX == {2026: 1, 2027: 2, 2028: 3}
|
||||
assert X_LABELS == ["Initial", "2026", "2027", "2028"]
|
||||
assert by_calendar({"1": 10, "2": 20, "3": 30}) == {2026: 10, 2027: 20, 2028: 30}
|
||||
|
||||
|
||||
def test_primitives():
|
||||
assert discount_factor(0, 0.10) == 1.0
|
||||
assert discount_factor(1, 0.10) == pytest.approx(1 / 1.1)
|
||||
assert npv([110], 0.10) == pytest.approx(100)
|
||||
assert npv([110], 0.10, initial=-50) == pytest.approx(50)
|
||||
assert roi_pct(101_696_791, 22_983_076) == pytest.approx(342.48, abs=0.1)
|
||||
assert roi_pct(100, 0) == 0.0
|
||||
assert money(78_713_492) == "$78.7M"
|
||||
assert money(-250_000) == "-$250K"
|
||||
|
||||
|
||||
def test_payback_edges():
|
||||
assert payback_years(0, [100]) == 0.0
|
||||
assert payback_years(500, []) is None
|
||||
assert payback_years(500, [-100, 200]) is None # gap widens, never covered
|
||||
assert payback_years(300, [-100, 400]) == pytest.approx(2.0)
|
||||
assert payback_months(100, [1_200]) == pytest.approx(1.0)
|
||||
assert payback_label(None) == "beyond 2028"
|
||||
assert payback_label(0.0) == "immediate"
|
||||
assert payback_label(0.7178) == "0.7 months (~Jan 2026)"
|
||||
assert payback_label(14.2) == "14.2 months (~Mar 2027)"
|
||||
|
||||
|
||||
def test_per_row_pvs(composite):
|
||||
rows = composite["rows"]["benefits"] + composite["rows"]["costs"]
|
||||
assert len(rows) == 8
|
||||
for row in rows:
|
||||
assert row["pv"] == pytest.approx(ROW_PVS[row["field_key"]], abs=1)
|
||||
|
||||
|
||||
def test_composite_totals_engine_exact(composite):
|
||||
assert composite["benefits_pv"] == pytest.approx(101_696_567.55, abs=1)
|
||||
assert composite["costs_pv"] == pytest.approx(22_983_075.78, abs=1)
|
||||
assert composite["npv"] == pytest.approx(78_713_491.78, abs=1)
|
||||
assert composite["roi_pct"] == pytest.approx(342.4846, abs=0.01)
|
||||
assert composite["payback_months"] == pytest.approx(0.7178, abs=0.001)
|
||||
assert composite["initial_costs"] == pytest.approx(1_196_250, abs=0.01)
|
||||
|
||||
|
||||
def test_composite_reproduces_published(composite):
|
||||
assert composite["benefits_pv"] == pytest.approx(PUBLISHED["benefits_pv"], abs=1_000)
|
||||
assert composite["costs_pv"] == pytest.approx(PUBLISHED["costs_pv"], abs=1_000)
|
||||
assert composite["npv"] == pytest.approx(PUBLISHED["npv"], abs=1_000)
|
||||
assert round(composite["roi_pct"]) == PUBLISHED["roi_pct"]
|
||||
assert composite["payback_months"] < PUBLISHED["payback_months_max"]
|
||||
assert composite["payback_label"] == "0.7 months (~Jan 2026)"
|
||||
|
||||
|
||||
def test_yearly_schedules(composite):
|
||||
assert composite["benefits_by_year"][2026] == pytest.approx(27_279_019.00, abs=0.01)
|
||||
assert composite["benefits_by_year"][2027] == pytest.approx(40_333_658.20, abs=0.01)
|
||||
assert composite["benefits_by_year"][2028] == pytest.approx(57_983_494.40, abs=0.01)
|
||||
assert composite["costs_by_year"][2026] == pytest.approx(7_281_066.70, abs=0.01)
|
||||
assert composite["costs_by_year"][2027] == pytest.approx(8_771_168.50, abs=0.01)
|
||||
assert composite["costs_by_year"][2028] == pytest.approx(10_539_889.05, abs=0.01)
|
||||
assert composite["cumulative_net_by_year"][2028] == pytest.approx(97_807_797.35, abs=0.01)
|
||||
|
||||
|
||||
def test_cross_foots(composite):
|
||||
assert composite["npv"] == pytest.approx(
|
||||
composite["benefits_pv"] - composite["costs_pv"], abs=0.01)
|
||||
for y in YEARS:
|
||||
assert composite["net_by_year"][y] == pytest.approx(
|
||||
composite["benefits_by_year"][y] - composite["costs_by_year"][y], abs=0.01)
|
||||
assert composite["cumulative_net_by_year"][2028] == pytest.approx(
|
||||
sum(composite["net_by_year"].values()) - composite["initial_costs"], abs=0.01)
|
||||
for table, total in (("benefits", "benefits_pv"), ("costs", "costs_pv")):
|
||||
assert sum(r["pv"] for r in composite["rows"][table]) == pytest.approx(
|
||||
composite[total], abs=0.01)
|
||||
96
studies/202602_TEI_Amazon_Connect/tests/test_overlay.py
Normal file
96
studies/202602_TEI_Amazon_Connect/tests/test_overlay.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Client-overlay pins — identity at the composite, linear per-driver
|
||||
scaling, growth re-basing, and copy semantics."""
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
from teicalc import (
|
||||
BENEFIT_DRIVERS,
|
||||
BENEFITS_VERBATIM,
|
||||
COMPOSITE,
|
||||
COST_DRIVERS,
|
||||
COSTS_VERBATIM,
|
||||
ClientDrivers,
|
||||
compute_summary,
|
||||
growth_multiplier,
|
||||
overlay_rows,
|
||||
scale_factor,
|
||||
)
|
||||
|
||||
|
||||
def _row(rows, key):
|
||||
return next(r for r in rows if r["field_key"] == key)
|
||||
|
||||
|
||||
def test_identity_at_composite():
|
||||
"""overlay_rows(COMPOSITE) reproduces the verbatim study to the cent."""
|
||||
ob, oc = overlay_rows(COMPOSITE)
|
||||
got = compute_summary(ob, oc, 0.10)
|
||||
want = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
||||
assert got["benefits_pv"] == pytest.approx(want["benefits_pv"], abs=0.01)
|
||||
assert got["costs_pv"] == pytest.approx(want["costs_pv"], abs=0.01)
|
||||
assert got["npv"] == pytest.approx(want["npv"], abs=0.01)
|
||||
|
||||
|
||||
def test_driver_map_covers_every_row():
|
||||
assert set(BENEFIT_DRIVERS) == {r["field_key"] for r in BENEFITS_VERBATIM}
|
||||
assert set(COST_DRIVERS) == {r["field_key"] for r in COSTS_VERBATIM}
|
||||
|
||||
|
||||
def test_scale_factor():
|
||||
d = ClientDrivers(agents_fte=1_000, annual_contacts_y1=40_000_000)
|
||||
assert scale_factor("agents", d) == pytest.approx(0.5)
|
||||
assert scale_factor("contacts", d) == pytest.approx(2.0)
|
||||
assert scale_factor("fixed", d) == 1.0
|
||||
with pytest.raises(KeyError):
|
||||
scale_factor("revenue", d)
|
||||
|
||||
|
||||
def test_half_agents_halves_agent_rows_only():
|
||||
ob, oc = overlay_rows(ClientDrivers(agents_fte=1_000))
|
||||
assert _row(ob, "ai_forecasting_supervision")["year_values"]["1"] == \
|
||||
pytest.approx(6_651_680 / 2)
|
||||
assert _row(ob, "legacy_solution_savings")["year_values"]["1"] == \
|
||||
pytest.approx(6_177_600 / 2)
|
||||
# Contact-driven and fixed rows unmoved.
|
||||
assert _row(ob, "ai_contact_resolution")["year_values"]["1"] == \
|
||||
pytest.approx(13_911_040)
|
||||
assert _row(oc, "amazon_connect_usage")["year_values"]["1"] == \
|
||||
pytest.approx(6_456_448)
|
||||
assert _row(oc, "implementation_migration")["initial"] == 1_087_500
|
||||
|
||||
|
||||
def test_double_contacts_doubles_usage_only():
|
||||
_, oc = overlay_rows(ClientDrivers(annual_contacts_y1=40_000_000))
|
||||
assert _row(oc, "amazon_connect_usage")["year_values"]["1"] == \
|
||||
pytest.approx(6_456_448 * 2)
|
||||
assert _row(oc, "implementation_migration")["year_values"]["1"] == \
|
||||
pytest.approx(188_333)
|
||||
assert _row(oc, "ongoing_management")["year_values"]["1"] == \
|
||||
pytest.approx(256_200)
|
||||
|
||||
|
||||
def test_growth_rebase():
|
||||
assert growth_multiplier(1, 0.0) == 1.0 # Y1 always 1.0
|
||||
assert growth_multiplier(2, 0.0) == pytest.approx(1 / 1.3)
|
||||
assert growth_multiplier(3, 0.0) == pytest.approx((1 / 1.3) ** 2)
|
||||
assert growth_multiplier(3, 0.30) == 1.0 # composite growth = identity
|
||||
|
||||
ob, oc = overlay_rows(ClientDrivers(growth_rate=0.0))
|
||||
row = _row(ob, "ai_contact_resolution")
|
||||
assert row["year_values"]["1"] == pytest.approx(13_911_040)
|
||||
assert row["year_values"]["2"] == pytest.approx(23_932_480 / 1.3)
|
||||
assert row["year_values"]["3"] == pytest.approx(37_797_760 / 1.3**2)
|
||||
# Fixed rows ignore the growth re-base too.
|
||||
assert _row(oc, "ongoing_management")["year_values"]["2"] == pytest.approx(187_200)
|
||||
|
||||
|
||||
def test_drivers_frozen_and_rows_are_copies():
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
COMPOSITE.agents_fte = 1 # type: ignore[misc]
|
||||
ob, oc = overlay_rows(COMPOSITE)
|
||||
ob[0]["year_values"]["1"] = -1
|
||||
oc[0]["year_values"]["1"] = -1
|
||||
assert BENEFITS_VERBATIM[0]["year_values"]["1"] == 13_911_040
|
||||
assert COSTS_VERBATIM[0]["year_values"]["1"] == 6_456_448
|
||||
75
studies/202602_TEI_Amazon_Connect/tests/test_scenarios.py
Normal file
75
studies/202602_TEI_Amazon_Connect/tests/test_scenarios.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Scenario pins — hand-checked composite results per scenario, clamp
|
||||
behaviour, and copy semantics."""
|
||||
|
||||
import pytest
|
||||
|
||||
from teicalc import (
|
||||
BENEFITS_VERBATIM,
|
||||
COSTS_VERBATIM,
|
||||
SCENARIOS,
|
||||
apply_scenario,
|
||||
compute_summary,
|
||||
)
|
||||
|
||||
|
||||
def _summary(scenario):
|
||||
return compute_summary(
|
||||
apply_scenario(BENEFITS_VERBATIM, scenario),
|
||||
apply_scenario(COSTS_VERBATIM, scenario),
|
||||
0.10,
|
||||
)
|
||||
|
||||
|
||||
def test_scenario_definitions():
|
||||
assert SCENARIOS == {
|
||||
"conservative": {"adoption": 0.80, "risk_delta": 0.10},
|
||||
"moderate": {"adoption": 1.00, "risk_delta": 0.00},
|
||||
"aggressive": {"adoption": 1.15, "risk_delta": -0.05},
|
||||
}
|
||||
|
||||
|
||||
def test_moderate_is_identity():
|
||||
got = _summary("moderate")
|
||||
want = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
||||
assert got["benefits_pv"] == pytest.approx(want["benefits_pv"], abs=0.01)
|
||||
assert got["costs_pv"] == pytest.approx(want["costs_pv"], abs=0.01)
|
||||
|
||||
|
||||
def test_conservative_pins():
|
||||
s = _summary("conservative")
|
||||
assert s["benefits_pv"] == pytest.approx(71_672_867.65, abs=1)
|
||||
assert s["costs_pv"] == pytest.approx(17_437_916.34, abs=1)
|
||||
assert s["npv"] == pytest.approx(54_234_951.31, abs=1)
|
||||
assert s["roi_pct"] == pytest.approx(311.02, abs=0.01)
|
||||
assert s["payback_months"] == pytest.approx(0.763, abs=0.001)
|
||||
|
||||
|
||||
def test_aggressive_pins():
|
||||
s = _summary("aggressive")
|
||||
assert s["benefits_pv"] == pytest.approx(123_911_705.40, abs=1)
|
||||
assert s["costs_pv"] == pytest.approx(27_682_368.61, abs=1)
|
||||
assert s["npv"] == pytest.approx(96_229_336.79, abs=1)
|
||||
assert s["roi_pct"] == pytest.approx(347.62, abs=0.01)
|
||||
assert s["payback_months"] == pytest.approx(0.705, abs=0.001)
|
||||
|
||||
|
||||
def test_risk_delta_clamps_at_zero():
|
||||
"""Conservative subtracts 0.10 from cost risk; usage (0.05) clamps to 0."""
|
||||
rows = apply_scenario(COSTS_VERBATIM, "conservative")
|
||||
usage = next(r for r in rows if r["field_key"] == "amazon_connect_usage")
|
||||
assert usage["risk_adjustment"] == 0.0
|
||||
impl = next(r for r in rows if r["field_key"] == "implementation_migration")
|
||||
assert impl["risk_adjustment"] == pytest.approx(0.0) # 0.10 − 0.10
|
||||
assert impl["initial"] == pytest.approx(1_087_500 * 0.80) # adoption scales initial
|
||||
|
||||
|
||||
def test_unknown_scenario_raises():
|
||||
with pytest.raises(KeyError):
|
||||
apply_scenario(BENEFITS_VERBATIM, "wildly_optimistic")
|
||||
|
||||
|
||||
def test_inputs_not_mutated():
|
||||
apply_scenario(BENEFITS_VERBATIM, "aggressive")
|
||||
apply_scenario(COSTS_VERBATIM, "conservative")
|
||||
assert BENEFITS_VERBATIM[0]["year_values"]["1"] == 13_911_040
|
||||
assert COSTS_VERBATIM[1]["initial"] == 1_087_500
|
||||
15
studies/202602_TEI_Amazon_Connect/tests/test_staging.py
Normal file
15
studies/202602_TEI_Amazon_Connect/tests/test_staging.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
|
||||
|
||||
from teicalc import staging
|
||||
|
||||
|
||||
def test_backstage_prints_only_off_stage(monkeypatch, capsys):
|
||||
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
||||
assert not staging.on_stage()
|
||||
staging.backstage("visible")
|
||||
assert capsys.readouterr().out == "visible\n"
|
||||
|
||||
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
||||
assert staging.on_stage()
|
||||
staging.backstage("hidden")
|
||||
assert capsys.readouterr().out == ""
|
||||
Reference in New Issue
Block a user