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:
2026-07-03 15:15:43 +02:00
38 changed files with 6539 additions and 45 deletions

View File

@ -0,0 +1,120 @@
use std::str::FromStr;
use domain::{
AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueError, IssueId, IssueLink,
IssueLinkKind, IssueNumber, IssuePriority, IssueRef, IssueStatus, IssueVersion, MarkdownDoc,
};
fn issue_number(n: u64) -> IssueNumber {
IssueNumber::new(n).unwrap()
}
fn issue() -> Issue {
Issue::new(
IssueId::new_random(),
issue_number(7),
"Fix boot reconcile",
MarkdownDoc::new("Description"),
IssueStatus::Open,
IssuePriority::Medium,
MarkdownDoc::new("Carnet"),
vec![IssueLink {
target: IssueRef::from(issue_number(8)),
kind: IssueLinkKind::RelatesTo,
}],
vec![AgentIssueRef {
agent_id: domain::AgentId::new_random(),
role: AgentIssueRole::Assigned,
}],
IssueActor::User,
1_000,
)
.unwrap()
}
#[test]
fn issue_ref_parses_and_formats_strict_hash_number() {
let reference = IssueRef::from_str("#42").unwrap();
assert_eq!(reference.number().get(), 42);
assert_eq!(reference.to_string(), "#42");
assert_eq!(
IssueRef::from_str("42"),
Err(IssueError::InvalidRef("42".into()))
);
assert_eq!(IssueRef::from_str("#0"), Err(IssueError::InvalidNumber));
assert_eq!(
IssueRef::from_str("#abc"),
Err(IssueError::InvalidRef("#abc".into()))
);
}
#[test]
fn issue_number_must_be_positive() {
assert_eq!(IssueNumber::new(0), Err(IssueError::InvalidNumber));
}
#[test]
fn issue_title_must_not_be_empty() {
let err = Issue::new(
IssueId::new_random(),
issue_number(1),
" ",
MarkdownDoc::new("Description"),
IssueStatus::Open,
IssuePriority::Medium,
MarkdownDoc::default(),
Vec::new(),
Vec::new(),
IssueActor::User,
1,
)
.unwrap_err();
assert_eq!(err, IssueError::EmptyTitle);
}
#[test]
fn issue_rejects_self_link() {
let err = Issue::new(
IssueId::new_random(),
issue_number(9),
"Self link",
MarkdownDoc::new("Description"),
IssueStatus::Open,
IssuePriority::Medium,
MarkdownDoc::default(),
vec![IssueLink {
target: IssueRef::from(issue_number(9)),
kind: IssueLinkKind::Blocks,
}],
Vec::new(),
IssueActor::User,
1,
)
.unwrap_err();
assert_eq!(
err,
IssueError::SelfLink {
reference: IssueRef::from(issue_number(9))
}
);
}
#[test]
fn issue_mutation_increments_version_and_updates_actor_time() {
let issue = issue();
assert_eq!(issue.version, IssueVersion::INITIAL);
let updated = issue
.mutate(IssueActor::System, 2_000, |i| {
i.status = IssueStatus::InProgress;
})
.unwrap();
assert_eq!(updated.version.get(), 2);
assert_eq!(updated.status, IssueStatus::InProgress);
assert_eq!(updated.updated_by, IssueActor::System);
assert_eq!(updated.updated_at, 2_000);
}