Ajoute created_by sur IssueIndexEntry/TicketSummaryDto + filtre createdBy (#5), et les use cases BulkUpdateIssueStatus/Priority + BulkDeleteIssues avec les DTO/outils MCP idea_ticket_bulk_* associés (#6). NON VERT : BulkDeleteIssues::execute référence BatchIssueResult::deleted(), constructeur pas encore ajouté à BatchIssueResult (seuls ok/err existent) -> `cargo check` échoue (E0599 dans application::issues::mod). Reste à DevBackend avant toute QA/merge. Branché sur feature/sdk-integration (dépend du commit #2 dans orchestrator/mcp/server.rs et tools.rs) ; à rebaser sur develop une fois feature/sdk-integration mergé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -242,6 +242,11 @@ fn filter_matches(row: &IssueIndexEntry, filter: &IssueListFilter) -> bool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(created_by) = &filter.created_by {
|
||||
if &row.created_by != created_by {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if filter
|
||||
.sprint
|
||||
.is_some_and(|sprint| row.sprint != Some(sprint))
|
||||
|
||||
@ -693,15 +693,39 @@ impl McpServer {
|
||||
),
|
||||
));
|
||||
}
|
||||
if matches!(
|
||||
name,
|
||||
"idea_ticket_update"
|
||||
| "idea_ticket_update_status"
|
||||
| "idea_ticket_update_priority"
|
||||
| "idea_ticket_update_carnet"
|
||||
| "idea_ticket_link"
|
||||
| "idea_ticket_unlink"
|
||||
) {
|
||||
if is_ticket_policy_bulk_mutation_tool(name) {
|
||||
let refs = arguments
|
||||
.get("refs")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| {
|
||||
JsonRpcError::new(
|
||||
error_codes::INVALID_PARAMS,
|
||||
format!("tool `{name}` requires ticket refs under the active policy"),
|
||||
)
|
||||
})?;
|
||||
for raw_ref in refs {
|
||||
let raw_ref = raw_ref.as_str().ok_or_else(|| {
|
||||
JsonRpcError::new(
|
||||
error_codes::INVALID_PARAMS,
|
||||
format!(
|
||||
"tool `{name}` requires string ticket refs under the active policy"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let issue_ref = IssueRef::from_str(raw_ref).map_err(|e| {
|
||||
JsonRpcError::new(
|
||||
error_codes::INVALID_PARAMS,
|
||||
format!("invalid ticket ref: {e}"),
|
||||
)
|
||||
})?;
|
||||
if !policy.permits_ticket_mutation(name, issue_ref) {
|
||||
return Err(JsonRpcError::new(
|
||||
error_codes::INVALID_PARAMS,
|
||||
format!("tool `{name}` is not permitted for ticket {issue_ref}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if is_ticket_policy_mutation_tool(name) {
|
||||
let raw_ref = arguments
|
||||
.get("ref")
|
||||
.and_then(Value::as_str)
|
||||
@ -880,6 +904,25 @@ fn parse_ask_agents_requests(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn is_ticket_policy_mutation_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"idea_ticket_update"
|
||||
| "idea_ticket_update_status"
|
||||
| "idea_ticket_update_priority"
|
||||
| "idea_ticket_update_carnet"
|
||||
| "idea_ticket_link"
|
||||
| "idea_ticket_unlink"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_ticket_policy_bulk_mutation_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"idea_ticket_bulk_update_status" | "idea_ticket_bulk_update_priority"
|
||||
)
|
||||
}
|
||||
|
||||
/// Maps a [`ToolMapError`] to the right JSON-RPC error code.
|
||||
fn map_err_to_jsonrpc(err: ToolMapError) -> JsonRpcError {
|
||||
match err {
|
||||
|
||||
@ -60,6 +60,8 @@ pub fn is_ticket_tool(name: &str) -> bool {
|
||||
| "idea_ticket_update"
|
||||
| "idea_ticket_update_status"
|
||||
| "idea_ticket_update_priority"
|
||||
| "idea_ticket_bulk_update_status"
|
||||
| "idea_ticket_bulk_update_priority"
|
||||
| "idea_ticket_read_carnet"
|
||||
| "idea_ticket_update_carnet"
|
||||
| "idea_ticket_link"
|
||||
@ -74,6 +76,31 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
let ticket_ref = json!({ "type": "string", "pattern": "^#[1-9][0-9]*$" });
|
||||
let status = json!({ "type": "string", "enum": ["open", "inProgress", "QA", "closed"] });
|
||||
let priority = json!({ "type": "string", "enum": ["low", "medium", "high", "critical"] });
|
||||
let refs = json!({
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": ticket_ref.clone()
|
||||
});
|
||||
let created_by = json!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": { "kind": { "const": "user" } },
|
||||
"required": ["kind"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "const": "agent" },
|
||||
"agentId": { "type": "string", "format": "uuid" }
|
||||
},
|
||||
"required": ["kind", "agentId"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
});
|
||||
let sort = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -145,6 +172,7 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
"uniqueItems": true
|
||||
},
|
||||
"assignedAgentId": { "type": "string", "format": "uuid" },
|
||||
"createdBy": created_by,
|
||||
"text": { "type": "string" },
|
||||
"sort": sort.clone(),
|
||||
"limit": { "type": "integer", "minimum": 1 },
|
||||
@ -199,6 +227,34 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_bulk_update_status",
|
||||
description:
|
||||
"Update the status of several IdeA tickets and return one result per ticket.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refs": refs.clone(),
|
||||
"status": status.clone()
|
||||
},
|
||||
"required": ["refs", "status"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_bulk_update_priority",
|
||||
description:
|
||||
"Update the priority of several IdeA tickets and return one result per ticket.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refs": refs.clone(),
|
||||
"priority": priority.clone()
|
||||
},
|
||||
"required": ["refs", "priority"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_read_carnet",
|
||||
description: "Read the editable carnet attached to a ticket.",
|
||||
|
||||
@ -74,6 +74,8 @@ pub const WRITE_ACTION_TOOLS: &[&str] = &[
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_bulk_update_status",
|
||||
"idea_ticket_bulk_update_priority",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
|
||||
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use domain::{
|
||||
AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueId, IssueListFilter,
|
||||
AgentId, AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueId, IssueListFilter,
|
||||
IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError,
|
||||
MarkdownDoc, ProjectPath, SprintId,
|
||||
};
|
||||
@ -184,6 +184,60 @@ async fn issue_store_lists_by_index_filters_with_empty_or_and_and_semantics() {
|
||||
assert_eq!(by_status_and_priority[0].title, "Beta");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issue_store_filters_by_created_by_from_index() {
|
||||
let tmp = TempDir::new();
|
||||
let root = tmp.root();
|
||||
let store = FsIssueStore::new();
|
||||
let agent_id = AgentId::new_random();
|
||||
store
|
||||
.create(&root, &issue(&root, 1, "User ticket"))
|
||||
.await
|
||||
.unwrap();
|
||||
let agent_ticket = Issue::new(
|
||||
IssueId::new_random(),
|
||||
domain::IssueNumber::new(2).unwrap(),
|
||||
"Agent ticket",
|
||||
MarkdownDoc::new("Initial description"),
|
||||
IssueStatus::Open,
|
||||
IssuePriority::High,
|
||||
MarkdownDoc::new("Initial carnet"),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
IssueActor::Agent { agent_id },
|
||||
1_000,
|
||||
)
|
||||
.unwrap();
|
||||
store.create(&root, &agent_ticket).await.unwrap();
|
||||
|
||||
let by_user = store
|
||||
.list(
|
||||
&root,
|
||||
IssueListFilter {
|
||||
created_by: Some(IssueActor::User),
|
||||
..IssueListFilter::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let by_agent = store
|
||||
.list(
|
||||
&root,
|
||||
IssueListFilter {
|
||||
created_by: Some(IssueActor::Agent { agent_id }),
|
||||
..IssueListFilter::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(by_user.len(), 1);
|
||||
assert_eq!(by_user[0].title, "User ticket");
|
||||
assert_eq!(by_agent.len(), 1);
|
||||
assert_eq!(by_agent[0].title, "Agent ticket");
|
||||
assert_eq!(by_agent[0].created_by, IssueActor::Agent { agent_id });
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn issue_store_text_filter_matches_exact_ref_or_number_without_numeric_substring() {
|
||||
let tmp = TempDir::new();
|
||||
|
||||
@ -749,6 +749,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_bulk_update_status",
|
||||
"idea_ticket_bulk_update_priority",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_link",
|
||||
@ -767,11 +769,11 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
);
|
||||
}
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
31,
|
||||
"exactly the thirty-one exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
33,
|
||||
"exactly the thirty-three exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
for t in tools {
|
||||
@ -1361,11 +1363,21 @@ async fn bounded_ticket_mutation_tools_reject_other_issue_before_ticket_provider
|
||||
),
|
||||
(
|
||||
23,
|
||||
"idea_ticket_bulk_update_status",
|
||||
json!({ "refs": ["#7", "#8"], "status": "closed" }),
|
||||
),
|
||||
(
|
||||
24,
|
||||
"idea_ticket_bulk_update_priority",
|
||||
json!({ "refs": ["#8"], "priority": "high" }),
|
||||
),
|
||||
(
|
||||
25,
|
||||
"idea_ticket_link",
|
||||
json!({ "ref": "#8", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
(
|
||||
24,
|
||||
26,
|
||||
"idea_ticket_unlink",
|
||||
json!({ "ref": "#8", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
@ -1422,11 +1434,21 @@ async fn bounded_ticket_mutation_tools_allow_bound_issue_to_reach_ticket_provide
|
||||
),
|
||||
(
|
||||
33,
|
||||
"idea_ticket_bulk_update_status",
|
||||
json!({ "refs": ["#7"], "status": "closed" }),
|
||||
),
|
||||
(
|
||||
34,
|
||||
"idea_ticket_bulk_update_priority",
|
||||
json!({ "refs": ["#7"], "priority": "high" }),
|
||||
),
|
||||
(
|
||||
35,
|
||||
"idea_ticket_link",
|
||||
json!({ "ref": "#7", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
(
|
||||
34,
|
||||
36,
|
||||
"idea_ticket_unlink",
|
||||
json!({ "ref": "#7", "targetRef": "#9", "kind": "blocks", "expectedVersion": 1 }),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user