feat(slash): fondation du système de commandes slash pour la CLI custom — #162 (QA verte)

Introduit le contrat unifié des commandes slash consommable par le frontend,
indépendant de la source (native, puis plugin plus tard) :

- domain: modèle pur SlashCommand / SlashCommandSource / SlashCommandAvailability,
  métadonnées UI (nom, description, disponibilité, confirmation requise) et
  validation des noms/préfixes.
- application: registry + list/filter par préfixe + plan d'exécution natif ;
  commandes natives /help et /clean (/clean = nettoyage de la vue de session
  courante ; pas de /reset distinct tant qu'aucune utilité produit ne le justifie).
- backend + app-tauri: exposition du contrat via DTO/transport + tests DTO.

Source neutre : les commandes contribuées par plugins (#165) transiteront par
le même registry sans changer le contrat frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 14:52:36 +02:00
parent 4e57161e1d
commit 6997138a71
11 changed files with 793 additions and 49 deletions

View File

@ -3,13 +3,19 @@
//! its tagged, camelCase JSON shape.
use app_tauri_lib::dto::{
parse_node_id, parse_session_id, ErrorDto, HealthRequestDto, HealthResponseDto, LayoutDto,
parse_node_id, parse_session_id, ErrorDto, ExecuteSlashCommandRequestDto,
ExecuteSlashCommandResponseDto, HealthRequestDto, HealthResponseDto, LayoutDto,
LayoutOperationDto, OpenTerminalRequestDto, ReattachResultDto, ResizeTerminalRequestDto,
TerminalClosedDto, WriteTerminalRequestDto,
SlashCommandEffectDto, SlashCommandListDto, TerminalClosedDto, WriteTerminalRequestDto,
};
use app_tauri_lib::events::{DomainEventDto, DOMAIN_EVENT};
use application::{CloseTerminalOutput, LayoutOperation, LoadLayoutOutput, OpenTerminalInput};
use domain::{Direction, LayoutNode, LayoutTree, LeafCell, NodeId, PreferredView};
use application::{
CloseTerminalOutput, ExecuteSlashCommandOutput, LayoutOperation, ListSlashCommandsOutput,
LoadLayoutOutput, OpenTerminalInput, SlashCommandEffect,
};
use domain::{
native_slash_commands, Direction, LayoutNode, LayoutTree, LeafCell, NodeId, PreferredView,
};
use application::{AppError, HealthInput};
use domain::events::DomainEvent;
@ -52,6 +58,73 @@ fn health_request_deserialises_camel_case_and_defaults() {
assert_eq!(HealthInput::from(empty).note, None);
}
#[test]
fn slash_command_list_dto_serialises_unified_metadata() {
let dto = SlashCommandListDto::from(ListSlashCommandsOutput {
commands: native_slash_commands(),
});
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(v["commands"][0]["name"], "/help");
assert_eq!(v["commands"][0]["source"], "native");
assert_eq!(v["commands"][0]["native"], "help");
assert_eq!(
v["commands"][0]["availability"],
json!({ "status": "available" })
);
assert_eq!(v["commands"][1]["name"], "/clean");
assert!(v["commands"][2]["requiresConfirmation"].as_bool().unwrap());
assert_eq!(v["commands"][2]["availability"]["status"], "unavailable");
assert!(v.get("requires_confirmation").is_none());
}
#[test]
fn slash_command_execute_request_deserialises_camelcase_session() {
let session_id = Uuid::from_u128(123).to_string();
let dto: ExecuteSlashCommandRequestDto =
serde_json::from_value(json!({ "name": "clean", "sessionId": session_id })).unwrap();
assert_eq!(dto.name, "clean");
assert_eq!(dto.session_id.as_deref(), Some(session_id.as_str()));
}
#[test]
fn slash_command_execute_response_serialises_effect_shape() {
let session_id = SessionId::from_uuid(Uuid::from_u128(456));
let command = native_slash_commands()
.into_iter()
.find(|command| command.name == "/clean")
.unwrap();
let dto = ExecuteSlashCommandResponseDto::from(ExecuteSlashCommandOutput {
command,
effect: SlashCommandEffect::CleanConversation { session_id },
});
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(v["command"]["name"], "/clean");
assert_eq!(
v["effect"],
json!({
"kind": "cleanConversation",
"sessionId": session_id.to_string(),
"clearedChunks": 0
})
);
assert!(v["effect"].get("cleared_chunks").is_none());
}
#[test]
fn slash_command_help_effect_carries_command_metadata() {
let dto = SlashCommandEffectDto::Help {
commands: native_slash_commands(),
};
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(v["kind"], "help");
assert_eq!(v["commands"][0]["name"], "/help");
assert_eq!(v["commands"][1]["name"], "/clean");
}
#[test]
fn error_dto_carries_stable_code_and_message() {
let dto = ErrorDto::from(AppError::NotFound("project".into()));