Make the runtime keep the contracts the tests now hold it to
Card: hey-sdk Rust: runtime and consumer contract tests
Based on origin/main; independent of #165 (tracing), which touches the same attempt() loop — whichever lands second rebases.
Contracts, and what the runtime had to change to keep them
- Operation deadline ≠ transport timeout. New
ClientBuilder::operation_timeout(Duration): bounds the whole call — gate wait, credentials, every attempt, every backoff, the refresh resend, the body read. Past it the work is dropped where it stands, which is what makes the existingRunningguard report the end and the resilience layer give back its permit; the caller gets a retryableNetworkerror naming the limit. Unset by default (no behaviour change). Tests intests/deadline.rs: cut off while the transport would still wait, cut off during a 5 s backoff after one request, cut off while the token provider is slow before anything is sent, a cut-off call ending for the hooks and freeing the bulkhead's one permit for the next call, zero refused. - Concurrent 401s coalesce into one refresh.
Sharedgains a refresh count and a refresh mutex; a request remembers the count it was signed under, and a 401 answered after someone else refreshed is resent on the new credentials without refreshing again. A refresh that fails leaves the count alone so the next 401 asks again. Tests intests/credential_refresh.rsover an in-testHttpClient(reqwest's pool holds later requests to a new host until the first connection settles its protocol, which would sign them after the refresh and prove nothing): ten concurrent 401s → one refresh, twenty requests, ten on the new token; a call signed before the refresh does not burn a second rotating token; a failed refresh leaves every caller with its 401. - Pagination completeness and an explicit cap signal.
get_all,get_all_with_limit,follow_paginationandeach_pageend asError::pagination_capped(max_pages)(Usage) when they reach the limit with a next page still named, instead of returning a shorter answer that looks complete; a walk that ends inside the limit, or a visitor that stops, is not a truncation. Tests: cap signalled, ends-inside-limit complete, empty page with a cursor followed, repeating cursor ends at the limit, failing page ends the walk with its status and request id. The two existing "stops at the limit" tests flip to expect the signal. - Diagnostics survive a decode failure.
Response::jsonkeeps the status andx-request-id;send/send_optional/send_pageprefix the operation. Test intests/errors.rs. - Compile-time guarantees.
tests/guarantees.rs:Client: Clone + Send + Sync,Error/Response/Page/the trait objectsSend + Sync, and every kind of call (generated read/write, raw,get_all,execute,for_account,next_page,each_page) aSendfuture — checked in a never-run function, so nothing needs a live server. - Redaction in anything formatted.
tests/redaction.rs: the token provider, the OAuth requests andToken, and credential headers print no secret; anOperationprints its body as content type and length, never bytes (customDebugforBody); a transport failure names no URL — this one found a real leak: reqwest 0.13 writes the full URL, query included, into its error text, which became the hint and theDebugsource. Fixed withreqwest::Error::without_url()at the three mapping sites. - External consumer build.
make rs-consumer-check(scripts/rs-consumer-check):cargo package, unpack the.crateinto a temp dir, build a throwaway crate against it by path — its own[workspace], no dev-dependencies, fresh resolution — with the default features and withdefault-features = false. Passes locally for 0.30.0. CI step: the CI-hardening card ownstest.yml; the step to add ismake rs-consumer-checkin the Rust test job afterrs-check(noted on that card). TokenProvideris now implemented forArc<P>, asHooksalready is, so a provider can be shared with the client and watched by the application.
Gate
On the branch as pushed (on main after #154): make rs-check green; make rs-check-drift green; make conformance-rs 191/191; make rs-consumer-check green (default and no-default features).
Adversarial review
Codex CLI (gpt-6-astra, reasoning xhigh) review runs against this diff; findings and dispositions follow in a comment.
Summary by cubic
Hardens the Rust SDK runtime and public contracts so failure handling is explicit, concurrent calls are safe, and diagnostics remain useful without exposing secrets. Pagination now reports incomplete walks instead of returning results that look complete.
Runtime behavior
ClientBuilder::operation_timeoutcovers credentials, retries, backoff, refreshes, response reads, downloads, and multi-request helpers; the existingtimeoutremains per-request.- Timed-out operations report the timeout to hooks, end dropped requests, and release resilience permits.
- Concurrent 401s serialize refreshes and reuse newly refreshed credentials; refreshes continue after a caller times out, while failed refreshes remain retryable.
get_all,get_all_with_limit,follow_pagination, andeach_pagereturnError::pagination_capped(max_pages)when more pages remain at the limit.- Complete walks, visitor-initiated stops, and empty pages with cursors continue to work normally.
- JSON decode errors retain the HTTP status and request ID, and
send,send_optional, andsend_pageinclude the operation name. - Debug output redacts credentials, request bodies, query values, and OAuth secrets; transport errors no longer include URLs.
TokenProviderimplementations can now be shared throughArc.
Tests and tooling
- Adds runtime, pagination, redaction, diagnostics, and compile-time
Send/Synccontract tests. - Adds
make rs-consumer-checkto package the crate and build an external consumer with default and no default features.