Add transport adapters and candidates

This commit is contained in:
jwinterm
2026-07-05 21:00:00 -04:00
parent 6c209b6557
commit a8cb056225
15 changed files with 393 additions and 109 deletions
+1
View File
@@ -46,6 +46,7 @@ python3 -m unittest discover -s tests
## Documentation ## Documentation
- [Architecture](docs/architecture.md) - [Architecture](docs/architecture.md)
- [Candidate transports](docs/candidate-transports.md)
- [Configuration](docs/configuration.md) - [Configuration](docs/configuration.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)
+137
View File
@@ -0,0 +1,137 @@
from __future__ import annotations
from dataclasses import dataclass
import shutil
import socket
@dataclass(frozen=True)
class TransportStatus:
transport: str
installed: bool
running: bool
endpoint: str
adoptable: bool
manage_supported: bool
note: str
@dataclass(frozen=True)
class TransportAdapter:
name: str
profile: str
endpoint: str
adopt_check: str
managed_state: str
install_strategy: str
start_strategy: str
stop_policy: str
note: str
commands: tuple[str, ...] = ()
connect_host: str = ""
connect_port: int = 0
builtin_running: bool = False
manage_supported: bool = True
def inspect(self) -> TransportStatus:
installed = self.builtin_running or any(shutil.which(command) for command in self.commands)
running = self.builtin_running or (
bool(self.connect_host) and bool(self.connect_port) and _can_connect(self.connect_host, self.connect_port)
)
return TransportStatus(
transport=self.name,
installed=installed,
running=running,
endpoint=self.endpoint,
adoptable=running,
manage_supported=self.manage_supported,
note=self.note,
)
def setup_steps(self, *, installed: bool, platform: str) -> tuple[str, ...]:
steps: list[str] = []
if not installed and self.install_strategy != "unsupported":
steps.append(self.install_strategy)
if platform == "android":
steps.append(f"start visible Android foreground service for {self.name}")
steps.append(f"start managed Android {self.name} transport")
steps.append(f"wait for {self.endpoint}")
return tuple(steps)
elif platform == "ios":
steps.append(f"start foreground-only iOS session for {self.name}")
steps.append(f"start foreground-only iOS {self.name} transport")
steps.append(f"wait for {self.endpoint}")
return tuple(steps)
if self.start_strategy != "unsupported":
steps.append(self.start_strategy)
steps.append(f"wait for {self.endpoint}")
return tuple(steps)
ADAPTERS: dict[str, TransportAdapter] = {
"tor": TransportAdapter(
name="tor",
profile="tor",
endpoint="socks5://127.0.0.1:9050",
adopt_check="SOCKS on 127.0.0.1:9050",
managed_state=".ampb/transports/tor",
install_strategy="install Tor provider",
start_strategy="start managed Tor daemon",
stop_policy="stop only AMPB-owned Tor daemon",
note="Tor SOCKS proxy",
commands=("tor",),
connect_host="127.0.0.1",
connect_port=9050,
),
"i2p": TransportAdapter(
name="i2p",
profile="i2p",
endpoint="http://127.0.0.1:4444",
adopt_check="HTTP proxy on 127.0.0.1:4444",
managed_state=".ampb/transports/i2p",
install_strategy="install I2P provider",
start_strategy="start managed I2P router",
stop_policy="stop only AMPB-owned I2P router",
note="I2P HTTP proxy",
commands=("i2pd", "i2prouter"),
connect_host="127.0.0.1",
connect_port=4444,
),
"reticulum": TransportAdapter(
name="reticulum",
profile="reticulum",
endpoint="rns://local",
adopt_check="RNS tools and configured interfaces",
managed_state=".ampb/transports/reticulum",
install_strategy="install Reticulum provider",
start_strategy="start configured Reticulum interface",
stop_policy="stop only AMPB-owned Reticulum process",
note="Reticulum adapter is planned; interface readiness needs explicit config",
commands=("rnsd", "rnstatus", "rnodeconf"),
manage_supported=False,
),
"gemini": TransportAdapter(
name="gemini",
profile="gemini",
endpoint="builtin://gemtext-renderer",
adopt_check="built-in renderer available",
managed_state=".ampb/transports/gemini",
install_strategy="unsupported",
start_strategy="use built-in Gemtext fetch/render path",
stop_policy="no daemon to stop",
note="Built-in Gemtext fetch/render path",
builtin_running=True,
),
}
def adapter_for(name: str) -> TransportAdapter | None:
return ADAPTERS.get(name)
def _can_connect(host: str, port: int) -> bool:
try:
with socket.create_connection((host, port), timeout=0.2):
return True
except OSError:
return False
+97
View File
@@ -0,0 +1,97 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class CandidateTransport:
name: str
role: str
status: str
route_examples: tuple[str, ...]
browser_fit: str
publisher_fit: str
mobile_fit: str
source: str
note: str
CANDIDATE_TRANSPORTS = (
CandidateTransport(
name="ipfs",
role="content-addressed web",
status="next-evaluate",
route_examples=("ipfs://<cid>", "/ipfs/<cid>", "ipns://<name>"),
browser_fit="native fetch or local gateway",
publisher_fit="static site export and pinning",
mobile_fit="gateway first; embedded node later",
source="https://docs.ipfs.tech/concepts/what-is-ipfs/",
note="Strong fit for AMPG static output; not an anonymity layer by itself.",
),
CandidateTransport(
name="yggdrasil",
role="encrypted IPv6 overlay",
status="next-evaluate",
route_examples=("http://[0200::]/", "https://<yggdrasil-host>/"),
browser_fit="ordinary HTTP over overlay IPv6",
publisher_fit="serve normal HTTP/Gemini over overlay address",
mobile_fit="cross-platform, but mobile lifecycle needs adapter proof",
source="https://yggdrasil-network.github.io/",
note="Good fit for old-phone nodes and local/community mesh scenarios.",
),
CandidateTransport(
name="lokinet",
role="low-latency onion-routed overlay",
status="evaluate",
route_examples=("http://<name>.loki/", "https://<name>.loki/"),
browser_fit="ordinary IP traffic over overlay",
publisher_fit="SNApp-style hidden service publishing",
mobile_fit="Android likely first; iOS constrained",
source="https://docs.oxen.io/oxen-docs/products-built-on-oxen/lokinet",
note="Interesting Tor/I2P-adjacent candidate; depends on project health and packaging.",
),
CandidateTransport(
name="hyphanet",
role="privacy-preserving publishing datastore",
status="explore",
route_examples=("USK@...", "CHK@...", "SSK@..."),
browser_fit="local proxy/freesite viewer",
publisher_fit="censorship-resistant site publishing",
mobile_fit="possible on Android; iOS unlikely for full node",
source="https://www.hyphanet.org/",
note="Strong ideological fit; UX and key/address model need careful design.",
),
CandidateTransport(
name="gnunet",
role="privacy-preserving network stack",
status="explore",
route_examples=("gnunet://...", "gns://..."),
browser_fit="adapter required",
publisher_fit="future decentralized naming and publishing",
mobile_fit="research first",
source="https://www.gnunet.org/en/about.html",
note="Powerful but early-alpha per upstream; keep as research track.",
),
CandidateTransport(
name="arweave",
role="permanent web archive",
status="publisher-target",
route_examples=("ar://<txid>", "https://arweave.net/<txid>"),
browser_fit="gateway or HTTP API fetch",
publisher_fit="permanent static snapshots",
mobile_fit="publisher/export flow, not daemon-first",
source="https://docs.arweave.org/developers/arweave-node-server/http-api",
note="Great archival target; permanence has moderation and privacy implications.",
),
CandidateTransport(
name="nostr",
role="signed event relay network",
status="discovery-layer",
route_examples=("nostr:<event>", "nprofile...", "nevent..."),
browser_fit="discovery, comments, mirrors, address announcements",
publisher_fit="announce transport addresses and signed updates",
mobile_fit="good mobile fit; relay selection and spam controls matter",
source="https://github.com/nostr-protocol/nips/blob/master/01.md",
note="Not a web-page transport; useful as signed metadata and social distribution.",
),
)
+32
View File
@@ -2,6 +2,8 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from .adapters import ADAPTERS
from .candidates import CANDIDATE_TRANSPORTS
from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS
from .platforms import PLATFORM_CAPABILITIES from .platforms import PLATFORM_CAPABILITIES
@@ -13,6 +15,8 @@ def generate_docs(root: Path, *, check: bool = False) -> list[Path]:
docs = { docs = {
GENERATED_DIR / "route-rules.md": _route_rules_doc(), GENERATED_DIR / "route-rules.md": _route_rules_doc(),
GENERATED_DIR / "transports.md": _transports_doc(), GENERATED_DIR / "transports.md": _transports_doc(),
GENERATED_DIR / "adapters.md": _adapters_doc(),
GENERATED_DIR / "candidate-transports.md": _candidate_transports_doc(),
GENERATED_DIR / "platform-capabilities.md": _platform_capabilities_doc(), GENERATED_DIR / "platform-capabilities.md": _platform_capabilities_doc(),
} }
changed: list[Path] = [] changed: list[Path] = []
@@ -72,3 +76,31 @@ def _platform_capabilities_doc() -> str:
f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |" f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |"
) )
return _header("Generated Platform Capabilities") + "\n".join(rows) + "\n" return _header("Generated Platform Capabilities") + "\n".join(rows) + "\n"
def _adapters_doc() -> str:
rows = [
"| Adapter | Endpoint | Adopt Check | Install Strategy | Start Strategy | Stop Policy | Note |",
"| --- | --- | --- | --- | --- | --- | --- |",
]
for adapter in ADAPTERS.values():
rows.append(
f"| `{adapter.name}` | `{adapter.endpoint}` | {adapter.adopt_check} | "
f"{adapter.install_strategy} | {adapter.start_strategy} | {adapter.stop_policy} | {adapter.note} |"
)
return _header("Generated Transport Adapters") + "\n".join(rows) + "\n"
def _candidate_transports_doc() -> str:
rows = [
"| Candidate | Role | Status | Routes | Browser Fit | Publisher Fit | Mobile Fit | Source | Note |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
]
for candidate in CANDIDATE_TRANSPORTS:
routes = ", ".join(f"`{route}`" for route in candidate.route_examples)
rows.append(
f"| `{candidate.name}` | {candidate.role} | `{candidate.status}` | {routes} | "
f"{candidate.browser_fit} | {candidate.publisher_fit} | {candidate.mobile_fit} | "
f"[source]({candidate.source}) | {candidate.note} |"
)
return _header("Generated Candidate Transports") + "\n".join(rows) + "\n"
+11 -24
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from .adapters import adapter_for
from .config import AppConfig, default_config from .config import AppConfig, default_config
from .plan import BrowsePlan, plan_url from .plan import BrowsePlan, plan_url
@@ -88,33 +89,19 @@ def _proxy_for(browse_plan: BrowsePlan) -> str:
def _setup_steps(browse_plan: BrowsePlan) -> tuple[str, ...]: def _setup_steps(browse_plan: BrowsePlan) -> tuple[str, ...]:
if not browse_plan.status: if not browse_plan.status:
return () return ()
adapter = adapter_for(browse_plan.route.transport)
if not adapter:
return ()
steps: list[str] = [] steps: list[str] = []
platform = browse_plan.platform_capability.platform platform = browse_plan.platform_capability.platform
if not browse_plan.status.installed: if platform == "android" and not browse_plan.status.installed:
if platform == "android": steps.append(f"install or enable Android {browse_plan.route.transport} provider")
steps.append(f"install or enable Android {browse_plan.route.transport} provider") steps.extend(adapter.setup_steps(installed=True, platform=platform))
elif platform == "ios":
steps.append(f"enable foreground iOS {browse_plan.route.transport} session")
else:
steps.append(f"install {browse_plan.route.transport}")
if platform == "android":
steps.append("start visible Android foreground service")
steps.extend(
[
f"start managed Android {browse_plan.route.transport} transport",
f"wait for {browse_plan.status.endpoint}",
]
)
return tuple(steps) return tuple(steps)
elif platform == "ios": if platform == "ios" and not browse_plan.status.installed:
steps.append("start foreground-only iOS session") steps.append(f"enable foreground iOS {browse_plan.route.transport} session")
steps.extend( steps.extend(adapter.setup_steps(installed=True, platform=platform))
[
f"start foreground-only iOS {browse_plan.route.transport} transport",
f"wait for {browse_plan.status.endpoint}",
]
)
return tuple(steps) return tuple(steps)
steps.extend([f"start managed {browse_plan.route.transport}", f"wait for {browse_plan.status.endpoint}"]) steps.extend(adapter.setup_steps(installed=browse_plan.status.installed, platform=platform))
return tuple(steps) return tuple(steps)
+3 -82
View File
@@ -1,31 +1,13 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from .adapters import ADAPTERS, TransportStatus
import shutil
import socket
@dataclass(frozen=True) TRANSPORTS = tuple(ADAPTERS)
class TransportStatus:
transport: str
installed: bool
running: bool
endpoint: str
adoptable: bool
manage_supported: bool
note: str
TRANSPORTS = ("tor", "i2p", "reticulum", "gemini")
def inspect_transports() -> list[TransportStatus]: def inspect_transports() -> list[TransportStatus]:
return [ return [adapter.inspect() for adapter in ADAPTERS.values()]
_inspect_tor(),
_inspect_i2p(),
_inspect_reticulum(),
_inspect_gemini(),
]
def inspect_transport(name: str) -> TransportStatus | None: def inspect_transport(name: str) -> TransportStatus | None:
@@ -33,64 +15,3 @@ def inspect_transport(name: str) -> TransportStatus | None:
if status.transport == name: if status.transport == name:
return status return status
return None return None
def _inspect_tor() -> TransportStatus:
installed = shutil.which("tor") is not None
running = _can_connect("127.0.0.1", 9050)
return TransportStatus(
transport="tor",
installed=installed,
running=running,
endpoint="socks5://127.0.0.1:9050",
adoptable=running,
manage_supported=True,
note="Tor SOCKS proxy",
)
def _inspect_i2p() -> TransportStatus:
installed = shutil.which("i2pd") is not None or shutil.which("i2prouter") is not None
running = _can_connect("127.0.0.1", 4444)
return TransportStatus(
transport="i2p",
installed=installed,
running=running,
endpoint="http://127.0.0.1:4444",
adoptable=running,
manage_supported=True,
note="I2P HTTP proxy",
)
def _inspect_reticulum() -> TransportStatus:
installed = any(shutil.which(command) for command in ("rnsd", "rnstatus", "rnodeconf"))
return TransportStatus(
transport="reticulum",
installed=installed,
running=False,
endpoint="rns://local",
adoptable=False,
manage_supported=False,
note="Reticulum adapter is planned; interface readiness needs explicit config",
)
def _inspect_gemini() -> TransportStatus:
return TransportStatus(
transport="gemini",
installed=True,
running=True,
endpoint="builtin://gemtext-renderer",
adoptable=True,
manage_supported=True,
note="Built-in Gemtext fetch/render path",
)
def _can_connect(host: str, port: int) -> bool:
try:
with socket.create_connection((host, port), timeout=0.2):
return True
except OSError:
return False
+2
View File
@@ -19,10 +19,12 @@ browser engine until those contracts are stable.
## Main Components ## Main Components
- `ampbrowser.routing`: deterministic URL-to-transport rules. - `ampbrowser.routing`: deterministic URL-to-transport rules.
- `ampbrowser.adapters`: transport ownership, setup, inspection, and health contracts.
- `ampbrowser.transports`: local readiness checks for Tor, I2P, Reticulum, and Gemini. - `ampbrowser.transports`: local readiness checks for Tor, I2P, Reticulum, and Gemini.
- `ampbrowser.plan`: dry-run action planner for browser launches. - `ampbrowser.plan`: dry-run action planner for browser launches.
- `ampbrowser.launch`: side-effect-free launch specs and first-use consent state. - `ampbrowser.launch`: side-effect-free launch specs and first-use consent state.
- `ampbrowser.platforms`: platform capability matrix for desktop, Android, and iOS. - `ampbrowser.platforms`: platform capability matrix for desktop, Android, and iOS.
- `ampbrowser.candidates`: evaluated transport candidates before active adapter support.
- `ampbrowser.docsgen`: generated public documentation from code-owned metadata. - `ampbrowser.docsgen`: generated public documentation from code-owned metadata.
## Browser Strategy ## Browser Strategy
+19
View File
@@ -0,0 +1,19 @@
# Candidate Transports
> Status: draft | Updated 2026-07-06 | Applies to: AMPB and AMPG transport planning
Candidate transports are tracked separately from active adapters. A candidate should not
be routed by default until AMPB has an adapter contract, fixture behavior, mobile posture,
and publishing story for it.
## Selection Rules
- Prefer transports that can open AMPG static output or serve normal web/Gemini content.
- Treat discovery/social protocols as metadata layers, not page transports.
- Treat permanent archives as publishing targets with explicit user warnings.
- Keep mobile constraints visible before a candidate becomes active.
## Current Matrix
Generated candidate metadata lives in
[generated candidate transports](generated/candidate-transports.md).
+12
View File
@@ -0,0 +1,12 @@
# Generated Transport Adapters
> Status: generated | Updated by `python3 -m ampbrowser docs generate` | Applies to: AMPB
This file is generated from code. Do not edit it by hand.
| Adapter | Endpoint | Adopt Check | Install Strategy | Start Strategy | Stop Policy | Note |
| --- | --- | --- | --- | --- | --- | --- |
| `tor` | `socks5://127.0.0.1:9050` | SOCKS on 127.0.0.1:9050 | install Tor provider | start managed Tor daemon | stop only AMPB-owned Tor daemon | Tor SOCKS proxy |
| `i2p` | `http://127.0.0.1:4444` | HTTP proxy on 127.0.0.1:4444 | install I2P provider | start managed I2P router | stop only AMPB-owned I2P router | I2P HTTP proxy |
| `reticulum` | `rns://local` | RNS tools and configured interfaces | install Reticulum provider | start configured Reticulum interface | stop only AMPB-owned Reticulum process | Reticulum adapter is planned; interface readiness needs explicit config |
| `gemini` | `builtin://gemtext-renderer` | built-in renderer available | unsupported | use built-in Gemtext fetch/render path | no daemon to stop | Built-in Gemtext fetch/render path |
+15
View File
@@ -0,0 +1,15 @@
# Generated Candidate Transports
> Status: generated | Updated by `python3 -m ampbrowser docs generate` | Applies to: AMPB
This file is generated from code. Do not edit it by hand.
| Candidate | Role | Status | Routes | Browser Fit | Publisher Fit | Mobile Fit | Source | Note |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `ipfs` | content-addressed web | `next-evaluate` | `ipfs://<cid>`, `/ipfs/<cid>`, `ipns://<name>` | native fetch or local gateway | static site export and pinning | gateway first; embedded node later | [source](https://docs.ipfs.tech/concepts/what-is-ipfs/) | Strong fit for AMPG static output; not an anonymity layer by itself. |
| `yggdrasil` | encrypted IPv6 overlay | `next-evaluate` | `http://[0200::]/`, `https://<yggdrasil-host>/` | ordinary HTTP over overlay IPv6 | serve normal HTTP/Gemini over overlay address | cross-platform, but mobile lifecycle needs adapter proof | [source](https://yggdrasil-network.github.io/) | Good fit for old-phone nodes and local/community mesh scenarios. |
| `lokinet` | low-latency onion-routed overlay | `evaluate` | `http://<name>.loki/`, `https://<name>.loki/` | ordinary IP traffic over overlay | SNApp-style hidden service publishing | Android likely first; iOS constrained | [source](https://docs.oxen.io/oxen-docs/products-built-on-oxen/lokinet) | Interesting Tor/I2P-adjacent candidate; depends on project health and packaging. |
| `hyphanet` | privacy-preserving publishing datastore | `explore` | `USK@...`, `CHK@...`, `SSK@...` | local proxy/freesite viewer | censorship-resistant site publishing | possible on Android; iOS unlikely for full node | [source](https://www.hyphanet.org/) | Strong ideological fit; UX and key/address model need careful design. |
| `gnunet` | privacy-preserving network stack | `explore` | `gnunet://...`, `gns://...` | adapter required | future decentralized naming and publishing | research first | [source](https://www.gnunet.org/en/about.html) | Powerful but early-alpha per upstream; keep as research track. |
| `arweave` | permanent web archive | `publisher-target` | `ar://<txid>`, `https://arweave.net/<txid>` | gateway or HTTP API fetch | permanent static snapshots | publisher/export flow, not daemon-first | [source](https://docs.arweave.org/developers/arweave-node-server/http-api) | Great archival target; permanence has moderation and privacy implications. |
| `nostr` | signed event relay network | `discovery-layer` | `nostr:<event>`, `nprofile...`, `nevent...` | discovery, comments, mirrors, address announcements | announce transport addresses and signed updates | good mobile fit; relay selection and spam controls matter | [source](https://github.com/nostr-protocol/nips/blob/master/01.md) | Not a web-page transport; useful as signed metadata and social distribution. |
+1
View File
@@ -36,3 +36,4 @@ leases, tunnels, and daemon logs in that tree are local state and must not be co
- Reticulum: route contract exists; adapter and readiness model are planned. - Reticulum: route contract exists; adapter and readiness model are planned.
Generated transport metadata lives in [generated transports](generated/transports.md). Generated transport metadata lives in [generated transports](generated/transports.md).
Adapter ownership and setup contracts live in [generated adapters](generated/adapters.md).
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
import unittest
from ampbrowser.adapters import ADAPTERS, adapter_for
from ampbrowser.transports import inspect_transport
class AdaptersTest(unittest.TestCase):
def test_known_adapters_are_registered(self) -> None:
self.assertEqual({"tor", "i2p", "reticulum", "gemini"}, set(ADAPTERS))
def test_gemini_adapter_is_builtin_ready(self) -> None:
status = inspect_transport("gemini")
self.assertIsNotNone(status)
self.assertTrue(status.installed)
self.assertTrue(status.running)
self.assertTrue(status.adoptable)
def test_tor_adapter_setup_steps_respect_ownership_language(self) -> None:
adapter = adapter_for("tor")
self.assertIsNotNone(adapter)
self.assertEqual("stop only AMPB-owned Tor daemon", adapter.stop_policy)
self.assertEqual(("install Tor provider", "start managed Tor daemon", "wait for socks5://127.0.0.1:9050"), adapter.setup_steps(installed=False, platform="desktop"))
if __name__ == "__main__":
unittest.main()
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
import unittest
from ampbrowser.candidates import CANDIDATE_TRANSPORTS
class CandidateTransportsTest(unittest.TestCase):
def test_candidate_matrix_contains_next_evaluation_targets(self) -> None:
by_name = {candidate.name: candidate for candidate in CANDIDATE_TRANSPORTS}
self.assertIn("ipfs", by_name)
self.assertIn("yggdrasil", by_name)
self.assertEqual("next-evaluate", by_name["ipfs"].status)
self.assertEqual("next-evaluate", by_name["yggdrasil"].status)
def test_nostr_is_not_classified_as_page_transport(self) -> None:
by_name = {candidate.name: candidate for candidate in CANDIDATE_TRANSPORTS}
self.assertEqual("discovery-layer", by_name["nostr"].status)
self.assertIn("Not a web-page transport", by_name["nostr"].note)
if __name__ == "__main__":
unittest.main()
+2
View File
@@ -15,6 +15,8 @@ class DocsGenTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
{ {
Path("docs/generated/adapters.md"),
Path("docs/generated/candidate-transports.md"),
Path("docs/generated/platform-capabilities.md"), Path("docs/generated/platform-capabilities.md"),
Path("docs/generated/route-rules.md"), Path("docs/generated/route-rules.md"),
Path("docs/generated/transports.md"), Path("docs/generated/transports.md"),
+6 -3
View File
@@ -33,7 +33,10 @@ class PrepareOpenTest(unittest.TestCase):
self.assertEqual("consent-required", plan.status) self.assertEqual("consent-required", plan.status)
self.assertFalse(plan.consent_granted) self.assertFalse(plan.consent_granted)
self.assertEqual(("install i2p", "start managed i2p", "wait for http://127.0.0.1:4444"), plan.setup_steps) self.assertEqual(
("install I2P provider", "start managed I2P router", "wait for http://127.0.0.1:4444"),
plan.setup_steps,
)
self.assertIn("install and start", plan.message) self.assertIn("install and start", plan.message)
def test_yes_approves_setup_without_executing_it(self) -> None: def test_yes_approves_setup_without_executing_it(self) -> None:
@@ -53,7 +56,7 @@ class PrepareOpenTest(unittest.TestCase):
self.assertEqual("setup-approved", plan.status) self.assertEqual("setup-approved", plan.status)
self.assertTrue(plan.dry_run) self.assertTrue(plan.dry_run)
self.assertTrue(plan.consent_granted) self.assertTrue(plan.consent_granted)
self.assertEqual(("start managed tor", "wait for socks5://127.0.0.1:9050"), plan.setup_steps) self.assertEqual(("start managed Tor daemon", "wait for socks5://127.0.0.1:9050"), plan.setup_steps)
def test_custom_state_dir_changes_profile_path(self) -> None: def test_custom_state_dir_changes_profile_path(self) -> None:
config = AppConfig(state_dir=".local/ampb", transport_modes={}) config = AppConfig(state_dir=".local/ampb", transport_modes={})
@@ -80,7 +83,7 @@ class PrepareOpenTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
( (
"install or enable Android i2p provider", "install or enable Android i2p provider",
"start visible Android foreground service", "start visible Android foreground service for i2p",
"start managed Android i2p transport", "start managed Android i2p transport",
"wait for http://127.0.0.1:4444", "wait for http://127.0.0.1:4444",
), ),