Add IPFS dry-run adapter support

This commit is contained in:
jwinterm
2026-07-05 21:05:46 -04:00
parent a8cb056225
commit 3a51468d29
24 changed files with 169 additions and 33 deletions
+1
View File
@@ -31,6 +31,7 @@ multi-tenant platform.
python3 -m ampbrowser route https://wownero.org/
python3 -m ampbrowser route http://example.b32.i2p/
python3 -m ampbrowser route gemini://wownero.org/
python3 -m ampbrowser route ipfs://bafyexample/
python3 -m ampbrowser inspect
python3 -m ampbrowser plan gemini://wownero.org/
python3 -m ampbrowser open https://wownero.org/
+15 -1
View File
@@ -106,10 +106,24 @@ ADAPTERS: dict[str, TransportAdapter] = {
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",
note="Reticulum adapter is planned for resilient/private routing; it is not an anonymity layer",
commands=("rnsd", "rnstatus", "rnodeconf"),
manage_supported=False,
),
"ipfs": TransportAdapter(
name="ipfs",
profile="ipfs",
endpoint="http://127.0.0.1:8080",
adopt_check="HTTP gateway on 127.0.0.1:8080",
managed_state=".ampb/transports/ipfs",
install_strategy="install IPFS/Kubo provider",
start_strategy="start managed IPFS gateway",
stop_policy="stop only AMPB-owned IPFS process",
note="IPFS local gateway for content-addressed distribution; not an anonymity layer",
commands=("ipfs",),
connect_host="127.0.0.1",
connect_port=8080,
),
"gemini": TransportAdapter(
name="gemini",
profile="gemini",
+1 -1
View File
@@ -20,7 +20,7 @@ CANDIDATE_TRANSPORTS = (
CandidateTransport(
name="ipfs",
role="content-addressed web",
status="next-evaluate",
status="active-dry-run",
route_examples=("ipfs://<cid>", "/ipfs/<cid>", "ipns://<name>"),
browser_fit="native fetch or local gateway",
publisher_fit="static site export and pinning",
+10 -2
View File
@@ -23,8 +23,9 @@ class TransportDefinition:
ROUTE_RULES = (
RouteRule("*.onion", "tor", "tor", "route through Tor SOCKS"),
RouteRule("*.i2p", "i2p", "i2p", "route through I2P HTTP proxy"),
RouteRule("ipfs://*, ipns://*, /ipfs/*, /ipns/*", "ipfs", "ipfs", "route through local IPFS gateway"),
RouteRule("gemini://*", "gemini", "gemini", "fetch and render Gemtext"),
RouteRule("rns://*, lxmf://*, nomad://*", "reticulum", "reticulum", "route through Reticulum adapter"),
RouteRule("rns://*, lxmf://*, nomad://*", "reticulum", "reticulum", "route to Reticulum adapter"),
RouteRule("http://*, https://*", "clearnet", "clearnet", "ordinary web profile"),
)
@@ -43,6 +44,13 @@ TRANSPORT_DEFINITIONS = (
"i2p",
"Adopt existing i2pd/I2P router when healthy; otherwise prompt before managed setup.",
),
TransportDefinition(
"ipfs",
"HTTP gateway on 127.0.0.1:8080",
".ampb/transports/ipfs",
"ipfs",
"Adopt existing IPFS gateway when healthy; otherwise prompt before managed setup.",
),
TransportDefinition(
"gemini",
"built-in renderer available",
@@ -55,6 +63,6 @@ TRANSPORT_DEFINITIONS = (
"RNS tools and configured interfaces",
".ampb/transports/reticulum",
"reticulum",
"Adapter planned; physical/link-layer setup may require operator config.",
"Adapter planned for resilient/private routing; physical/link-layer setup may require operator config.",
),
)
+5 -2
View File
@@ -33,11 +33,14 @@ PLATFORM_CAPABILITIES = (
PlatformCapability("i2p", "desktop", "ready", "ready", "planned", "planned", "Adopt local I2P proxy or prompt before managed desktop daemon setup."),
PlatformCapability("i2p", "android", "planned", "planned", "planned", "planned", "Use an app-owned foreground service or compatible installed I2P provider."),
PlatformCapability("i2p", "ios", "constrained", "planned", "foreground-only", "constrained", "Treat iOS I2P support as foreground-only until native adapter constraints are proven."),
PlatformCapability("ipfs", "desktop", "ready", "ready", "planned", "planned", "Adopt local IPFS gateway or prompt before managed gateway setup."),
PlatformCapability("ipfs", "android", "planned", "planned", "planned", "planned", "Use gateway-first IPFS browsing; embedded node support needs adapter proof."),
PlatformCapability("ipfs", "ios", "constrained", "planned", "foreground-only", "constrained", "Use gateway-first IPFS browsing; embedded node support is constrained."),
PlatformCapability("gemini", "desktop", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by desktop shell."),
PlatformCapability("gemini", "android", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by Android shell."),
PlatformCapability("gemini", "ios", "ready", "ready", "ready", "unsupported", "Built-in fetch/render path can be shared by iOS shell."),
PlatformCapability("reticulum", "desktop", "planned", "planned", "planned", "planned", "Adapter and local interface readiness checks are planned."),
PlatformCapability("reticulum", "android", "planned", "planned", "planned", "planned", "Mobile adapter should use explicit foreground service consent."),
PlatformCapability("reticulum", "desktop", "planned", "planned", "planned", "planned", "Resilient/private networking adapter and interface checks are planned."),
PlatformCapability("reticulum", "android", "planned", "planned", "planned", "planned", "Mobile resilient-network adapter should use explicit foreground service consent."),
PlatformCapability("reticulum", "ios", "constrained", "planned", "foreground-only", "constrained", "Do not assume background Reticulum operation on iOS."),
PlatformCapability("unknown", "desktop", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."),
PlatformCapability("unknown", "android", "unsupported", "unsupported", "unsupported", "unsupported", "No platform route is available."),
+20 -1
View File
@@ -14,6 +14,10 @@ class Route:
def route_url(raw_url: str) -> Route:
path_route = _route_path_address(raw_url)
if path_route:
return path_route
normalized = _normalize_url(raw_url)
parsed = urlparse(normalized)
scheme = parsed.scheme.lower()
@@ -21,6 +25,8 @@ def route_url(raw_url: str) -> Route:
if scheme == "gemini":
return Route(raw_url, normalized, "gemini", "gemini", "gemini scheme")
if scheme in {"ipfs", "ipns"}:
return Route(raw_url, normalized, "ipfs", "ipfs", f"{scheme} scheme")
if scheme in {"rns", "lxmf", "nomad"}:
return Route(raw_url, normalized, "reticulum", "reticulum", f"{scheme} scheme")
if host.endswith(".onion"):
@@ -34,6 +40,19 @@ def route_url(raw_url: str) -> Route:
def _normalize_url(raw_url: str) -> str:
value = raw_url.strip()
if "://" not in value and not value.startswith(("rns:", "lxmf:")):
if "://" not in value and not value.startswith(("rns:", "lxmf:", "ipfs:", "ipns:")):
value = "https://" + value
return value
def _route_path_address(raw_url: str) -> Route | None:
value = raw_url.strip()
if value.startswith("/ipfs/"):
cid = value.removeprefix("/ipfs/").strip("/")
normalized = f"ipfs://{cid}" if cid else "ipfs://"
return Route(raw_url, normalized, "ipfs", "ipfs", "/ipfs path")
if value.startswith("/ipns/"):
name = value.removeprefix("/ipns/").strip("/")
normalized = f"ipns://{name}" if name else "ipns://"
return Route(raw_url, normalized, "ipfs", "ipfs", "/ipns path")
return None
+5 -5
View File
@@ -2,9 +2,9 @@
> Status: draft | Updated 2026-07-05 | Applies to: AMPB contributors and integrators
AMPB is a browser orchestration layer for privacy and alternate-network transports. It
keeps the routing, transport management, and profile isolation decisions outside the
browser engine until those contracts are stable.
AMPB is a browser orchestration layer for privacy, resilient-network, content-addressed,
and alternate-web transports. It keeps the routing, transport management, and profile
isolation decisions outside the browser engine until those contracts are stable.
## Operating Model
@@ -20,7 +20,7 @@ browser engine until those contracts are stable.
- `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, IPFS, 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.
@@ -31,7 +31,7 @@ browser engine until those contracts are stable.
The first implementation should be a launcher and profile manager, not a browser-engine
fork. Tor Browser, Mullvad Browser, Firefox profiles, Chromium profiles, terminal Gemini
clients, and future Reticulum-native viewers can all sit behind the same route and
clients, local IPFS gateways, and future Reticulum-native viewers can all sit behind the same route and
transport contracts.
The long-term browser can become more integrated once the orchestration surface is boring:
+4 -3
View File
@@ -2,15 +2,16 @@
> 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.
Candidate transports are tracked from evaluation through active dry-run support. A
candidate should not become an enabled-by-default route 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.
- Treat resilient overlays separately from anonymity networks.
- Keep mobile constraints visible before a candidate becomes active.
## Current Matrix
+7 -1
View File
@@ -14,8 +14,9 @@ user opens a URL that needs it and approves the first-use prompt.
- Clearnet URLs use a clearnet profile.
- `.onion` hosts use Tor through `127.0.0.1:9050` when available.
- `.i2p` hosts use I2P through `127.0.0.1:4444` when available.
- `ipfs://`, `ipns://`, `/ipfs/`, and `/ipns/` addresses use a local IPFS gateway when available.
- `gemini://` URLs use the Gemini profile.
- Reticulum-family URLs are routed to the Reticulum adapter once configured.
- Reticulum-family URLs use the Reticulum adapter for resilient/private routing once configured.
## Config File
@@ -39,6 +40,11 @@ enabled = true
mode = "adopt-or-prompt-manage"
http_proxy = "127.0.0.1:4444"
[transports.ipfs]
enabled = true
mode = "adopt-or-prompt-manage"
gateway = "127.0.0.1:8080"
[transports.reticulum]
enabled = false
mode = "adopt"
+2 -1
View File
@@ -8,5 +8,6 @@ This file is generated from code. Do not edit it by hand.
| --- | --- | --- | --- | --- | --- | --- |
| `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 |
| `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 for resilient/private routing; it is not an anonymity layer |
| `ipfs` | `http://127.0.0.1:8080` | HTTP gateway on 127.0.0.1:8080 | install IPFS/Kubo provider | start managed IPFS gateway | stop only AMPB-owned IPFS process | IPFS local gateway for content-addressed distribution; not an anonymity layer |
| `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 |
+1 -1
View File
@@ -6,7 +6,7 @@ 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. |
| `ipfs` | content-addressed web | `active-dry-run` | `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. |
+5 -2
View File
@@ -15,11 +15,14 @@ This file is generated from code. Do not edit it by hand.
| `i2p` | `desktop` | `ready` | `ready` | `planned` | `planned` | Adopt local I2P proxy or prompt before managed desktop daemon setup. |
| `i2p` | `android` | `planned` | `planned` | `planned` | `planned` | Use an app-owned foreground service or compatible installed I2P provider. |
| `i2p` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Treat iOS I2P support as foreground-only until native adapter constraints are proven. |
| `ipfs` | `desktop` | `ready` | `ready` | `planned` | `planned` | Adopt local IPFS gateway or prompt before managed gateway setup. |
| `ipfs` | `android` | `planned` | `planned` | `planned` | `planned` | Use gateway-first IPFS browsing; embedded node support needs adapter proof. |
| `ipfs` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Use gateway-first IPFS browsing; embedded node support is constrained. |
| `gemini` | `desktop` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by desktop shell. |
| `gemini` | `android` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by Android shell. |
| `gemini` | `ios` | `ready` | `ready` | `ready` | `unsupported` | Built-in fetch/render path can be shared by iOS shell. |
| `reticulum` | `desktop` | `planned` | `planned` | `planned` | `planned` | Adapter and local interface readiness checks are planned. |
| `reticulum` | `android` | `planned` | `planned` | `planned` | `planned` | Mobile adapter should use explicit foreground service consent. |
| `reticulum` | `desktop` | `planned` | `planned` | `planned` | `planned` | Resilient/private networking adapter and interface checks are planned. |
| `reticulum` | `android` | `planned` | `planned` | `planned` | `planned` | Mobile resilient-network adapter should use explicit foreground service consent. |
| `reticulum` | `ios` | `constrained` | `planned` | `foreground-only` | `constrained` | Do not assume background Reticulum operation on iOS. |
| `unknown` | `desktop` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. |
| `unknown` | `android` | `unsupported` | `unsupported` | `unsupported` | `unsupported` | No platform route is available. |
+2 -1
View File
@@ -8,6 +8,7 @@ This file is generated from code. Do not edit it by hand.
| --- | --- | --- | --- |
| `*.onion` | `tor` | `tor` | route through Tor SOCKS |
| `*.i2p` | `i2p` | `i2p` | route through I2P HTTP proxy |
| `ipfs://*, ipns://*, /ipfs/*, /ipns/*` | `ipfs` | `ipfs` | route through local IPFS gateway |
| `gemini://*` | `gemini` | `gemini` | fetch and render Gemtext |
| `rns://*, lxmf://*, nomad://*` | `reticulum` | `reticulum` | route through Reticulum adapter |
| `rns://*, lxmf://*, nomad://*` | `reticulum` | `reticulum` | route to Reticulum adapter |
| `http://*, https://*` | `clearnet` | `clearnet` | ordinary web profile |
+2 -1
View File
@@ -8,5 +8,6 @@ This file is generated from code. Do not edit it by hand.
| --- | --- | --- | --- | --- |
| `tor` | SOCKS on 127.0.0.1:9050 | `.ampb/transports/tor` | `tor` | Adopt existing Tor when healthy; otherwise prompt before managed setup. |
| `i2p` | HTTP proxy on 127.0.0.1:4444 | `.ampb/transports/i2p` | `i2p` | Adopt existing i2pd/I2P router when healthy; otherwise prompt before managed setup. |
| `ipfs` | HTTP gateway on 127.0.0.1:8080 | `.ampb/transports/ipfs` | `ipfs` | Adopt existing IPFS gateway when healthy; otherwise prompt before managed setup. |
| `gemini` | built-in renderer available | `.ampb/transports/gemini` | `gemini` | No daemon required for static Gemtext browsing. |
| `reticulum` | RNS tools and configured interfaces | `.ampb/transports/reticulum` | `reticulum` | Adapter planned; physical/link-layer setup may require operator config. |
| `reticulum` | RNS tools and configured interfaces | `.ampb/transports/reticulum` | `reticulum` | Adapter planned for resilient/private routing; physical/link-layer setup may require operator config. |
+7 -6
View File
@@ -8,16 +8,17 @@ mobile shells should implement only the platform-specific browser and transport
## Android
Android is the primary mobile target for full AMPB browsing. Tor, I2P, and Reticulum
adapters should run only after a URL selects that transport and the user approves setup.
Managed adapters must use visible, stoppable foreground services and app-owned state.
Android is the primary mobile target for full AMPB browsing. Tor, I2P, IPFS, and
Reticulum adapters should run only after a URL selects that transport and the user
approves setup. Managed adapters must use visible, stoppable foreground services and
app-owned state.
## iOS
iOS is a supported design target, but transport management is constrained. AMPB should
assume foreground-only sessions unless a reviewed platform capability proves otherwise.
The iOS shell should still share routing, policy, consent, fixture, Gemini, and clearnet
behavior with the rest of AMPB.
assume foreground-only sessions for managed transports unless a reviewed platform
capability proves otherwise. The iOS shell should still share routing, policy, consent,
fixture, Gemini, IPFS gateway, and clearnet behavior with the rest of AMPB.
## Capability Matrix
+1 -1
View File
@@ -8,7 +8,7 @@ first-use setup prompt.
## Consent Model
AMPB does not install or start Tor, I2P, Reticulum, or other optional transports when the
AMPB does not install or start Tor, I2P, IPFS, Reticulum, or other optional transports when the
application starts. Setup is lazy:
1. The user opens a URL.
+2 -1
View File
@@ -32,8 +32,9 @@ leases, tunnels, and daemon logs in that tree are local state and must not be co
- Tor: detect SOCKS on `127.0.0.1:9050`; managed start planned.
- I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start planned.
- IPFS: detect HTTP gateway on `127.0.0.1:8080`; managed gateway start planned.
- Gemini: built-in route and render path; no daemon required for static browsing.
- Reticulum: route contract exists; adapter and readiness model are planned.
- Reticulum: resilient/private 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).
+4
View File
@@ -10,6 +10,10 @@ mode = "adopt-or-prompt-manage"
enabled = true
mode = "adopt-or-prompt-manage"
[transports.ipfs]
enabled = true
mode = "adopt-or-prompt-manage"
[transports.reticulum]
enabled = false
+12 -2
View File
@@ -8,7 +8,7 @@ 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))
self.assertEqual({"tor", "i2p", "ipfs", "reticulum", "gemini"}, set(ADAPTERS))
def test_gemini_adapter_is_builtin_ready(self) -> None:
status = inspect_transport("gemini")
@@ -23,7 +23,17 @@ class AdaptersTest(unittest.TestCase):
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"))
self.assertEqual(
("install Tor provider", "start managed Tor daemon", "wait for socks5://127.0.0.1:9050"),
adapter.setup_steps(installed=False, platform="desktop"),
)
def test_ipfs_adapter_declares_gateway_not_anonymity(self) -> None:
adapter = adapter_for("ipfs")
self.assertIsNotNone(adapter)
self.assertEqual("http://127.0.0.1:8080", adapter.endpoint)
self.assertIn("not an anonymity layer", adapter.note)
if __name__ == "__main__":
+1 -1
View File
@@ -11,7 +11,7 @@ class CandidateTransportsTest(unittest.TestCase):
self.assertIn("ipfs", by_name)
self.assertIn("yggdrasil", by_name)
self.assertEqual("next-evaluate", by_name["ipfs"].status)
self.assertEqual("active-dry-run", by_name["ipfs"].status)
self.assertEqual("next-evaluate", by_name["yggdrasil"].status)
def test_nostr_is_not_classified_as_page_transport(self) -> None:
+20
View File
@@ -90,6 +90,26 @@ class PrepareOpenTest(unittest.TestCase):
plan.setup_steps,
)
def test_ipfs_desktop_setup_uses_gateway_adapter(self) -> None:
status = TransportStatus(
transport="ipfs",
installed=False,
running=False,
endpoint="http://127.0.0.1:8080",
adoptable=False,
manage_supported=True,
note="IPFS local gateway",
)
with patch("ampbrowser.plan.inspect_transport", return_value=status):
plan = prepare_open("ipfs://bafyexample", consent=True)
self.assertEqual("setup-approved", plan.status)
self.assertEqual(
("install IPFS/Kubo provider", "start managed IPFS gateway", "wait for http://127.0.0.1:8080"),
plan.setup_steps,
)
if __name__ == "__main__":
unittest.main()
+18
View File
@@ -107,6 +107,24 @@ class PlanUrlTest(unittest.TestCase):
self.assertEqual("planned", plan.platform_capability.manage)
self.assertTrue(plan.requires_consent)
def test_ipfs_missing_gateway_prompts_before_setup(self) -> None:
status = TransportStatus(
transport="ipfs",
installed=False,
running=False,
endpoint="http://127.0.0.1:8080",
adoptable=False,
manage_supported=True,
note="IPFS local gateway",
)
with patch("ampbrowser.plan.inspect_transport", return_value=status):
plan = plan_url("ipfs://bafyexample")
self.assertEqual("ipfs", plan.route.transport)
self.assertEqual("prompt to install and start managed transport", plan.action)
self.assertTrue(plan.requires_consent)
def test_ios_foreground_only_transport_prompt(self) -> None:
status = TransportStatus(
transport="tor",
+6
View File
@@ -20,6 +20,12 @@ class PlatformsTest(unittest.TestCase):
self.assertTrue(capability.can_manage_setup)
self.assertIn("foreground", capability.note)
def test_ipfs_android_is_gateway_first_planned(self) -> None:
capability = capability_for("ipfs", "android")
self.assertEqual("planned", capability.manage)
self.assertIn("gateway-first", capability.note)
def test_unknown_platform_is_rejected(self) -> None:
with self.assertRaises(ValueError):
normalize_platform("watch")
+18
View File
@@ -31,6 +31,24 @@ class RouteUrlTest(unittest.TestCase):
self.assertEqual("gemini", route.transport)
self.assertEqual("gemini", route.profile)
def test_routes_ipfs_scheme(self) -> None:
route = route_url("ipfs://bafyexample")
self.assertEqual("ipfs", route.transport)
self.assertEqual("ipfs", route.profile)
def test_routes_ipns_scheme(self) -> None:
route = route_url("ipns://wownero.example")
self.assertEqual("ipfs", route.transport)
self.assertEqual("ipfs", route.profile)
def test_routes_ipfs_gateway_path(self) -> None:
route = route_url("/ipfs/bafyexample")
self.assertEqual("ipfs://bafyexample", route.normalized)
self.assertEqual("ipfs", route.transport)
def test_routes_reticulum_family_scheme(self) -> None:
route = route_url("rns://wownero")