chore: monorepo inicial con arje + minga + yahweh absorbidos
Workspace en 4 ejes (core/modules/apps/shared):
- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial
Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.
cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
//! Smoke tests del CLI: init → ingest → status, todo persistido.
|
||||
|
||||
use std::fs;
|
||||
|
||||
use minga_cli::{cmd_ingest, cmd_init, cmd_status, CliError};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn init_creates_keypair_and_repo() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
let did = cmd_init(&repo, "passphrase-secreta").unwrap();
|
||||
|
||||
// El keypair existe en disco.
|
||||
assert!(repo.join("keypair").exists());
|
||||
// El repo sled existe (es un directorio).
|
||||
assert!(repo.join("repo").is_dir());
|
||||
// El DID retornado es no-trivial.
|
||||
assert_ne!(did, minga_core::Did([0u8; 32]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_refuses_existing_non_empty_directory() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
fs::create_dir(&repo).unwrap();
|
||||
fs::write(repo.join("garbage"), b"hello").unwrap();
|
||||
let r = cmd_init(&repo, "p");
|
||||
assert!(matches!(r, Err(CliError::AlreadyExists(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_shows_empty_state_after_init() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
cmd_init(&repo, "p").unwrap();
|
||||
let s = cmd_status(&repo, "p").unwrap();
|
||||
assert_eq!(s.mst_len, 0);
|
||||
assert_eq!(s.nodes_len, 0);
|
||||
assert_eq!(s.attestations_len, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_with_wrong_passphrase_errors() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
cmd_init(&repo, "correcta").unwrap();
|
||||
let r = cmd_status(&repo, "incorrecta");
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ingest_persists_function_with_self_attestation() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
let did = cmd_init(&repo, "p").unwrap();
|
||||
|
||||
// Escribir un archivo Rust de ejemplo.
|
||||
let src = dir.path().join("ejemplo.rs");
|
||||
fs::write(&src, "fn add(x: i32, y: i32) -> i32 { x + y }").unwrap();
|
||||
|
||||
let r = cmd_ingest(&repo, "p", &src).unwrap();
|
||||
assert_eq!(r.did, did, "la firma debe ser del repo, no de otro");
|
||||
|
||||
let s = cmd_status(&repo, "p").unwrap();
|
||||
assert_eq!(s.mst_len, 1);
|
||||
assert!(s.nodes_len > 1, "el AST tiene más de un nodo");
|
||||
assert_eq!(s.attestations_len, 1, "una autoatestación");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ingest_persists_across_runs() {
|
||||
// Simulamos "reiniciar el proceso": cmd_init en una llamada,
|
||||
// cmd_ingest en otra (que reabre el repo).
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
cmd_init(&repo, "p").unwrap();
|
||||
|
||||
let src1 = dir.path().join("uno.rs");
|
||||
fs::write(&src1, "fn one() -> i32 { 1 }").unwrap();
|
||||
cmd_ingest(&repo, "p", &src1).unwrap();
|
||||
|
||||
let src2 = dir.path().join("dos.rs");
|
||||
fs::write(&src2, "fn two() -> i32 { 2 }").unwrap();
|
||||
cmd_ingest(&repo, "p", &src2).unwrap();
|
||||
|
||||
let s = cmd_status(&repo, "p").unwrap();
|
||||
assert_eq!(s.mst_len, 2);
|
||||
assert_eq!(s.attestations_len, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ingest_same_file_twice_is_idempotent() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
cmd_init(&repo, "p").unwrap();
|
||||
|
||||
let src = dir.path().join("f.rs");
|
||||
fs::write(&src, "fn f() -> i32 { 42 }").unwrap();
|
||||
|
||||
let r1 = cmd_ingest(&repo, "p", &src).unwrap();
|
||||
let r2 = cmd_ingest(&repo, "p", &src).unwrap();
|
||||
assert_eq!(r1.hash, r2.hash);
|
||||
|
||||
let s = cmd_status(&repo, "p").unwrap();
|
||||
// El MST tiene 1 entrada (mismo hash). Atestaciones también: 1
|
||||
// por (autor, contenido) — idempotente.
|
||||
assert_eq!(s.mst_len, 1);
|
||||
assert_eq!(s.attestations_len, 1);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Tests del file watcher: el "puente humano" que convierte Minga en
|
||||
//! un VCS de fondo — el usuario edita archivos y Minga los versiona
|
||||
//! sin acción explícita.
|
||||
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use minga_cli::{cmd_init, cmd_status, cmd_watch};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Espera hasta que el `cmd_status` reporte `expected` claves en MST,
|
||||
/// o hasta `timeout`. Devuelve `true` si se alcanzó la cuenta.
|
||||
async fn wait_until_mst_size(
|
||||
repo: &std::path::Path,
|
||||
pass: &str,
|
||||
expected: usize,
|
||||
timeout: Duration,
|
||||
) -> bool {
|
||||
let deadline = std::time::Instant::now() + timeout;
|
||||
loop {
|
||||
if let Ok(s) = cmd_status(repo, pass) {
|
||||
if s.mst_len >= expected {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(80)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_initial_scan_picks_up_existing_files() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
let watch = dir.path().join("src");
|
||||
fs::create_dir(&watch).unwrap();
|
||||
cmd_init(&repo, "p").unwrap();
|
||||
|
||||
// Escribimos archivos ANTES de arrancar el watcher.
|
||||
fs::write(watch.join("a.rs"), "fn a() -> i32 { 1 }").unwrap();
|
||||
fs::write(watch.join("b.rs"), "fn b() -> i32 { 2 }").unwrap();
|
||||
|
||||
// Arrancamos el watcher en una task. La pasada inicial debería
|
||||
// ingerir ambos.
|
||||
let repo_clone = repo.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = cmd_watch(&repo_clone, "p", &watch).await;
|
||||
});
|
||||
|
||||
// Damos margen para la pasada inicial. cmd_watch tiene el repo
|
||||
// abierto, pero cmd_status no puede mientras tanto (sled lock).
|
||||
// Solución: cancelamos el watcher antes de medir.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
|
||||
let s = cmd_status(&repo, "p").unwrap();
|
||||
assert_eq!(s.mst_len, 2, "esperaba 2 funciones del initial scan");
|
||||
assert_eq!(s.attestations_len, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_ingests_new_file_after_creation() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
let watch = dir.path().join("src");
|
||||
fs::create_dir(&watch).unwrap();
|
||||
cmd_init(&repo, "p").unwrap();
|
||||
|
||||
// Watcher arranca con directorio vacío.
|
||||
let repo_clone = repo.clone();
|
||||
let watch_clone = watch.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = cmd_watch(&repo_clone, "p", &watch_clone).await;
|
||||
});
|
||||
|
||||
// Margen para que el watcher se inicialice y registre con notify.
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Creamos un archivo. notify debería emitir un evento y el
|
||||
// watcher debería ingerirlo.
|
||||
fs::write(watch.join("new.rs"), "fn new() -> i32 { 42 }").unwrap();
|
||||
|
||||
// Esperamos a que el evento se procese.
|
||||
tokio::time::sleep(Duration::from_millis(800)).await;
|
||||
|
||||
// Detenemos el watcher para liberar el lock de sled antes de
|
||||
// hacer cmd_status.
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
|
||||
// Polling con timeout — algunos sistemas de archivos tienen
|
||||
// latencia de eventos.
|
||||
assert!(
|
||||
wait_until_mst_size(&repo, "p", 1, Duration::from_secs(3)).await,
|
||||
"el watcher no ingirió el archivo creado",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_ignores_non_rs_files() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
let watch = dir.path().join("src");
|
||||
fs::create_dir(&watch).unwrap();
|
||||
cmd_init(&repo, "p").unwrap();
|
||||
|
||||
// Pre-poblamos con un .rs y varios archivos no-Rust.
|
||||
fs::write(watch.join("real.rs"), "fn real() -> i32 { 0 }").unwrap();
|
||||
fs::write(watch.join("readme.md"), "# proyecto").unwrap();
|
||||
fs::write(watch.join("data.json"), "{}").unwrap();
|
||||
|
||||
let repo_clone = repo.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = cmd_watch(&repo_clone, "p", &watch).await;
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
|
||||
let s = cmd_status(&repo, "p").unwrap();
|
||||
assert_eq!(s.mst_len, 1, "solo el .rs debe haberse ingerido");
|
||||
}
|
||||
Reference in New Issue
Block a user