Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Protocol Conformance Audit — Rust Client

- **Date:** 2026-04-10 (protocol conformance), 2026-04-19 (supply-chain coverage — cargo-audit workflow added), 2026-05-08 (crates.io metadata refresh — description and keywords broadened to cover spend / risk / audit, no behavioral changes)
- **Spec:** `cycles-protocol-v0.yaml` v0.1.24 (OpenAPI 3.1.0)
- **Date:** 2026-05-21 (v0.2.5 — `from` / `to` ISO-8601 window-filter fields added to `ListReservationsParams` per `cycles-protocol-v0.yaml` revision 2026-05-21; closes the Rust-client side of runcycles/cycles-server#159. Both `Option<String>`, both inclusive bounds on `created_at_ms`, both serialize via `#[serde(rename = "...")]` to land on the wire under the spec-mandated names. Pure additive struct change — callers using `Default::default()` or struct-update syntax stay compile-clean. Wire-format regression test added using wiremock's `query_param` matcher. 134 tests pass; clippy + doc-tests clean.), 2026-04-10 (protocol conformance), 2026-04-19 (supply-chain coverage — cargo-audit workflow added), 2026-05-08 (crates.io metadata refresh — description and keywords broadened to cover spend / risk / audit, no behavioral changes)
- **Spec:** `cycles-protocol-v0.yaml` v0.1.25 (OpenAPI 3.1.0)
- **Client:** Rust 1.88+ (MSRV), reqwest 0.12, serde 1, tokio 1, bon 3
- **Cross-reference:** [cycles-server AUDIT.md](https://github.com/runcycles/cycles-server/blob/main/AUDIT.md)
- **Supply-chain coverage:** `.github/workflows/cargo-audit.yml` runs `cargo audit` against rustsec/advisory-db on PRs touching `Cargo.lock` / `Cargo.toml`, on push to `main`, and weekly (Monday 06:00 UTC). Fills the gap left by CodeQL default-setup, which has no Rust analyzer.
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [0.2.5] - 2026-05-21

`from` / `to` ISO-8601 window-filter fields on `ListReservationsParams`. Implements `cycles-protocol-v0.yaml` revision 2026-05-21 ([runcycles/cycles-protocol#97](https://github.com/runcycles/cycles-protocol/pull/97)) on the client side; runcycles/cycles-server#160 ships the server impl. Closes the Rust-client side of runcycles/cycles-server#159.

### Added

- `ListReservationsParams::from` and `::to` (`Option<String>`, ISO 8601 date-time). Both are inclusive bounds on `created_at_ms`. Either may be supplied alone (open interval) or together (closed window). The filter binds to `created_at_ms` regardless of any sort key. Servers reject `from > to` with HTTP 400 `INVALID_REQUEST`.
- Regression test `list_reservations_forwards_from_to_window` in `tests/client_test.rs` using wiremock `query_param` matchers to assert that the new fields land on the wire under the spec-mandated query-string names.

### Notes

- Pure additive struct change for callers using `ListReservationsParams::default()` or struct-update syntax `..Default::default()` — the new fields default to `None` and serialize as absent.
- **Source-level breakage for exhaustive constructors.** `ListReservationsParams` is not `#[non_exhaustive]`, so downstream callers who construct it field-by-field without `..Default::default()` (e.g. `let p = ListReservationsParams { status, tenant, app, agent, cursor, limit };`) will need to add `from: None, to: None` or switch to the `..Default::default()` shape. Mirrors the previous additive bumps to this struct.
- 134 tests pass across the integration + unit suites; doc-tests + clippy clean.

## [0.2.4] - 2026-05-08

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "runcycles"
version = "0.2.4"
version = "0.2.5"
edition = "2021"
rust-version = "1.88"
license = "Apache-2.0"
Expand Down
14 changes: 14 additions & 0 deletions src/models/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,20 @@ pub struct ListReservationsParams {
/// Filter by agent.
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
/// Inclusive lower bound on `created_at_ms`. ISO 8601 date-time.
/// Per cycles-protocol-v0.yaml revision 2026-05-21: the filter always
/// binds to `created_at_ms` regardless of any sort key; may be supplied
/// alone (open upper bound) or paired with `to`. Serializes to the
/// `from` query-string key (the field name already matches the wire
/// name — no `#[serde(rename)]` needed).
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
/// Inclusive upper bound on `created_at_ms`. ISO 8601 date-time.
/// Same binding and open-interval rules as [`from`]. Servers reject
/// `from > to` with HTTP 400 INVALID_REQUEST. Serializes to the `to`
/// query-string key.
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
/// Cursor for pagination.
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
Expand Down
32 changes: 31 additions & 1 deletion tests/client_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use runcycles::models::*;
use runcycles::{CyclesClient, Error};
use serde_json::json;
use wiremock::matchers::{header, method, path, path_regex};
use wiremock::matchers::{header, method, path, path_regex, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};

async fn setup() -> (MockServer, CyclesClient) {
Expand Down Expand Up @@ -412,6 +412,36 @@ async fn list_reservations_success() {
assert_eq!(resp.has_more, Some(false));
}

#[tokio::test]
async fn list_reservations_forwards_from_to_window() {
// cycles-protocol-v0.yaml revision 2026-05-21: new `from` / `to` query
// params on listReservations. Both are ISO-8601 date-time strings,
// inclusive bounds on the reservation's `created_at_ms`. The mock matchers
// assert the params land on the wire under the spec-mandated names.
let (server, client) = setup().await;

Mock::given(method("GET"))
.and(path("/v1/reservations"))
.and(query_param("from", "2026-05-21T00:00:00Z"))
.and(query_param("to", "2026-05-22T00:00:00Z"))
.and(query_param("tenant", "acme"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"reservations": [],
"has_more": false
})))
.mount(&server)
.await;

let params = ListReservationsParams {
tenant: Some("acme".to_string()),
from: Some("2026-05-21T00:00:00Z".to_string()),
to: Some("2026-05-22T00:00:00Z".to_string()),
..Default::default()
};
let resp = client.list_reservations(&params).await.unwrap();
assert_eq!(resp.reservations.len(), 0);
}

// ─── get_reservation ─────────────────────────────────────────────

#[tokio::test]
Expand Down