diff --git a/api-coverage-report.md b/api-coverage-report.md index db3b9da..61743b0 100644 --- a/api-coverage-report.md +++ b/api-coverage-report.md @@ -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 diff --git a/api-coverage.toml b/api-coverage.toml index d0d4cc3..8fab441 100644 --- a/api-coverage.toml +++ b/api-coverage.toml @@ -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" diff --git a/src/client/models/review_project.rs b/src/client/models/review_project.rs index fc485e2..b05574a 100644 --- a/src/client/models/review_project.rs +++ b/src/client/models/review_project.rs @@ -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, - /// 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>, pub linked_simulation_ids: Vec, @@ -65,24 +62,24 @@ pub struct CreateReviewProjectRequest { pub notifications: Option, #[serde(skip_serializing_if = "Option::is_none")] pub project_rules: Option>, - /// Must be a subset of `linked_metric_ids`. #[serde(skip_serializing_if = "Option::is_none")] pub blind_labeling_shown_metric_ids: Option>, - /// Only takes effect on a collaborative project. #[serde(skip_serializing_if = "Option::is_none")] pub enforced_collaboration: Option, - /// Lets reviewers skip linked metrics. #[serde(skip_serializing_if = "Option::is_none")] pub allow_metric_skipping: Option, + #[serde( + default, + deserialize_with = "super::explicit_option", + skip_serializing_if = "Option::is_none" + )] + pub skippable_metric_ids: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub review_label_input_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub review_label_options: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub review_label_selection_mode: Option, - /// 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, } @@ -95,8 +92,6 @@ pub struct UpdateReviewProjectRequest { pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub assignees: Option>, - /// 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>, #[serde(skip_serializing_if = "Option::is_none")] @@ -109,41 +104,34 @@ pub struct UpdateReviewProjectRequest { pub notifications: Option, #[serde(skip_serializing_if = "Option::is_none")] pub opted_out_assignees: Option>, - /// 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>, - /// 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>, - /// 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>, #[serde(skip_serializing_if = "Option::is_none")] pub project_rules: Option>, - /// Must be a subset of `linked_metric_ids`. #[serde(skip_serializing_if = "Option::is_none")] pub blind_labeling_shown_metric_ids: Option>, - /// 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, - /// Only takes effect on a collaborative project. #[serde(skip_serializing_if = "Option::is_none")] pub enforced_collaboration: Option, - /// Lets reviewers skip linked metrics. Left out, the saved setting is kept. #[serde(skip_serializing_if = "Option::is_none")] pub allow_metric_skipping: Option, + #[serde( + default, + deserialize_with = "super::explicit_option", + skip_serializing_if = "Option::is_none" + )] + pub skippable_metric_ids: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub review_label_input_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub review_label_options: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub review_label_selection_mode: Option, - /// 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", diff --git a/src/commands/review_projects.rs b/src/commands/review_projects.rs index ceec7c7..eebf7ef 100644 --- a/src/commands/review_projects.rs +++ b/src/commands/review_projects.rs @@ -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, + #[arg(long, value_delimiter = ',')] + skippable_metric_ids: Option>, } #[derive(Args)] @@ -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, + #[arg(long, value_delimiter = ',')] + skippable_metric_ids: Option>, } #[derive(Args)] @@ -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( @@ -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() diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index e7bb46b..f082860 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -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; @@ -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(); @@ -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()); } @@ -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; @@ -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] @@ -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");