1use chrono::Utc;
14use serde::{Deserialize, Serialize};
15use std::collections::{BTreeMap, BTreeSet};
16
17use crate::models::artifact::{Artifact, ArtifactStatus, ArtifactType};
18use crate::models::kanban::KanbanBoard;
19use crate::models::task::{
20 build_task_invest_validation, build_task_story_readiness, Task, TaskLaneSessionStatus,
21 TaskStatus,
22};
23use crate::rpc::error::RpcError;
24use crate::state::AppState;
25
26const KANBAN_HAPPY_PATH_COLUMN_ORDER: [&str; 5] = ["backlog", "todo", "dev", "review", "done"];
27
28#[derive(Debug, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct TaskArtifactSummary {
31 pub total: usize,
32 pub by_type: BTreeMap<String, usize>,
33 pub required_satisfied: bool,
34 pub missing_required: Vec<String>,
35}
36
37#[derive(Debug, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct TaskVerificationSummary {
40 pub has_verdict: bool,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub verdict: Option<String>,
43 pub has_report: bool,
44}
45
46#[derive(Debug, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct TaskCompletionSummary {
49 pub has_summary: bool,
50}
51
52#[derive(Debug, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct TaskRunSummary {
55 pub total: usize,
56 pub latest_status: String,
57}
58
59#[derive(Debug, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct TaskEvidenceSummary {
62 pub artifact: TaskArtifactSummary,
63 pub verification: TaskVerificationSummary,
64 pub completion: TaskCompletionSummary,
65 pub runs: TaskRunSummary,
66}
67
68#[derive(Debug, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct ListParams {
75 #[serde(default = "default_workspace_id")]
76 pub workspace_id: String,
77 pub session_id: Option<String>,
78 pub status: Option<String>,
79 pub assigned_to: Option<String>,
80}
81
82fn default_workspace_id() -> String {
83 "default".into()
84}
85
86#[derive(Debug, Serialize)]
87pub struct ListResult {
88 pub tasks: Vec<serde_json::Value>,
89}
90
91pub async fn list(state: &AppState, params: ListParams) -> Result<ListResult, RpcError> {
92 let tasks = if let Some(session_id) = ¶ms.session_id {
93 state.task_store.list_by_session(session_id).await?
95 } else if let Some(assignee) = ¶ms.assigned_to {
96 state.task_store.list_by_assignee(assignee).await?
97 } else if let Some(status_str) = ¶ms.status {
98 let status = TaskStatus::from_str(status_str)
99 .ok_or_else(|| RpcError::BadRequest(format!("Invalid status: {}", status_str)))?;
100 state
101 .task_store
102 .list_by_status(¶ms.workspace_id, &status)
103 .await?
104 } else {
105 state
106 .task_store
107 .list_by_workspace(¶ms.workspace_id)
108 .await?
109 };
110
111 Ok(ListResult {
112 tasks: serialize_tasks_with_evidence(state, &tasks).await?,
113 })
114}
115
116#[derive(Debug, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct GetParams {
123 pub id: String,
124}
125
126pub async fn get(state: &AppState, params: GetParams) -> Result<serde_json::Value, RpcError> {
127 let task = state
128 .task_store
129 .get(¶ms.id)
130 .await?
131 .ok_or_else(|| RpcError::NotFound(format!("Task {} not found", params.id)))?;
132 serialize_task_with_evidence(state, &task).await
133}
134
135#[derive(Debug, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct CreateParams {
142 pub title: String,
143 pub objective: String,
144 #[serde(default = "default_workspace_id")]
145 pub workspace_id: String,
146 pub session_id: Option<String>,
147 pub scope: Option<String>,
148 pub acceptance_criteria: Option<Vec<String>>,
149 pub verification_commands: Option<Vec<String>>,
150 pub test_cases: Option<Vec<String>>,
151 pub dependencies: Option<Vec<String>>,
152 pub parallel_group: Option<String>,
153}
154
155#[derive(Debug, Serialize)]
156pub struct CreateResult {
157 pub task: serde_json::Value,
158}
159
160pub async fn create(state: &AppState, params: CreateParams) -> Result<CreateResult, RpcError> {
161 let task = Task::new(
162 uuid::Uuid::new_v4().to_string(),
163 params.title,
164 params.objective,
165 params.workspace_id,
166 params.session_id,
167 params.scope,
168 params.acceptance_criteria,
169 params.verification_commands,
170 params.test_cases,
171 params.dependencies,
172 params.parallel_group,
173 );
174
175 state.task_store.save(&task).await?;
176 Ok(CreateResult {
177 task: serialize_task_with_evidence(state, &task).await?,
178 })
179}
180
181#[derive(Debug, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct DeleteParams {
188 pub id: String,
189}
190
191#[derive(Debug, Serialize)]
192pub struct DeleteResult {
193 pub deleted: bool,
194}
195
196pub async fn delete(state: &AppState, params: DeleteParams) -> Result<DeleteResult, RpcError> {
197 state.task_store.delete(¶ms.id).await?;
198 Ok(DeleteResult { deleted: true })
199}
200
201#[derive(Debug, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct UpdateStatusParams {
208 pub id: String,
209 pub status: String,
210}
211
212#[derive(Debug, Serialize)]
213pub struct UpdateStatusResult {
214 pub updated: bool,
215}
216
217pub async fn update_status(
218 state: &AppState,
219 params: UpdateStatusParams,
220) -> Result<UpdateStatusResult, RpcError> {
221 let status = TaskStatus::from_str(¶ms.status)
222 .ok_or_else(|| RpcError::BadRequest(format!("Invalid status: {}", params.status)))?;
223 state.task_store.update_status(¶ms.id, &status).await?;
224 Ok(UpdateStatusResult { updated: true })
225}
226
227#[derive(Debug, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct FindReadyParams {
234 #[serde(default = "default_workspace_id")]
235 pub workspace_id: String,
236}
237
238pub async fn find_ready(state: &AppState, params: FindReadyParams) -> Result<ListResult, RpcError> {
239 let tasks = state
240 .task_store
241 .find_ready_tasks(¶ms.workspace_id)
242 .await?;
243 Ok(ListResult {
244 tasks: serialize_tasks_with_evidence(state, &tasks).await?,
245 })
246}
247
248#[derive(Debug, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct ListArtifactsParams {
255 pub task_id: String,
256 #[serde(rename = "type")]
257 pub artifact_type: Option<String>,
258}
259
260#[derive(Debug, Serialize)]
261pub struct ListArtifactsResult {
262 pub artifacts: Vec<Artifact>,
263}
264
265pub async fn list_artifacts(
266 state: &AppState,
267 params: ListArtifactsParams,
268) -> Result<ListArtifactsResult, RpcError> {
269 let artifacts = if let Some(artifact_type) = params.artifact_type.as_deref() {
270 let artifact_type = parse_artifact_type(artifact_type)?;
271 state
272 .artifact_store
273 .list_by_task_and_type(¶ms.task_id, &artifact_type)
274 .await?
275 } else {
276 state.artifact_store.list_by_task(¶ms.task_id).await?
277 };
278
279 Ok(ListArtifactsResult { artifacts })
280}
281
282#[derive(Debug, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct ProvideArtifactParams {
289 pub task_id: String,
290 pub agent_id: String,
291 #[serde(rename = "type")]
292 pub artifact_type: String,
293 pub content: String,
294 pub context: Option<String>,
295 pub request_id: Option<String>,
296 pub metadata: Option<BTreeMap<String, String>>,
297}
298
299#[derive(Debug, Serialize)]
300pub struct ProvideArtifactResult {
301 pub artifact: Artifact,
302}
303
304pub async fn provide_artifact(
305 state: &AppState,
306 params: ProvideArtifactParams,
307) -> Result<ProvideArtifactResult, RpcError> {
308 let task = state
309 .task_store
310 .get(¶ms.task_id)
311 .await?
312 .ok_or_else(|| RpcError::NotFound(format!("Task {} not found", params.task_id)))?;
313
314 let agent_id = params.agent_id.trim();
315 if agent_id.is_empty() {
316 return Err(RpcError::BadRequest(
317 "agentId is required for artifact submission".to_string(),
318 ));
319 }
320
321 let content = params.content.trim();
322 if content.is_empty() {
323 return Err(RpcError::BadRequest(
324 "artifact content cannot be blank".to_string(),
325 ));
326 }
327
328 let artifact = Artifact {
329 id: uuid::Uuid::new_v4().to_string(),
330 artifact_type: parse_artifact_type(¶ms.artifact_type)?,
331 task_id: task.id,
332 workspace_id: task.workspace_id,
333 provided_by_agent_id: Some(agent_id.to_string()),
334 requested_by_agent_id: None,
335 request_id: params.request_id,
336 content: Some(content.to_string()),
337 context: params
338 .context
339 .as_deref()
340 .map(str::trim)
341 .filter(|value| !value.is_empty())
342 .map(str::to_string),
343 status: ArtifactStatus::Provided,
344 expires_at: None,
345 metadata: params.metadata,
346 created_at: Utc::now(),
347 updated_at: Utc::now(),
348 };
349
350 state.artifact_store.save(&artifact).await?;
351 Ok(ProvideArtifactResult { artifact })
352}
353
354fn parse_artifact_type(value: &str) -> Result<ArtifactType, RpcError> {
355 ArtifactType::from_str(value).ok_or_else(|| {
356 RpcError::BadRequest(format!(
357 "Invalid artifact type: {}. Expected one of: screenshot, test_results, code_diff, logs",
358 value
359 ))
360 })
361}
362
363async fn serialize_tasks_with_evidence(
364 state: &AppState,
365 tasks: &[Task],
366) -> Result<Vec<serde_json::Value>, RpcError> {
367 let mut serialized = Vec::with_capacity(tasks.len());
368 for task in tasks {
369 serialized.push(serialize_task_with_evidence(state, task).await?);
370 }
371 Ok(serialized)
372}
373
374async fn serialize_task_with_evidence(
375 state: &AppState,
376 task: &Task,
377) -> Result<serde_json::Value, RpcError> {
378 let evidence_summary = build_task_evidence_summary(state, task).await?;
379 let board = match task.board_id.as_deref() {
380 Some(board_id) => state.kanban_store.get(board_id).await?,
381 None => None,
382 };
383 let story_readiness = build_task_story_readiness(
384 task,
385 &resolve_next_required_task_fields(board.as_ref(), task.column_id.as_deref()),
386 );
387 let invest_validation = build_task_invest_validation(task);
388 let mut task_value = serde_json::to_value(task)
389 .map_err(|error| RpcError::Internal(format!("Failed to serialize task: {error}")))?;
390 let task_object = task_value.as_object_mut().ok_or_else(|| {
391 RpcError::Internal("Task payload must serialize to a JSON object".to_string())
392 })?;
393 task_object.insert(
394 "artifactSummary".to_string(),
395 serde_json::to_value(&evidence_summary.artifact).map_err(|error| {
396 RpcError::Internal(format!(
397 "Failed to serialize task artifact summary: {error}"
398 ))
399 })?,
400 );
401 task_object.insert(
402 "evidenceSummary".to_string(),
403 serde_json::to_value(&evidence_summary).map_err(|error| {
404 RpcError::Internal(format!(
405 "Failed to serialize task evidence summary: {error}"
406 ))
407 })?,
408 );
409 task_object.insert(
410 "storyReadiness".to_string(),
411 serde_json::to_value(&story_readiness).map_err(|error| {
412 RpcError::Internal(format!(
413 "Failed to serialize task story readiness summary: {error}"
414 ))
415 })?,
416 );
417 task_object.insert(
418 "investValidation".to_string(),
419 serde_json::to_value(&invest_validation).map_err(|error| {
420 RpcError::Internal(format!(
421 "Failed to serialize task INVEST validation summary: {error}"
422 ))
423 })?,
424 );
425 Ok(task_value)
426}
427
428async fn build_task_evidence_summary(
429 state: &AppState,
430 task: &Task,
431) -> Result<TaskEvidenceSummary, RpcError> {
432 let artifacts = state.artifact_store.list_by_task(&task.id).await?;
433 let mut by_type = BTreeMap::new();
434 for artifact in &artifacts {
435 let key = artifact.artifact_type.as_str().to_string();
436 *by_type.entry(key).or_insert(0) += 1;
437 }
438
439 let board = match task.board_id.as_deref() {
440 Some(board_id) => state.kanban_store.get(board_id).await?,
441 None => None,
442 };
443 let required_artifacts =
444 resolve_next_required_artifacts(board.as_ref(), task.column_id.as_deref());
445 let present_artifacts = by_type.keys().cloned().collect::<BTreeSet<_>>();
446 let missing_required = required_artifacts
447 .into_iter()
448 .filter(|artifact| !present_artifacts.contains(artifact))
449 .collect::<Vec<_>>();
450
451 let latest_status = task
452 .lane_sessions
453 .last()
454 .map(|session| task_lane_session_status_as_str(&session.status).to_string())
455 .unwrap_or_else(|| {
456 if task.session_ids.is_empty() {
457 "idle".to_string()
458 } else {
459 "unknown".to_string()
460 }
461 });
462
463 Ok(TaskEvidenceSummary {
464 artifact: TaskArtifactSummary {
465 total: artifacts.len(),
466 by_type,
467 required_satisfied: missing_required.is_empty(),
468 missing_required,
469 },
470 verification: TaskVerificationSummary {
471 has_verdict: task.verification_verdict.is_some(),
472 verdict: task
473 .verification_verdict
474 .as_ref()
475 .map(|verdict| verdict.as_str().to_string()),
476 has_report: task
477 .verification_report
478 .as_ref()
479 .is_some_and(|report| !report.trim().is_empty()),
480 },
481 completion: TaskCompletionSummary {
482 has_summary: task
483 .completion_summary
484 .as_ref()
485 .is_some_and(|summary| !summary.trim().is_empty()),
486 },
487 runs: TaskRunSummary {
488 total: task.session_ids.len(),
489 latest_status,
490 },
491 })
492}
493
494fn resolve_next_required_artifacts(
495 board: Option<&KanbanBoard>,
496 current_column_id: Option<&str>,
497) -> Vec<String> {
498 let current_column_id = current_column_id.unwrap_or("backlog").to_ascii_lowercase();
499 let next_column_id = KANBAN_HAPPY_PATH_COLUMN_ORDER
500 .iter()
501 .position(|column_id| *column_id == current_column_id)
502 .and_then(|index| KANBAN_HAPPY_PATH_COLUMN_ORDER.get(index + 1))
503 .copied();
504 let Some(next_column_id) = next_column_id else {
505 return Vec::new();
506 };
507
508 board
509 .and_then(|board| {
510 board
511 .columns
512 .iter()
513 .find(|column| column.id == next_column_id)
514 })
515 .and_then(|column| column.automation.as_ref())
516 .and_then(|automation| automation.required_artifacts.clone())
517 .unwrap_or_default()
518}
519
520fn resolve_next_required_task_fields(
521 board: Option<&KanbanBoard>,
522 current_column_id: Option<&str>,
523) -> Vec<String> {
524 let current_column_id = current_column_id.unwrap_or("backlog").to_ascii_lowercase();
525 let next_column_id = KANBAN_HAPPY_PATH_COLUMN_ORDER
526 .iter()
527 .position(|column_id| *column_id == current_column_id)
528 .and_then(|index| KANBAN_HAPPY_PATH_COLUMN_ORDER.get(index + 1))
529 .copied();
530 let Some(next_column_id) = next_column_id else {
531 return Vec::new();
532 };
533
534 board
535 .and_then(|board| {
536 board
537 .columns
538 .iter()
539 .find(|column| column.id == next_column_id)
540 })
541 .and_then(|column| column.automation.as_ref())
542 .and_then(|automation| automation.required_task_fields.clone())
543 .unwrap_or_default()
544}
545
546fn task_lane_session_status_as_str(status: &TaskLaneSessionStatus) -> &'static str {
547 match status {
548 TaskLaneSessionStatus::Running => "running",
549 TaskLaneSessionStatus::Completed => "completed",
550 TaskLaneSessionStatus::Failed => "failed",
551 TaskLaneSessionStatus::TimedOut => "timed_out",
552 TaskLaneSessionStatus::Transitioned => "transitioned",
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::models::kanban::KanbanColumnAutomation;
560 use crate::models::task::{TaskLaneSession, VerificationVerdict};
561 use crate::{AppState, AppStateInner, Database};
562 use std::sync::Arc;
563
564 async fn setup_state() -> AppState {
565 let db = Database::open_in_memory().expect("in-memory db should open");
566 let state: AppState = Arc::new(AppStateInner::new(db));
567 state
568 .workspace_store
569 .ensure_default()
570 .await
571 .expect("default workspace should exist");
572 state
573 }
574
575 #[tokio::test]
576 async fn provide_and_list_artifacts_roundtrip() {
577 let state = setup_state().await;
578 let created = create(
579 &state,
580 CreateParams {
581 title: "Artifact task".to_string(),
582 objective: "Store screenshot evidence".to_string(),
583 workspace_id: "default".to_string(),
584 session_id: None,
585 scope: None,
586 acceptance_criteria: None,
587 verification_commands: None,
588 test_cases: None,
589 dependencies: None,
590 parallel_group: None,
591 },
592 )
593 .await
594 .expect("task should be created");
595 let created_task_id = created.task["id"]
596 .as_str()
597 .expect("created task id")
598 .to_string();
599
600 let provided = provide_artifact(
601 &state,
602 ProvideArtifactParams {
603 task_id: created_task_id.clone(),
604 agent_id: "agent-1".to_string(),
605 artifact_type: "screenshot".to_string(),
606 content: "base64-content".to_string(),
607 context: Some("Verification screenshot".to_string()),
608 request_id: None,
609 metadata: None,
610 },
611 )
612 .await
613 .expect("artifact should be created");
614
615 assert_eq!(provided.artifact.artifact_type, ArtifactType::Screenshot);
616 assert_eq!(
617 provided.artifact.provided_by_agent_id.as_deref(),
618 Some("agent-1")
619 );
620
621 let listed = list_artifacts(
622 &state,
623 ListArtifactsParams {
624 task_id: created_task_id,
625 artifact_type: Some("screenshot".to_string()),
626 },
627 )
628 .await
629 .expect("artifacts should be listed");
630
631 assert_eq!(listed.artifacts.len(), 1);
632 assert_eq!(
633 listed.artifacts[0].context.as_deref(),
634 Some("Verification screenshot")
635 );
636 }
637
638 #[tokio::test]
639 async fn rpc_task_methods_include_evidence_summary() {
640 let state = setup_state().await;
641 let mut board = state
642 .kanban_store
643 .ensure_default_board("default")
644 .await
645 .expect("default board should exist");
646 let dev_column = board
647 .columns
648 .iter_mut()
649 .find(|column| column.id == "dev")
650 .expect("dev column");
651 dev_column.automation = Some(KanbanColumnAutomation {
652 enabled: true,
653 required_artifacts: Some(vec!["screenshot".to_string()]),
654 required_task_fields: Some(vec![
655 "scope".to_string(),
656 "acceptance_criteria".to_string(),
657 "verification_plan".to_string(),
658 ]),
659 ..Default::default()
660 });
661 state
662 .kanban_store
663 .update(&board)
664 .await
665 .expect("board should update");
666
667 let mut task = Task::new(
668 "task-rpc-1".to_string(),
669 "RPC evidence".to_string(),
670 "Return parity task payload".to_string(),
671 "default".to_string(),
672 None,
673 None,
674 None,
675 None,
676 None,
677 None,
678 None,
679 );
680 task.board_id = Some(board.id.clone());
681 task.column_id = Some("todo".to_string());
682 task.session_ids = vec!["session-1".to_string()];
683 task.lane_sessions = vec![TaskLaneSession {
684 session_id: "session-1".to_string(),
685 routa_agent_id: None,
686 column_id: Some("todo".to_string()),
687 column_name: Some("Todo".to_string()),
688 step_id: None,
689 step_index: None,
690 step_name: None,
691 provider: None,
692 role: None,
693 specialist_id: None,
694 specialist_name: None,
695 transport: None,
696 external_task_id: None,
697 context_id: None,
698 attempt: None,
699 loop_mode: None,
700 completion_requirement: None,
701 objective: None,
702 last_activity_at: None,
703 recovered_from_session_id: None,
704 recovery_reason: None,
705 status: TaskLaneSessionStatus::Running,
706 started_at: "2026-03-27T00:00:00Z".to_string(),
707 completed_at: None,
708 }];
709 task.completion_summary = Some("Done".to_string());
710 task.verification_verdict = Some(VerificationVerdict::Approved);
711 task.verification_report = Some("Verified".to_string());
712 state
713 .task_store
714 .save(&task)
715 .await
716 .expect("task should save");
717
718 let artifact = Artifact {
719 id: "artifact-rpc-1".to_string(),
720 artifact_type: ArtifactType::Screenshot,
721 task_id: task.id.clone(),
722 workspace_id: task.workspace_id.clone(),
723 provided_by_agent_id: Some("agent-1".to_string()),
724 requested_by_agent_id: None,
725 request_id: None,
726 content: Some("base64".to_string()),
727 context: None,
728 status: ArtifactStatus::Provided,
729 expires_at: None,
730 metadata: None,
731 created_at: Utc::now(),
732 updated_at: Utc::now(),
733 };
734 state
735 .artifact_store
736 .save(&artifact)
737 .await
738 .expect("artifact should save");
739
740 let get_value = get(
741 &state,
742 GetParams {
743 id: task.id.clone(),
744 },
745 )
746 .await
747 .expect("task should load");
748 assert_eq!(get_value["artifactSummary"]["total"], serde_json::json!(1));
749 assert_eq!(
750 get_value["evidenceSummary"]["artifact"]["requiredSatisfied"],
751 serde_json::json!(true)
752 );
753 assert_eq!(
754 get_value["evidenceSummary"]["verification"]["verdict"],
755 serde_json::json!("APPROVED")
756 );
757 assert_eq!(
758 get_value["evidenceSummary"]["runs"]["latestStatus"],
759 serde_json::json!("running")
760 );
761 assert_eq!(
762 get_value["storyReadiness"]["requiredTaskFields"],
763 serde_json::json!(["scope", "acceptance_criteria", "verification_plan"])
764 );
765 assert_eq!(
766 get_value["storyReadiness"]["ready"],
767 serde_json::json!(false)
768 );
769 assert_eq!(
770 get_value["investValidation"]["source"],
771 serde_json::json!("heuristic")
772 );
773
774 let listed = list(
775 &state,
776 ListParams {
777 workspace_id: "default".to_string(),
778 session_id: None,
779 status: None,
780 assigned_to: None,
781 },
782 )
783 .await
784 .expect("tasks should list");
785 assert_eq!(listed.tasks.len(), 1);
786 assert_eq!(
787 listed.tasks[0]["evidenceSummary"]["completion"]["hasSummary"],
788 serde_json::json!(true)
789 );
790 assert_eq!(
791 listed.tasks[0]["storyReadiness"]["ready"],
792 serde_json::json!(false)
793 );
794
795 let ready = find_ready(
796 &state,
797 FindReadyParams {
798 workspace_id: "default".to_string(),
799 },
800 )
801 .await
802 .expect("ready tasks should list");
803 assert_eq!(ready.tasks.len(), 1);
804 assert_eq!(
805 ready.tasks[0]["artifactSummary"]["byType"]["screenshot"],
806 serde_json::json!(1)
807 );
808 assert_eq!(
809 ready.tasks[0]["investValidation"]["source"],
810 serde_json::json!("heuristic")
811 );
812
813 let created = create(
814 &state,
815 CreateParams {
816 title: "Fresh task".to_string(),
817 objective: "No evidence yet".to_string(),
818 workspace_id: "default".to_string(),
819 session_id: None,
820 scope: None,
821 acceptance_criteria: None,
822 verification_commands: None,
823 test_cases: None,
824 dependencies: None,
825 parallel_group: None,
826 },
827 )
828 .await
829 .expect("task should create");
830 assert_eq!(
831 created.task["artifactSummary"]["total"],
832 serde_json::json!(0)
833 );
834 assert_eq!(
835 created.task["evidenceSummary"]["runs"]["latestStatus"],
836 serde_json::json!("idle")
837 );
838 assert_eq!(
839 created.task["storyReadiness"]["requiredTaskFields"],
840 serde_json::json!([])
841 );
842 }
843}