English · 日本語
Network observation (request assertions)¶
Verify the HTTP(S) traffic an app makes, as a step/expect assertion. On iOS and Android, observation is in-app: the app reports each exchange to a collector bajutsu runs. On web, Playwright observes the page's traffic natively, with no app-side reporting involved. Either way, a
requestassertion checks the accumulated exchanges.Implementation:
bajutsu/evidence/network.py(model + collector),bajutsu/assertions/network.py(requesteval),bajutsu/web_network.py(the Playwright-native collector and mocking, BE-0054), and the in-app SDK (software development kit) —BajutsuKiton iOS,BajutsuAndroidon Android. The web (Playwright) backend needs no SDK: it observes the page's traffic natively.
How traffic is observed¶
A Simulator app runs as a host process and shares the Mac's loopback, so:
- On
run, bajutsu starts a collector (NetworkCollector) on127.0.0.1:<port>and injects its URL into the app via theBAJUTSU_COLLECTORlaunch env, together with a per-run shared token viaBAJUTSU_COLLECTOR_TOKEN. - The app (linked with BajutsuKit) installs a
URLProtocolthat records each request/response and POSTs it to the collector — after TLS (Transport Layer Security) (no proxy, no CA / certificate authority), so it works regardless of which backend drives the app and is readable programmatically. Each POST carries the token as anAuthorization: Bearerheader, and the collector rejects any request without the matching token (401), so another local process can't inject fabricated exchanges into the run's evidence. - The collector keeps the exchanges in memory; a step's
requestassertion is evaluated against them in real time, and bajutsu writes them (redacted) to<sid>/network.jsonas scenario evidence.
--no-network disables the collector. Apps without the SDK report nothing (the
collector stays empty); the feature is opt-in per app.
The same collector, on iOS, also receives screen-transition events on a separate /transitions
endpoint (BE-0310): BajutsuKit's BajutsuScreen swizzles UIViewController.viewDidAppear(_:)
and reports each completed view-controller appearance there, kept in its own store independent of the
network exchanges above. That signal is not a request-assertion concern — the post-launch
readiness gate and the settled wait consult it instead; see run-loop.
Android works the same way, with two differences (BE-0283). The app links
BajutsuAndroid and adds BajutsuNet.interceptor() to its OkHttp
client — Android has no single OS-level HTTP hook like iOS's URLProtocol, so the interceptor is
per-client and captures OkHttp traffic. And the emulator's 127.0.0.1 is its own loopback, not
the host's, so bajutsu bridges the collector to the device with adb reverse (the same port both
ways, so the injected BAJUTSU_COLLECTOR URL resolves unchanged). Because that one number has to be
bindable on both sides, an Android lane takes the collector's port from a small reserved band
rather than letting the operating system pick a free one. An operating-system pick comes from the
host's ephemeral range, which is also the range the emulator hands out to its own sockets, so it
periodically names a port the guest already holds and the bridge fails to bind. The band is scoped to
Android alone: the iOS Simulator shares the Mac's loopback, bridges nothing, and keeps the
operating-system pick. The collector, the token check, and the assertion pipeline are identical.
Web (Playwright) needs none of this in-app machinery: Playwright already sees every request the
page makes (BE-0054). PlaywrightDriver.network_collector() hooks the page's requestfinished
event directly into the same NetworkExchange model. There is no BAJUTSU_COLLECTOR launch env,
no port, and no token to inject — the driver is the collector. The resulting NetworkExchange
records match the same request assertion as the iOS and Android path.
This mechanism is the in-app path. RocketSim's GUI network inspector and a TLS-intercepting proxy were both rejected — the former is not exposed on its CLI (unusable for automated assertions), the latter needs CA install and breaks on pinning. See the design notes.
The request assertion¶
request is an assertion kind (alongside exists / value / count / …). Match fields
are AND-ed. Each plain request corresponds to one observed exchange: multiple
request assertions in a block are matched one-to-one to distinct exchanges — two
request lines need two separate requests. count is the exception: it is an explicit
aggregate (exact when set, otherwise the lone matcher needs at least one).
expect:
- request: { method: POST, path: /login, status: 200, bodyMatches: "\"user\"" } # one login POST
- request: { method: GET, urlMatches: "q=hello&n=42" } # a *different* request
- request: { pathMatches: "^/items", count: 2 } # aggregate: exactly 2
| field | meaning |
|---|---|
method |
HTTP method (case-insensitive) |
url |
exact full URL (the endpoint) |
urlMatches |
regex/substring over the URL (query strings live here) |
path |
exact URL path (query ignored) |
pathMatches |
regex over the path |
status |
response status code |
bodyMatches |
regex/substring over the request body |
count |
exact number of matching exchanges — an aggregate, exempt from the 1:1 rule (omit ⇒ at least one) |
Deterministic mocks¶
A scenario's mocks make the network deterministic: when an outgoing request matches a
rule, the platform's collector returns the canned response instead of hitting the network, so
a test never depends on a live server (and runs offline). On iOS, BajutsuKit serves the stub
inside the URL protocol — after TLS, no proxy/CA. On web, WebNetworkCollector fulfills it
in-process via Playwright's page.route. Either way the stub is still observed (it appears in
network.json flagged mocked, and request assertions match it like any exchange). BajutsuAndroid
observes but does not yet stub (a follow-up to BE-0283).
mocks:
- match: { method: GET, urlMatches: "example.com" } # request-side matcher
respond:
status: 418 # default 200
headers: { Content-Type: text/plain }
body: "stubbed by bajutsu"
# delayMs: 200 # optional artificial latency
- match: { method: POST, pathMatches: "/login$" }
respond: { status: 201, body: "{\"token\":\"t\"}" }
The first matching rule wins. match reuses the request matcher's request-side fields
(method / url / urlMatches / path / pathMatches / bodyMatches). Mocks ride the
same channel as observation, so they need --network. On iOS, the rules reach the app via the
BAJUTSU_MOCKS launch env (like BAJUTSU_COLLECTOR); on web there is no app to launch into, so
network_collector() takes the scenario's mocks directly and fulfills them in-process.
Timing¶
Network I/O is asynchronous, so a step can run before the response lands. Bridge the gap with a wait
on the UI that reflects the response (e.g. wait: { until: settled }, or wait for an
element the response reveals) before the request assertion. On iOS and Android the SDK POSTs
on completion; on web, Playwright's own requestfinished event fires then. Either way, by the
time the UI has updated, the exchange is in the collector.
App contract¶
iOS — link BajutsuKit and call BajutsuNet.startIfEnabled() early.
It is inert unless BAJUTSU_COLLECTOR is set, captures URLSession HTTP(S) only, and is
test/debug-only (it records headers/bodies — keep it out of release and use redact).
Android — link BajutsuAndroid, call BajutsuNet.configure(env) at
launch (with the launch-env map), and add BajutsuNet.interceptor() to the app's OkHttpClient. It
is inert unless BAJUTSU_COLLECTOR is set, captures OkHttp HTTP(S) only, and is likewise
test/debug-only (it records headers/bodies — keep it out of release and use redact, the same
caveat as iOS above). Android also needs a network_security_config cleartext exception for
127.0.0.1 in the test/debug build (the collector URL is plain HTTP, which API 28+ blocks by default;
iOS exempts loopback from App Transport Security (ATS), so it needs none) — without it the report POST
fails — logged, but otherwise silent, so no exchange ever reaches the collector.
Web — no app-side contract at all: Playwright observes and mocks the page's traffic natively, so nothing in the app under test needs to change.