diff --git a/README.md b/README.md index 7b83083..bb5b8cd 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ python3 -m unittest discover -s tests ## Documentation - [Architecture](docs/architecture.md) +- [Candidate transports](docs/candidate-transports.md) - [Configuration](docs/configuration.md) - [Mobile browser](docs/mobile-browser.md) - [Open flow](docs/open-flow.md) diff --git a/ampbrowser/adapters.py b/ampbrowser/adapters.py new file mode 100644 index 0000000..32904b4 --- /dev/null +++ b/ampbrowser/adapters.py @@ -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 diff --git a/ampbrowser/candidates.py b/ampbrowser/candidates.py new file mode 100644 index 0000000..896e4f9 --- /dev/null +++ b/ampbrowser/candidates.py @@ -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://", "/ipfs/", "ipns://"), + 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:///"), + 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://.loki/", "https://.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://", "https://arweave.net/"), + 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:", "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.", + ), +) diff --git a/ampbrowser/docsgen.py b/ampbrowser/docsgen.py index 2d6b8bd..6c4a512 100644 --- a/ampbrowser/docsgen.py +++ b/ampbrowser/docsgen.py @@ -2,6 +2,8 @@ from __future__ import annotations from pathlib import Path +from .adapters import ADAPTERS +from .candidates import CANDIDATE_TRANSPORTS from .metadata import ROUTE_RULES, TRANSPORT_DEFINITIONS from .platforms import PLATFORM_CAPABILITIES @@ -13,6 +15,8 @@ def generate_docs(root: Path, *, check: bool = False) -> list[Path]: docs = { GENERATED_DIR / "route-rules.md": _route_rules_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(), } changed: list[Path] = [] @@ -72,3 +76,31 @@ def _platform_capabilities_doc() -> str: f"`{capability.adopt}` | `{capability.manage}` | `{capability.install}` | {capability.note} |" ) 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" diff --git a/ampbrowser/launch.py b/ampbrowser/launch.py index f810904..7624ede 100644 --- a/ampbrowser/launch.py +++ b/ampbrowser/launch.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from .adapters import adapter_for from .config import AppConfig, default_config 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, ...]: if not browse_plan.status: return () + adapter = adapter_for(browse_plan.route.transport) + if not adapter: + return () steps: list[str] = [] platform = browse_plan.platform_capability.platform - if not browse_plan.status.installed: - if platform == "android": - steps.append(f"install or enable Android {browse_plan.route.transport} provider") - 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}", - ] - ) + if platform == "android" and not browse_plan.status.installed: + steps.append(f"install or enable Android {browse_plan.route.transport} provider") + steps.extend(adapter.setup_steps(installed=True, platform=platform)) return tuple(steps) - elif platform == "ios": - steps.append("start foreground-only iOS session") - steps.extend( - [ - f"start foreground-only iOS {browse_plan.route.transport} transport", - f"wait for {browse_plan.status.endpoint}", - ] - ) + if platform == "ios" and not browse_plan.status.installed: + steps.append(f"enable foreground iOS {browse_plan.route.transport} session") + steps.extend(adapter.setup_steps(installed=True, platform=platform)) 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) diff --git a/ampbrowser/transports.py b/ampbrowser/transports.py index e992d6e..99c5e0b 100644 --- a/ampbrowser/transports.py +++ b/ampbrowser/transports.py @@ -1,31 +1,13 @@ from __future__ import annotations -from dataclasses import dataclass -import shutil -import socket +from .adapters import ADAPTERS, TransportStatus -@dataclass(frozen=True) -class TransportStatus: - transport: str - installed: bool - running: bool - endpoint: str - adoptable: bool - manage_supported: bool - note: str - - -TRANSPORTS = ("tor", "i2p", "reticulum", "gemini") +TRANSPORTS = tuple(ADAPTERS) def inspect_transports() -> list[TransportStatus]: - return [ - _inspect_tor(), - _inspect_i2p(), - _inspect_reticulum(), - _inspect_gemini(), - ] + return [adapter.inspect() for adapter in ADAPTERS.values()] def inspect_transport(name: str) -> TransportStatus | None: @@ -33,64 +15,3 @@ def inspect_transport(name: str) -> TransportStatus | None: if status.transport == name: return status 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 diff --git a/docs/architecture.md b/docs/architecture.md index 44c137d..5c38548 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,10 +19,12 @@ browser engine until those contracts are stable. ## Main Components - `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.plan`: dry-run action planner for browser launches. - `ampbrowser.launch`: side-effect-free launch specs and first-use consent state. - `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. ## Browser Strategy diff --git a/docs/candidate-transports.md b/docs/candidate-transports.md new file mode 100644 index 0000000..ced7f1b --- /dev/null +++ b/docs/candidate-transports.md @@ -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). diff --git a/docs/generated/adapters.md b/docs/generated/adapters.md new file mode 100644 index 0000000..cf56980 --- /dev/null +++ b/docs/generated/adapters.md @@ -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 | diff --git a/docs/generated/candidate-transports.md b/docs/generated/candidate-transports.md new file mode 100644 index 0000000..5e5b6b6 --- /dev/null +++ b/docs/generated/candidate-transports.md @@ -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://`, `/ipfs/`, `ipns://` | 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:///` | 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://.loki/`, `https://.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://`, `https://arweave.net/` | 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:`, `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. | diff --git a/docs/transport-management.md b/docs/transport-management.md index ab18531..845d064 100644 --- a/docs/transport-management.md +++ b/docs/transport-management.md @@ -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. Generated transport metadata lives in [generated transports](generated/transports.md). +Adapter ownership and setup contracts live in [generated adapters](generated/adapters.md). diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..5b234ba --- /dev/null +++ b/tests/test_adapters.py @@ -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() diff --git a/tests/test_candidates.py b/tests/test_candidates.py new file mode 100644 index 0000000..d6110db --- /dev/null +++ b/tests/test_candidates.py @@ -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() diff --git a/tests/test_docsgen.py b/tests/test_docsgen.py index 644eca4..e90cb8c 100644 --- a/tests/test_docsgen.py +++ b/tests/test_docsgen.py @@ -15,6 +15,8 @@ class DocsGenTest(unittest.TestCase): self.assertEqual( { + Path("docs/generated/adapters.md"), + Path("docs/generated/candidate-transports.md"), Path("docs/generated/platform-capabilities.md"), Path("docs/generated/route-rules.md"), Path("docs/generated/transports.md"), diff --git a/tests/test_launch.py b/tests/test_launch.py index 12354a5..d13b56f 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -33,7 +33,10 @@ class PrepareOpenTest(unittest.TestCase): self.assertEqual("consent-required", plan.status) 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) 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.assertTrue(plan.dry_run) 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: config = AppConfig(state_dir=".local/ampb", transport_modes={}) @@ -80,7 +83,7 @@ class PrepareOpenTest(unittest.TestCase): self.assertEqual( ( "install or enable Android i2p provider", - "start visible Android foreground service", + "start visible Android foreground service for i2p", "start managed Android i2p transport", "wait for http://127.0.0.1:4444", ),