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 api-coverage-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ only when coverage actually changes.
| First-class CLI operations | 119 |
| Reviewed gaps | 67 |
| Client operations | 143 |
| Published request fields on covered operations | 449 |
| Request fields modeled by the CLI | 449 |
| Published request fields on covered operations | 451 |
| Request fields modeled by the CLI | 451 |
| Reviewed request-field gaps | 0 |

Catalog: https://api.coval.dev/v1/openapi
Expand Down
6 changes: 3 additions & 3 deletions api-coverage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

[snapshot]
catalog_url = "https://api.coval.dev/v1/openapi"
reviewed_at = "2026-10-14"
reviewed_at = "2026-10-19"
published_operations = 186
cli_supported_operations = 119
published_request_fields = 449
cli_modeled_request_fields = 449
published_request_fields = 451
cli_modeled_request_fields = 451

[[allowed_extra]]
operation = "POST /test-cases/{test_case_id}/media:upload-url"
Expand Down
36 changes: 12 additions & 24 deletions src/client/models/review_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,8 @@ impl std::fmt::Display for ProjectType {
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateReviewProjectRequest {
pub display_name: String,
/// Reviewer emails. Left out when `assignee_user_ids` selects the reviewers.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assignees: Vec<String>,
/// Reviewer user IDs, instead of `assignees`; the API rejects a request that
/// sets both.
#[serde(skip_serializing_if = "Option::is_none")]
pub assignee_user_ids: Option<Vec<String>>,
pub linked_simulation_ids: Vec<String>,
Expand All @@ -65,24 +62,24 @@ pub struct CreateReviewProjectRequest {
pub notifications: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_rules: Option<Vec<String>>,
/// Must be a subset of `linked_metric_ids`.
#[serde(skip_serializing_if = "Option::is_none")]
pub blind_labeling_shown_metric_ids: Option<Vec<String>>,
/// Only takes effect on a collaborative project.
#[serde(skip_serializing_if = "Option::is_none")]
pub enforced_collaboration: Option<bool>,
/// Lets reviewers skip linked metrics.
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_metric_skipping: Option<bool>,
#[serde(
default,
deserialize_with = "super::explicit_option",
skip_serializing_if = "Option::is_none"
)]
pub skippable_metric_ids: Option<Option<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_input_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_options: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_selection_mode: Option<String>,
/// False pauses conversation triage while keeping its categories, modes and
/// saved labels; true enables it. Left out, activation follows the triage
/// configuration.
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_enabled: Option<bool>,
}
Expand All @@ -95,8 +92,6 @@ pub struct UpdateReviewProjectRequest {
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assignees: Option<Vec<String>>,
/// Reviewer user IDs, instead of `assignees`; the API rejects a request that
/// sets both.
#[serde(skip_serializing_if = "Option::is_none")]
pub assignee_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -109,41 +104,34 @@ pub struct UpdateReviewProjectRequest {
pub notifications: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub opted_out_assignees: Option<Vec<String>>,
/// Reviewer user IDs opting out of notifications, instead of
/// `opted_out_assignees`; an empty list clears the opt-outs.
#[serde(skip_serializing_if = "Option::is_none")]
pub opted_out_assignee_user_ids: Option<Vec<String>>,
/// Adds to the linked simulations instead of replacing them; cannot be combined
/// with `linked_simulation_ids`.
#[serde(skip_serializing_if = "Option::is_none")]
pub add_linked_simulation_ids: Option<Vec<String>>,
/// Removes from the linked simulations instead of replacing them; cannot be
/// combined with `linked_simulation_ids`.
#[serde(skip_serializing_if = "Option::is_none")]
pub remove_linked_simulation_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_rules: Option<Vec<String>>,
/// Must be a subset of `linked_metric_ids`.
#[serde(skip_serializing_if = "Option::is_none")]
pub blind_labeling_shown_metric_ids: Option<Vec<String>>,
/// What to do with already-completed assignments when linking a new metric.
#[serde(skip_serializing_if = "Option::is_none")]
pub metric_addition_completion_action: Option<String>,
/// Only takes effect on a collaborative project.
#[serde(skip_serializing_if = "Option::is_none")]
pub enforced_collaboration: Option<bool>,
/// Lets reviewers skip linked metrics. Left out, the saved setting is kept.
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_metric_skipping: Option<bool>,
#[serde(
default,
deserialize_with = "super::explicit_option",
skip_serializing_if = "Option::is_none"
)]
pub skippable_metric_ids: Option<Option<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_input_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_options: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_label_selection_mode: Option<String>,
/// False pauses conversation triage while keeping its categories, modes and
/// saved labels; true enables it. An explicit null returns activation to the
/// triage configuration, and leaving it out keeps the current state.
#[serde(
default,
deserialize_with = "super::explicit_option",
Expand Down
14 changes: 14 additions & 0 deletions src/commands/review_projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub struct CreateArgs {
/// Let reviewers skip linked metrics
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
allow_metric_skipping: Option<bool>,
#[arg(long, value_delimiter = ',')]
skippable_metric_ids: Option<Vec<String>>,
}

