Skip to content

feat(rust-sdk): contacts parity — unblock + stats, typed responses & tests#228

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
senamakel:fix/rust-sdk-contacts-parity
Jul 7, 2026
Merged

feat(rust-sdk): contacts parity — unblock + stats, typed responses & tests#228
senamakel merged 1 commit into
tinyhumansai:mainfrom
senamakel:fix/rust-sdk-contacts-parity

Conversation

@senamakel

@senamakel senamakel commented Jul 7, 2026

Copy link
Copy Markdown
Member

Why

The Rust SDK's ContactsApi had drifted behind the backend and the TypeScript SDK (the source of truth):

  • Missing unblock() — the backend exposes POST /contacts/:other/unblock and the TS SDK has contacts.unblock(), but Rust could block and never unblock.
  • Missing stats()GET /contacts/stats / contacts.stats() in TS, absent in Rust.
  • Untyped — every method returned serde_json::Value.
  • Zero test coverage for the contacts module.

An accepted contact relationship is the gate in front of direct messaging (the relay refuses DMs between non-contacts), so this surface matters for any Rust agent that wants to message.

What

  • Add contacts.unblock() (POST /contacts/:other/unblock) and contacts.stats() (GET /contacts/stats).
  • New src/types/contacts.rs mirroring sdk/typescript/src/types/contacts.ts (Contact, ContactView, ContactsResponse, ContactRequestsResponse, ContactStats, ContactListParams), tolerant serde that normalizes the backend cryptoId wire name to agent_id (matching the TS SDK's normalized surface).
  • Give every method a typed return; add list/requests pagination params.
  • New tests/api_contacts.rs: 10 wiremock tests covering all 9 methods + typed-response deserialization (incl. the cryptoId → agent_id alias).

Testing

  • cargo test — full suite green, including the 10 new contacts tests.
  • cargo clippy --all-targets — clean.
  • Cross-checked live against the docker-compose backend: the umbrella e2e-contacts flow (request/accept/block/unblock/stats/remove) passes end to end.

https://claude.ai/code/session_01MhVGKa9mSYkgY3kFtddR87

Summary by CodeRabbit

  • New Features

    • Contact-related API responses are now returned as structured objects, making contact data easier to consume and more consistent.
    • Contact list and pending request views now support pagination options.
  • Bug Fixes

    • Improved compatibility with contact data formats by handling legacy field names more reliably.
    • Added stronger response handling for contact status, requests, and stats.

…s & tests

The Rust ContactsApi was missing unblock() and stats() (both exist in the
backend and the TS SDK), and returned untyped serde_json::Value. Add typed
contact types mirroring sdk/typescript/src/types/contacts.ts (normalizing the
backend cryptoId wire name to agent_id), give every method a typed return, add
list/requests pagination params, and add 10 wiremock tests (previously zero
contacts coverage).

Claude-Session: https://claude.ai/code/session_01MhVGKa9mSYkgY3kFtddR87
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

@senamakel is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds typed contact-graph structs to the Rust SDK, updates ContactsApi methods (request, accept, block, status, list, requests, stats) to return typed responses instead of serde_json::Value, supports optional query parameters for list/requests, and adds integration tests covering each endpoint.

Changes

Typed Contacts API

Layer / File(s) Summary
Contact type definitions and module wiring
sdk/rust/src/types/contacts.rs, sdk/rust/src/types/mod.rs
New Contact, ContactView, ContactListParams, ContactsResponse, ContactRequestsResponse, and ContactStats structs with camelCase serde and cryptoId/legacy agentId aliasing; module declared and re-exported.
ContactsApi methods return typed models
sdk/rust/src/api/contacts.rs
request, accept, block, status now return Contact; list/requests/stats return typed response structs; list/requests accept optional ContactListParams built via a list_query helper.
Integration tests for ContactsApi endpoints
sdk/rust/tests/api_contacts.rs
New wiremock-based tests validate HTTP methods/paths and typed deserialization (including cryptoId normalization) for all contact endpoints.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ContactsApi
  participant HttpBackend
  Client->>ContactsApi: list(Some(params))
  ContactsApi->>ContactsApi: list_query(params) builds limit/offset
  ContactsApi->>HttpBackend: GET /contacts?limit&offset
  HttpBackend-->>ContactsApi: JSON payload
  ContactsApi-->>Client: ContactsResponse (typed)
Loading

Related Issues: None specified.

Related PRs: None specified.

Suggested labels: rust-sdk, api, enhancement

Suggested reviewers: None specified.

Poem

A rabbit hops through JSON fields so wide,
Now typed and tidy, nothing left to hide.
Contacts requested, blocked, or stats to see,
Structs replace the blobs of serde_json::Value
Hooray, my burrow's code compiles clean! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main Rust contacts API changes: unblock/stats support, typed responses, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a49fda73d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// Get the relationship status with `agent_id`. Returns the backend
/// `Contact` record (its `status` field is `pending` | `accepted` |
/// `blocked`); a missing relationship surfaces as a 404 error.
pub async fn status(&self, agent_id: &str) -> Result<Contact> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return the status endpoint's status shape

When /contacts/{id}/status returns the status response shape (agentId, status, optional direction) exposed by the TS SDK in sdk/typescript/src/types/contacts.ts:42-45 and used by the test mock in sdk/typescript/tests/codex-cli.test.ts:603-609, deserializing it as Contact silently drops agentId/direction and fills requester/addressee with empty defaults. Rust callers then cannot distinguish pending incoming vs outgoing (or read the returned peer), so the typed surface is lossy; add a ContactStatusResponse type and return it here.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
sdk/rust/tests/api_contacts.rs (2)

91-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: nested contact field and outgoing[0].direction aren't asserted.

contacts_list_parses_and_paginates doesn't assert on res.contacts[0].contact, and contacts_requests_parses_incoming_outgoing doesn't assert outgoing[0].direction. Not a correctness issue, just leaves part of the typed surface unexercised.

Also applies to: 135-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/rust/tests/api_contacts.rs` around lines 91 - 120, The contacts API tests
are not fully exercising the typed response surface:
`contacts_list_parses_and_paginates` should also assert the nested
`res.contacts[0].contact` fields, and
`contacts_requests_parses_incoming_outgoing` should assert
`outgoing[0].direction`. Update the assertions in
`contacts_list_parses_and_paginates` and the incoming/outgoing parsing test so
both the nested contact object and the outgoing direction field are validated.

16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a realistic Contact response hererequest / accept / block all return Result<Contact>, but these tests only exercise a {} body. Because every Contact field defaults, they won’t catch typed-response regressions; assert at least one parsed field from a JSON payload instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/rust/tests/api_contacts.rs` around lines 16 - 24, The contact request
test currently only validates an empty `{}` response, so it can miss
typed-response regressions in the `client.contacts.request` flow. Update the
relevant tests around `contacts_request_posts_to_other` (and the related
`accept`/`block` cases if present) to use a realistic JSON `Contact` payload and
assert at least one parsed field from the returned `Result<Contact>` instead of
relying solely on defaults.
sdk/rust/src/api/contacts.rs (1)

122-133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

list_query duplicates ContactListParams's own serialization logic.

ContactListParams already derives Serialize with rename_all = "camelCase", but list_query manually re-extracts limit/offset into a Vec<(String, String)>. If a new field is added to ContactListParams later, it's easy to forget to also add it here. Consider deriving the query pairs from the struct's own Serialize impl (e.g. via serde_urlencoded or a small helper that pairs values), so the query stays in sync with the type definition automatically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/rust/src/api/contacts.rs` around lines 122 - 133, The list_query helper
is manually duplicating ContactListParams field handling, so updates to the
struct can get out of sync. Refactor list_query to derive the query pairs from
ContactListParams’ existing Serialize implementation instead of hand-pushing
limit and offset, using a helper like serde_urlencoded or equivalent; keep the
logic centered around list_query and ContactListParams so any new fields are
automatically included.
sdk/rust/src/types/contacts.rs (1)

57-62: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider unsigned integer types for pagination/counts.

ContactListParams.limit/offset and ContactStats.contact_count/pending_incoming/pending_outgoing are typed as i64, but none of these values can be meaningfully negative. Using u32/u64 would make invalid states unrepresentable at the type level rather than relying on the backend to reject bad values.

♻️ Example diff
 pub struct ContactListParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub limit: Option<i64>,
+    pub limit: Option<u32>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub offset: Option<i64>,
+    pub offset: Option<u32>,
 }

Also applies to: 85-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/rust/src/types/contacts.rs` around lines 57 - 62, The pagination and
count fields in ContactListParams and ContactStats are using signed integers
even though negative values are invalid. Update the relevant type definitions in
contacts.rs so ContactListParams.limit and offset, plus
ContactStats.contact_count, pending_incoming, and pending_outgoing, use unsigned
integer types such as u32 or u64 as appropriate, and keep the serde attributes
aligned so serialization still works correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sdk/rust/src/api/contacts.rs`:
- Around line 122-133: The list_query helper is manually duplicating
ContactListParams field handling, so updates to the struct can get out of sync.
Refactor list_query to derive the query pairs from ContactListParams’ existing
Serialize implementation instead of hand-pushing limit and offset, using a
helper like serde_urlencoded or equivalent; keep the logic centered around
list_query and ContactListParams so any new fields are automatically included.

In `@sdk/rust/src/types/contacts.rs`:
- Around line 57-62: The pagination and count fields in ContactListParams and
ContactStats are using signed integers even though negative values are invalid.
Update the relevant type definitions in contacts.rs so ContactListParams.limit
and offset, plus ContactStats.contact_count, pending_incoming, and
pending_outgoing, use unsigned integer types such as u32 or u64 as appropriate,
and keep the serde attributes aligned so serialization still works correctly.

In `@sdk/rust/tests/api_contacts.rs`:
- Around line 91-120: The contacts API tests are not fully exercising the typed
response surface: `contacts_list_parses_and_paginates` should also assert the
nested `res.contacts[0].contact` fields, and
`contacts_requests_parses_incoming_outgoing` should assert
`outgoing[0].direction`. Update the assertions in
`contacts_list_parses_and_paginates` and the incoming/outgoing parsing test so
both the nested contact object and the outgoing direction field are validated.
- Around line 16-24: The contact request test currently only validates an empty
`{}` response, so it can miss typed-response regressions in the
`client.contacts.request` flow. Update the relevant tests around
`contacts_request_posts_to_other` (and the related `accept`/`block` cases if
present) to use a realistic JSON `Contact` payload and assert at least one
parsed field from the returned `Result<Contact>` instead of relying solely on
defaults.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e6ffeab-8c1c-4757-84ff-5508dbd4a22a

📥 Commits

Reviewing files that changed from the base of the PR and between c917407 and 9a49fda.

📒 Files selected for processing (4)
  • sdk/rust/src/api/contacts.rs
  • sdk/rust/src/types/contacts.rs
  • sdk/rust/src/types/mod.rs
  • sdk/rust/tests/api_contacts.rs

@senamakel senamakel merged commit abaacac into tinyhumansai:main Jul 7, 2026
9 of 10 checks passed
senamakel added a commit that referenced this pull request Jul 7, 2026
…in wrapper

The live backend (and the Rust SDK, #228) return 404 from GET /contacts/:id/status
when no relationship exists yet. The TS SDK's ContactStatusResponse already models
a "none" status, but contacts.status() rethrew the 404 — so any status-before-request
caller (the wrapper's contact bootstrap) threw before it ever sent the request,
which is why `pnpm cli:ts codex` never connected to a fresh owner.

- ContactsApi.status(): map a 404 to { agentId, status: "none" } (honor the typed
  contract); rethrow other errors. Verified against staging.
- harness-wrapper ensureContactNow + ensureOwnerContact: request-first (idempotent;
  auto-accepts a reverse request), then read status — never pre-check, so a fresh
  relationship's 404 can't abort the handshake.
- Tests: new contacts.test.ts (404→none, passthrough, non-404 rethrow); mock relay
  models contacts.request idempotency (dedupe) since the wrapper now requests from
  both the outbound publisher and inbound receiver.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant