From 4c0dec16868c65a36b6abd55b279c9880ed1696e Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Wed, 2 Sep 2026 19:04:09 -0700 Subject: [PATCH 1/2] [COVAL-5823] Model the remaining dropped request fields Closes the request-field gaps left after the metric, persona, review, and report work. UpdateAgentRequest gains attributes, customer_agent_id, language, tags, and workflows. CreateAgentRequest already modeled all five, so until now a field could be set when the agent was created and never changed again. The agent merge patch writes only supplied columns and normalizes an explicit null to an empty value, so null is how a caller clears one of these. language, attributes, workflows, and tags therefore take explicit_option; customer_agent_id does not, because the API documents it as non-nullable and rejects null. The six fields the struct already declared have the same clearing problem and are left for COVAL-5829 rather than widening this change. Test sets and run templates gain tags. Uploaded conversations gain tags on submit and metadata on patch. The API patches conversation audio and metadata separately so a rejected metadata key cannot leave audio half-attached, and accepts exactly one target per call, so the command enforces that locally rather than relaying a 400. Deliberately unmodeled: agent_id, persona_id, and test_set_id on the run-template requests. The published spec documents them, but the served model takes the plural arrays the CLI already sends and forbids extra fields, so modeling the documented names would break every call. Spec drift is COVAL-5825. --- README.md | 18 +- src/client/models/agent.rs | 30 +++ src/client/models/conversation.rs | 6 + src/client/models/run_template.rs | 6 + src/client/models/test_set.rs | 6 + src/commands/agents.rs | 28 ++- src/commands/conversations.rs | 38 +++- src/commands/run_templates.rs | 8 + src/commands/test_sets.rs | 8 + tests/cli_tests.rs | 329 ++++++++++++++++++++++++++++++ 10 files changed, 467 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 19c9b20..0fd99e5 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,23 @@ coval agents create \ # Create a test set coval test-sets create \ --name "Customer Support Scenarios" \ - --type SCENARIO + --type SCENARIO \ + --tags regression,voice + +# Change an agent's identity and routing fields +coval agents update \ + --customer-agent-id crm-42 \ + --language en-US \ + --attributes '{"tier":"gold"}' \ + --tags prod,voice + +# Clear one of them. A flag can only set a value, so clearing needs an explicit null. +coval agents update --input-json '{"attributes":null}' + +# Add metadata to an already-submitted conversation (additive; a key with a value is rejected) +coval uploaded-conversations patch \ + --metadata csat_bucket=promoter \ + --metadata called_back=yes # Create a test case coval test-cases create \ diff --git a/src/client/models/agent.rs b/src/client/models/agent.rs index 5aeefef..e4254cc 100644 --- a/src/client/models/agent.rs +++ b/src/client/models/agent.rs @@ -141,6 +141,36 @@ pub struct UpdateAgentRequest { pub metric_ids: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub test_set_ids: Option>, + /// Not nullable: the API treats an omitted value as "leave it alone" and + /// rejects null outright. + #[serde(skip_serializing_if = "Option::is_none")] + pub customer_agent_id: Option, + // The merge patch writes only supplied columns and normalizes an explicit null + // to an empty value, so null is how a caller clears these. + #[serde( + default, + deserialize_with = "super::explicit_option", + skip_serializing_if = "Option::is_none" + )] + pub language: Option>, + #[serde( + default, + deserialize_with = "super::explicit_option", + skip_serializing_if = "Option::is_none" + )] + pub attributes: Option>, + #[serde( + default, + deserialize_with = "super::explicit_option", + skip_serializing_if = "Option::is_none" + )] + pub workflows: Option>, + #[serde( + default, + deserialize_with = "super::explicit_option", + skip_serializing_if = "Option::is_none" + )] + pub tags: Option>>, } #[derive(Debug, Deserialize)] diff --git a/src/client/models/conversation.rs b/src/client/models/conversation.rs index c845102..9dfac17 100644 --- a/src/client/models/conversation.rs +++ b/src/client/models/conversation.rs @@ -101,6 +101,8 @@ pub struct SubmitConversationRequest { pub occurred_at: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, } #[derive(Debug, Deserialize)] @@ -139,6 +141,10 @@ pub struct PatchConversationRequest { pub audio_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub audio_reference: Option, + /// Additive only: the API rejects a key that already has a value. Exactly one + /// of the four fields on this request may be supplied. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, } #[derive(Debug, Deserialize)] diff --git a/src/client/models/run_template.rs b/src/client/models/run_template.rs index 7e8e5cd..d47cfe7 100644 --- a/src/client/models/run_template.rs +++ b/src/client/models/run_template.rs @@ -75,6 +75,9 @@ pub struct CreateRunTemplateRequest { pub sub_sample_seed: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, + /// Tag names. Omitted leaves tags unchanged; an empty list clears them. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -103,6 +106,9 @@ pub struct UpdateRunTemplateRequest { pub sub_sample_seed: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, + /// Tag names. Omitted leaves tags unchanged; an empty list clears them. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, } #[derive(Debug, Deserialize)] diff --git a/src/client/models/test_set.rs b/src/client/models/test_set.rs index b664a62..43cb62d 100644 --- a/src/client/models/test_set.rs +++ b/src/client/models/test_set.rs @@ -39,6 +39,9 @@ pub struct CreateTestSetRequest { pub test_set_metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub parameters: Option, + /// Tag names. Omitted leaves tags unchanged; an empty list clears them. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -55,6 +58,9 @@ pub struct UpdateTestSetRequest { pub test_set_metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub parameters: Option, + /// Tag names. Omitted leaves tags unchanged; an empty list clears them. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, } #[derive(Debug, Deserialize)] diff --git a/src/commands/agents.rs b/src/commands/agents.rs index cb88066..8ef5ba9 100644 --- a/src/commands/agents.rs +++ b/src/commands/agents.rs @@ -118,6 +118,21 @@ pub struct UpdateArgs { /// JSON string for metadata #[arg(long)] metadata: Option, + /// Your own stable identifier for the agent + #[arg(long)] + customer_agent_id: Option, + /// Primary agent language + #[arg(long)] + language: Option, + /// JSON object of free-form agent attributes + #[arg(long)] + attributes: Option, + /// JSON object containing workflow configuration + #[arg(long)] + workflows: Option, + /// Comma-separated tag names + #[arg(long, value_delimiter = ',')] + tags: Option>, } #[derive(Args)] @@ -174,11 +189,9 @@ pub async fn execute(cmd: AgentCommands, client: &CovalClient, ctx: &OutputConte } AgentCommands::Update(args) => { let mut input = args.input_json.object()?; - let metadata: Option = args - .metadata - .map(|s| serde_json::from_str(&s)) - .transpose() - .map_err(|e| anyhow::anyhow!("Invalid JSON for --metadata: {e}"))?; + let metadata = parse_json_argument(args.metadata, "metadata")?; + let attributes = parse_json_argument(args.attributes, "attributes")?; + let workflows = parse_json_argument(args.workflows, "workflows")?; input_json::insert(&mut input, "display_name", args.name)?; input_json::insert(&mut input, "model_type", args.r#type)?; @@ -188,6 +201,11 @@ pub async fn execute(cmd: AgentCommands, client: &CovalClient, ctx: &OutputConte input_json::insert(&mut input, "metadata", metadata)?; input_json::insert(&mut input, "metric_ids", args.metric_ids)?; input_json::insert(&mut input, "test_set_ids", args.test_set_ids)?; + input_json::insert(&mut input, "customer_agent_id", args.customer_agent_id)?; + input_json::insert(&mut input, "language", args.language)?; + input_json::insert(&mut input, "attributes", attributes)?; + input_json::insert(&mut input, "workflows", workflows)?; + input_json::insert(&mut input, "tags", args.tags)?; let req: UpdateAgentRequest = input_json::finish(input)?; let agent = client.agents().update(&args.agent_id, req).await?; emit_one_with_actions(ctx, "agents", operation, &agent, agent_actions(&agent.id)); diff --git a/src/commands/conversations.rs b/src/commands/conversations.rs index 95fc11c..69a623b 100644 --- a/src/commands/conversations.rs +++ b/src/commands/conversations.rs @@ -109,6 +109,9 @@ pub struct SubmitArgs { /// When the conversation occurred (ISO 8601, e.g. 2026-05-05T12:34:56Z) #[arg(long)] occurred_at: Option>, + /// Tag to apply to the conversation (repeat for multiple) + #[arg(long = "tag")] + tags: Vec, } fn parse_kv(raw: &str) -> Result<(String, String), String> { @@ -139,6 +142,10 @@ pub struct PatchArgs { audio_url: Option, #[arg(long)] audio_file: Option, + /// Metadata to add as key=value (repeat for multiple). Additive only: the API + /// rejects a key that already has a value. + #[arg(long = "metadata", value_parser = parse_kv)] + metadata: Vec<(String, String)>, } #[derive(Clone, Copy)] @@ -398,11 +405,22 @@ async fn execute_for( } ConversationCommands::Patch(args) => { use crate::client::models::PatchConversationRequest; - if args.audio_file.is_some() && args.audio_url.is_some() { - anyhow::bail!("--audio-file and --audio-url are mutually exclusive"); + // The API patches audio and metadata separately so a rejected metadata + // key can never leave audio half-attached, and accepts exactly one + // target per call. + let targets = [ + args.audio_file.is_some(), + args.audio_url.is_some(), + !args.metadata.is_empty(), + ] + .into_iter() + .filter(|supplied| *supplied) + .count(); + if targets > 1 { + anyhow::bail!("--audio-file, --audio-url, and --metadata are mutually exclusive"); } - if args.audio_file.is_none() && args.audio_url.is_none() { - anyhow::bail!("must provide at least one of: --audio-file, --audio-url"); + if targets == 0 { + anyhow::bail!("must provide exactly one of: --audio-file, --audio-url, --metadata"); } let audio_b64 = match args.audio_file { Some(path) => { @@ -413,10 +431,17 @@ async fn execute_for( } None => None, }; + let metadata = (!args.metadata.is_empty()).then(|| { + args.metadata + .into_iter() + .map(|(key, value)| (key, serde_json::Value::String(value))) + .collect() + }); let req = PatchConversationRequest { audio: audio_b64, audio_url: args.audio_url, audio_reference: None, + metadata, }; let result = match collection { ConversationCollection::Uploaded => { @@ -605,6 +630,11 @@ fn build_submit_request(args: SubmitArgs) -> Result { input_json::insert(&mut input, "external_conversation_id", args.external_id)?; input_json::insert(&mut input, "occurred_at", args.occurred_at)?; input_json::insert(&mut input, "agent_id", args.agent_id)?; + input_json::insert( + &mut input, + "tags", + (!args.tags.is_empty()).then_some(args.tags), + )?; input_json::finish(input) } diff --git a/src/commands/run_templates.rs b/src/commands/run_templates.rs index 8fa1aab..216b225 100644 --- a/src/commands/run_templates.rs +++ b/src/commands/run_templates.rs @@ -73,6 +73,9 @@ pub struct CreateArgs { sub_sample_size: Option, #[arg(long)] sub_sample_seed: Option, + /// Comma-separated tag names; pass an empty value to clear all tags + #[arg(long, value_delimiter = ',')] + tags: Option>, } #[derive(Args)] @@ -102,6 +105,9 @@ pub struct UpdateArgs { sub_sample_size: Option, #[arg(long)] sub_sample_seed: Option, + /// Comma-separated tag names; pass an empty value to clear all tags + #[arg(long, value_delimiter = ',')] + tags: Option>, } #[derive(Args)] @@ -164,6 +170,7 @@ pub async fn execute( input_json::insert(&mut input, "concurrency", args.concurrency)?; input_json::insert(&mut input, "sub_sample_size", args.sub_sample_size)?; input_json::insert(&mut input, "sub_sample_seed", args.sub_sample_seed)?; + input_json::insert(&mut input, "tags", args.tags)?; let req: CreateRunTemplateRequest = input_json::finish(input)?; let template = client.run_templates().create(req).await?; emit_one_with_actions( @@ -187,6 +194,7 @@ pub async fn execute( input_json::insert(&mut input, "concurrency", args.concurrency)?; input_json::insert(&mut input, "sub_sample_size", args.sub_sample_size)?; input_json::insert(&mut input, "sub_sample_seed", args.sub_sample_seed)?; + input_json::insert(&mut input, "tags", args.tags)?; let req: UpdateRunTemplateRequest = input_json::finish(input)?; let template = client .run_templates() diff --git a/src/commands/test_sets.rs b/src/commands/test_sets.rs index d55f6f8..7e09d68 100644 --- a/src/commands/test_sets.rs +++ b/src/commands/test_sets.rs @@ -67,6 +67,9 @@ pub struct CreateArgs { /// Test set type (e.g. DEFAULT, SCENARIO, TRANSCRIPT, WORKFLOW) #[arg(long)] r#type: Option, + /// Comma-separated tag names; pass an empty value to clear all tags + #[arg(long, value_delimiter = ',')] + tags: Option>, } #[derive(Args)] @@ -83,6 +86,9 @@ pub struct UpdateArgs { /// Human-readable description #[arg(long)] description: Option, + /// Comma-separated tag names; pass an empty value to clear all tags + #[arg(long, value_delimiter = ',')] + tags: Option>, } #[derive(Args)] @@ -137,6 +143,7 @@ pub async fn execute( input_json::insert(&mut input, "slug", args.slug)?; input_json::insert(&mut input, "description", args.description)?; input_json::insert(&mut input, "test_set_type", args.r#type)?; + input_json::insert(&mut input, "tags", args.tags)?; let req: CreateTestSetRequest = input_json::finish(input)?; let test_set = client.test_sets().create(req).await?; emit_one_with_actions( @@ -152,6 +159,7 @@ pub async fn execute( input_json::insert(&mut input, "display_name", args.name)?; input_json::insert(&mut input, "slug", args.slug)?; input_json::insert(&mut input, "description", args.description)?; + input_json::insert(&mut input, "tags", args.tags)?; let req: UpdateTestSetRequest = input_json::finish(input)?; let test_set = client.test_sets().update(&args.test_set_id, req).await?; emit_one_with_actions( diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 7b756df..f69f7aa 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -2957,6 +2957,335 @@ async fn test_mutations_list() { .stdout(predicate::str::contains("GPT-4 Fast")); } +fn agent_patch_response() -> Value { + json!({ + "agent": { + "id": "abc123", + "display_name": "Test Agent", + "model_type": "MODEL_TYPE_VOICE", + "create_time": "2026-09-02T10:30:00Z" + } + }) +} + +fn run_template_response() -> Value { + json!({ + "run_template": { + "id": "rt123", + "display_name": "My Template", + "metric_ids": [], + "mutation_ids": [], + "metadata": {}, + "create_time": "2026-09-02T10:30:00Z" + } + }) +} + +fn test_set_patch_response() -> Value { + json!({ + "test_set": { + "name": "testSets/ts123", + "id": "ts123", + "slug": "tagged", + "display_name": "Tagged", + "create_time": "2026-09-02T10:30:00Z" + } + }) +} + +#[tokio::test] +async fn test_agents_update_forwards_every_modeled_field() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("PATCH")) + .and(path("/v1/agents/abc123")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(agent_patch_response())) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("agents") + .arg("update") + .arg("abc123") + .arg("--customer-agent-id") + .arg("crm-42") + .arg("--language") + .arg("en-US") + .arg("--attributes") + .arg(r#"{"tier":"gold"}"#) + .arg("--workflows") + .arg(r#"{"greeting":"short"}"#) + .arg("--tags") + .arg("prod,voice") + .assert() + .success(); + + let body = capture.take(); + assert_eq!(body["customer_agent_id"], "crm-42"); + assert_eq!(body["language"], "en-US"); + assert_eq!(body["attributes"], json!({"tier": "gold"})); + assert_eq!(body["workflows"], json!({"greeting": "short"})); + assert_eq!(body["tags"], json!(["prod", "voice"])); +} + +#[tokio::test] +async fn test_agents_update_forwards_an_explicit_null_to_clear() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("PATCH")) + .and(path("/v1/agents/abc123")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(agent_patch_response())) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("agents") + .arg("update") + .arg("abc123") + .arg("--input-json") + .arg(r#"{"language":null,"attributes":null,"workflows":null,"tags":null}"#) + .assert() + .success(); + + // The merge patch writes only supplied columns and normalizes null to an empty + // value, so dropping the key would turn a deliberate clear into a no-op. + let body = capture.take(); + for key in ["language", "attributes", "workflows", "tags"] { + assert!(body.get(key).is_some(), "{key} must be sent"); + assert!(body[key].is_null(), "{key} must be sent as null"); + } +} + +#[tokio::test] +async fn test_agents_update_omits_unset_fields() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("PATCH")) + .and(path("/v1/agents/abc123")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(agent_patch_response())) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("agents") + .arg("update") + .arg("abc123") + .arg("--name") + .arg("Renamed") + .assert() + .success(); + + let body = capture.take(); + assert_eq!(body["display_name"], "Renamed"); + for key in [ + "customer_agent_id", + "language", + "attributes", + "workflows", + "tags", + ] { + assert!(body.get(key).is_none(), "unset {key} must be omitted"); + } +} + +#[tokio::test] +async fn test_test_sets_update_forwards_tags() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("PATCH")) + .and(path("/v1/test-sets/ts123")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(test_set_patch_response())) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("test-sets") + .arg("update") + .arg("ts123") + .arg("--tags") + .arg("regression,voice") + .assert() + .success(); + + assert_eq!(capture.take()["tags"], json!(["regression", "voice"])); +} + +#[tokio::test] +async fn test_run_templates_update_forwards_tags() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("PATCH")) + .and(path("/v1/run-templates/rt123")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(run_template_response())) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("run-templates") + .arg("update") + .arg("rt123") + .arg("--tags") + .arg("nightly") + .assert() + .success(); + + assert_eq!(capture.take()["tags"], json!(["nightly"])); +} + +#[tokio::test] +async fn test_uploaded_conversations_submit_forwards_tags() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("POST")) + .and(path("/v1/conversations/uploaded:submit")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "uploaded_conversation": { + "name": "conversations/uploaded123", + "conversation_id": "uploaded123", + "status": "COMPLETED", + "create_time": "2026-09-02T12:00:00Z", + "has_audio": false + } + }))) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("uploaded-conversations") + .arg("submit") + .arg("--audio-url") + .arg("https://example.com/call.wav") + .arg("--tag") + .arg("restaurant") + .arg("--tag") + .arg("support-tier-1") + .assert() + .success(); + + assert_eq!( + capture.take()["tags"], + json!(["restaurant", "support-tier-1"]) + ); +} + +#[tokio::test] +async fn test_uploaded_conversations_patch_forwards_metadata() { + let mock_server = MockServer::start().await; + let capture = BodyCapture::default(); + + Mock::given(method("PATCH")) + .and(path("/v1/conversations/uploaded/uploaded123")) + .and(header("X-API-Key", "test_key")) + .and(capture.clone()) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "uploaded_conversation": { + "name": "conversations/uploaded123", + "conversation_id": "uploaded123", + "status": "COMPLETED", + "create_time": "2026-09-02T12:00:00Z", + "has_audio": true + } + }))) + .mount(&mock_server) + .await; + + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("uploaded-conversations") + .arg("patch") + .arg("uploaded123") + .arg("--metadata") + .arg("csat_bucket=promoter") + .arg("--metadata") + .arg("called_back=yes") + .assert() + .success(); + + let body = capture.take(); + assert_eq!( + body["metadata"], + json!({"csat_bucket": "promoter", "called_back": "yes"}) + ); + // The API patches audio and metadata separately, so neither audio key rides along. + assert!(body.get("audio").is_none()); + assert!(body.get("audio_url").is_none()); +} + +#[test] +fn test_uploaded_conversations_patch_rejects_audio_with_metadata() { + coval() + .arg("--api-key") + .arg("test_key") + .arg("uploaded-conversations") + .arg("patch") + .arg("uploaded123") + .arg("--audio-url") + .arg("https://example.com/call.wav") + .arg("--metadata") + .arg("csat_bucket=promoter") + .assert() + .failure() + .stderr(predicate::str::contains("mutually exclusive")); +} + +#[test] +fn test_uploaded_conversations_patch_requires_a_target() { + coval() + .arg("--api-key") + .arg("test_key") + .arg("uploaded-conversations") + .arg("patch") + .arg("uploaded123") + .assert() + .failure() + .stderr(predicate::str::contains("must provide exactly one of")); +} + #[tokio::test] async fn test_run_templates_list_hyphenated_path() { let mock_server = MockServer::start().await; From 9430c330ee2afd655db03feb8d47c6c12bd367b5 Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Thu, 3 Sep 2026 15:40:14 -0700 Subject: [PATCH 2/2] [COVAL-5823] Drop 11 stale agent, test-set, run-template, and upload field gaps --- api-coverage-report.md | 17 +++---------- api-coverage.toml | 55 ------------------------------------------ 2 files changed, 3 insertions(+), 69 deletions(-) diff --git a/api-coverage-report.md b/api-coverage-report.md index c173e10..be72e35 100644 --- a/api-coverage-report.md +++ b/api-coverage-report.md @@ -18,8 +18,8 @@ only when coverage actually changes. | Reviewed gaps | 62 | | Client operations | 143 | | Published request fields on covered operations | 363 | -| Request fields modeled by the CLI | 343 | -| Reviewed request-field gaps | 17 | +| Request fields modeled by the CLI | 354 | +| Reviewed request-field gaps | 6 | Catalog: https://api.coval.dev/v1/openapi @@ -50,7 +50,7 @@ Catalog: https://api.coval.dev/v1/openapi ## Coverage snapshot mismatches - `published_request_fields: recorded 360, current 363` -- `cli_modeled_request_fields: recorded 313, current 343` +- `cli_modeled_request_fields: recorded 313, current 354` ## Client-only operations @@ -141,23 +141,12 @@ Catalog: https://api.coval.dev/v1/openapi ## All current request-field gaps -- `PATCH /agents/{id} attributes` -- `PATCH /agents/{id} customer_agent_id` -- `PATCH /agents/{id} language` -- `PATCH /agents/{id} tags` -- `PATCH /agents/{id} workflows` -- `PATCH /conversations/uploaded/{id} metadata` - `PATCH /metrics/{id} ivr_flow` - `PATCH /run-templates/{id} agent_id` - `PATCH /run-templates/{id} persona_id` -- `PATCH /run-templates/{id} tags` - `PATCH /run-templates/{id} test_set_id` -- `PATCH /test-sets/{id} tags` -- `POST /conversations/uploaded:submit tags` - `POST /metrics ivr_flow` - `POST /run-templates agent_id` - `POST /run-templates persona_id` -- `POST /run-templates tags` - `POST /run-templates test_set_id` - `POST /runs config_overrides` -- `POST /test-sets tags` diff --git a/api-coverage.toml b/api-coverage.toml index b11ccf6..4dbe4a4 100644 --- a/api-coverage.toml +++ b/api-coverage.toml @@ -391,36 +391,6 @@ reason = "Workspace management remains to be modeled under COVAL-2079." # Pydantic model; coval-ai/backend records the known divergence in # src/services/api/tests/v1/openapi_parity_baseline.txt. -[[known_field_gap]] -operation = "PATCH /agents/{agent_id}" -field = "attributes" -reason = "Served by the agent merge-patch and already modeled on create, so update is asymmetric; tracked under COVAL-5823." - -[[known_field_gap]] -operation = "PATCH /agents/{agent_id}" -field = "customer_agent_id" -reason = "Served by the agent merge-patch and already modeled on create, so update is asymmetric; tracked under COVAL-5823." - -[[known_field_gap]] -operation = "PATCH /agents/{agent_id}" -field = "language" -reason = "Served by the agent merge-patch and already modeled on create, so update is asymmetric; tracked under COVAL-5823." - -[[known_field_gap]] -operation = "PATCH /agents/{agent_id}" -field = "tags" -reason = "Resource tagging on write is served but unmodeled; tracked under COVAL-5823." - -[[known_field_gap]] -operation = "PATCH /agents/{agent_id}" -field = "workflows" -reason = "Served by the agent merge-patch and already modeled on create, so update is asymmetric; tracked under COVAL-5823." - -[[known_field_gap]] -operation = "PATCH /conversations/uploaded/{conversation_id}" -field = "metadata" -reason = "The additive customer-metadata patch path is served but unmodeled; tracked under COVAL-5823." - [[known_field_gap]] operation = "PATCH /run-templates/{run_template_id}" field = "agent_id" @@ -431,26 +401,11 @@ operation = "PATCH /run-templates/{run_template_id}" field = "persona_id" reason = "Documented but not served: the transport forbids extra fields and takes the plural `persona_ids` array the CLI already sends, so modeling this name would break the call. Spec drift tracked under COVAL-5825." -[[known_field_gap]] -operation = "PATCH /run-templates/{run_template_id}" -field = "tags" -reason = "Resource tagging on write is served but unmodeled; tracked under COVAL-5823." - [[known_field_gap]] operation = "PATCH /run-templates/{run_template_id}" field = "test_set_id" reason = "Documented but not served: the transport forbids extra fields and takes the plural `test_set_ids` array the CLI already sends, so modeling this name would break the call. Spec drift tracked under COVAL-5825." -[[known_field_gap]] -operation = "PATCH /test-sets/{test_set_id}" -field = "tags" -reason = "Resource tagging on write is served but unmodeled; tracked under COVAL-5823." - -[[known_field_gap]] -operation = "POST /conversations/uploaded:submit" -field = "tags" -reason = "Resource tagging on write is served but unmodeled; tracked under COVAL-5823." - [[known_field_gap]] operation = "POST /run-templates" field = "agent_id" @@ -461,21 +416,11 @@ operation = "POST /run-templates" field = "persona_id" reason = "Documented but not served: the transport forbids extra fields and takes the plural `persona_ids` array the CLI already sends, so modeling this name would break the call. Spec drift tracked under COVAL-5825." -[[known_field_gap]] -operation = "POST /run-templates" -field = "tags" -reason = "Resource tagging on write is served but unmodeled; tracked under COVAL-5823." - [[known_field_gap]] operation = "POST /run-templates" field = "test_set_id" reason = "Documented but not served: the transport forbids extra fields and takes the plural `test_set_ids` array the CLI already sends, so modeling this name would break the call. Spec drift tracked under COVAL-5825." -[[known_field_gap]] -operation = "POST /test-sets" -field = "tags" -reason = "Resource tagging on write is served but unmodeled; tracked under COVAL-5823." - # Fields the CLI sends that the published schema does not declare. Most are served # but undocumented, so removing them from the CLI would lose working behavior.