mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add deployment profiles
This commit is contained in:
@@ -105,9 +105,11 @@ python3 -m ampg --config examples/wownero.gateway.toml plan --write-artifacts
|
||||
python3 -m ampg --config examples/wownero.gateway.toml status
|
||||
python3 -m ampg --config examples/wownero.gateway.toml doctor
|
||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run
|
||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --protocol i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --write-artifacts
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build --profile tor-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build --protocol tor --protocol i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml manifest
|
||||
python3 -m ampg --config examples/wownero.gateway.toml preview endpoints
|
||||
@@ -133,6 +135,10 @@ Use `--protocol` to scope operational commands to one or more enabled protocols.
|
||||
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
||||
unselected transports blocking the run.
|
||||
|
||||
Use `--profile` to load a named deployment target from `gateway.toml`. Profiles can set
|
||||
selected protocols, a default platform provider, and safe artifact-writing defaults.
|
||||
Explicit `--protocol` and `--platform` flags override the profile when present.
|
||||
|
||||
## Reusable checklist
|
||||
|
||||
- [ ] Pick source kind: `static-html`, `markdown`, or `ssg-output`.
|
||||
|
||||
+36
-18
@@ -24,7 +24,7 @@ from .route_manifest import (
|
||||
validate_route_manifest,
|
||||
)
|
||||
from .route_policy import RouteExposure, RouteIssue, route_exposures, route_issues
|
||||
from .selection import parse_protocol_filters, select_protocols
|
||||
from .selection import protocols_for_selection, select_profile, select_protocols
|
||||
from .status import DoctorIssue, TransportStatus, doctor_gateway, gateway_status
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
subcommands = parser.add_subparsers(dest="command", required=True)
|
||||
plan_parser = subcommands.add_parser("plan", help="Print a dry-run plan.")
|
||||
_add_protocol_filter(plan_parser)
|
||||
_add_target_selection(plan_parser)
|
||||
plan_parser.add_argument(
|
||||
"--write-artifacts",
|
||||
action="store_true",
|
||||
@@ -48,7 +48,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"status",
|
||||
help="Show enabled transport daemon decisions.",
|
||||
)
|
||||
_add_protocol_filter(status_parser)
|
||||
_add_target_selection(status_parser)
|
||||
status_parser.add_argument(
|
||||
"--platform",
|
||||
choices=PLATFORM_NAMES,
|
||||
@@ -58,7 +58,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"doctor",
|
||||
help="Check source, renderer, route, and daemon readiness.",
|
||||
)
|
||||
_add_protocol_filter(doctor_parser)
|
||||
_add_target_selection(doctor_parser)
|
||||
doctor_parser.add_argument(
|
||||
"--platform",
|
||||
choices=PLATFORM_NAMES,
|
||||
@@ -68,7 +68,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"apply",
|
||||
help="Show or run the transport activation sequence.",
|
||||
)
|
||||
_add_protocol_filter(apply_parser)
|
||||
_add_target_selection(apply_parser)
|
||||
apply_parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
@@ -85,9 +85,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
help="Override platform detection for dry-run checks.",
|
||||
)
|
||||
build_parser = subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
||||
_add_protocol_filter(build_parser)
|
||||
_add_target_selection(build_parser)
|
||||
fixture_parser = subcommands.add_parser("manifest", help="Write AMPB fixture manifests.")
|
||||
_add_protocol_filter(fixture_parser)
|
||||
_add_target_selection(fixture_parser)
|
||||
preview_parser = subcommands.add_parser("preview", help="Preview generated outputs locally.")
|
||||
preview_subcommands = preview_parser.add_subparsers(dest="preview_command", required=True)
|
||||
for preview_command in ("endpoints", "manifest", "serve"):
|
||||
@@ -95,7 +95,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
preview_command,
|
||||
help=f"Preview {preview_command} for generated outputs.",
|
||||
)
|
||||
_add_protocol_filter(command_parser)
|
||||
_add_target_selection(command_parser)
|
||||
command_parser.add_argument(
|
||||
"--base-port",
|
||||
type=int,
|
||||
@@ -113,12 +113,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"explain",
|
||||
help="Print per-protocol route decisions.",
|
||||
)
|
||||
_add_protocol_filter(routes_explain)
|
||||
_add_target_selection(routes_explain)
|
||||
routes_validate = routes_subcommands.add_parser(
|
||||
"validate",
|
||||
help="Fail when a public route has no compatible enabled protocol.",
|
||||
)
|
||||
_add_protocol_filter(routes_validate)
|
||||
_add_target_selection(routes_validate)
|
||||
manifest_parser = subcommands.add_parser(
|
||||
"route-manifest",
|
||||
help="Validate or print the app route manifest contract.",
|
||||
@@ -165,7 +165,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return _cmd_route_manifest(args)
|
||||
config = _selected_config(load_config(args.config), args)
|
||||
if args.command == "plan":
|
||||
return _cmd_plan(config, write_artifacts=args.write_artifacts)
|
||||
return _cmd_plan(config, write_artifacts=_write_artifacts_enabled(config, args))
|
||||
if args.command == "status":
|
||||
return _cmd_status(config, args)
|
||||
if args.command == "doctor":
|
||||
@@ -196,11 +196,11 @@ def _cmd_apply(config, args) -> int:
|
||||
)
|
||||
return 1
|
||||
|
||||
if args.write_artifacts:
|
||||
if _write_artifacts_enabled(config, args):
|
||||
for path in write_activation_artifacts(config):
|
||||
print(f"AMPG_APPLY_ARTIFACT path={path} status=written")
|
||||
|
||||
steps = activation_steps(config, platform_provider=_platform_override(args))
|
||||
steps = activation_steps(config, platform_provider=_platform_override(config, args))
|
||||
for step in steps:
|
||||
_print_activation_step(step)
|
||||
|
||||
@@ -218,7 +218,11 @@ def _cmd_apply(config, args) -> int:
|
||||
return 1 if blocked else 0
|
||||
|
||||
|
||||
def _add_protocol_filter(parser) -> None:
|
||||
def _add_target_selection(parser) -> None:
|
||||
parser.add_argument(
|
||||
"--profile",
|
||||
help="Named deployment profile from gateway config.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--protocol",
|
||||
action="append",
|
||||
@@ -228,12 +232,23 @@ def _add_protocol_filter(parser) -> None:
|
||||
|
||||
|
||||
def _selected_config(config, args):
|
||||
protocols = parse_protocol_filters(getattr(args, "protocol", None))
|
||||
protocols = protocols_for_selection(
|
||||
config,
|
||||
raw_protocols=getattr(args, "protocol", None),
|
||||
profile_name=getattr(args, "profile", None),
|
||||
)
|
||||
return select_protocols(config, protocols)
|
||||
|
||||
|
||||
def _write_artifacts_enabled(config, args) -> bool:
|
||||
if getattr(args, "write_artifacts", False):
|
||||
return True
|
||||
profile = select_profile(config, getattr(args, "profile", None))
|
||||
return bool(profile and profile.write_artifacts)
|
||||
|
||||
|
||||
def _cmd_status(config, args) -> int:
|
||||
statuses = gateway_status(config, platform_provider=_platform_override(args))
|
||||
statuses = gateway_status(config, platform_provider=_platform_override(config, args))
|
||||
for status in statuses:
|
||||
_print_transport_status(status)
|
||||
print(
|
||||
@@ -248,7 +263,7 @@ def _cmd_status(config, args) -> int:
|
||||
|
||||
|
||||
def _cmd_doctor(config, args) -> int:
|
||||
platform_provider = _platform_override(args)
|
||||
platform_provider = _platform_override(config, args)
|
||||
statuses = gateway_status(config, platform_provider=platform_provider)
|
||||
issues = doctor_gateway(config, platform_provider=platform_provider, statuses=statuses)
|
||||
for status in statuses:
|
||||
@@ -559,9 +574,12 @@ def _cmd_docs(args) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
def _platform_override(args):
|
||||
def _platform_override(config, args):
|
||||
if getattr(args, "platform", None):
|
||||
return platform_by_name(args.platform)
|
||||
profile = select_profile(config, getattr(args, "profile", None))
|
||||
if profile and profile.platform:
|
||||
return platform_by_name(profile.platform)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+40
-1
@@ -78,10 +78,20 @@ class SiteConfig:
|
||||
interactions: InteractionConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProfileConfig:
|
||||
name: str
|
||||
protocols: tuple[str, ...] = ()
|
||||
platform: str | None = None
|
||||
write_artifacts: bool = False
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GatewayConfig:
|
||||
config_path: Path
|
||||
sites: list[SiteConfig]
|
||||
profiles: dict[str, ProfileConfig] = field(default_factory=dict)
|
||||
|
||||
|
||||
def load_config(path: Path) -> GatewayConfig:
|
||||
@@ -90,7 +100,36 @@ def load_config(path: Path) -> GatewayConfig:
|
||||
sites = [_parse_site(raw_site, config_path.parent) for raw_site in data.get("site", [])]
|
||||
if not sites:
|
||||
raise ValueError("config must declare at least one [[site]]")
|
||||
return GatewayConfig(config_path=config_path, sites=sites)
|
||||
profiles = _parse_profiles(data.get("profiles", {}))
|
||||
return GatewayConfig(config_path=config_path, sites=sites, profiles=profiles)
|
||||
|
||||
|
||||
def _parse_profiles(raw_profiles: Any) -> dict[str, ProfileConfig]:
|
||||
if raw_profiles is None:
|
||||
return {}
|
||||
if not isinstance(raw_profiles, dict):
|
||||
raise ValueError("profiles must be a table")
|
||||
profiles: dict[str, ProfileConfig] = {}
|
||||
for name, raw_profile in raw_profiles.items():
|
||||
if not isinstance(raw_profile, dict):
|
||||
raise ValueError(f"profile {name!r} must be a table")
|
||||
platform = raw_profile.get("platform")
|
||||
if platform is not None and not isinstance(platform, str):
|
||||
raise ValueError(f"profile {name!r}.platform must be a string")
|
||||
description = raw_profile.get("description", "")
|
||||
if not isinstance(description, str):
|
||||
raise ValueError(f"profile {name!r}.description must be a string")
|
||||
profiles[str(name)] = ProfileConfig(
|
||||
name=str(name),
|
||||
protocols=_string_array(
|
||||
raw_profile.get("protocols", ()),
|
||||
f"profile {name!r}.protocols",
|
||||
),
|
||||
platform=platform,
|
||||
write_artifacts=bool(raw_profile.get("write_artifacts", False)),
|
||||
description=description,
|
||||
)
|
||||
return profiles
|
||||
|
||||
|
||||
def _parse_site(raw_site: dict[str, Any], base_dir: Path) -> SiteConfig:
|
||||
|
||||
@@ -203,6 +203,38 @@ CONFIG_FIELDS = (
|
||||
"transport default",
|
||||
"Maximum interaction tier this protocol target may expose.",
|
||||
),
|
||||
ConfigField(
|
||||
"profiles.<name>",
|
||||
"description",
|
||||
"string",
|
||||
False,
|
||||
"",
|
||||
"Human-readable deployment profile description.",
|
||||
),
|
||||
ConfigField(
|
||||
"profiles.<name>",
|
||||
"protocols",
|
||||
"array<string>",
|
||||
False,
|
||||
"all enabled protocols",
|
||||
"Enabled protocols selected by this deployment profile.",
|
||||
),
|
||||
ConfigField(
|
||||
"profiles.<name>",
|
||||
"platform",
|
||||
"enum",
|
||||
False,
|
||||
"detected platform",
|
||||
"Platform provider used by status, doctor, and apply when not overridden.",
|
||||
),
|
||||
ConfigField(
|
||||
"profiles.<name>",
|
||||
"write_artifacts",
|
||||
"boolean",
|
||||
False,
|
||||
"false",
|
||||
"Whether plan/apply dry-run writes review artifacts by default.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+31
-2
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .config import GatewayConfig, SiteConfig
|
||||
from .config import GatewayConfig, ProfileConfig, SiteConfig
|
||||
|
||||
|
||||
def parse_protocol_filters(raw_filters: list[str] | tuple[str, ...] | None) -> tuple[str, ...]:
|
||||
@@ -56,4 +56,33 @@ def select_protocols(config: GatewayConfig, protocols: tuple[str, ...]) -> Gatew
|
||||
)
|
||||
for site in config.sites
|
||||
]
|
||||
return GatewayConfig(config_path=config.config_path, sites=sites)
|
||||
return GatewayConfig(
|
||||
config_path=config.config_path,
|
||||
sites=sites,
|
||||
profiles=config.profiles,
|
||||
)
|
||||
|
||||
|
||||
def select_profile(config: GatewayConfig, profile_name: str | None) -> ProfileConfig | None:
|
||||
if not profile_name:
|
||||
return None
|
||||
profile = config.profiles.get(profile_name)
|
||||
if profile is None:
|
||||
names = ", ".join(sorted(config.profiles)) or "-"
|
||||
raise ValueError(f"unknown profile {profile_name!r}; available profiles: {names}")
|
||||
return profile
|
||||
|
||||
|
||||
def protocols_for_selection(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
raw_protocols: list[str] | tuple[str, ...] | None,
|
||||
profile_name: str | None,
|
||||
) -> tuple[str, ...]:
|
||||
protocols = parse_protocol_filters(raw_protocols)
|
||||
if protocols:
|
||||
return protocols
|
||||
profile = select_profile(config, profile_name)
|
||||
if profile:
|
||||
return profile.protocols
|
||||
return ()
|
||||
|
||||
+29
-2
@@ -45,7 +45,7 @@ public_allowed = false
|
||||
[site.protocols.clearnet]
|
||||
enabled = true
|
||||
renderer = "clearnet"
|
||||
daemon = "caddy"
|
||||
daemon = "nginx"
|
||||
daemon_policy = "adopt"
|
||||
|
||||
[site.protocols.tor]
|
||||
@@ -100,7 +100,7 @@ workflow.
|
||||
|
||||
| Protocol | Default renderer | Default daemon | Default policy |
|
||||
| --- | --- | --- | --- |
|
||||
| clearnet | `clearnet` | `caddy` | `adopt` |
|
||||
| clearnet | `clearnet` | `nginx` | `adopt` |
|
||||
| tor | `privacy-html` | `tor` | `auto` |
|
||||
| i2p | `privacy-html` | `i2pd` | `auto` |
|
||||
| gemini | `gemtext` | `agate` | `auto` |
|
||||
@@ -246,6 +246,33 @@ not belong in it.
|
||||
Enabled protocols are independent. A config can omit clearnet entirely and publish only
|
||||
to I2P, Tor, Gemini, IPFS, Reticulum, or any combination of supported targets.
|
||||
|
||||
## Deployment profiles
|
||||
|
||||
Profiles are named command defaults stored in `gateway.toml`. They can select enabled
|
||||
protocols, set a default platform provider, and turn on safe plan artifact writes.
|
||||
|
||||
```toml
|
||||
[profiles.mobile-i2p]
|
||||
description = "User-space I2P-only deployment for Android/Termux-style hosts."
|
||||
protocols = ["i2p"]
|
||||
platform = "android-termux"
|
||||
|
||||
[profiles.vps-full]
|
||||
description = "Full VPS deployment."
|
||||
protocols = ["clearnet", "tor", "i2p", "gemini"]
|
||||
platform = "linux-systemd"
|
||||
write_artifacts = true
|
||||
```
|
||||
|
||||
Use profiles with operational commands:
|
||||
|
||||
```sh
|
||||
python3 -m ampg --config gateway.toml build --profile mobile-i2p
|
||||
python3 -m ampg --config gateway.toml apply --dry-run --profile mobile-i2p
|
||||
```
|
||||
|
||||
Explicit `--protocol` and `--platform` flags override profile defaults for that command.
|
||||
|
||||
For an existing HTML site that should publish only to I2P:
|
||||
|
||||
```toml
|
||||
|
||||
@@ -40,6 +40,7 @@ config artifacts.
|
||||
python3 -m ampg --config gateway.toml status
|
||||
python3 -m ampg --config gateway.toml doctor
|
||||
python3 -m ampg --config gateway.toml apply --dry-run
|
||||
python3 -m ampg --config gateway.toml apply --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config gateway.toml apply --dry-run --protocol i2p
|
||||
python3 -m ampg --config gateway.toml apply --dry-run --write-artifacts
|
||||
python3 -m ampg --config gateway.toml doctor --platform android-termux
|
||||
@@ -62,6 +63,10 @@ services.
|
||||
`adopt` failure will block a full activation run, but it will not block
|
||||
`apply --dry-run --protocol i2p` because clearnet is outside that selected run.
|
||||
|
||||
`--profile` loads named command defaults from `gateway.toml`. Profiles can set selected
|
||||
protocols, platform provider, and safe artifact-writing defaults. Explicit command-line
|
||||
flags override profile defaults.
|
||||
|
||||
Platform providers describe how AMPG may supervise managed daemons:
|
||||
|
||||
- `linux-systemd`: system service management after approval.
|
||||
|
||||
@@ -29,3 +29,7 @@ This file is generated from code. Do not edit it by hand.
|
||||
| site.protocols.<name> | `max_asset_bytes` | integer | no | 1048576 | Maximum asset size copied by privacy-html render targets. |
|
||||
| site.protocols.<name> | `script_policy` | enum | no | strip | Script handling policy for privacy-html render targets. |
|
||||
| site.protocols.<name> | `max_tier` | enum | no | transport default | Maximum interaction tier this protocol target may expose. |
|
||||
| profiles.<name> | `description` | string | no | - | Human-readable deployment profile description. |
|
||||
| profiles.<name> | `protocols` | array<string> | no | all enabled protocols | Enabled protocols selected by this deployment profile. |
|
||||
| profiles.<name> | `platform` | enum | no | detected platform | Platform provider used by status, doctor, and apply when not overridden. |
|
||||
| profiles.<name> | `write_artifacts` | boolean | no | false | Whether plan/apply dry-run writes review artifacts by default. |
|
||||
|
||||
@@ -4,6 +4,21 @@ cache_dir = "./.ampg/cache"
|
||||
run_dir = "./.ampg/run"
|
||||
user = "ampg"
|
||||
|
||||
[profiles.mobile-i2p]
|
||||
description = "User-space I2P-only deployment for Android/Termux-style hosts."
|
||||
protocols = ["i2p"]
|
||||
platform = "android-termux"
|
||||
|
||||
[profiles.tor-i2p]
|
||||
description = "Privacy-network deployment without clearnet ingress."
|
||||
protocols = ["tor", "i2p"]
|
||||
|
||||
[profiles.vps-full]
|
||||
description = "Full VPS deployment for clearnet plus generated alternate transports."
|
||||
protocols = ["clearnet", "tor", "i2p", "gemini"]
|
||||
platform = "linux-systemd"
|
||||
write_artifacts = true
|
||||
|
||||
[[site]]
|
||||
id = "wownero"
|
||||
domain = "wownero.org"
|
||||
|
||||
@@ -6,7 +6,12 @@ from pathlib import Path
|
||||
|
||||
from ampg.cli import main
|
||||
from ampg.config import load_config
|
||||
from ampg.selection import parse_protocol_filters, select_protocols
|
||||
from ampg.selection import (
|
||||
parse_protocol_filters,
|
||||
protocols_for_selection,
|
||||
select_profile,
|
||||
select_protocols,
|
||||
)
|
||||
|
||||
|
||||
def _write_config(root: Path) -> Path:
|
||||
@@ -20,6 +25,18 @@ def _write_config(root: Path) -> Path:
|
||||
config_path = root / "gateway.toml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
[profiles.mobile-i2p]
|
||||
protocols = ["i2p"]
|
||||
platform = "android-termux"
|
||||
|
||||
[profiles.tor-i2p]
|
||||
protocols = ["tor", "i2p"]
|
||||
|
||||
[profiles.artifact-profile]
|
||||
protocols = ["i2p"]
|
||||
platform = "android-termux"
|
||||
write_artifacts = true
|
||||
|
||||
[[site]]
|
||||
id = "example"
|
||||
domain = "example.test"
|
||||
@@ -69,6 +86,37 @@ class ProtocolSelectionTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(["i2p"], list(selected.sites[0].protocols))
|
||||
|
||||
def test_config_parses_profiles(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = load_config(_write_config(Path(tmp)))
|
||||
|
||||
profile = select_profile(config, "mobile-i2p")
|
||||
self.assertIsNotNone(profile)
|
||||
self.assertEqual(("i2p",), profile.protocols)
|
||||
self.assertEqual("android-termux", profile.platform)
|
||||
self.assertFalse(profile.write_artifacts)
|
||||
|
||||
def test_profile_supplies_protocols_when_no_cli_protocol_is_given(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = load_config(_write_config(Path(tmp)))
|
||||
|
||||
self.assertEqual(
|
||||
("i2p",),
|
||||
protocols_for_selection(
|
||||
config,
|
||||
raw_protocols=[],
|
||||
profile_name="mobile-i2p",
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
("tor",),
|
||||
protocols_for_selection(
|
||||
config,
|
||||
raw_protocols=["tor"],
|
||||
profile_name="mobile-i2p",
|
||||
),
|
||||
)
|
||||
|
||||
def test_select_protocols_rejects_unknown_protocol(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = load_config(_write_config(Path(tmp)))
|
||||
@@ -122,6 +170,87 @@ class ProtocolSelectionTest(unittest.TestCase):
|
||||
self.assertIn("blocked=0", output_text)
|
||||
self.assertNotIn("protocol=clearnet", output_text)
|
||||
|
||||
def test_apply_profile_filters_protocol_and_uses_platform(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
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")
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
status = main(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
output_text = stdout.getvalue()
|
||||
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("protocol=i2p", output_text)
|
||||
self.assertIn("i2pd is missing; AMPG would prepare managed setup", output_text)
|
||||
self.assertIn("blocked=0", output_text)
|
||||
self.assertNotIn("protocol=clearnet", output_text)
|
||||
|
||||
def test_cli_protocol_overrides_profile_protocols(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_path = _write_config(Path(tmp))
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
status = main(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"status",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
"--protocol",
|
||||
"tor",
|
||||
]
|
||||
)
|
||||
|
||||
output_text = stdout.getvalue()
|
||||
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("protocol=tor", output_text)
|
||||
self.assertIn("platform=android-termux", output_text)
|
||||
self.assertNotIn("protocol=i2p", output_text)
|
||||
|
||||
def test_profile_write_artifacts_defaults_apply_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
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")
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
status = main(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"artifact-profile",
|
||||
]
|
||||
)
|
||||
|
||||
output_text = stdout.getvalue()
|
||||
|
||||
self.assertEqual(0, status)
|
||||
self.assertIn("AMPG_APPLY_ARTIFACT", output_text)
|
||||
self.assertIn("blocked=0", output_text)
|
||||
|
||||
def test_cli_rejects_unknown_protocol_filter(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_path = _write_config(Path(tmp))
|
||||
@@ -133,6 +262,17 @@ class ProtocolSelectionTest(unittest.TestCase):
|
||||
self.assertEqual(1, status)
|
||||
self.assertIn("selected protocol(s) not declared", stderr.getvalue())
|
||||
|
||||
def test_cli_rejects_unknown_profile(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_path = _write_config(Path(tmp))
|
||||
|
||||
stderr = io.StringIO()
|
||||
with contextlib.redirect_stderr(stderr):
|
||||
status = main(["--config", str(config_path), "status", "--profile", "nope"])
|
||||
|
||||
self.assertEqual(1, status)
|
||||
self.assertIn("unknown profile", stderr.getvalue())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user