mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
Check AMPG fixture manifests
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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('"', "'")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user