#[derive(Args)]
Expand Down Expand Up @@ -144,6 +146,8 @@ pub struct UpdateArgs {
/// Let reviewers skip linked metrics
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
allow_metric_skipping: Option<bool>,
#[arg(long, value_delimiter = ',')]
skippable_metric_ids: Option<Vec<String>>,
}

#[derive(Args)]
Expand Down Expand Up @@ -219,6 +223,11 @@ pub async fn execute(
"allow_metric_skipping",
args.allow_metric_skipping,
)?;
input_json::insert(
&mut input,
"skippable_metric_ids",
args.skippable_metric_ids,
)?;
let req: CreateReviewProjectRequest = input_json::finish(input)?;
let project = client.review_projects().create(req).await?;
emit_one_with_actions(
Expand Down Expand Up @@ -275,6 +284,11 @@ pub async fn execute(
"allow_metric_skipping",
args.allow_metric_skipping,
)?;
input_json::insert(
&mut input,
"skippable_metric_ids",
args.skippable_metric_ids,
)?;
let req: UpdateReviewProjectRequest = input_json::finish(input)?;
let project = client
.review_projects()
Expand Down
48 changes: 47 additions & 1 deletion tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4778,6 +4778,45 @@ async fn test_review_projects_forward_reviewer_user_ids_and_skipping() {
}
}

#[tokio::test]
async fn test_review_projects_preserve_skippable_metric_selections() {
for (method_name, command, path_value, required) in [
("POST", "create", "/v1/review-projects", true),
("PATCH", "update", "/v1/review-projects/proj456", false),
] {
for selection in [json!(["met1", "met2"]), json!([]), Value::Null] {
let mock_server = MockServer::start().await;
let capture = BodyCapture::default();
Mock::given(method(method_name))
.and(path(path_value))
.and(capture.clone())
.respond_with(ResponseTemplate::new(200).set_body_json(review_project_response()))
.expect(1)
.mount(&mock_server)
.await;
let mut input = json!({"skippable_metric_ids": selection});
if required {
input["display_name"] = json!("New Project");
input["assignees"] = json!(["reviewer@example.com"]);
input["linked_simulation_ids"] = json!(["sim1"]);
input["linked_metric_ids"] = json!(["met1", "met2"]);
}
let mut command_line = coval_with_api(&mock_server);
command_line.arg("review-projects").arg(command);
if !required {
command_line.arg("proj456");
}
command_line
.arg("--input-json")
.arg(input.to_string())
.assert()
.success();
let body = capture.take();
assert_eq!(body.get("skippable_metric_ids"), Some(&selection));
}
}
}

#[tokio::test]
async fn test_review_projects_create_flags_select_reviewers_by_user_id() {
let mock_server = MockServer::start().await;
Expand All @@ -4801,8 +4840,10 @@ async fn test_review_projects_create_flags_select_reviewers_by_user_id() {
"--simulation-ids",
"sim1",
"--metric-ids",
"met1",
"met1,met2",
"--allow-metric-skipping",
"--skippable-metric-ids",
"met1,met2",
])
.assert()
.success();
Expand All @@ -4813,6 +4854,7 @@ async fn test_review_projects_create_flags_select_reviewers_by_user_id() {
json!([REVIEWER_USER_ID, OTHER_REVIEWER_USER_ID])
);
assert_eq!(body["allow_metric_skipping"], true);
assert_eq!(body["skippable_metric_ids"], json!(["met1", "met2"]));
assert!(body.get("assignees").is_none());
}

Expand All @@ -4831,6 +4873,8 @@ async fn test_review_projects_update_flags_forward_reviewer_user_ids() {
OTHER_REVIEWER_USER_ID,
"--allow-metric-skipping",
"false",
"--skippable-metric-ids",
"met1,met2",
],
)
.await;
Expand All @@ -4842,6 +4886,7 @@ async fn test_review_projects_update_flags_forward_reviewer_user_ids() {
);
// A deliberate false is a real change, unlike the omitted default.
assert_eq!(body["allow_metric_skipping"], false);
assert_eq!(body["skippable_metric_ids"], json!(["met1", "met2"]));
}

#[tokio::test]
Expand All @@ -4860,6 +4905,7 @@ async fn test_review_projects_update_omits_unset_reviewer_and_label_fields() {
"assignee_user_ids",
"opted_out_assignee_user_ids",
"allow_metric_skipping",
"skippable_metric_ids",
"review_label_enabled",
] {
assert!(body.get(key).is_none(), "unset {key} must be omitted");
Expand Down
Loading