feat(layout): supporte la création de layouts plugins (customPluginLayout)

Étend le flux de création de layout backend (DTO, usecases, store) et le
frontend (sélecteur, adaptateurs, LayoutTabs/LayoutGrid) pour permettre
d'ouvrir un layout déclaré par un plugin installé (ex. Android Health),
sans passer par le message bloquant "extension backend pas encore livrée".

Ticket #141 — QA vert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 18:34:21 +02:00
parent 168f93df78
commit 45617992ef
29 changed files with 907 additions and 99 deletions

View File

@ -10,8 +10,10 @@ use app_tauri_lib::dto::{
};
use application::{
CreateLayoutOutput, DeleteLayoutOutput, LayoutInfo, LayoutKind, ListLayoutsOutput,
PluginLayoutOrigin,
};
use domain::ids::{AgentId, LayoutId, NodeId};
use domain::{PluginId, PluginLayoutType};
use serde_json::json;
use uuid::Uuid;
@ -55,6 +57,24 @@ fn layout_info_dto_git_graph_kind() {
assert_eq!(v["kind"], "gitGraph");
}
#[test]
fn layout_info_dto_plugin_kind() {
let info = LayoutInfo {
id: lid(3),
name: "Android Health".to_owned(),
kind: LayoutKind::Plugin {
plugin_origin: PluginLayoutOrigin {
plugin_id: PluginId::new("dev.idea.android-plugin").unwrap(),
layout_type: PluginLayoutType::new("idea-android.health").unwrap(),
},
state: json!({}),
},
};
let dto = LayoutInfoDto::from(info);
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(v["kind"], "plugin");
}
// ---------------------------------------------------------------------------
// ListLayoutsDto
// ---------------------------------------------------------------------------
@ -138,6 +158,43 @@ fn create_layout_request_with_git_graph_kind() {
assert_eq!(kind, application::LayoutKind::GitGraph);
}
#[test]
fn create_layout_request_with_plugin_kind() {
let project_id = Uuid::from_u128(13).to_string();
let raw = json!({
"projectId": project_id,
"name": "Android Health",
"kind": "plugin",
"pluginOrigin": {
"pluginId": "dev.idea.android-plugin",
"layoutType": "idea-android.health"
},
"state": { "deviceId": "pixel-8" }
});
let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
let kind = dto.parse_kind().unwrap();
match kind {
application::LayoutKind::Plugin {
plugin_origin,
state,
} => {
assert_eq!(plugin_origin.plugin_id.as_str(), "dev.idea.android-plugin");
assert_eq!(plugin_origin.layout_type.as_str(), "idea-android.health");
assert_eq!(state["deviceId"], "pixel-8");
}
_ => panic!("expected plugin layout kind"),
}
}
#[test]
fn create_layout_request_plugin_kind_requires_origin() {
let project_id = Uuid::from_u128(14).to_string();
let raw = json!({ "projectId": project_id, "name": "Android Health", "kind": "plugin" });
let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
let err = dto.parse_kind().unwrap_err();
assert_eq!(err.code, "INVALID");
}
#[test]
fn create_layout_request_unknown_kind_is_invalid() {
let project_id = Uuid::from_u128(12).to_string();
@ -260,6 +317,30 @@ fn set_cell_agent_op_deserialises_with_absent_agent_defaults_to_none() {
}
}
// ---------------------------------------------------------------------------
// setPluginLayoutState operation deserialisation (#141)
// ---------------------------------------------------------------------------
#[test]
fn set_plugin_layout_state_op_deserialises_opaque_state() {
let target = nid(3);
let raw = json!({
"type": "setPluginLayoutState",
"target": target.to_string(),
"state": { "deviceId": "pixel-8", "healthy": true }
});
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
let op = dto.into_operation().unwrap();
match op {
application::LayoutOperation::SetPluginLayoutState { target: t, state } => {
assert_eq!(t, target);
assert_eq!(state["deviceId"], "pixel-8");
assert_eq!(state["healthy"], true);
}
_ => panic!("expected SetPluginLayoutState"),
}
}
// ---------------------------------------------------------------------------
// setCellConversation operation deserialisation (T4b)
// ---------------------------------------------------------------------------