mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add apply preflight gate
This commit is contained in:
@@ -57,8 +57,9 @@ ampg apply --dry-run
|
|||||||
```
|
```
|
||||||
|
|
||||||
`plan` prints rendered output paths, daemon decisions, port bindings, config diffs, and
|
`plan` prints rendered output paths, daemon decisions, port bindings, config diffs, and
|
||||||
operator actions. The current `apply --dry-run` prints the activation sequence without
|
operator actions. The current `apply --dry-run` prints the activation sequence and a
|
||||||
changing services. Live apply support will perform only approved changes.
|
preflight verdict without changing services. Live apply support will perform only
|
||||||
|
approved changes.
|
||||||
|
|
||||||
## Interaction tiers
|
## Interaction tiers
|
||||||
|
|
||||||
@@ -149,10 +150,12 @@ Captured transport addresses are written under `gateway.state_dir` by
|
|||||||
`addresses capture` or `addresses set`. Fixture manifests and health plans use explicit
|
`addresses capture` or `addresses set`. Fixture manifests and health plans use explicit
|
||||||
config first, then captured addresses, then deterministic placeholders.
|
config first, then captured addresses, then deterministic placeholders.
|
||||||
|
|
||||||
`apply --dry-run` includes an address stage. Placeholder addresses remain review items
|
`apply --dry-run` includes an address stage and an `AMPG_APPLY_PREFLIGHT` gate.
|
||||||
until the generated transport identity is captured or configured. With `--write-artifacts`,
|
Placeholder addresses remain review items until the generated transport identity is
|
||||||
apply also prints reviewed config artifacts that would be copied into managed state and
|
captured or configured. With `--write-artifacts`, apply also prints reviewed config
|
||||||
the supervisor services that would be registered or started.
|
artifacts that would be copied into managed state and the supervisor services that would
|
||||||
|
be registered or started. The preflight gate reports `blocked`, `review`, or `ready`
|
||||||
|
across activation steps, managed-state copies, and supervisor actions.
|
||||||
|
|
||||||
Use `--protocol` to scope operational commands to one or more enabled protocols. This
|
Use `--protocol` to scope operational commands to one or more enabled protocols. This
|
||||||
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
||||||
@@ -172,5 +175,5 @@ Explicit `--protocol` and `--platform` flags override the profile when present.
|
|||||||
- [ ] Run `ampg build`; inspect generated variants.
|
- [ ] Run `ampg build`; inspect generated variants.
|
||||||
- [ ] Run AMPB fixture checks against generated manifests.
|
- [ ] Run AMPB fixture checks against generated manifests.
|
||||||
- [ ] Run `ampg health-plan`; review post-start transport checks.
|
- [ ] Run `ampg health-plan`; review post-start transport checks.
|
||||||
- [ ] Run `ampg apply --dry-run`; review the activation sequence.
|
- [ ] Run `ampg apply --dry-run`; review the activation sequence and preflight gate.
|
||||||
- [ ] Run live apply after the activation sequence is clean.
|
- [ ] Run live apply after the preflight gate is ready.
|
||||||
|
|||||||
+107
-1
@@ -6,7 +6,11 @@ import shlex
|
|||||||
|
|
||||||
from .addresses import AddressRecord, address_registry_path, effective_address_records
|
from .addresses import AddressRecord, address_registry_path, effective_address_records
|
||||||
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
||||||
from .install_plan import write_install_artifacts
|
from .install_plan import (
|
||||||
|
InstallStateCopy,
|
||||||
|
InstallSupervisorAction,
|
||||||
|
write_install_artifacts,
|
||||||
|
)
|
||||||
from .plan import plan_artifacts, write_plan_artifacts
|
from .plan import plan_artifacts, write_plan_artifacts
|
||||||
from .platforms import PlatformProvider
|
from .platforms import PlatformProvider
|
||||||
from .status import (
|
from .status import (
|
||||||
@@ -29,6 +33,30 @@ class ActivationStep:
|
|||||||
command: str = "-"
|
command: str = "-"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ApplyPreflightItem:
|
||||||
|
phase: str
|
||||||
|
site_id: str
|
||||||
|
protocol: str
|
||||||
|
status: str
|
||||||
|
action: str
|
||||||
|
target: str
|
||||||
|
command: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ApplyPreflight:
|
||||||
|
status: str
|
||||||
|
items: tuple[ApplyPreflightItem, ...]
|
||||||
|
ready: int
|
||||||
|
review: int
|
||||||
|
planned: int
|
||||||
|
blocked: int
|
||||||
|
skipped: int
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
def activation_steps(
|
def activation_steps(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
@@ -93,6 +121,45 @@ def blocked_steps(steps: list[ActivationStep]) -> list[ActivationStep]:
|
|||||||
return [step for step in steps if step.status == "blocked"]
|
return [step for step in steps if step.status == "blocked"]
|
||||||
|
|
||||||
|
|
||||||
|
def apply_preflight(
|
||||||
|
*,
|
||||||
|
activation: list[ActivationStep],
|
||||||
|
state_copies: list[InstallStateCopy],
|
||||||
|
supervisor_actions: list[InstallSupervisorAction],
|
||||||
|
) -> ApplyPreflight:
|
||||||
|
items = tuple(
|
||||||
|
[
|
||||||
|
*(_activation_preflight_item(step) for step in activation),
|
||||||
|
*(_state_copy_preflight_item(copy) for copy in state_copies),
|
||||||
|
*(_supervisor_preflight_item(action) for action in supervisor_actions),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
ready = sum(1 for item in items if item.status == "ready")
|
||||||
|
review = sum(1 for item in items if item.status == "review")
|
||||||
|
planned = sum(1 for item in items if item.status == "planned")
|
||||||
|
blocked = sum(1 for item in items if item.status == "blocked")
|
||||||
|
skipped = sum(1 for item in items if item.status == "skipped")
|
||||||
|
if blocked:
|
||||||
|
status = "blocked"
|
||||||
|
message = "live apply is blocked until reported items are fixed"
|
||||||
|
elif review:
|
||||||
|
status = "review"
|
||||||
|
message = "live apply requires operator review before mutation"
|
||||||
|
else:
|
||||||
|
status = "ready"
|
||||||
|
message = "live apply preflight is clean"
|
||||||
|
return ApplyPreflight(
|
||||||
|
status=status,
|
||||||
|
items=items,
|
||||||
|
ready=ready,
|
||||||
|
review=review,
|
||||||
|
planned=planned,
|
||||||
|
blocked=blocked,
|
||||||
|
skipped=skipped,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _protocol_steps(
|
def _protocol_steps(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
@@ -258,6 +325,45 @@ def _transport_activation_status(status: TransportStatus) -> str:
|
|||||||
return "ready"
|
return "ready"
|
||||||
|
|
||||||
|
|
||||||
|
def _activation_preflight_item(step: ActivationStep) -> ApplyPreflightItem:
|
||||||
|
return ApplyPreflightItem(
|
||||||
|
phase=f"activation:{step.stage}",
|
||||||
|
site_id=step.site_id,
|
||||||
|
protocol=step.protocol,
|
||||||
|
status=step.status,
|
||||||
|
action=step.action,
|
||||||
|
target=step.target,
|
||||||
|
command=step.command,
|
||||||
|
message=step.message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _state_copy_preflight_item(copy: InstallStateCopy) -> ApplyPreflightItem:
|
||||||
|
return ApplyPreflightItem(
|
||||||
|
phase="state-copy",
|
||||||
|
site_id=copy.site_id,
|
||||||
|
protocol=copy.protocol,
|
||||||
|
status=copy.status,
|
||||||
|
action=copy.kind,
|
||||||
|
target=str(copy.target),
|
||||||
|
command=copy.command,
|
||||||
|
message=copy.message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _supervisor_preflight_item(action: InstallSupervisorAction) -> ApplyPreflightItem:
|
||||||
|
return ApplyPreflightItem(
|
||||||
|
phase="supervisor",
|
||||||
|
site_id=action.site_id,
|
||||||
|
protocol=action.protocol,
|
||||||
|
status=action.status,
|
||||||
|
action=action.kind,
|
||||||
|
target=action.service,
|
||||||
|
command=action.command,
|
||||||
|
message=action.message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def write_activation_artifacts(
|
def write_activation_artifacts(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
|
|||||||
+51
-6
@@ -5,8 +5,11 @@ from pathlib import Path
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .activation import (
|
from .activation import (
|
||||||
|
ApplyPreflight,
|
||||||
|
ApplyPreflightItem,
|
||||||
ActivationStep,
|
ActivationStep,
|
||||||
activation_steps,
|
activation_steps,
|
||||||
|
apply_preflight,
|
||||||
blocked_steps,
|
blocked_steps,
|
||||||
write_activation_artifacts,
|
write_activation_artifacts,
|
||||||
)
|
)
|
||||||
@@ -302,19 +305,33 @@ def _cmd_apply(config, args) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
platform_provider = _platform_override(config, args)
|
platform_provider = _platform_override(config, args)
|
||||||
if _write_artifacts_enabled(config, args):
|
write_artifacts = _write_artifacts_enabled(config, args)
|
||||||
|
if write_artifacts:
|
||||||
for path in write_activation_artifacts(config, platform_provider=platform_provider):
|
for path in write_activation_artifacts(config, platform_provider=platform_provider):
|
||||||
print(f"AMPG_APPLY_ARTIFACT path={path} status=written")
|
print(f"AMPG_APPLY_ARTIFACT path={path} status=written")
|
||||||
for copy in install_state_copies(config, platform_provider=platform_provider):
|
|
||||||
|
state_copies = install_state_copies(config, platform_provider=platform_provider)
|
||||||
|
supervisor_actions = install_supervisor_actions(config, platform_provider=platform_provider)
|
||||||
|
if write_artifacts:
|
||||||
|
for copy in state_copies:
|
||||||
_print_install_state_copy(copy)
|
_print_install_state_copy(copy)
|
||||||
for action in install_supervisor_actions(config, platform_provider=platform_provider):
|
for action in supervisor_actions:
|
||||||
_print_supervisor_action(action)
|
_print_supervisor_action(action)
|
||||||
|
|
||||||
steps = activation_steps(config, platform_provider=platform_provider)
|
steps = activation_steps(config, platform_provider=platform_provider)
|
||||||
for step in steps:
|
for step in steps:
|
||||||
_print_activation_step(step)
|
_print_activation_step(step)
|
||||||
|
|
||||||
blocked = blocked_steps(steps)
|
preflight = apply_preflight(
|
||||||
|
activation=steps,
|
||||||
|
state_copies=state_copies,
|
||||||
|
supervisor_actions=supervisor_actions,
|
||||||
|
)
|
||||||
|
for item in preflight.items:
|
||||||
|
if item.status in {"blocked", "review"}:
|
||||||
|
_print_apply_preflight_item(item)
|
||||||
|
_print_apply_preflight(preflight)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"AMPG_APPLY_SUMMARY "
|
"AMPG_APPLY_SUMMARY "
|
||||||
"mode=dry-run "
|
"mode=dry-run "
|
||||||
@@ -323,9 +340,9 @@ def _cmd_apply(config, args) -> int:
|
|||||||
f"ready={sum(1 for step in steps if step.status == 'ready')} "
|
f"ready={sum(1 for step in steps if step.status == 'ready')} "
|
||||||
f"review={sum(1 for step in steps if step.status == 'review')} "
|
f"review={sum(1 for step in steps if step.status == 'review')} "
|
||||||
f"planned={sum(1 for step in steps if step.status == 'planned')} "
|
f"planned={sum(1 for step in steps if step.status == 'planned')} "
|
||||||
f"blocked={len(blocked)}"
|
f"blocked={len(blocked_steps(steps))}"
|
||||||
)
|
)
|
||||||
return 1 if blocked else 0
|
return 1 if preflight.status == "blocked" else 0
|
||||||
|
|
||||||
|
|
||||||
def _cmd_install_plan(config, args) -> int:
|
def _cmd_install_plan(config, args) -> int:
|
||||||
@@ -562,6 +579,34 @@ def _print_supervisor_action(action: InstallSupervisorAction) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_apply_preflight(preflight: ApplyPreflight) -> None:
|
||||||
|
print(
|
||||||
|
"AMPG_APPLY_PREFLIGHT "
|
||||||
|
f"status={preflight.status} "
|
||||||
|
f"items={len(preflight.items)} "
|
||||||
|
f"ready={preflight.ready} "
|
||||||
|
f"review={preflight.review} "
|
||||||
|
f"planned={preflight.planned} "
|
||||||
|
f"skipped={preflight.skipped} "
|
||||||
|
f"blocked={preflight.blocked} "
|
||||||
|
f"message=\"{_quote(preflight.message)}\""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_apply_preflight_item(item: ApplyPreflightItem) -> None:
|
||||||
|
print(
|
||||||
|
"AMPG_APPLY_PREFLIGHT_ITEM "
|
||||||
|
f"phase={item.phase} "
|
||||||
|
f"site={item.site_id} "
|
||||||
|
f"protocol={item.protocol} "
|
||||||
|
f"status={item.status} "
|
||||||
|
f"action={item.action} "
|
||||||
|
f"target=\"{_quote(item.target)}\" "
|
||||||
|
f"command=\"{_quote(item.command)}\" "
|
||||||
|
f"message=\"{_quote(item.message)}\""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _print_activation_step(step: ActivationStep) -> None:
|
def _print_activation_step(step: ActivationStep) -> None:
|
||||||
print(
|
print(
|
||||||
"AMPG_APPLY_STEP "
|
"AMPG_APPLY_STEP "
|
||||||
|
|||||||
@@ -113,8 +113,9 @@ render-only environment while making daemon ownership decisions explicit.
|
|||||||
activation sequence. Each enabled protocol gets output, artifact, daemon, and health
|
activation sequence. Each enabled protocol gets output, artifact, daemon, and health
|
||||||
steps, plus an address step when AMPG must capture or confirm a generated transport
|
steps, plus an address step when AMPG must capture or confirm a generated transport
|
||||||
identity. With `--write-artifacts`, it also prints planned copies from reviewed artifacts
|
identity. With `--write-artifacts`, it also prints planned copies from reviewed artifacts
|
||||||
into managed state and planned supervisor start/reload actions. Blocked steps stop the
|
into managed state and planned supervisor start/reload actions. The command emits an
|
||||||
command before any live apply path can touch services.
|
`AMPG_APPLY_PREFLIGHT` verdict across activation, state-copy, and supervisor rows.
|
||||||
|
Blocked preflight items stop the command before any live apply path can touch services.
|
||||||
|
|
||||||
The dry-run command is intentionally stricter than `doctor`: missing generated output is
|
The dry-run command is intentionally stricter than `doctor`: missing generated output is
|
||||||
blocked during activation, because AMPG must not point a transport at an unbuilt output
|
blocked during activation, because AMPG must not point a transport at an unbuilt output
|
||||||
|
|||||||
+3
-3
@@ -49,9 +49,9 @@ python3 tools/docs_check.py
|
|||||||
## Public/private boundary
|
## Public/private boundary
|
||||||
|
|
||||||
Public docs include reusable contracts, generated references, and concise operator-facing
|
Public docs include reusable contracts, generated references, and concise operator-facing
|
||||||
guidance. Private docs are ignored under `docs/private/` and are the place for strategy,
|
guidance. Private docs are ignored under `docs/private/` and are the place for
|
||||||
internal discussion, deployment inventory, working notes, tracking, timelines, and any
|
non-public planning, deployment inventory, working notes, and real host-specific
|
||||||
real host-specific material.
|
material.
|
||||||
|
|
||||||
Public docs can include example domains, ports, and fake keys. Real daemon keys,
|
Public docs can include example domains, ports, and fake keys. Real daemon keys,
|
||||||
production paths with secrets, private host inventory, and internal planning notes belong
|
production paths with secrets, private host inventory, and internal planning notes belong
|
||||||
|
|||||||
@@ -84,10 +84,12 @@ Missing output blocks the plan, while placeholder transport addresses are review
|
|||||||
|
|
||||||
`apply --dry-run` prints the activation sequence for each enabled protocol: generated
|
`apply --dry-run` prints the activation sequence for each enabled protocol: generated
|
||||||
output readiness, config artifacts to review, daemon action, address capture or reuse,
|
output readiness, config artifacts to review, daemon action, address capture or reuse,
|
||||||
and post-apply health checks. It exits nonzero when any step is blocked. `--write-artifacts`
|
and post-apply health checks. It also prints `AMPG_APPLY_PREFLIGHT` plus blocked or
|
||||||
writes reviewable config snippets to the configured plan root, then prints the planned
|
review preflight items across activation, managed-state copies, and supervisor actions.
|
||||||
copies from those reviewed artifacts into `gateway.state_dir` and the supervisor services
|
It exits nonzero when the preflight status is blocked. `--write-artifacts` writes
|
||||||
that would be registered or started. It still does not install or reload services.
|
reviewable config snippets to the configured plan root, then prints the planned copies
|
||||||
|
from those reviewed artifacts into `gateway.state_dir` and the supervisor services that
|
||||||
|
would be registered or started. It still does not install or reload services.
|
||||||
|
|
||||||
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
||||||
`adopt` failure will block a full activation run, but it will not block
|
`adopt` failure will block a full activation run, but it will not block
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
> Status: draft | Updated 2026-07-06 | Applies to: AMPG route exposure policy
|
> Status: draft | Updated 2026-07-06 | Applies to: AMPG route exposure policy
|
||||||
|
|
||||||
Interaction capabilities define what AMPG may expose for dynamic applications. They are
|
Interaction capabilities define what AMPG may expose for dynamic applications. They are
|
||||||
public, generic rules. Project strategy, target names, and implementation notes belong in
|
public, generic rules. Non-public project notes belong in `docs/private/`.
|
||||||
`docs/private/`.
|
|
||||||
|
|
||||||
## Generated Policy
|
## Generated Policy
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Current managed defaults:
|
|||||||
AMPG must never delete sensitive contract paths without an explicit destructive command.
|
AMPG must never delete sensitive contract paths without an explicit destructive command.
|
||||||
`apply --dry-run --write-artifacts` prints `AMPG_APPLY_STATE_COPY` rows that show which
|
`apply --dry-run --write-artifacts` prints `AMPG_APPLY_STATE_COPY` rows that show which
|
||||||
reviewed artifacts would be copied into managed state and `AMPG_APPLY_SUPERVISOR` rows
|
reviewed artifacts would be copied into managed state and `AMPG_APPLY_SUPERVISOR` rows
|
||||||
for the services that would be registered or started. Live apply should perform those
|
for the services that would be registered or started. The accompanying
|
||||||
reviewed copies, start daemons, capture public addresses, and then run published health
|
`AMPG_APPLY_PREFLIGHT` verdict includes those rows so live apply can refuse mutation when
|
||||||
checks.
|
state or supervisor inputs still need review. Live apply should perform reviewed copies,
|
||||||
|
start daemons, capture public addresses, and then run published health checks.
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ampg.activation import activation_steps, blocked_steps
|
from ampg.activation import activation_steps, apply_preflight, blocked_steps
|
||||||
from ampg.cli import main
|
from ampg.cli import main
|
||||||
from ampg.config import load_config
|
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.platforms import platform_by_name
|
||||||
from ampg.status import DaemonProbe
|
from ampg.status import DaemonProbe
|
||||||
|
|
||||||
@@ -139,6 +140,52 @@ daemon_policy = "auto"
|
|||||||
self.assertTrue(any(step.stage == "output" for step in blocked))
|
self.assertTrue(any(step.stage == "output" for step in blocked))
|
||||||
self.assertTrue(any(step.stage == "health" 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):
|
def test_apply_cli_requires_dry_run(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
config_path = _write_config(
|
config_path = _write_config(
|
||||||
@@ -251,6 +298,44 @@ daemon_policy = "auto"
|
|||||||
self.assertIn("service=ampg-example-i2p-i2pd", output_text)
|
self.assertIn("service=ampg-example-i2p-i2pd", output_text)
|
||||||
self.assertIn("service=ampg-example-i2p-nginx-loopback", 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user