story-kit: merge 236_story_show_test_results_for_a_story_in_expanded_work_item
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::config::ProjectConfig;
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request, not_found};
|
||||
use crate::workflow::{StoryTestResults, TestCaseResult, TestStatus};
|
||||
use crate::worktree;
|
||||
use poem_openapi::{Object, OpenApi, Tags, param::Path, payload::Json};
|
||||
use serde::Serialize;
|
||||
@@ -69,6 +70,41 @@ struct WorkItemContentResponse {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
/// A single test case result for the OpenAPI response.
|
||||
#[derive(Object, Serialize)]
|
||||
struct TestCaseResultResponse {
|
||||
name: String,
|
||||
status: String,
|
||||
details: Option<String>,
|
||||
}
|
||||
|
||||
/// Response for the work item test results endpoint.
|
||||
#[derive(Object, Serialize)]
|
||||
struct TestResultsResponse {
|
||||
unit: Vec<TestCaseResultResponse>,
|
||||
integration: Vec<TestCaseResultResponse>,
|
||||
}
|
||||
|
||||
impl TestResultsResponse {
|
||||
fn from_story_results(results: &StoryTestResults) -> Self {
|
||||
Self {
|
||||
unit: results.unit.iter().map(Self::map_case).collect(),
|
||||
integration: results.integration.iter().map(Self::map_case).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_case(tc: &TestCaseResult) -> TestCaseResultResponse {
|
||||
TestCaseResultResponse {
|
||||
name: tc.name.clone(),
|
||||
status: match tc.status {
|
||||
TestStatus::Pass => "pass".to_string(),
|
||||
TestStatus::Fail => "fail".to_string(),
|
||||
},
|
||||
details: tc.details.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the story file exists in `work/5_done/` or `work/6_archived/`.
|
||||
///
|
||||
/// Used to exclude agents for already-archived stories from the `list_agents`
|
||||
@@ -326,6 +362,44 @@ impl AgentsApi {
|
||||
Err(not_found(format!("Work item not found: {}", story_id.0)))
|
||||
}
|
||||
|
||||
/// Get test results for a work item by its story_id.
|
||||
///
|
||||
/// Returns unit and integration test results. Checks in-memory workflow
|
||||
/// state first, then falls back to results persisted in the story file.
|
||||
#[oai(path = "/work-items/:story_id/test-results", method = "get")]
|
||||
async fn get_test_results(
|
||||
&self,
|
||||
story_id: Path<String>,
|
||||
) -> OpenApiResult<Json<Option<TestResultsResponse>>> {
|
||||
// Try in-memory workflow state first.
|
||||
let workflow = self
|
||||
.ctx
|
||||
.workflow
|
||||
.lock()
|
||||
.map_err(|e| bad_request(format!("Lock error: {e}")))?;
|
||||
|
||||
if let Some(results) = workflow.results.get(&story_id.0) {
|
||||
return Ok(Json(Some(TestResultsResponse::from_story_results(results))));
|
||||
}
|
||||
drop(workflow);
|
||||
|
||||
// Fall back to file-persisted results.
|
||||
let project_root = self
|
||||
.ctx
|
||||
.agents
|
||||
.get_project_root(&self.ctx.state)
|
||||
.map_err(bad_request)?;
|
||||
|
||||
let file_results = crate::http::workflow::read_test_results_from_story_file(
|
||||
&project_root,
|
||||
&story_id.0,
|
||||
);
|
||||
|
||||
Ok(Json(
|
||||
file_results.map(|r| TestResultsResponse::from_story_results(&r)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Remove a git worktree and its feature branch for a story.
|
||||
#[oai(path = "/agents/worktrees/:story_id", method = "delete")]
|
||||
async fn remove_worktree(&self, story_id: Path<String>) -> OpenApiResult<Json<bool>> {
|
||||
@@ -824,4 +898,113 @@ allowed_tools = ["Read", "Bash"]
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// --- get_test_results tests ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_test_results_returns_none_when_no_results() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = make_work_dirs(&tmp);
|
||||
let ctx = AppContext::new_test(root);
|
||||
let api = AgentsApi {
|
||||
ctx: Arc::new(ctx),
|
||||
};
|
||||
let result = api
|
||||
.get_test_results(Path("42_story_foo".to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_test_results_returns_in_memory_results() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = make_work_dirs(&tmp);
|
||||
let ctx = AppContext::new_test(root);
|
||||
|
||||
// Record test results in-memory.
|
||||
{
|
||||
let mut workflow = ctx.workflow.lock().unwrap();
|
||||
workflow
|
||||
.record_test_results_validated(
|
||||
"42_story_foo".to_string(),
|
||||
vec![crate::workflow::TestCaseResult {
|
||||
name: "unit_test_1".to_string(),
|
||||
status: crate::workflow::TestStatus::Pass,
|
||||
details: None,
|
||||
}],
|
||||
vec![crate::workflow::TestCaseResult {
|
||||
name: "int_test_1".to_string(),
|
||||
status: crate::workflow::TestStatus::Fail,
|
||||
details: Some("assertion failed".to_string()),
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let api = AgentsApi {
|
||||
ctx: Arc::new(ctx),
|
||||
};
|
||||
let result = api
|
||||
.get_test_results(Path("42_story_foo".to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.expect("should have test results");
|
||||
|
||||
assert_eq!(result.unit.len(), 1);
|
||||
assert_eq!(result.unit[0].name, "unit_test_1");
|
||||
assert_eq!(result.unit[0].status, "pass");
|
||||
assert!(result.unit[0].details.is_none());
|
||||
|
||||
assert_eq!(result.integration.len(), 1);
|
||||
assert_eq!(result.integration[0].name, "int_test_1");
|
||||
assert_eq!(result.integration[0].status, "fail");
|
||||
assert_eq!(
|
||||
result.integration[0].details.as_deref(),
|
||||
Some("assertion failed")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_test_results_falls_back_to_file_persisted_results() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
// Create work dirs including 2_current for the story file.
|
||||
for stage in &["1_upcoming", "2_current", "5_done", "6_archived"] {
|
||||
std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap();
|
||||
}
|
||||
|
||||
// Write a story file with persisted test results.
|
||||
let story_content = r#"---
|
||||
name: "Test story"
|
||||
---
|
||||
# Test story
|
||||
|
||||
## Test Results
|
||||
|
||||
<!-- story-kit-test-results: {"unit":[{"name":"from_file","status":"pass","details":null}],"integration":[]} -->
|
||||
"#;
|
||||
std::fs::write(
|
||||
root.join(".story_kit/work/2_current/42_story_foo.md"),
|
||||
story_content,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ctx = AppContext::new_test(root);
|
||||
let api = AgentsApi {
|
||||
ctx: Arc::new(ctx),
|
||||
};
|
||||
let result = api
|
||||
.get_test_results(Path("42_story_foo".to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.expect("should fall back to file results");
|
||||
|
||||
assert_eq!(result.unit.len(), 1);
|
||||
assert_eq!(result.unit[0].name, "from_file");
|
||||
assert_eq!(result.unit[0].status, "pass");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user