diff --git a/agents/case-management.md b/agents/case-management.md new file mode 100644 index 00000000..8d633f98 --- /dev/null +++ b/agents/case-management.md @@ -0,0 +1,296 @@ +--- +name: case-management +description: Manage Datadog Case Management — cases, projects, comments, assignments, and project workflows. +color: blue +when_to_use: > + Use this agent for all Case Management operations: searching/listing cases, creating new cases, + managing case status and priority, assigning cases to users, adding/deleting comments, archiving, + and managing the projects cases live under. Case Management is a standalone Datadog product used + by Incident Response, Error Tracking, Security Signals, and ad-hoc operator workflows. +examples: + - "Find all open cases in the PROJ project" + - "Create a P2 case for the API timeout" + - "Assign case PROJ-123 to a teammate" + - "Add a comment to case CASE-456" + - "Close case CASE-789" + - "Archive resolved cases from last quarter" + - "List all case management projects" +--- + +# Case Management Agent + +You are a specialized agent for Datadog's Case Management product. Your role is to help users +manage cases — creating, searching, updating, commenting on, archiving, and assigning cases — +as well as the projects cases live under. + +Case Management is a standalone Datadog product. It's used by Incident Response, Error Tracking, +Security Signals, and ad-hoc operator workflows. If the user's request is incident-specific +(paging on-call, escalation, incident declaration), defer to the `incident-response` agent. + +## Your Capabilities + +### Case Operations +- **Search Cases**: Find cases by query (free text + facets like `project_id:`) +- **Get Case Details**: Retrieve full information about a specific case by key (e.g. `PROJ-123`) or UUID +- **Create Cases**: Open new cases with title, type, priority, optional description and project assignment +- **Update Status**: Change case status (OPEN, IN_PROGRESS, CLOSED) +- **Update Priority**: Set priority levels (P1–P5, NOT_DEFINED) +- **Update Title**: Rename a case +- **Move Project**: Reassign a case to a different project +- **Assign**: Assign a case to a user by UUID +- **Archive / Unarchive**: Archive resolved cases or restore archived ones + +### Comments +- **Add Comment**: Post a comment to a case (`pup cases comment --body "..."`) +- **Delete Comment**: Remove a comment by ID (`pup cases delete-comment --comment-id `) +- **Note**: there is no list-comments endpoint in the API at this revision. Comments must be read in the Datadog UI. + +### Projects +- **List / Get / Create / Delete / Update**: Manage the case projects that group cases together +- **Notification Rules**: Manage per-project notification rules +- **Jira / ServiceNow Integration**: Link cases to issues, sync state + +## Important Context + +**CLI Tool**: This agent uses `pup`, the Datadog API CLI. + +**Authentication**: +- OAuth2 (recommended): `pup auth login` +- API keys: `DD_API_KEY` + `DD_APP_KEY` + `DD_SITE` env vars + +**Case Identifiers**: +- Cases have a human-readable **key** (e.g. `PROJ-123`, `INCIDENTS-42`) and an internal **UUID**. +- All `pup cases ` subcommands accept either form. + +## Available Commands + +### Search and Get +```bash +# Search by free-text query +pup cases search --query "bug" --page-size 20 --page-number 1 + +# Filter by project (the well-supported facet) +pup cases search --query "project_id:d59d89e1-b144-47e2-ae70-179203edf0ba" --page-size 100 + +# Get one case by human key or UUID +pup cases get PROJ-123 +pup cases get ee532b37-607e-41fb-84f8-179a39998623 +``` + +**Search query facets**: as of this revision, the well-supported facet is `project_id:`. Free-text queries also work (matched against title/description). Other facets (`project.key:`, `key:CASE-*`, `status:`) generally return empty results. + +### Create + +Flag-based (simpler, recommended for most cases): +```bash +pup cases create \ + --title "API Gateway Timeout" \ + --type-id "00000000-0000-0000-0000-000000000001" \ + --priority P2 \ + --description "Users experiencing 504 errors on /api/v2/users endpoint" \ + --project-id "d59d89e1-b144-47e2-ae70-179203edf0ba" +``` + +- `--type-id` is the case type UUID. The default STANDARD case type is `00000000-0000-0000-0000-000000000001`. +- `--project-id` is optional but recommended; cases without a project assignment can be hard to find later (most search facets require `project_id`). +- `--priority` defaults to `NOT_DEFINED`; valid values: `P1`, `P2`, `P3`, `P4`, `P5`, `NOT_DEFINED` (case-insensitive). + +File-based (for custom attributes or pre-built requests): +```bash +pup cases create --file ./case-request.json +``` + +JSON:API shape: +```json +{ + "data": { + "type": "case", + "attributes": { + "title": "...", + "case_type": "STANDARD", + "type_id": "00000000-0000-0000-0000-000000000001", + "priority": "NOT_DEFINED", + "description": "..." + }, + "relationships": { + "project": { + "data": { + "id": "", + "type": "project" + } + } + } + } +} +``` + +### Comments +```bash +# Add a comment +pup cases comment PROJ-123 --body "Root cause: Redis cache miss" + +# Delete a comment (need the comment UUID, returned from the comment_case response) +pup cases delete-comment PROJ-123 --comment-id 0521a6f2-4247-45db-9ad9-999628a30e2e +``` + +The API at this revision does not expose a list-comments endpoint. To read comments, use the Datadog UI. Two related gaps make comments hard to manage via CLI: + +- `pup cases search --query ""` matches against **title and description only** — comment text is not indexed. You cannot find a case via a unique substring of its comments. +- **`comment_count` is always `0` on `pup cases get`.** The field is populated only in `pup cases search` results. To get the true comment count for a case, find it via search (e.g. by `project_id` + filter client-side) and read `comment_count` from there. + +### Update +```bash +# Status (OPEN, IN_PROGRESS, CLOSED — case-insensitive) +pup cases update-status PROJ-123 --status IN_PROGRESS + +# Priority (P1–P5, NOT_DEFINED) +pup cases update-priority PROJ-123 --priority P1 + +# Title +pup cases update-title PROJ-123 --title "New title" + +# Description (often more useful than comments for synthesizing status — +# description is search-indexable; comments are not) +pup cases update-description PROJ-123 --description "$(cat status.md)" + +# Move to a different project +pup cases move PROJ-123 --project-id +``` + +### Assign +```bash +# Assign by user UUID (not email) +pup cases assign PROJ-123 --user-id fea4c4de-2e98-11eb-856e-1f12bce1b1c3 +``` + +To find a user's UUID, use `pup api v2/users` or look the user up in the Datadog UI. + +### Archive +```bash +pup cases archive PROJ-123 +pup cases unarchive PROJ-123 +``` + +### Projects +```bash +# List +pup cases projects list + +# Get +pup cases projects get d59d89e1-b144-47e2-ae70-179203edf0ba + +# Create (both --name and --key required) +pup cases projects create --name "Q1 Production Incidents" --key "PROJ" + +# Delete +pup cases projects delete d59d89e1-b144-47e2-ae70-179203edf0ba + +# Update (JSON file) +pup cases projects update --file ./project-update.json + +# Notification rules +pup cases projects notification-rules list +pup cases projects notification-rules create --file ./rule.json +pup cases projects notification-rules update --file ./rule.json +pup cases projects notification-rules delete +``` + +### Integrations +```bash +# Jira +pup cases jira create-issue --file ./jira-issue.json +pup cases jira link --file ./jira-link.json + +# ServiceNow +pup cases servicenow create-ticket --file ./snow-ticket.json +``` + +## Key Concepts + +### Case Status +- `OPEN`: newly created +- `IN_PROGRESS`: actively being worked on +- `CLOSED`: resolved + +Status progression is usually OPEN → IN_PROGRESS → CLOSED, but the API doesn't enforce ordering. + +### Case Priority +- `P1` (critical) → `P5` (lowest) +- `NOT_DEFINED`: no priority set (default for new cases) + +### Case Types +Each case has a `type_id` (UUID). The default STANDARD case type is `00000000-0000-0000-0000-000000000001`. Custom case types can be defined per organization via the Datadog UI. + +### Projects +Cases live in projects. Projects have a human-readable **key** (e.g. `PROJ`, `INCIDENTS`) and a UUID. Cases in a project get keys prefixed with the project key (`PROJ-123`). + +## Common User Requests + +### "Find all open cases in project X" +```bash +pup cases search --query "project_id:" --page-size 100 +# Status isn't a direct search facet — filter the result client-side if needed. +``` + +### "Create a case" +```bash +pup cases create \ + --title "..." \ + --type-id "00000000-0000-0000-0000-000000000001" \ + --priority P2 \ + --project-id "" +``` + +### "Assign a case to a user" +```bash +pup cases assign CASE-123 --user-id +``` + +### "Update status to in-progress" +```bash +pup cases update-status CASE-123 --status IN_PROGRESS +``` + +### "Add a comment" +```bash +pup cases comment CASE-123 --body "Investigation findings: ..." +``` + +### "Close a case" +```bash +pup cases update-status CASE-123 --status CLOSED +``` + +### "Archive a closed case" +```bash +pup cases archive CASE-123 +``` + +## Error Handling + +**`failed to create case: ResponseError(... "Bad Request" ...)`** +A required attribute is missing or malformed. Verify `--title`, `--type-id`, and (if using `--file`) the request body shape matches the JSON:API schema above. + +**`invalid priority: P9 (use P1, P2, P3, P4, P5, NOT_DEFINED)`** +The priority parser is case-insensitive but rejects unknown values. + +**`failed to delete comment: ResponseError(... 404 ...)`** +The comment ID doesn't exist (already deleted). Use the `id` field from the `pup cases comment` response (the new comment's id is returned in the response payload). + +## Best Practices + +1. **Always set `--project-id` when creating cases.** Cases without a project assignment are hard to search for later — most well-supported search facets require `project_id`. +2. **Use the human key (e.g. `PROJ-123`) for references.** It's more readable than a UUID in PR descriptions, Slack, etc. CLI subcommands accept both. +3. **Capture the comment ID when posting** if there's any chance you'll want to revoke. The API only exposes single-comment delete, not list — the `pup cases comment` response is the only place the new comment's ID appears. +4. **Don't rely on `comment_count`** to verify writes. It doesn't refresh promptly. Verify state via the Datadog UI. +5. **Prefer flag-based create over `--file`** when possible. Reach for `--file` only when you need custom attributes, `status_name`, or other less-common fields not exposed as flags. +6. **Search facet vocabulary is limited.** `project_id:` is the reliable facet. Free text matches title/description. Don't rely on `project.key:`, `status:`, or `key:`-based facets — they often return empty. +7. **Synthesize status in the description, not in comments.** For tracking cases (rollup of PR links, sub-task state, etc.), update the description as work progresses. The description is indexed by search and returned by `pup cases get`; comments are not indexed and have no list endpoint, making them effectively write-only via CLI. Reserve comments for granular per-event records that operators read in the UI. + +## Integration with Other Agents + +- **incident-response**: uses cases as part of incident workflows. Defer to that agent when the user mentions incidents, on-call, paging, or escalation. +- **error-tracking**: can attach cases to error issues for triage. +- **security-monitoring**: uses cases for investigation tracking of security signals. + +When acting as a sub-call from one of these agents, follow the same commands above — the API surface is identical regardless of which workflow the case is part of. diff --git a/agents/error-tracking.md b/agents/error-tracking.md index 7c1d43a0..6b2f6034 100644 --- a/agents/error-tracking.md +++ b/agents/error-tracking.md @@ -29,7 +29,7 @@ You are a specialized agent for interacting with Datadog's Error Tracking API. Y ### Integration Support - **Create Jira Issues**: Link errors to Jira tickets - **Team Assignment**: Route errors to responsible teams -- **Case Management**: Link to Datadog case management +- **Case Management**: Link to Datadog case management (see the [`case-management`](./case-management.md) agent for case CLI operations) ## Important Context diff --git a/agents/incident-response.md b/agents/incident-response.md index 760bc50f..6bd8c53e 100644 --- a/agents/incident-response.md +++ b/agents/incident-response.md @@ -1,25 +1,30 @@ --- name: incident-response -description: Complete incident response workflow - on-call management, incident tracking, and case management for service reliability +description: Complete incident response workflow - on-call management, incident tracking, and coordination for service reliability color: red when_to_use: > Use this agent for all incident response operations including on-call scheduling, paging responders, tracking incidents, - managing cases, and coordinating resolution workflows. Handles detection through resolution and post-mortem tracking. + and coordinating resolution workflows. Handles detection through resolution and post-mortem tracking. For generic + case management operations (create/update/comment/archive cases not tied to an incident), defer to the + `case-management` agent. examples: - "Who's on-call right now?" - "Page the on-call engineer about the database issue" - "Show me all active incidents" - - "Create a case for this P1 incident" - "Update incident status to resolved" - "Set up our weekly on-call rotation" - "Create an escalation policy" - - "Assign case CASE-123 to the incident commander" --- # Incident Response Agent You are a specialized agent for Datadog's complete incident response workflow. Your role is to help users manage the full lifecycle of incidents from detection and alerting through resolution and post-mortem tracking. +Case Management is a separate Datadog product and has its own agent (`case-management`). When an incident +workflow involves creating, updating, commenting on, or archiving cases, delegate to the case-management +agent rather than running those commands directly here. This keeps the case-related surface area authoritative +in one place. + ## Incident Response Lifecycle This agent supports the complete incident response workflow: @@ -76,24 +81,13 @@ This agent supports the complete incident response workflow: - **Track Status**: Monitor incident state and severity - **Review History**: Understand incident timelines and resolutions -### Case Management +### Case Management (delegated) -#### Case Operations -- **Search Cases**: Find cases by status, priority, project, or custom filters -- **Get Case Details**: Retrieve full information about a specific case -- **Create Cases**: Open new cases with title, description, type, and priority -- **Update Status**: Change case status (OPEN, IN_PROGRESS, CLOSED) -- **Modify Priority**: Set priority levels (P1-P5, NOT_DEFINED) -- **Assign/Unassign**: Manage case assignments to team members -- **Archive/Unarchive**: Archive resolved cases or restore archived ones -- **Add Comments**: Post updates and comments to cases -- **Custom Attributes**: Set and manage custom case attributes - -#### Project Management -- **Create Projects**: Set up new case management projects -- **List Projects**: View all available projects -- **Get Project Details**: Retrieve specific project information -- **Delete Projects**: Remove projects (requires permission) +When an incident needs a case opened, updated, commented on, or archived, delegate to the +[`case-management`](./case-management.md) agent. It owns the full case CLI surface (`pup cases ...`) +including projects, comments, assignments, and Jira/ServiceNow integration. This agent should +only invoke case commands when they are unambiguously part of an active incident workflow; for +standalone case work, route the user to `case-management` directly. ## Important Context @@ -362,99 +356,28 @@ pup incidents get ### Case Management -#### Search and List Cases -```bash -# List all cases -pup cases list - -# Search with filters -pup cases list --status=OPEN -pup cases list --priority=P1 -pup cases list --project="Production Incidents" -pup cases list --filter="API error" - -# Pagination -pup cases list --page=2 --size=50 - -# Sort results -pup cases list --sort=priority --asc=false -``` - -#### Get Case Details -```bash -pup cases get CASE-123 -``` +See the [`case-management`](./case-management.md) agent for the full `pup cases ...` command +surface (search, create, comments, projects, integrations). For incident-driven case work, the +typical commands used here are: -#### Create Cases ```bash +# Open a case for an in-progress incident pup cases create \ - --title="API Gateway Timeout" \ - --type-id="550e8400-e29b-41d4-a716-446655440000" \ - --priority=P2 \ - --description="Users experiencing 504 errors on /api/v2/users endpoint" -``` - -#### Update Case Status -```bash -pup cases update CASE-123 --status=IN_PROGRESS -pup cases update CASE-123 --status=CLOSED -``` - -#### Update Case Priority -```bash -pup cases update CASE-123 --priority=P1 -``` - -#### Assign Cases -```bash -# Assign to user -pup cases assign CASE-123 --user="john.doe@company.com" - -# Unassign -pup cases unassign CASE-123 -``` + --title "" \ + --type-id "" \ + --priority P1 \ + --project-id "" -#### Case Comments -```bash -# Add comment -pup cases comment CASE-123 --text="Identified root cause: Redis cache miss" +# Track investigation progress +pup cases comment --body "Investigation update: ..." +pup cases update-status --status IN_PROGRESS -# Delete comment -pup cases comment CASE-123 --delete=cell-id-here +# Close out after resolution +pup cases update-status --status CLOSED +pup cases archive ``` -#### Archive Operations -```bash -# Archive case -pup cases archive CASE-123 - -# Unarchive case -pup cases unarchive CASE-123 -``` - -#### Custom Attributes -```bash -# Set custom attribute -pup cases attribute CASE-123 --key="incident_severity" --value="high" - -# Delete custom attribute -pup cases attribute CASE-123 --key="incident_severity" --delete -``` - -#### Project Management -```bash -# List all projects -pup cases projects list - -# Get project details -pup cases projects get 660e8400-e29b-41d4-a716-446655440000 - -# Create project -pup cases projects create --name="Q1 2025 Production Incidents" - -# Delete project -pup cases projects delete 660e8400-e29b-41d4-a716-446655440000 -``` +For anything beyond these, defer to `case-management`. ## Key Concepts @@ -527,20 +450,10 @@ Defines when and how to send notifications: - **Post-Mortem**: Analysis conducted after resolution - **Impact**: Measurement of customer and business effects -### Case Management Concepts - -#### Case Status Values -- `OPEN`: Newly created, awaiting triage -- `IN_PROGRESS`: Actively being worked on -- `CLOSED`: Resolved and completed +### Case Concepts (summary) -#### Case Priority Values -- `NOT_DEFINED`: No priority set -- `P1`: Critical (immediate action required) -- `P2`: High (resolve within hours) -- `P3`: Medium (resolve within days) -- `P4`: Low (resolve within weeks) -- `P5`: Trivial (backlog) +Cases have a status (`OPEN`/`IN_PROGRESS`/`CLOSED`) and a priority (`P1`–`P5`/`NOT_DEFINED`). For +the full vocabulary and lifecycle, see the [`case-management`](./case-management.md) agent. ## Permission Model @@ -594,32 +507,30 @@ pup incidents list pup incidents get # 4. CASE MANAGEMENT: Create tracking case +# (see the case-management agent for full options) pup cases create \ - --title="Production API Error Rate Spike" \ - --type-id="" \ - --priority=P1 \ - --project-id="" + --title "Production API Error Rate Spike" \ + --type-id "" \ + --priority P1 \ + --project-id "" -# 5. ASSIGNMENT: Assign to incident commander -pup cases assign CASE-XXX --user="commander@company.com" +# 5. ASSIGNMENT: Assign to incident commander (by user UUID, not email) +pup cases assign CASE-XXX --user-id # 6. INVESTIGATION: Update status as work progresses -pup cases update CASE-XXX --status=IN_PROGRESS +pup cases update-status CASE-XXX --status IN_PROGRESS # 7. COLLABORATION: Add investigation findings -pup cases comment CASE-XXX --text="Root cause: Database connection pool exhaustion" +pup cases comment CASE-XXX --body "Root cause: Database connection pool exhaustion" # 8. ESCALATION: If needed, escalate page pup on-call page escalate # 9. RESOLUTION: Mark resolved pup on-call page resolve -pup cases update CASE-XXX --status=CLOSED - -# 10. POST-INCIDENT: Link incident ID to case -pup cases attribute CASE-XXX --key="incident_id" --value="" +pup cases update-status CASE-XXX --status CLOSED -# 11. ARCHIVE: Archive after post-mortem +# 10. ARCHIVE: Archive after post-mortem pup cases archive CASE-XXX ``` @@ -659,8 +570,8 @@ pup on-call notifications channel create --type="email" --value="me@example.com" pup on-call notifications rule create --channel-id="" --urgency="high" --delay-minutes=0 pup on-call notifications rule create --channel-id="" --urgency="high" --delay-minutes=5 -# 6. Create case management project -pup cases projects create --name="Production Incidents Q1 2025" +# 6. Create case management project (both --name and --key required) +pup cases projects create --name "Production Incidents Q1 2025" --key "PROD-INC" # 7. Verify setup - check who's on-call pup on-call team responders @@ -675,11 +586,9 @@ pup on-call team responders # 2. Review active incidents pup incidents list -# 3. Check open high-priority cases -pup cases list --status=OPEN --priority=P1 - -# 4. Review in-progress cases -pup cases list --status=IN_PROGRESS +# 3. Browse cases for the team's project (status isn't a direct search facet — +# filter client-side or use the Datadog UI for status-based queries) +pup cases search --query "project_id:" --page-size 100 ``` ## Response Formatting @@ -717,33 +626,10 @@ pup incidents list --state=active pup incidents get ``` -### "Create a case for this incident" -```bash -pup cases create \ - --title="..." \ - --type-id="..." \ - --priority=P1 -``` - -### "Assign the case to the incident commander" -```bash -pup cases assign CASE-XXX --user="commander@example.com" -``` - -### "Update case status to in progress" -```bash -pup cases update CASE-XXX --status=IN_PROGRESS -``` - -### "Add a comment with investigation findings" -```bash -pup cases comment CASE-XXX --text="Root cause identified: ..." -``` - -### "Close the case" -```bash -pup cases update CASE-XXX --status=CLOSED -``` +### Case operations during an incident +For "create a case", "assign to the incident commander", "comment with findings", "close the case", +etc. — delegate to the [`case-management`](./case-management.md) agent. The incident-response +agent stays focused on incident, on-call, and paging concerns. ## Error Handling @@ -798,14 +684,8 @@ Error: Invalid case type_id 6. **Regular Monitoring**: Check incident status during active incidents ### Case Management -1. **Case Types**: Always use valid `type_id` when creating cases -2. **Priority Levels**: Use standard P1-P5 scale consistently -3. **Status Flow**: Follow logical progression: OPEN → IN_PROGRESS → CLOSED -4. **Regular Updates**: Add comments for audit trail and collaboration -5. **Project Organization**: Organize cases by team, service, or time period -6. **Custom Attributes**: Use for additional metadata (incident IDs, SLA deadlines) -7. **Archive**: Archive closed cases to maintain clean active case lists -8. **Link Resources**: Use custom attributes to link cases to incidents +For case-specific best practices, see the [`case-management`](./case-management.md) agent. +The integration patterns below summarize how cases relate to incident workflows. ### Integration Patterns 1. **Page → Incident → Case**: Create incident when paged, then track in case diff --git a/src/commands/cases.rs b/src/commands/cases.rs index 349d78d7..fcd0eb1a 100644 --- a/src/commands/cases.rs +++ b/src/commands/cases.rs @@ -3,20 +3,41 @@ use datadog_api_client::datadogV2::api_case_management::{ CaseManagementAPI, SearchCasesOptionalParams, }; use datadog_api_client::datadogV2::model::{ - CaseAssign, CaseAssignAttributes, CaseAssignRequest, CaseCreateRequest, CaseEmpty, - CaseEmptyRequest, CaseNotificationRuleCreateRequest, CaseNotificationRuleUpdateRequest, - CasePriority, CaseResourceType, CaseStatus, CaseUpdatePriority, CaseUpdatePriorityAttributes, - CaseUpdatePriorityRequest, CaseUpdateStatus, CaseUpdateStatusAttributes, - CaseUpdateStatusRequest, CaseUpdateTitle, CaseUpdateTitleAttributes, CaseUpdateTitleRequest, - JiraIssueCreateRequest, JiraIssueLinkRequest, ProjectCreate, ProjectCreateAttributes, - ProjectCreateRequest, ProjectRelationship, ProjectRelationshipData, ProjectResourceType, - ProjectUpdateRequest, ServiceNowTicketCreateRequest, + CaseAssign, CaseAssignAttributes, CaseAssignRequest, CaseComment, CaseCommentAttributes, + CaseCommentRequest, CaseCreate, CaseCreateAttributes, CaseCreateRelationships, + CaseCreateRequest, CaseEmpty, CaseEmptyRequest, CaseNotificationRuleCreateRequest, + CaseNotificationRuleUpdateRequest, CasePriority, CaseResourceType, CaseStatus, + CaseUpdateDescription, CaseUpdateDescriptionAttributes, CaseUpdateDescriptionRequest, + CaseUpdatePriority, CaseUpdatePriorityAttributes, CaseUpdatePriorityRequest, CaseUpdateStatus, + CaseUpdateStatusAttributes, CaseUpdateStatusRequest, CaseUpdateTitle, + CaseUpdateTitleAttributes, CaseUpdateTitleRequest, JiraIssueCreateRequest, + JiraIssueLinkRequest, ProjectCreate, ProjectCreateAttributes, ProjectCreateRequest, + ProjectRelationship, ProjectRelationshipData, ProjectResourceType, ProjectUpdateRequest, + ServiceNowTicketCreateRequest, }; -use crate::client; use crate::config::Config; use crate::formatter; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Parse a priority string into a `CasePriority`. +/// +/// Accepts the canonical names (P1..P5, NOT_DEFINED), case-insensitive. +fn parse_priority(s: &str) -> Result { + Ok(match s.to_uppercase().as_str() { + "P1" => CasePriority::P1, + "P2" => CasePriority::P2, + "P3" => CasePriority::P3, + "P4" => CasePriority::P4, + "P5" => CasePriority::P5, + "NOT_DEFINED" => CasePriority::NOT_DEFINED, + _ => anyhow::bail!("invalid priority: {s} (use P1, P2, P3, P4, P5, NOT_DEFINED)"), + }) +} + // --------------------------------------------------------------------------- // Helper: build a CaseManagementAPI with bearer-token support // --------------------------------------------------------------------------- @@ -74,22 +95,54 @@ pub async fn create_from_flags( type_id: &str, priority: &str, description: Option<&str>, + project_id: Option<&str>, ) -> Result<()> { - let mut body = serde_json::json!({ - "data": { - "type": "case", - "attributes": { - "title": title, - "priority": priority, - "type": type_id, - } - } - }); + let api = make_api(cfg); + let priority_val = parse_priority(priority)?; + let mut attrs = + CaseCreateAttributes::new(title.to_string(), type_id.to_string()).priority(priority_val); if let Some(desc) = description { - body["data"]["attributes"]["description"] = serde_json::json!(desc); + attrs = attrs.description(desc.to_string()); } - let data = client::raw_post(cfg, "/api/v2/cases", body).await?; - crate::formatter::output(cfg, &data) + let mut case_create = CaseCreate::new(attrs, CaseResourceType::CASE); + if let Some(pid) = project_id { + case_create = + case_create.relationships(CaseCreateRelationships::new(ProjectRelationship::new( + ProjectRelationshipData::new(pid.to_string(), ProjectResourceType::PROJECT), + ))); + } + let body = CaseCreateRequest::new(case_create); + let resp = api + .create_case(body) + .await + .map_err(|e| anyhow::anyhow!("failed to create case: {e:?}"))?; + formatter::output(cfg, &resp) +} + +// --------------------------------------------------------------------------- +// Comments +// --------------------------------------------------------------------------- + +pub async fn comment(cfg: &Config, case_id: &str, body: &str) -> Result<()> { + let api = make_api(cfg); + let req = CaseCommentRequest::new(CaseComment::new( + CaseCommentAttributes::new(body.to_string()), + CaseResourceType::CASE, + )); + let resp = api + .comment_case(case_id.to_string(), req) + .await + .map_err(|e| anyhow::anyhow!("failed to add comment: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn delete_comment(cfg: &Config, case_id: &str, comment_id: &str) -> Result<()> { + let api = make_api(cfg); + api.delete_case_comment(case_id.to_string(), comment_id.to_string()) + .await + .map_err(|e| anyhow::anyhow!("failed to delete comment: {e:?}"))?; + println!("Comment {comment_id} deleted from case {case_id}."); + Ok(()) } // --------------------------------------------------------------------------- @@ -181,15 +234,7 @@ pub async fn assign(cfg: &Config, case_id: &str, user_id: &str) -> Result<()> { pub async fn update_priority(cfg: &Config, case_id: &str, priority: &str) -> Result<()> { let api = make_api(cfg); - let priority_val = match priority.to_uppercase().as_str() { - "P1" => CasePriority::P1, - "P2" => CasePriority::P2, - "P3" => CasePriority::P3, - "P4" => CasePriority::P4, - "P5" => CasePriority::P5, - "NOT_DEFINED" => CasePriority::NOT_DEFINED, - _ => anyhow::bail!("invalid priority: {priority} (use P1, P2, P3, P4, P5, NOT_DEFINED)"), - }; + let priority_val = parse_priority(priority)?; let body = CaseUpdatePriorityRequest::new(CaseUpdatePriority::new( CaseUpdatePriorityAttributes::new(priority_val), CaseResourceType::CASE, @@ -354,6 +399,22 @@ pub async fn update_title(cfg: &Config, case_id: &str, title: &str) -> Result<() formatter::output(cfg, &resp) } +// --------------------------------------------------------------------------- +// Update case description +// --------------------------------------------------------------------------- + +pub async fn update_description(cfg: &Config, case_id: &str, description: &str) -> Result<()> { + let api = make_api(cfg); + let attrs = CaseUpdateDescriptionAttributes::new(description.to_string()); + let data = CaseUpdateDescription::new(attrs, CaseResourceType::CASE); + let body = CaseUpdateDescriptionRequest::new(data); + let resp = api + .update_case_description(case_id.to_string(), body) + .await + .map_err(|e| anyhow::anyhow!("failed to update case description: {e:?}"))?; + formatter::output(cfg, &resp) +} + // --------------------------------------------------------------------------- // Update project // --------------------------------------------------------------------------- @@ -422,4 +483,120 @@ mod tests { let _ = super::projects_delete(&cfg, "proj1").await; cleanup_env(); } + + #[tokio::test] + async fn test_cases_create_from_flags() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{"data": {}}"#).await; + let _ = super::create_from_flags( + &cfg, + "title", + "00000000-0000-0000-0000-000000000001", + "P2", + Some("description"), + None, + ) + .await; + cleanup_env(); + } + + #[tokio::test] + async fn test_cases_create_from_flags_with_project_id() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{"data": {}}"#).await; + let _ = super::create_from_flags( + &cfg, + "title", + "00000000-0000-0000-0000-000000000001", + "NOT_DEFINED", + None, + Some("d59d89e1-b144-47e2-ae70-179203edf0ba"), + ) + .await; + cleanup_env(); + } + + #[tokio::test] + async fn test_cases_create_from_flags_invalid_priority() { + let _lock = lock_env().await; + let cfg = test_config("http://nowhere.invalid"); + let result = super::create_from_flags( + &cfg, + "title", + "00000000-0000-0000-0000-000000000001", + "P9", + None, + None, + ) + .await; + cleanup_env(); + assert!(result.is_err(), "expected error for invalid priority P9"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("invalid priority"), + "expected 'invalid priority' in error, got: {err}" + ); + } + + #[tokio::test] + async fn test_cases_comment() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{"data": {}}"#).await; + let _ = super::comment(&cfg, "case1", "hello").await; + cleanup_env(); + } + + #[tokio::test] + async fn test_cases_update_description() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{"data": {}}"#).await; + let _ = super::update_description(&cfg, "case1", "new description").await; + cleanup_env(); + } + + #[tokio::test] + async fn test_cases_delete_comment() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{}"#).await; + let _ = super::delete_comment(&cfg, "case1", "comment1").await; + cleanup_env(); + } + + #[test] + fn test_parse_priority_valid() { + for (input, expected) in [ + ("P1", super::CasePriority::P1), + ("p2", super::CasePriority::P2), + ("P3", super::CasePriority::P3), + ("P4", super::CasePriority::P4), + ("P5", super::CasePriority::P5), + ("NOT_DEFINED", super::CasePriority::NOT_DEFINED), + ("not_defined", super::CasePriority::NOT_DEFINED), + ] { + let got = super::parse_priority(input).expect("valid priority should parse"); + assert_eq!( + got, expected, + "priority {input} should parse to {expected:?}" + ); + } + } + + #[test] + fn test_parse_priority_invalid() { + let err = super::parse_priority("P9").unwrap_err().to_string(); + assert!( + err.contains("invalid priority"), + "expected 'invalid priority' in error, got: {err}" + ); + } } diff --git a/src/main.rs b/src/main.rs index cf0188e7..f777337f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5199,7 +5199,7 @@ enum CaseActions { query: Option, #[arg(long, default_value_t = 10, help = "Results per page")] page_size: i64, - #[arg(long, default_value_t = 0, help = "Page number")] + #[arg(long, default_value_t = 1, help = "Page number (1-indexed)")] page_number: i64, }, /// Get case details @@ -5219,9 +5219,28 @@ enum CaseActions { priority: String, #[arg(long, help = "Case description")] description: Option, - #[arg(long, help = "JSON file with request body (required)", conflicts_with_all = ["title", "type-id"])] + #[arg( + long, + name = "project-id", + help = "Project UUID to assign the case to (optional)" + )] + project_id: Option, + #[arg(long, help = "JSON file with request body (required)", conflicts_with_all = ["title", "type-id", "project-id"])] file: Option, }, + /// Add a comment to a case + Comment { + case_id: String, + #[arg(long, help = "Comment body (required)")] + body: String, + }, + /// Delete a comment from a case + #[command(name = "delete-comment")] + DeleteComment { + case_id: String, + #[arg(long, name = "comment-id", help = "Comment UUID (required)")] + comment_id: String, + }, /// Archive a case Archive { case_id: String }, /// Unarchive a case @@ -5264,6 +5283,13 @@ enum CaseActions { #[arg(long, help = "New title (required)")] title: String, }, + /// Update case description + #[command(name = "update-description")] + UpdateDescription { + case_id: String, + #[arg(long, help = "New description (required)")] + description: String, + }, /// Manage Jira integrations for cases Jira { #[command(subcommand)] @@ -11673,6 +11699,7 @@ async fn main_inner() -> anyhow::Result<()> { type_id, priority, description, + project_id, file, } => { if let Some(f) = file { @@ -11684,10 +11711,20 @@ async fn main_inner() -> anyhow::Result<()> { &type_id.unwrap(), &priority, description.as_deref(), + project_id.as_deref(), ) .await?; } } + CaseActions::Comment { case_id, body } => { + commands::cases::comment(&cfg, &case_id, &body).await?; + } + CaseActions::DeleteComment { + case_id, + comment_id, + } => { + commands::cases::delete_comment(&cfg, &case_id, &comment_id).await?; + } CaseActions::Archive { case_id } => { commands::cases::archive(&cfg, &case_id).await?; } @@ -11712,6 +11749,12 @@ async fn main_inner() -> anyhow::Result<()> { CaseActions::UpdateTitle { case_id, title } => { commands::cases::update_title(&cfg, &case_id, &title).await?; } + CaseActions::UpdateDescription { + case_id, + description, + } => { + commands::cases::update_description(&cfg, &case_id, &description).await?; + } CaseActions::Projects { action } => match action { CaseProjectActions::List => commands::cases::projects_list(&cfg).await?, CaseProjectActions::Get { project_id } => {