feat(sprints): modèle de sprints — domaine, use-cases, persistance et surfaces (backend)
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>
This commit is contained in:
@ -52,6 +52,7 @@ use application::{
|
||||
OrchestratorService, TerminalSessions, UpdateAgentContext,
|
||||
};
|
||||
use infrastructure::orchestrator::mcp::jsonrpc::error_codes;
|
||||
use infrastructure::orchestrator::mcp::{TicketToolError, TicketToolProvider};
|
||||
use infrastructure::{
|
||||
InMemoryConversationRegistry, InMemoryMailbox, McpServer, MediatedInbox, MemoryTransport,
|
||||
SystemMillisClock,
|
||||
@ -459,6 +460,59 @@ fn result_text(result: &Value) -> &str {
|
||||
.expect("text content block")
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct FakeTicketTools {
|
||||
calls: Arc<Mutex<Vec<String>>>,
|
||||
mutation_attempts: Arc<Mutex<usize>>,
|
||||
sprints: Arc<Mutex<Vec<Value>>>,
|
||||
}
|
||||
|
||||
impl FakeTicketTools {
|
||||
fn seed_sprint(&self, order: u32, name: &str) {
|
||||
self.sprints.lock().unwrap().push(json!({
|
||||
"id": Uuid::new_v4().to_string(),
|
||||
"order": order,
|
||||
"name": name,
|
||||
"status": "planned",
|
||||
"ticketCount": 0,
|
||||
"version": 1
|
||||
}));
|
||||
}
|
||||
|
||||
fn calls(&self) -> Vec<String> {
|
||||
self.calls.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn mutation_attempts(&self) -> usize {
|
||||
*self.mutation_attempts.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TicketToolProvider for FakeTicketTools {
|
||||
async fn handle_ticket_tool(
|
||||
&self,
|
||||
_project: &Project,
|
||||
_requester: &str,
|
||||
name: &str,
|
||||
_arguments: Value,
|
||||
) -> Result<Value, TicketToolError> {
|
||||
self.calls.lock().unwrap().push(name.to_owned());
|
||||
match name {
|
||||
"idea_sprint_list" => Ok(json!({
|
||||
"items": self.sprints.lock().unwrap().clone()
|
||||
})),
|
||||
_ => {
|
||||
*self.mutation_attempts.lock().unwrap() += 1;
|
||||
Err(TicketToolError::new(
|
||||
"unexpectedMutation",
|
||||
format!("unexpected mutable ticket tool {name}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. tools/list catalogue
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -512,6 +566,7 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_sprint_list",
|
||||
] {
|
||||
assert!(
|
||||
names.contains(&expected),
|
||||
@ -521,8 +576,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
24,
|
||||
"exactly the twenty-four exposed idea_* tools; got {names:?}"
|
||||
25,
|
||||
"exactly the twenty-five exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
@ -696,6 +751,56 @@ async fn list_agents_returns_the_agents_inline_as_json_array() {
|
||||
assert!(names.contains(&"dev-backend"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sprint_list_tool_call_returns_seeded_sprints_read_only() {
|
||||
let (service, _s) = build_service(FakeContexts::new());
|
||||
let ticket_tools = Arc::new(FakeTicketTools::default());
|
||||
ticket_tools.seed_sprint(1, "Sprint Alpha");
|
||||
ticket_tools.seed_sprint(2, "Sprint Beta");
|
||||
let server = server(service).with_ticket_tools(ticket_tools.clone());
|
||||
|
||||
let raw = tools_call(41, "idea_sprint_list", json!({}));
|
||||
let response = server.handle_raw(&raw).await.expect("reply owed");
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"transport error: {:?}",
|
||||
response.error
|
||||
);
|
||||
let result = response.result.expect("result");
|
||||
assert_eq!(result["isError"], json!(false), "got {result}");
|
||||
|
||||
let payload: Value =
|
||||
serde_json::from_str(result_text(&result)).expect("sprint list payload is JSON");
|
||||
let items = payload["items"].as_array().expect("SprintListDto.items");
|
||||
assert_eq!(items.len(), 2, "got payload {payload}");
|
||||
assert_eq!(items[0]["order"], json!(1));
|
||||
assert_eq!(items[0]["name"], json!("Sprint Alpha"));
|
||||
assert_eq!(items[1]["order"], json!(2));
|
||||
assert_eq!(items[1]["name"], json!("Sprint Beta"));
|
||||
assert_eq!(ticket_tools.calls(), vec!["idea_sprint_list".to_owned()]);
|
||||
assert_eq!(
|
||||
ticket_tools.mutation_attempts(),
|
||||
0,
|
||||
"idea_sprint_list must be a read-only provider call"
|
||||
);
|
||||
|
||||
let raw = tools_call(42, "idea_sprint_create", json!({ "name": "Not exposed" }));
|
||||
let response = server.handle_raw(&raw).await.expect("reply owed");
|
||||
let error = response.error.expect("unknown sprint mutation expected");
|
||||
assert_eq!(error.code, error_codes::METHOD_NOT_FOUND);
|
||||
assert!(
|
||||
error.message.contains("unknown tool"),
|
||||
"message should name the cause; got {}",
|
||||
error.message
|
||||
);
|
||||
assert_eq!(
|
||||
ticket_tools.calls(),
|
||||
vec!["idea_sprint_list".to_owned()],
|
||||
"no sprint mutation tool should be routed to the provider"
|
||||
);
|
||||
assert_eq!(ticket_tools.mutation_attempts(), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. A failed IdeA command ⇒ isError: true (not a transport error, no panic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user