Files
adaptive-multi-protocol-gat…/tests/test_activation.py
2026-07-06 12:29:08 -04:00

342 lines
11 KiB
Python

import contextlib
import io
import tempfile
import unittest
from pathlib import Path
from ampg.activation import activation_steps, apply_preflight, blocked_steps
from ampg.cli import main
from ampg.config import load_config
from ampg.install_plan import install_state_copies, install_supervisor_actions
from ampg.platforms import platform_by_name
from ampg.status import DaemonProbe
def _probe(*, installed: bool = False, running: bool = False):
def probe(adapter):
executable_path = f"/usr/bin/{adapter.executable}" if installed else None
return DaemonProbe(
installed=installed,
running=running,
executable_path=executable_path,
)
return probe
def _write_config(root: Path, protocol_block: str) -> Path:
source = root / "site"
source.mkdir()
(source / "index.html").write_text("<h1>Hello</h1>", encoding="utf-8")
config_path = root / "gateway.toml"
config_path.write_text(
f"""
[[site]]
id = "example"
domain = "example.test"
[site.source]
kind = "static-html"
path = "./site"
[site.outputs]
root = "./out"
plan_root = "./plan"
{protocol_block}
""",
encoding="utf-8",
)
return config_path
class ActivationTest(unittest.TestCase):
def test_activation_plan_reviews_placeholder_address_before_health(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config_path = _write_config(
root,
"""
[site.protocols.i2p]
enabled = true
renderer = "privacy-html"
daemon = "i2pd"
daemon_policy = "auto"
local_port = 19081
""",
)
output = root / "out/i2p"
output.mkdir(parents=True)
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
config = load_config(config_path)
steps = activation_steps(
config,
platform_provider=platform_by_name("android-termux"),
daemon_probe=_probe(installed=False, running=False),
)
self.assertEqual([], blocked_steps(steps))
by_stage = {step.stage: step for step in steps if step.stage != "artifact"}
self.assertEqual("ready", by_stage["output"].status)
self.assertEqual("manage-owned", by_stage["daemon"].action)
self.assertEqual("capture-address", by_stage["address"].action)
self.assertEqual("review", by_stage["address"].status)
self.assertIn("addresses capture --protocol i2p", by_stage["address"].command)
self.assertEqual("review", by_stage["health"].status)
self.assertTrue(any(step.stage == "artifact" for step in steps))
def test_configured_address_allows_planned_activation_health(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config_path = _write_config(
root,
"""
[site.protocols.i2p]
enabled = true
renderer = "privacy-html"
daemon = "i2pd"
daemon_policy = "auto"
i2p_url = "ready.b32.i2p"
""",
)
output = root / "out/i2p"
output.mkdir(parents=True)
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
config = load_config(config_path)
steps = activation_steps(
config,
platform_provider=platform_by_name("android-termux"),
daemon_probe=_probe(installed=False, running=False),
)
by_stage = {step.stage: step for step in steps if step.stage != "artifact"}
self.assertEqual("use-address", by_stage["address"].action)
self.assertEqual("ready", by_stage["address"].status)
self.assertEqual("planned", by_stage["health"].status)
def test_missing_output_blocks_activation(self):
with tempfile.TemporaryDirectory() as tmp:
config_path = _write_config(
Path(tmp),
"""
[site.protocols.tor]
enabled = true
renderer = "privacy-html"
daemon = "tor"
daemon_policy = "auto"
""",
)
config = load_config(config_path)
steps = activation_steps(
config,
platform_provider=platform_by_name("linux-user"),
daemon_probe=_probe(installed=False, running=False),
)
blocked = blocked_steps(steps)
self.assertTrue(any(step.stage == "output" for step in blocked))
self.assertTrue(any(step.stage == "health" for step in blocked))
def test_apply_preflight_reports_blocked_and_review_rows(self):
with tempfile.TemporaryDirectory() as tmp:
config_path = _write_config(
Path(tmp),
"""
[site.protocols.i2p]
enabled = true
renderer = "privacy-html"
daemon = "i2pd"
daemon_policy = "auto"
""",
)
config = load_config(config_path)
platform_provider = platform_by_name("android-termux")
preflight = apply_preflight(
activation=activation_steps(
config,
platform_provider=platform_provider,
daemon_probe=_probe(installed=False, running=False),
),
state_copies=install_state_copies(
config,
platform_provider=platform_provider,
daemon_probe=_probe(installed=False, running=False),
),
supervisor_actions=install_supervisor_actions(
config,
platform_provider=platform_provider,
daemon_probe=_probe(installed=False, running=False),
),
)
self.assertEqual("blocked", preflight.status)
self.assertGreaterEqual(preflight.blocked, 1)
self.assertGreaterEqual(preflight.review, 1)
self.assertTrue(
any(
item.phase == "activation:output" and item.status == "blocked"
for item in preflight.items
)
)
self.assertTrue(
any(item.phase == "state-copy" and item.status == "review" for item in preflight.items)
)
def test_apply_cli_requires_dry_run(self):
with tempfile.TemporaryDirectory() as tmp:
config_path = _write_config(
Path(tmp),
"""
[site.protocols.gemini]
enabled = true
renderer = "gemtext"
daemon = "agate"
daemon_policy = "external"
""",
)
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
status = main(["--config", str(config_path), "apply"])
self.assertEqual(1, status)
self.assertIn("live apply is not implemented", stderr.getvalue())
def test_apply_dry_run_can_write_review_artifacts(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config_path = _write_config(
root,
"""
[site.protocols.gemini]
enabled = true
renderer = "gemtext"
daemon = "agate"
daemon_policy = "external"
port = 1965
""",
)
output = root / "out/gemini"
output.mkdir(parents=True)
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
status = main(
[
"--config",
str(config_path),
"apply",
"--dry-run",
"--write-artifacts",
"--platform",
"unknown",
]
)
self.assertEqual(0, status)
output_text = stdout.getvalue()
self.assertIn("AMPG_APPLY_ARTIFACT", output_text)
self.assertIn("AMPG_APPLY_STEP", output_text)
self.assertIn('command="-"', output_text)
self.assertIn("blocked=0", output_text)
artifact_line = next(
line for line in output_text.splitlines() if line.startswith("AMPG_APPLY_ARTIFACT")
)
artifact_path = Path(
next(
part.removeprefix("path=")
for part in artifact_line.split()
if part.startswith("path=")
)
)
self.assertTrue(artifact_path.exists())
def test_apply_dry_run_write_artifacts_prints_state_copy_plan(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
config_path = _write_config(
root,
"""
[site.protocols.i2p]
enabled = true
renderer = "privacy-html"
daemon = "i2pd"
daemon_policy = "auto"
""",
)
output = root / "out/i2p"
output.mkdir(parents=True)
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
status = main(
[
"--config",
str(config_path),
"apply",
"--dry-run",
"--write-artifacts",
"--platform",
"android-termux",
]
)
output_text = stdout.getvalue()
self.assertEqual(0, status)
self.assertIn("AMPG_APPLY_STATE_COPY site=example protocol=i2p", output_text)
self.assertIn("kind=daemon-config", output_text)
self.assertIn("kind=loopback-config", output_text)
self.assertIn(str(root / ".ampg/state/example/i2p/i2pd-tunnels.conf"), output_text)
self.assertIn(str(root / ".ampg/state/example/i2p/nginx-loopback.conf"), output_text)
self.assertIn("AMPG_APPLY_SUPERVISOR site=example protocol=i2p", output_text)
self.assertIn("service=ampg-example-i2p-i2pd", output_text)
self.assertIn("service=ampg-example-i2p-nginx-loopback", output_text)
def test_apply_dry_run_prints_preflight_gate(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
config_path = _write_config(
root,
"""
[site.protocols.i2p]
enabled = true
renderer = "privacy-html"
daemon = "i2pd"
daemon_policy = "auto"
""",
)
output = root / "out/i2p"
output.mkdir(parents=True)
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
status = main(
[
"--config",
str(config_path),
"apply",
"--dry-run",
"--write-artifacts",
"--platform",
"android-termux",
]
)
output_text = stdout.getvalue()
self.assertEqual(0, status)
self.assertIn("AMPG_APPLY_PREFLIGHT status=review", output_text)
self.assertIn("AMPG_APPLY_PREFLIGHT_ITEM phase=activation:artifact", output_text)
self.assertIn("AMPG_APPLY_PREFLIGHT_ITEM phase=activation:address", output_text)
self.assertIn('message="live apply requires operator review before mutation"', output_text)
if __name__ == "__main__":
unittest.main()