Ticket #10 — introduction du modèle de sprints côté backend. - Domaine : nouvel agrégat Sprint (sprint.rs), IDs, événements et invariants ; rattachement des issues à un sprint (issue.rs) et ports associés. - Application : use-cases sprints (application/src/sprints) + erreurs dédiées. - Infrastructure : store de sprints (infrastructure/src/sprints.rs), adaptation du store d'issues et exposition MCP via orchestrator/mcp/tickets.rs. - app-tauri : commandes, state et events pour piloter les sprints depuis l'UI. Tests domaine/application/infra/app-tauri verts (sprint_usecases, sprint_store, issue_store, mcp_server). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
3.5 KiB
Rust
128 lines
3.5 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use domain::{
|
|
IssueActor, ProjectPath, Sprint, SprintId, SprintOrder, SprintStatus, SprintStore,
|
|
SprintStoreError,
|
|
};
|
|
use infrastructure::FsSprintStore;
|
|
use uuid::Uuid;
|
|
|
|
struct TempDir(PathBuf);
|
|
|
|
impl TempDir {
|
|
fn new() -> Self {
|
|
let path = std::env::temp_dir().join(format!("idea-sprints-{}", Uuid::new_v4()));
|
|
std::fs::create_dir_all(&path).unwrap();
|
|
Self(path)
|
|
}
|
|
|
|
fn root(&self) -> ProjectPath {
|
|
ProjectPath::new(self.0.to_string_lossy().to_string()).unwrap()
|
|
}
|
|
|
|
fn child(&self, rel: &str) -> PathBuf {
|
|
self.0.join(rel)
|
|
}
|
|
}
|
|
|
|
impl Drop for TempDir {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
fn sid(n: u128) -> SprintId {
|
|
SprintId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
|
|
fn sprint(id: SprintId, order: u32, name: &str) -> Sprint {
|
|
Sprint::new(
|
|
id,
|
|
SprintOrder::new(order).unwrap(),
|
|
name,
|
|
None,
|
|
IssueActor::User,
|
|
1_000,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sprint_store_writes_markdown_doc_and_index() {
|
|
let tmp = TempDir::new();
|
|
let root = tmp.root();
|
|
let store = FsSprintStore::new();
|
|
let sprint = sprint(sid(1), 1, "Delivery");
|
|
|
|
store.create(&root, &sprint).await.unwrap();
|
|
|
|
let sprint_md =
|
|
std::fs::read_to_string(tmp.child(&format!(".ideai/sprints/{}/sprint.md", sprint.id)))
|
|
.unwrap();
|
|
let index = std::fs::read_to_string(tmp.child(".ideai/sprints/index.json")).unwrap();
|
|
|
|
assert!(sprint_md.starts_with("---\n"));
|
|
assert!(sprint_md.contains("name: \"Delivery\""));
|
|
assert!(index.contains("\"name\": \"Delivery\""));
|
|
|
|
let loaded = store.get(&root, sprint.id).await.unwrap();
|
|
assert_eq!(loaded.name, "Delivery");
|
|
assert_eq!(loaded.order.get(), 1);
|
|
assert_eq!(loaded.status, SprintStatus::Planned);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sprint_store_update_checks_expected_version() {
|
|
let tmp = TempDir::new();
|
|
let root = tmp.root();
|
|
let store = FsSprintStore::new();
|
|
let original = sprint(sid(2), 1, "Plan");
|
|
store.create(&root, &original).await.unwrap();
|
|
let updated = original
|
|
.clone()
|
|
.mutate(IssueActor::System, 2_000, |sprint| {
|
|
sprint.name = "Build".to_owned();
|
|
})
|
|
.unwrap();
|
|
|
|
let err = store
|
|
.update(&root, &updated, updated.version)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, SprintStoreError::VersionConflict { .. }));
|
|
|
|
store
|
|
.update(&root, &updated, original.version)
|
|
.await
|
|
.unwrap();
|
|
let loaded = store.get(&root, updated.id).await.unwrap();
|
|
assert_eq!(loaded.name, "Build");
|
|
assert_eq!(loaded.version.get(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sprint_store_lists_by_order_and_deletes() {
|
|
let tmp = TempDir::new();
|
|
let root = tmp.root();
|
|
let store = FsSprintStore::new();
|
|
let first = sprint(sid(10), 2, "Second");
|
|
let second = sprint(sid(11), 1, "First");
|
|
store.create(&root, &first).await.unwrap();
|
|
store.create(&root, &second).await.unwrap();
|
|
|
|
let rows = store.list(&root).await.unwrap();
|
|
assert_eq!(
|
|
rows.iter().map(|row| row.name.as_str()).collect::<Vec<_>>(),
|
|
vec!["First", "Second"]
|
|
);
|
|
|
|
store.delete(&root, first.id).await.unwrap();
|
|
assert!(matches!(
|
|
store.get(&root, first.id).await.unwrap_err(),
|
|
SprintStoreError::NotFound
|
|
));
|
|
let rows = store.list(&root).await.unwrap();
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(rows[0].id, second.id);
|
|
}
|