Merge branch 'feature/issue-ticket-system' into feature/background-tasks-first-class
Intègre le système de tickets/issues V1 (validé QA vert de bout en bout, mémoire tickets-v1-e2e-validated-qa) dans la branche des tâches de fond. Conflits résolus : app-tauri/state.rs, domain/events.rs, domain/lib.rs, domain/ports.rs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
163
crates/infrastructure/tests/issue_store.rs
Normal file
163
crates/infrastructure/tests/issue_store.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use domain::{
|
||||
AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueId, IssueListFilter,
|
||||
IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError,
|
||||
MarkdownDoc, ProjectPath,
|
||||
};
|
||||
use infrastructure::{FsIssueNumberAllocator, FsIssueStore};
|
||||
use uuid::Uuid;
|
||||
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> Self {
|
||||
let path = std::env::temp_dir().join(format!("idea-issues-{}", 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 issue(root: &ProjectPath, number: u64, title: &str) -> Issue {
|
||||
let _ = root;
|
||||
Issue::new(
|
||||
IssueId::new_random(),
|
||||
domain::IssueNumber::new(number).unwrap(),
|
||||
title,
|
||||
MarkdownDoc::new("Initial description"),
|
||||
IssueStatus::Open,
|
||||
IssuePriority::High,
|
||||
MarkdownDoc::new("Initial carnet"),
|
||||
Vec::new(),
|
||||
vec![AgentIssueRef {
|
||||
agent_id: domain::AgentId::new_random(),
|
||||
role: AgentIssueRole::Assigned,
|
||||
}],
|
||||
IssueActor::User,
|
||||
1_000,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issue_store_writes_markdown_docs_and_index() {
|
||||
let tmp = TempDir::new();
|
||||
let root = tmp.root();
|
||||
let store = FsIssueStore::new();
|
||||
let issue = issue(&root, 1, "Wire issues");
|
||||
|
||||
store.create(&root, &issue).await.unwrap();
|
||||
|
||||
let issue_md = std::fs::read_to_string(tmp.child(".ideai/tickets/1/issue.md")).unwrap();
|
||||
let carnet_md = std::fs::read_to_string(tmp.child(".ideai/tickets/1/carnet.md")).unwrap();
|
||||
let index = std::fs::read_to_string(tmp.child(".ideai/tickets/index.json")).unwrap();
|
||||
|
||||
assert!(issue_md.starts_with("---\n"));
|
||||
assert!(issue_md.contains("title: \"Wire issues\""));
|
||||
assert!(issue_md.ends_with("Initial description"));
|
||||
assert!(carnet_md.contains("issueRef: \"#1\""));
|
||||
assert!(carnet_md.ends_with("Initial carnet"));
|
||||
assert!(index.contains("\"issueRef\": \"#1\""));
|
||||
|
||||
let loaded = store
|
||||
.get_by_ref(&root, IssueRef::from_str("#1").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(loaded.title, "Wire issues");
|
||||
assert_eq!(loaded.carnet.as_str(), "Initial carnet");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issue_store_update_checks_expected_version() {
|
||||
let tmp = TempDir::new();
|
||||
let root = tmp.root();
|
||||
let store = FsIssueStore::new();
|
||||
let original = issue(&root, 2, "Conflict");
|
||||
store.create(&root, &original).await.unwrap();
|
||||
let updated = original
|
||||
.clone()
|
||||
.mutate(IssueActor::System, 2_000, |i| {
|
||||
i.status = IssueStatus::InProgress;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let err = store
|
||||
.update(&root, &updated, updated.version)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, IssueStoreError::VersionConflict { .. }));
|
||||
|
||||
store
|
||||
.update(&root, &updated, original.version)
|
||||
.await
|
||||
.unwrap();
|
||||
let loaded = store.get_by_ref(&root, updated.reference()).await.unwrap();
|
||||
assert_eq!(loaded.status, IssueStatus::InProgress);
|
||||
assert_eq!(loaded.version.get(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issue_store_lists_by_index_filters() {
|
||||
let tmp = TempDir::new();
|
||||
let root = tmp.root();
|
||||
let store = FsIssueStore::new();
|
||||
store
|
||||
.create(&root, &issue(&root, 1, "Alpha"))
|
||||
.await
|
||||
.unwrap();
|
||||
let closed = issue(&root, 2, "Beta")
|
||||
.mutate(IssueActor::System, 2_000, |i| {
|
||||
i.status = IssueStatus::Closed;
|
||||
i.priority = IssuePriority::Low;
|
||||
})
|
||||
.unwrap();
|
||||
store.create(&root, &closed).await.unwrap();
|
||||
|
||||
let rows = store
|
||||
.list(
|
||||
&root,
|
||||
IssueListFilter {
|
||||
status: Some(IssueStatus::Closed),
|
||||
priority: Some(IssuePriority::Low),
|
||||
..IssueListFilter::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].title, "Beta");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allocator_never_reuses_numbers() {
|
||||
let tmp = TempDir::new();
|
||||
let root = tmp.root();
|
||||
let allocator = FsIssueNumberAllocator::new();
|
||||
|
||||
let first = allocator.allocate_next(&root).await.unwrap();
|
||||
let second = allocator.allocate_next(&root).await.unwrap();
|
||||
let third = FsIssueNumberAllocator::new()
|
||||
.allocate_next(&root)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(first.get(), 1);
|
||||
assert_eq!(second.get(), 2);
|
||||
assert_eq!(third.get(), 3);
|
||||
}
|
||||
@ -500,6 +500,17 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
// Live-state tools (programme live-state, lot LS4).
|
||||
"idea_workstate_read",
|
||||
"idea_workstate_set",
|
||||
// Public ticket tools (Issue domain).
|
||||
"idea_ticket_create",
|
||||
"idea_ticket_read",
|
||||
"idea_ticket_list",
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
] {
|
||||
assert!(
|
||||
names.contains(&expected),
|
||||
@ -509,8 +520,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
13,
|
||||
"exactly the thirteen exposed idea_* tools; got {names:?}"
|
||||
23,
|
||||
"exactly the twenty-three exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
|
||||
Reference in New Issue
Block a user