mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Require approved artifact digests for apply
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from ampg.cli import main
|
||||
|
||||
|
||||
def _write_config(root: Path) -> 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(
|
||||
"""
|
||||
[[site]]
|
||||
id = "example"
|
||||
domain = "example.test"
|
||||
|
||||
[site.source]
|
||||
kind = "static-html"
|
||||
path = "./site"
|
||||
|
||||
[site.outputs]
|
||||
root = "./out"
|
||||
plan_root = "./plan"
|
||||
|
||||
[site.protocols.i2p]
|
||||
enabled = true
|
||||
renderer = "privacy-html"
|
||||
daemon = "i2pd"
|
||||
daemon_policy = "auto"
|
||||
i2p_url = "ready.b32.i2p"
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return config_path
|
||||
|
||||
|
||||
def _run_cli(args: list[str]) -> tuple[int, str, str]:
|
||||
stdout = io.StringIO()
|
||||
stderr = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
||||
status = main(args)
|
||||
return status, stdout.getvalue(), stderr.getvalue()
|
||||
|
||||
|
||||
class ApprovalsTest(unittest.TestCase):
|
||||
def test_approval_cli_can_move_apply_preflight_to_ready(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _write_config(root)
|
||||
output = root / "out/i2p"
|
||||
output.mkdir(parents=True)
|
||||
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
|
||||
|
||||
status, output_text, _ = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--write-artifacts",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("AMPG_APPLY_PREFLIGHT status=review", output_text)
|
||||
|
||||
status, output_text, _ = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"approvals",
|
||||
"approve",
|
||||
"--all",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("AMPG_APPROVAL_SUMMARY mode=approve candidates=6", output_text)
|
||||
self.assertIn("written=6", output_text)
|
||||
self.assertTrue((root / ".ampg/state/approvals.json").exists())
|
||||
|
||||
status, output_text, _ = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--write-artifacts",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("AMPG_APPLY_PREFLIGHT status=ready", output_text)
|
||||
self.assertNotIn("AMPG_APPLY_PREFLIGHT_ITEM", output_text)
|
||||
|
||||
def test_approval_cli_reports_stale_artifact_after_edit(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _write_config(root)
|
||||
output = root / "out/i2p"
|
||||
output.mkdir(parents=True)
|
||||
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
|
||||
_run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--write-artifacts",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
_run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"approvals",
|
||||
"approve",
|
||||
"--kind",
|
||||
"daemon-config",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
artifact = root / "plan/example/i2p/managed/android-termux/i2pd-tunnels.conf"
|
||||
artifact.write_text(artifact.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8")
|
||||
|
||||
status, output_text, _ = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"approvals",
|
||||
"list",
|
||||
"--kind",
|
||||
"daemon-config",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("status=stale", output_text)
|
||||
self.assertIn("artifact differs from current generator output", output_text)
|
||||
|
||||
def test_approval_cli_requires_explicit_scope(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_path = _write_config(Path(tmp))
|
||||
|
||||
status, _, error_text = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"approvals",
|
||||
"approve",
|
||||
"--platform",
|
||||
"android-termux",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(1, status)
|
||||
self.assertIn("pass --all or --kind", error_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,6 +4,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from ampg.approvals import ApprovalInput, approve_artifacts
|
||||
from ampg.cli import main
|
||||
from ampg.config import load_config
|
||||
from ampg.install_plan import (
|
||||
@@ -294,7 +295,8 @@ daemon_policy = "auto"
|
||||
|
||||
by_kind = {copy.kind: copy for copy in copies}
|
||||
self.assertEqual({"daemon-config", "loopback-config"}, set(by_kind))
|
||||
self.assertEqual("planned", by_kind["daemon-config"].status)
|
||||
self.assertEqual("review", by_kind["daemon-config"].status)
|
||||
self.assertIn("not approved", by_kind["daemon-config"].message)
|
||||
self.assertEqual(
|
||||
root / ".ampg/state/example/i2p/i2pd-tunnels.conf",
|
||||
by_kind["daemon-config"].target,
|
||||
@@ -334,7 +336,8 @@ daemon_policy = "auto"
|
||||
|
||||
by_kind = {action.kind: action for action in actions}
|
||||
self.assertEqual({"daemon-supervisor", "loopback-supervisor"}, set(by_kind))
|
||||
self.assertEqual("planned", by_kind["daemon-supervisor"].status)
|
||||
self.assertEqual("review", by_kind["daemon-supervisor"].status)
|
||||
self.assertIn("not approved", by_kind["daemon-supervisor"].message)
|
||||
self.assertEqual("ampg-example-i2p-i2pd", by_kind["daemon-supervisor"].service)
|
||||
self.assertEqual("sv-enable ampg-example-i2p-i2pd", by_kind["daemon-supervisor"].command)
|
||||
self.assertEqual(
|
||||
@@ -346,6 +349,54 @@ daemon_policy = "auto"
|
||||
by_kind["loopback-supervisor"].command,
|
||||
)
|
||||
|
||||
def test_approved_managed_artifacts_enable_copy_and_supervisor_plans(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = load_config(
|
||||
_write_config(
|
||||
Path(tmp).resolve(),
|
||||
"""
|
||||
[site.protocols.i2p]
|
||||
enabled = true
|
||||
renderer = "privacy-html"
|
||||
daemon = "i2pd"
|
||||
daemon_policy = "auto"
|
||||
""",
|
||||
)
|
||||
)
|
||||
artifacts = write_install_artifacts(
|
||||
config,
|
||||
platform_provider=platform_by_name("android-termux"),
|
||||
daemon_probe=_probe(installed=False, running=False),
|
||||
)
|
||||
approve_artifacts(
|
||||
config,
|
||||
[
|
||||
ApprovalInput(
|
||||
site_id=artifact.site_id,
|
||||
protocol=artifact.protocol,
|
||||
platform=artifact.platform,
|
||||
kind=artifact.kind,
|
||||
path=artifact.path,
|
||||
content=artifact.content,
|
||||
)
|
||||
for artifact in artifacts
|
||||
],
|
||||
)
|
||||
|
||||
copies = install_state_copies(
|
||||
config,
|
||||
platform_provider=platform_by_name("android-termux"),
|
||||
daemon_probe=_probe(installed=False, running=False),
|
||||
)
|
||||
actions = install_supervisor_actions(
|
||||
config,
|
||||
platform_provider=platform_by_name("android-termux"),
|
||||
daemon_probe=_probe(installed=False, running=False),
|
||||
)
|
||||
|
||||
self.assertTrue(all(copy.status == "planned" for copy in copies))
|
||||
self.assertTrue(all(action.status == "planned" for action in actions))
|
||||
|
||||
def test_install_plan_cli_can_write_managed_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user