Check AMPG fixture manifests

This commit is contained in:
jwinterm
2026-07-05 21:16:29 -04:00
parent 3a51468d29
commit f930f606fd
6 changed files with 240 additions and 0 deletions
+2
View File
@@ -38,6 +38,7 @@ python3 -m ampbrowser open https://wownero.org/
python3 -m ampbrowser open http://example.b32.i2p/ --yes python3 -m ampbrowser open http://example.b32.i2p/ --yes
python3 -m ampbrowser open http://example.b32.i2p/ --config examples/config.toml python3 -m ampbrowser open http://example.b32.i2p/ --config examples/config.toml
python3 -m ampbrowser open http://example.b32.i2p/ --platform android python3 -m ampbrowser open http://example.b32.i2p/ --platform android
python3 -m ampbrowser fixture check ../adaptive-multi-protocol-gateway/dist/wownero/ampg-fixture-manifest.json
python3 -m ampbrowser docs generate python3 -m ampbrowser docs generate
python3 -m ampbrowser docs generate --check python3 -m ampbrowser docs generate --check
python3 tools/docs_check.py python3 tools/docs_check.py
@@ -49,6 +50,7 @@ python3 -m unittest discover -s tests
- [Architecture](docs/architecture.md) - [Architecture](docs/architecture.md)
- [Candidate transports](docs/candidate-transports.md) - [Candidate transports](docs/candidate-transports.md)
- [Configuration](docs/configuration.md) - [Configuration](docs/configuration.md)
- [Fixture manifests](docs/fixture-manifests.md)
- [Mobile browser](docs/mobile-browser.md) - [Mobile browser](docs/mobile-browser.md)
- [Open flow](docs/open-flow.md) - [Open flow](docs/open-flow.md)
- [Transport management](docs/transport-management.md) - [Transport management](docs/transport-management.md)
+35
View File
@@ -6,6 +6,7 @@ import sys
from .config import load_config from .config import load_config
from .docsgen import generate_docs from .docsgen import generate_docs
from .fixtures import check_fixture_manifest
from .launch import prepare_open from .launch import prepare_open
from .plan import plan_url from .plan import plan_url
from .platforms import PLATFORM_CHOICES from .platforms import PLATFORM_CHOICES
@@ -43,6 +44,11 @@ def main(argv: list[str] | None = None) -> int:
subcommands.add_parser("inspect", help="Inspect local transport readiness.") subcommands.add_parser("inspect", help="Inspect local transport readiness.")
fixture_parser = subcommands.add_parser("fixture", help="Check AMPG fixture manifests.")
fixture_subcommands = fixture_parser.add_subparsers(dest="fixture_command", required=True)
fixture_check = fixture_subcommands.add_parser("check", help="Validate route expectations.")
fixture_check.add_argument("manifest", type=Path)
docs_parser = subcommands.add_parser("docs", help="Generate or check generated docs.") docs_parser = subcommands.add_parser("docs", help="Generate or check generated docs.")
docs_subcommands = docs_parser.add_subparsers(dest="docs_command", required=True) docs_subcommands = docs_parser.add_subparsers(dest="docs_command", required=True)
docs_generate = docs_subcommands.add_parser("generate", help="Generate docs from code.") docs_generate = docs_subcommands.add_parser("generate", help="Generate docs from code.")
@@ -64,6 +70,8 @@ def main(argv: list[str] | None = None) -> int:
) )
if args.command == "inspect": if args.command == "inspect":
return _cmd_inspect() return _cmd_inspect()
if args.command == "fixture":
return _cmd_fixture(args)
if args.command == "docs": if args.command == "docs":
return _cmd_docs(args) return _cmd_docs(args)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
@@ -169,5 +177,32 @@ def _cmd_docs(args) -> int:
return 1 return 1
def _cmd_fixture(args) -> int:
if args.fixture_command == "check":
result = check_fixture_manifest(args.manifest)
for check in result.checks:
print(
"AMPBROWSER_FIXTURE "
f"site={check.site_id} "
f"protocol={check.protocol} "
f"url={check.url} "
f"expected_transport={check.expected_transport} "
f"actual_transport={check.actual_transport} "
f"expected_profile={check.expected_profile} "
f"actual_profile={check.actual_profile} "
f"status={check.status} "
f"message=\"{_safe(check.message)}\""
)
print(
"AMPBROWSER_FIXTURE_SUMMARY "
f"site={result.site_id} "
f"manifest={result.manifest_path} "
f"checks={len(result.checks)} "
f"status={'ok' if result.ok else 'fail'}"
)
return 0 if result.ok else 1
return 1
def _safe(value: str) -> str: def _safe(value: str) -> str:
return value.replace('"', "'") return value.replace('"', "'")
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
from typing import Any
from .routing import route_url
SUPPORTED_SCHEMAS = {"ampg.fixture-manifest.v1"}
@dataclass(frozen=True)
class FixtureCheck:
site_id: str
protocol: str
url: str
expected_transport: str
actual_transport: str
expected_profile: str
actual_profile: str
status: str
message: str
@dataclass(frozen=True)
class FixtureCheckResult:
manifest_path: Path
schema: str
site_id: str
checks: tuple[FixtureCheck, ...]
@property
def ok(self) -> bool:
return all(check.status == "ok" for check in self.checks)
def check_fixture_manifest(path: Path) -> FixtureCheckResult:
data = _load_manifest(path)
schema = _string(data, "schema")
if schema not in SUPPORTED_SCHEMAS:
raise ValueError(f"{path}: unsupported manifest schema {schema!r}")
site = data.get("site")
if not isinstance(site, dict):
raise ValueError(f"{path}: missing site table")
site_id = _string(site, "id")
fixtures = data.get("fixtures")
if not isinstance(fixtures, list):
raise ValueError(f"{path}: fixtures must be a list")
checks = tuple(_check_fixture(site_id, fixture, path) for fixture in fixtures)
return FixtureCheckResult(
manifest_path=path,
schema=schema,
site_id=site_id,
checks=checks,
)
def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
if not isinstance(fixture, dict):
raise ValueError(f"{path}: fixture entries must be tables")
protocol = _string(fixture, "protocol")
url = _string(fixture, "url")
checks = fixture.get("checks")
if not isinstance(checks, dict):
raise ValueError(f"{path}: {protocol}: missing checks table")
expected_transport = _string(checks, "transport")
expected_profile = _string(checks, "profile")
route = route_url(url)
failures: list[str] = []
if route.transport != expected_transport:
failures.append(f"transport expected {expected_transport} got {route.transport}")
if route.profile != expected_profile:
failures.append(f"profile expected {expected_profile} got {route.profile}")
return FixtureCheck(
site_id=site_id,
protocol=protocol,
url=route.normalized,
expected_transport=expected_transport,
actual_transport=route.transport,
expected_profile=expected_profile,
actual_profile=route.profile,
status="fail" if failures else "ok",
message="; ".join(failures) if failures else "route matches",
)
def _load_manifest(path: Path) -> dict[str, Any]:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"{path}: manifest must be a JSON object")
return data
def _string(data: dict[str, Any], key: str) -> str:
value = data.get(key)
if not isinstance(value, str) or not value:
raise ValueError(f"missing required string field {key!r}")
return value
+22
View File
@@ -0,0 +1,22 @@
# Fixture Manifests
> Status: draft | Updated 2026-07-06 | Applies to: AMPB and AMPG compatibility checks
AMPB can check AMPG fixture manifests without contacting any network. The check confirms
that each generated fixture URL routes to the expected AMPB transport and profile.
## Command
```sh
python3 -m ampbrowser fixture check ../adaptive-multi-protocol-gateway/dist/wownero/ampg-fixture-manifest.json
```
The command exits non-zero when any fixture route does not match the manifest.
## Scope
Fixture checks validate route selection only. They do not verify daemon health, hidden
service reachability, IPFS pinning, TLS, or content freshness.
The manifest must not contain private deployment notes, service keys, credentials, or
host inventory.
+1
View File
@@ -23,3 +23,4 @@ degradation.
- Private addresses, tunnel keys, host inventory, and credentials stay out of public docs. - Private addresses, tunnel keys, host inventory, and credentials stay out of public docs.
- Browser checks prefer dry-run plans before live launch. - Browser checks prefer dry-run plans before live launch.
- Static output from AMPG should be openable by AMPB without manual proxy setup. - Static output from AMPG should be openable by AMPB without manual proxy setup.
- AMPG fixture manifests should pass `ampbrowser fixture check`.
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from ampbrowser.fixtures import check_fixture_manifest
class FixtureCheckTest(unittest.TestCase):
def test_checks_ampg_manifest_routes(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "manifest.json"
path.write_text(
json.dumps(
{
"schema": "ampg.fixture-manifest.v1",
"site": {"id": "wownero", "domain": "wownero.org"},
"fixtures": [
{
"protocol": "clearnet",
"url": "https://wownero.org/",
"checks": {"transport": "clearnet", "profile": "clearnet"},
},
{
"protocol": "tor",
"url": "http://wownero.onion/",
"checks": {"transport": "tor", "profile": "tor"},
},
{
"protocol": "ipfs",
"url": "ipfs://bafyexample",
"checks": {"transport": "ipfs", "profile": "ipfs"},
},
],
}
),
encoding="utf-8",
)
result = check_fixture_manifest(path)
self.assertTrue(result.ok)
self.assertEqual(3, len(result.checks))
def test_reports_route_mismatch(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "manifest.json"
path.write_text(
json.dumps(
{
"schema": "ampg.fixture-manifest.v1",
"site": {"id": "bad", "domain": "example.test"},
"fixtures": [
{
"protocol": "tor",
"url": "https://example.test/",
"checks": {"transport": "tor", "profile": "tor"},
}
],
}
),
encoding="utf-8",
)
result = check_fixture_manifest(path)
self.assertFalse(result.ok)
self.assertEqual("fail", result.checks[0].status)
self.assertIn("transport expected tor got clearnet", result.checks[0].message)
if __name__ == "__main__":
unittest.main()