chantier: durcissement reprise headless inter-projets + persistance profils Opencode
- Implémentation reprise de session Opencode headless (conversational_recovery) - Persistance des profils IA Opencode dans .ideai/memory avec JSON Schema - Gestion des permissions MCP pour agents externes - Tests QA verts : structured_launch_d3, conversation_log, tickets_missing_carnet, agents, ProfilesSettings
This commit is contained in:
@ -72,8 +72,18 @@ impl TicketToolProvider for AppTicketToolProvider {
|
||||
.await
|
||||
.map_err(ticket_error)?
|
||||
.issue;
|
||||
let fallback_carnet = issue.carnet.as_str().to_owned();
|
||||
let carnet = if include_carnet {
|
||||
Some(read_carnet_body(&*self.read_carnet, project, issue_ref).await?)
|
||||
Some(
|
||||
read_ticket_carnet_body_best_effort(
|
||||
&*self.read_carnet,
|
||||
project,
|
||||
issue_ref,
|
||||
fallback_carnet,
|
||||
)
|
||||
.await
|
||||
.map_err(ticket_error)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@ -287,17 +297,17 @@ pub async fn ticket_read(
|
||||
.await
|
||||
.map_err(ErrorDto::from)?
|
||||
.issue;
|
||||
let fallback_carnet = issue.carnet.as_str().to_owned();
|
||||
let carnet = if request.include_carnet.unwrap_or(false) {
|
||||
Some(
|
||||
state
|
||||
.read_issue_carnet
|
||||
.execute(ReadIssueCarnetInput { project, issue_ref })
|
||||
.await
|
||||
.map_err(ErrorDto::from)?
|
||||
.carnet
|
||||
.carnet
|
||||
.as_str()
|
||||
.to_owned(),
|
||||
read_ticket_carnet_body_best_effort(
|
||||
&state.read_issue_carnet,
|
||||
&project,
|
||||
issue_ref,
|
||||
fallback_carnet,
|
||||
)
|
||||
.await
|
||||
.map_err(ErrorDto::from)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@ -680,6 +690,25 @@ async fn read_carnet_body(
|
||||
.map_err(ticket_error)
|
||||
}
|
||||
|
||||
async fn read_ticket_carnet_body_best_effort(
|
||||
read_carnet: &application::ReadIssueCarnet,
|
||||
project: &Project,
|
||||
issue_ref: IssueRef,
|
||||
fallback_carnet: String,
|
||||
) -> Result<String, AppError> {
|
||||
match read_carnet
|
||||
.execute(ReadIssueCarnetInput {
|
||||
project: project.clone(),
|
||||
issue_ref,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(out) => Ok(out.carnet.carnet.as_str().to_owned()),
|
||||
Err(AppError::NotFound(_)) => Ok(fallback_carnet),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn ticket_error(err: AppError) -> TicketToolError {
|
||||
let message = err.to_string();
|
||||
let code = if message.contains("issue version conflict") {
|
||||
|
||||
98
crates/app-tauri/tests/tickets_missing_carnet.rs
Normal file
98
crates/app-tauri/tests/tickets_missing_carnet.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use app_tauri_lib::state::AppState;
|
||||
use application::CreateProjectInput;
|
||||
use infrastructure::TicketToolProvider;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn temp_path(tag: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!("idea-app-tauri-test-{tag}-{}", Uuid::new_v4()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_ticket_read_is_best_effort_but_read_carnet_stays_strict_when_carnet_file_is_missing()
|
||||
{
|
||||
let state = AppState::build(temp_path("appdata"));
|
||||
let project_root = temp_path("project").to_string_lossy().into_owned();
|
||||
let project = state
|
||||
.create_project
|
||||
.execute(CreateProjectInput {
|
||||
name: "App Tauri Tickets".to_owned(),
|
||||
root: project_root,
|
||||
remote: None,
|
||||
default_profile_id: None,
|
||||
})
|
||||
.await
|
||||
.expect("test project is created")
|
||||
.project;
|
||||
|
||||
let created = state
|
||||
.ticket_tool_binder
|
||||
.handle_ticket_tool(
|
||||
&project,
|
||||
"qa-agent",
|
||||
"idea_ticket_create",
|
||||
json!({
|
||||
"title": "Missing carnet fallback",
|
||||
"description": "Created by app-tauri integration test",
|
||||
"priority": "high",
|
||||
"status": "open"
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("ticket create succeeds");
|
||||
let ticket_ref = created["ref"]
|
||||
.as_str()
|
||||
.expect("ticket ref is present")
|
||||
.to_owned();
|
||||
|
||||
let read = state
|
||||
.ticket_tool_binder
|
||||
.handle_ticket_tool(
|
||||
&project,
|
||||
"qa-agent",
|
||||
"idea_ticket_read",
|
||||
json!({ "ref": ticket_ref, "includeCarnet": true }),
|
||||
)
|
||||
.await
|
||||
.expect("ticket read succeeds");
|
||||
assert!(
|
||||
read["carnet"].is_string(),
|
||||
"ticket read returns a string carnet"
|
||||
);
|
||||
|
||||
std::fs::remove_file(
|
||||
PathBuf::from(project.root.as_str())
|
||||
.join(".ideai")
|
||||
.join("tickets")
|
||||
.join(ticket_ref.trim_start_matches('#'))
|
||||
.join("carnet.md"),
|
||||
)
|
||||
.expect("test removes ticket carnet");
|
||||
|
||||
let read_without_carnet_file = state
|
||||
.ticket_tool_binder
|
||||
.handle_ticket_tool(
|
||||
&project,
|
||||
"qa-agent",
|
||||
"idea_ticket_read",
|
||||
json!({ "ref": ticket_ref, "includeCarnet": true }),
|
||||
)
|
||||
.await
|
||||
.expect("ticket read stays best-effort");
|
||||
assert_eq!(read_without_carnet_file["ref"], ticket_ref);
|
||||
assert_eq!(read_without_carnet_file["carnet"], "");
|
||||
|
||||
let strict_err = state
|
||||
.ticket_tool_binder
|
||||
.handle_ticket_tool(
|
||||
&project,
|
||||
"qa-agent",
|
||||
"idea_ticket_read_carnet",
|
||||
json!({ "ref": ticket_ref }),
|
||||
)
|
||||
.await
|
||||
.expect_err("strict carnet read must fail when carnet.md is missing");
|
||||
assert_eq!(strict_err.code, "notFound");
|
||||
}
|
||||
Reference in New Issue
Block a user