Skip to content

Commit 64541af

Browse files
tclemCopilot
andcommitted
Fix Client::list_sessions wire shape — wrap filter under params.filter
The hand-authored `Client::list_sessions` was serializing the optional `SessionListFilter` directly onto the JSON-RPC `params` object, flattening fields like `repository` / `branch` / `cwd` / `gitRoot` to the top level. The `session.list` request shape that the runtime accepts puts the filter under `params.filter` — and that's what every other SDK sends: - Node `nodejs/src/client.ts:1178-1180`: `sendRequest("session.list", { filter })` - Go `go/types.go`: `listSessionsRequest { Filter *SessionListFilter }` - Python `python/copilot/client.py:1907-1911`: `payload["filter"] = ...` - .NET `dotnet/src/Client.cs`: `record ListSessionsRequest(SessionListFilter? Filter)` Because the runtime silently ignores unknown top-level keys on `session.list`, calling `list_sessions(Some(filter))` was functionally equivalent to `list_sessions(None)` in 0.0.x — every filter field was discarded by the runtime, returning an unfiltered session list. No runtime error, no log, just silently broken. Functionally dead on the wire, same class as the elicitation `requestedSchema` fix in `c58e2f2`. The mock-server test `list_sessions_serializes_typed_filter` asserted on the flat shape it observed (`request["params"]["repository"]`) rather than the schema's wrapped shape, so the bug round-tripped through both ends — the implementation produced the wrong shape, the test verified the wrong shape. Same root cause as the elicitation test gap. Fix: - `Client::list_sessions` now wraps the filter: `Some(f) -> serde_json::json!({ "filter": f })`, `None -> serde_json::json!({})`. `None` omits the filter key entirely (matches Go's `omitempty` behavior; Node's `{ filter: undefined }` also omits via JSON-stringify). - Mock-server test now asserts on the wrapped path (`params.filter.repository`, `params.filter.branch`) AND explicitly asserts the flattened fallback is gone (`params.get("repository")` must return `None`). Same regression-prevention pattern as the elicitation fix at `session_test.rs:1248-1251`. - CHANGELOG entry under `### Fixed` documenting the wire-shape fix and the test gap that masked it. Validation: - 209 tests pass (no count change — same test, stricter assertions). - `cargo doc -D warnings` clean. - `cargo +nightly-2026-04-14 fmt --check` clean. - `cargo clippy --all-features --all-targets -- -D warnings` clean. Caught by the gap-analysis structural-correctness pass walking every hand-authored `client.call("...")` site against the schema and the four other SDKs. This is the second wire-shape bug found by that pass; the first was the `SessionUi::elicitation` `schema` -> `requestedSchema` fix in `c58e2f2`. The audit confirms `session.list` is the only other new bug — three Rust-unique surfaces (`session.respondToUserInput`, `session.sendTelemetry`, top-level `sendTelemetry` / `server.sendTelemetry`) are uncheckable cross-SDK and queued as post-0.1.0 runtime-acceptance verification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a7c8215 commit 64541af

3 files changed

Lines changed: 24 additions & 6 deletions

File tree

rust/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,17 @@ public surface.
341341
round-tripped through the same misnamed field, so the bug slipped past
342342
unit tests; the test now asserts on `requestedSchema` and explicitly
343343
rejects a stray `schema` key.
344+
- `Client::list_sessions` now wraps the optional filter under `params.filter`
345+
on the wire, matching the `session.list` request shape that Node, Python,
346+
Go, and .NET ship. The hand-authored implementation was flattening the
347+
filter fields directly onto `params`, which the runtime silently ignored
348+
— so `list_sessions(Some(filter))` was functionally equivalent to
349+
`list_sessions(None)` in 0.0.x. Same class of bug as the elicitation
350+
wire fix above: the existing mock-server test asserted on the flat shape
351+
it observed rather than the schema's wrapped shape, so the bug
352+
round-tripped through both ends. The test now asserts the wrapped path
353+
(`params.filter.repository`) and explicitly rejects the flattened
354+
fallback (`params.repository`).
344355

345356
### Notes
346357
- Minimum supported Rust version (MSRV): 1.94.0 (pinned via

rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1199,7 +1199,7 @@ impl Client {
11991199
filter: Option<SessionListFilter>,
12001200
) -> Result<Vec<SessionMetadata>, Error> {
12011201
let params = match filter {
1202-
Some(f) => serde_json::to_value(f)?,
1202+
Some(f) => serde_json::json!({ "filter": f }),
12031203
None => serde_json::json!({}),
12041204
};
12051205
let result = self.call("session.list", Some(params)).await?;

rust/tests/session_test.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -679,11 +679,18 @@ async fn list_sessions_serializes_typed_filter() {
679679

680680
let request = read_framed(&mut server_read).await;
681681
assert_eq!(request["method"], "session.list");
682-
assert_eq!(request["params"]["repository"], "octocat/hello");
683-
assert_eq!(request["params"]["branch"], "main");
684-
// cwd / gitRoot are None and must be omitted from the wire payload.
685-
assert!(request["params"].get("cwd").is_none());
686-
assert!(request["params"].get("gitRoot").is_none());
682+
assert_eq!(request["params"]["filter"]["repository"], "octocat/hello");
683+
assert_eq!(request["params"]["filter"]["branch"], "main");
684+
// cwd / gitRoot are None and must be omitted from the filter object.
685+
assert!(request["params"]["filter"].get("cwd").is_none());
686+
assert!(request["params"]["filter"].get("gitRoot").is_none());
687+
// Regression check: filter must be wrapped under `params.filter`, not
688+
// flattened onto `params` directly. All other SDKs (Node/Python/Go/.NET)
689+
// wrap; flattening is silently ignored by the runtime.
690+
assert!(
691+
request["params"].get("repository").is_none(),
692+
"wire shape is `params.filter.*`, not `params.*` — see Node/Go/Python/.NET"
693+
);
687694

688695
let id = request["id"].as_u64().unwrap();
689696
let response = serde_json::json!({

0 commit comments

Comments
 (0)