test(tahuantinsuyu): tests de integración del Shell con TestAppContext

Cubre los wiring points del binario que las unit tests por-crate no
ven: construcción end-to-end de Shell, selección de carta, derivación
de PipelineRequests y NatalOptions desde module_configs, y roundtrip
de layout via la tabla settings.

- `gpui` con `test-support` en dev-dependencies.
- 5 tests en `shell::tests`:
  * `shell_constructs_smoke` — instancia Shell con store in-memory
    sin panic. Cubre cableado de suscripciones (tree/panel/canvas
    + 2 splitters) y arranque del background loop del broker.
  * `select_chart_updates_current` — apply_selection(Chart(id))
    puebla `current_chart` y avanza `render_seq`.
  * `module_toggles_produce_requests` — al habilitar 3 módulos
    overlay, `build_requests` devuelve esos 3 PipelineRequest en
    orden; deshabilitar uno lo remueve.
  * `natal_options_read_from_configs` — orb_multiplier, show_minors,
    show_dignities se leen correctamente desde module_configs["natal"].
  * `split_flex_round_trip_via_store` — load/save_split_flex con
    settings, incluyendo defaults para valores corruptos o ≤0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-18 01:13:02 +00:00
parent e044d47516
commit d2b6b8b12e
2 changed files with 179 additions and 0 deletions
+175
View File
@@ -1016,3 +1016,178 @@ impl Render for Shell {
.child(body)
}
}
// =====================================================================
// Tests de integración del Shell
// =====================================================================
//
// Cubren los caminos que combinan lógica del shell con persistencia y
// el bridge real de eternal. Los tests puramente unitarios de cada
// crate (engine, store, modules) viven en sus respectivos `tests`
// modules; acá testeamos los wiring points del binario.
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use tahuantinsuyu_model::{
ChartKind, ContactId, StoredBirthData, StoredChartConfig,
};
fn sample_chart_for(_contact_id: ContactId) -> (StoredBirthData, StoredChartConfig) {
(
StoredBirthData {
year: 1987,
month: 3,
day: 14,
hour: 5,
minute: 22,
second: 0.0,
tz_offset_minutes: -240,
latitude_deg: 10.4806,
longitude_deg: -66.9036,
altitude_m: 900.0,
time_certainty: Default::default(),
subject_name: Some("Sergio".into()),
birthplace_label: Some("Caracas".into()),
},
StoredChartConfig::default(),
)
}
/// Smoke test: el Shell se construye sin panic con una store
/// in-memory. Cubre que las suscripciones cross-widget (tree, panel,
/// canvas, ambos splitters) se cablean sin colisiones y que el
/// background loop del brahman status arranca limpio.
#[gpui::test]
fn shell_constructs_smoke(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::install_default(cx);
let store = Store::in_memory().expect("in-memory store");
let _shell = cx.new(|cx| Shell::new(store, cx));
// Si llegamos acá sin panic, el cableado funciona.
});
}
/// La selección de una carta vía `apply_selection` (mismo pathway
/// que dispara el TreeEvent) puebla `current_chart` y arranca un
/// compute. El render asíncrono se resuelve después; verificamos
/// solo los efectos sincrónicos: chart cargada y `render_seq`
/// avanzado.
#[gpui::test]
fn select_chart_updates_current(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::install_default(cx);
let store = Store::in_memory().expect("store");
let group = store.create_group(None, "Test", None).unwrap();
let contact = store
.create_contact(Some(group.id), "Subject", None)
.unwrap();
let (birth, config) = sample_chart_for(contact.id);
let chart = store
.create_chart(contact.id, ChartKind::Natal, "Natal", &birth, &config, None)
.unwrap();
let shell = cx.new(|cx| Shell::new(store, cx));
shell.update(cx, |s, cx| {
s.apply_selection(TreeSelection::Chart(chart.id), cx);
});
shell.read_with(cx, |s, _| {
let cur = s.current_chart.as_ref().expect("current_chart set");
assert_eq!(cur.id, chart.id);
assert_eq!(cur.label, "Natal");
assert!(s.render_seq >= 1, "render_seq debió avanzar al menos a 1");
});
});
}
/// Toggleando un módulo overlay vía `module_configs` directamente
/// (simulando el efecto de un `PanelEvent::ControlChanged`), la
/// función `build_requests` debe reflejar el cambio.
#[gpui::test]
fn module_toggles_produce_requests(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::install_default(cx);
let store = Store::in_memory().expect("store");
let shell = cx.new(|cx| Shell::new(store, cx));
shell.update(cx, |s, _cx| {
// Sin módulos activos → no hay requests.
assert!(s.build_requests().is_empty());
set_module_enabled(&mut s.module_configs, "transit", true);
set_module_enabled(&mut s.module_configs, "midpoints", true);
set_module_enabled(&mut s.module_configs, "uranian", true);
let reqs = s.build_requests();
assert_eq!(reqs.len(), 3);
assert!(matches!(reqs[0], PipelineRequest::Transit));
assert!(matches!(reqs[1], PipelineRequest::Midpoints));
assert!(matches!(reqs[2], PipelineRequest::Uranian));
set_module_enabled(&mut s.module_configs, "transit", false);
let reqs = s.build_requests();
assert_eq!(reqs.len(), 2);
assert!(!reqs
.iter()
.any(|r| matches!(r, PipelineRequest::Transit)));
});
});
}
/// `NatalOptions` derivados de `module_configs["natal"]` deben
/// respetar orb_multiplier, show_minors y show_dignities cuando los
/// hay, y caer a defaults razonables cuando no.
#[gpui::test]
fn natal_options_read_from_configs(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::install_default(cx);
let store = Store::in_memory().expect("store");
let shell = cx.new(|cx| Shell::new(store, cx));
shell.update(cx, |s, _cx| {
let opts = s.build_natal_options();
assert!(opts.show_majors);
assert!(!opts.show_minors);
assert_eq!(opts.orb_multiplier, 1.0);
assert!(!opts.show_dignities);
s.module_configs.insert(
"natal".into(),
serde_json::json!({
"aspect_majors": true,
"aspect_minors": true,
"orb_multiplier": 1.75,
"show_dignities": true,
}),
);
let opts = s.build_natal_options();
assert!(opts.show_minors);
assert_eq!(opts.orb_multiplier, 1.75);
assert!(opts.show_dignities);
});
});
}
/// El flex de los splitters persiste entre instancias de Shell que
/// comparten la misma store (in-memory): primera shell escribe via
/// `save_split_flex`, segunda shell lee via `load_split_flex` al
/// boot.
#[test]
fn split_flex_round_trip_via_store() {
let store = Store::in_memory().expect("store");
// No hay nada persistido todavía: defaults.
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (1.0, 4.0));
store.set_setting("layout.x", "2.5,3.5").unwrap();
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (2.5, 3.5));
// Valor corrupto → defaults.
store.set_setting("layout.x", "garbage").unwrap();
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (1.0, 4.0));
// Valores ≤ 0 → defaults (los splitters tratan 0 como hidden).
store.set_setting("layout.x", "0,5").unwrap();
assert_eq!(load_split_flex(&store, "layout.x", 1.0, 4.0), (1.0, 4.0));
}
}