fix(providers): refuse a live network dump the provider runtime cannot serve
What
network dump on a hosted provider reported success it had not earned.
The WebDriver provider runtime implemented networkDump by scanning the session app log for HTTP
request lines. A hosted provider owns the network path for its session, and the app writes those
lines only when it chooses to, so the scan answered an empty dump. A caller reads that as "the app
made no requests" when the truth is "this runtime has nothing to read".
This PR makes the refusal explicit and typed, and puts the typed reason on the wire. Nothing else
changes.
How
RuntimeOperationUnavailability['reason']gainsnetwork-capture-post-session: a hosted provider
delivers traffic evidence only as a report finalized after its own session ends, so a live dump has
no source.- The WebDriver provider runtime drops the app-log scan and declares
networkDumpunavailable with
that reason at bind time, so the operation is never admitted (ADR 0019 §2: no stubs that throw after
binding). This covers BrowserStack, AWS Device Farm, and TestMu AI, which share this runtime. Limrun
is unaffected and keeps live capture. - The reason is stated on the
networkDumpcell, not on thenetworkcell.networkis also the
fallbackcreateUnavailablePlatformRuntimeFactshands to every optional cell an owner leaves
unclassified, and this owner leavessnapshotunclassified.findTextandquerySelectorstart
from that snapshot cell, so the earlier shape of this change reportednetwork-capture-post-session
for a text read.networkkeeps the app-log gap it had before this PR. - The hint points at the provider dashboard, where a provider publishes a traffic report when it
records one at all. handleNetworkCommandreports the refusal throughunsupportedOperationResponsefrom
core/runtime-admission.ts, the one shared wire shape every runtime-admission refusal already
uses, sodetails.reasonreaches the caller. Its hand-built response helper dropped the reason and
left the hint text as the only discriminator.
Live proof
One short BrowserStack session (Google Pixel 8, Android 14.0, com.callstack.agentdevicelab), then
network dump:
{
"success": false,
"error": {
"code": "UNSUPPORTED_OPERATION",
"message": "network is not supported on this device",
"hint": "The provider publishes any network capture only after its session ends, on the provider dashboard.",
"diagnosticId": "mvnmz1hg-8784bf9e",
"details": {
"reason": "network-capture-post-session"
}
}
}
details.reason and the hint are both on the wire. The session ended and is gone: disconnect
returned its video and log URLs, and BrowserStack reports it status: "done" with
reason: "CLIENT_STOPPED_SESSION" (session dcad99ef7fc20b17fb5b10e5afd3cd3a237ef64e). Nothing is
left running and session list is empty.
That run was taken at 0d088d0d60. The commit on top of it only moves where the networkDump fact is
declared, so the refusal this PR proves is unchanged; the leak it fixes is covered by the test case
below instead.
Deliberately not here
Fetching that post-session report is its own design question, so this PR carries no new flag, no
provider capability row, no artifact kind, and no post-session fetch. One candidate design for that
sits on the parked branch feat/provider-network-har (unopened, stacked on this head) so the decision
does not start from scratch. This PR stands without it: the refusal is right whether or not we ever
fetch the report.
Tests
session-network-runtime-parity.test.tspinsdetails: { reason }on the refusal the route already
returned, so a lost reason fails the parity test.- One new case takes a
network-capture-post-sessionfact throughhandleSessionCommandsunder a
BrowserStack provider owner and assertserror.details.reason, withoperations.networkDumpnever
called. Its runtime stub answers the emptyapp-logdump the removed scan produced, so the case
fails if the refusal ever stops winning. Reverting the handler to a hand-built response makes both
the new case and the existing one fail, so neither is vacuous. - One new test: a provider with no capture of its own states the same refusal, so the wording is not
written per provider. It runs under AWS's own provider id and device, not BrowserStack's. - One test removed: it proved the app-log scan's entry cap, which no longer exists here.
- One case pins the placement: under a BrowserStack owner,
networkDumpreports
network-capture-post-sessionwhilefindTextandquerySelectorkeepunsupported-provider-mode,
the reason they report onmain. Reverting the reason back onto thenetworkcell makes this case
fail with exactly the leaked reason, so it holds the line and is not vacuous.
Docs
debugging-profiling.md: what a provider session'snetwork dumpanswers and where traffic
evidence lives instead, naming the three providers that refuse and noting Limrun keeps the
operation.device-clouds.md: the comparison table now states livenetwork dumpsupport per provider.
Verification
pnpm check:affected --run --head e205a96745— all runnable checks passed,exactCommit=true.pnpm exec vitest run --project unit-core— 15,980 passed, 1 skipped.pnpm exec vitest run --project provider-integration— 214 passed.npx tsc -bclean.- The live BrowserStack run above. The commit on top does not change what the provider session
answers, only which cell declares the fact, so it is verified by the leak test rather than a second
cloud session.
Size
7 files, +125 / −115.
🤖 Generated with Claude Code