Files
IdeA/crates/infrastructure/tests/model_server.rs
Blomios 38aecf2cac feat(model-server): source Hugging Face (-hf) + options structurées llama.cpp + migration store V2
Backend du support natif d'une source modèle Hugging Face en alternative au
chemin .gguf local pour le serveur llama.cpp intégré, et refonte des options.

- domaine : ModelSource {LocalPath|HuggingFace} + HfModelRef,
  LlamaCppOptions {host,gpu_layers,context_size,jinja}, invariant auto_start
  exigeant une source, validate_free_args (rejet des flags réservés).
- infra : build_argv partagé avec build_spawn_spec, émission -hf vs --model,
  ordre argv figé ; migration model-servers.json V1 -> V2.
- app-tauri : DTO V2 (modelSource, compat modelPath, conflit = INVALID),
  commande preview_model_server_command.

QA : cargo test ciblés verts (domain 7, application 8, infra model_server 4,
app-tauri dto 5). Échecs des suites complètes infra/app-tauri environnementaux
(sandbox bind/socket), hors périmètre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 14:31:54 +02:00

192 lines
5.3 KiB
Rust

//! Infrastructure tests for local model-server adapters.
use std::path::PathBuf;
use std::sync::Arc;
use domain::model_server::{
ExecutablePath, HfModelRef, LlamaCppOptions, LocalModelRef, LocalModelServerConfig,
LocalModelServerKind, ModelPath, ModelServerEndpoint, ModelSource, StopPolicy,
};
use domain::ports::{FileSystem, ModelServerRegistry, ModelServerRuntime, RemotePath};
use domain::LocalModelServerId;
use infrastructure::{FsModelServerRegistry, LlamaCppRuntime, LocalFileSystem};
use uuid::Uuid;
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let path = std::env::temp_dir().join(format!("idea-model-server-{}", Uuid::new_v4()));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn app_data_dir(&self) -> String {
self.0.to_string_lossy().into_owned()
}
fn child(&self, name: &str) -> RemotePath {
RemotePath::new(self.0.join(name).to_string_lossy().into_owned())
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn sid(n: u128) -> LocalModelServerId {
LocalModelServerId::from_uuid(Uuid::from_u128(n))
}
fn config(id: LocalModelServerId, port: u16) -> LocalModelServerConfig {
let binary = std::env::current_exe()
.unwrap()
.to_string_lossy()
.into_owned();
LocalModelServerConfig::new(
id,
LocalModelServerKind::LlamaCpp,
"Local Qwen",
ModelServerEndpoint::new(format!("http://localhost:{port}/v1"), port).unwrap(),
LocalModelRef::new(
"qwen",
"Qwen",
Some(ModelSource::LocalPath {
path: ModelPath::new("/models/qwen.gguf").unwrap(),
}),
"qwen3-coder-30b",
)
.unwrap(),
Some(ExecutablePath::new(binary).unwrap()),
LlamaCppOptions::new("127.0.0.1", Some(35), Some(8192), true).unwrap(),
vec!["--threads".to_owned(), "8".to_owned()],
true,
StopPolicy::StopOnAppExit,
)
.unwrap()
}
#[test]
fn llamacpp_runtime_builds_structured_argv() {
let argv = LlamaCppRuntime::new()
.build_argv(&config(sid(1), 8080))
.unwrap();
assert_eq!(
argv.command,
std::env::current_exe().unwrap().to_string_lossy()
);
assert_eq!(
argv.args,
vec![
"--model",
"/models/qwen.gguf",
"--port",
"8080",
"--host",
"127.0.0.1",
"-ngl",
"35",
"-c",
"8192",
"--jinja",
"--threads",
"8"
]
);
}
#[test]
fn llamacpp_runtime_builds_hf_argv() {
let mut config = config(sid(3), 8083);
config.model.source = Some(ModelSource::HuggingFace {
repo: HfModelRef::new("Qwen/Qwen3-Coder:Q4_K_M").unwrap(),
});
config.options = LlamaCppOptions::default();
config.args = Vec::new();
let spec = LlamaCppRuntime::new().build_spawn_spec(&config).unwrap();
assert_eq!(
spec.args,
vec![
"-hf",
"Qwen/Qwen3-Coder:Q4_K_M",
"--port",
"8083",
"--host",
"127.0.0.1"
]
);
assert!(spec.context_plan.is_none());
}
#[tokio::test]
async fn fs_model_server_registry_roundtrips_global_json() {
let tmp = TempDir::new();
let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
let registry = FsModelServerRegistry::new(Arc::clone(&fs), tmp.app_data_dir());
let first = config(sid(2), 8081);
registry.save(first.clone()).await.unwrap();
assert_eq!(registry.get(&first.id).await.unwrap(), Some(first.clone()));
assert_eq!(registry.list().await.unwrap(), vec![first.clone()]);
assert!(
fs.exists(&tmp.child("model-servers.json")).await.unwrap(),
"registry must live in global app-data dir"
);
let updated = config(sid(2), 8082);
registry.save(updated.clone()).await.unwrap();
assert_eq!(registry.list().await.unwrap(), vec![updated]);
}
#[tokio::test]
async fn fs_model_server_registry_migrates_v1_json_to_v2() {
let tmp = TempDir::new();
let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
fs.create_dir_all(&RemotePath::new(tmp.app_data_dir()))
.await
.unwrap();
fs.write(
&tmp.child("model-servers.json"),
br#"{
"version": 1,
"servers": [{
"id": "00000000-0000-0000-0000-000000000004",
"kind": "llamaCpp",
"name": "Legacy Qwen",
"endpoint": { "baseURL": "http://localhost:8084/v1", "port": 8084 },
"model": {
"id": "qwen",
"label": "Qwen",
"path": "/models/qwen.gguf",
"servedName": "qwen3-coder"
},
"binary": "/usr/bin/llama-server",
"args": ["--ctx-size", "4096"],
"autoStart": true,
"stopPolicy": "stopOnAppExit"
}]
}"#,
)
.await
.unwrap();
let registry = FsModelServerRegistry::new(Arc::clone(&fs), tmp.app_data_dir());
let servers = registry.list().await.unwrap();
assert_eq!(servers.len(), 1);
assert_eq!(servers[0].options, LlamaCppOptions::default());
assert_eq!(servers[0].args, vec!["--ctx-size", "4096"]);
assert_eq!(
servers[0].model.source,
Some(ModelSource::LocalPath {
path: ModelPath::new("/models/qwen.gguf").unwrap()
})
);
}