use std::sync::atomic::{AtomicUsize, Ordering};
use ferro_json_ui::projection::{RenderMode, VisualContext};
use ferro_projections::render::BaseContext;
use ferro_projections::{derive_intents, DataType, FieldMeaning, Intent, ServiceDef};
use crate::memo::{memo_scope, with_memo_scope};
use crate::memoize;
static FETCH_COUNTER: AtomicUsize = AtomicUsize::new(0);
#[memoize]
async fn load_model_set(key: u32) -> Vec<String> {
FETCH_COUNTER.fetch_add(1, Ordering::SeqCst);
vec![format!("item-{key}-a"), format!("item-{key}-b")]
}
fn product_service() -> ServiceDef {
ServiceDef::new("product")
.display_name("Product")
.field("id", DataType::Integer, FieldMeaning::Identifier)
.field("name", DataType::String, FieldMeaning::EntityName)
.field("price", DataType::Float, FieldMeaning::Money)
.field("stock", DataType::Integer, FieldMeaning::Quantity)
}
#[tokio::test]
async fn render_path_single_fetch() {
FETCH_COUNTER.store(0, Ordering::SeqCst);
let service = product_service();
let intents = derive_intents(&service);
assert!(
!intents.is_empty(),
"product fixture must derive at least one intent"
);
let browse_idx = intents
.iter()
.position(|s| matches!(s.intent, Intent::Browse))
.expect("Browse must be present (baseline signal)");
let summarize_idx = intents
.iter()
.position(|s| matches!(s.intent, Intent::Summarize))
.expect("Summarize must be present (Money/Quantity field signals)");
let (browse_spec, summarize_spec) = with_memo_scope(memo_scope(), async {
let _data_for_browse = load_model_set(1).await;
let browse_ctx = VisualContext {
base: BaseContext {
intent_index: browse_idx,
..Default::default()
},
mode: RenderMode::Display,
templates: None,
};
let browse_spec = ferro_json_ui::Spec::from_service_def(&service, &intents, &browse_ctx)
.expect("Browse render must succeed");
let _data_for_summarize = load_model_set(1).await;
let summarize_ctx = VisualContext {
base: BaseContext {
intent_index: summarize_idx,
..Default::default()
},
mode: RenderMode::Display,
templates: None,
};
let summarize_spec =
ferro_json_ui::Spec::from_service_def(&service, &intents, &summarize_ctx)
.expect("Summarize render must succeed");
(browse_spec, summarize_spec)
})
.await;
assert_eq!(
browse_spec.schema, "ferro-json-ui/v2",
"Browse Spec must have the correct schema version"
);
assert!(
browse_spec.elements.contains_key(&browse_spec.root),
"Browse Spec must have a root element"
);
assert_eq!(
summarize_spec.schema, "ferro-json-ui/v2",
"Summarize Spec must have the correct schema version"
);
assert!(
summarize_spec.elements.contains_key(&summarize_spec.root),
"Summarize Spec must have a root element"
);
assert_eq!(
FETCH_COUNTER.load(Ordering::SeqCst),
1,
"N intents over one key must issue exactly one underlying fetch (SC-3)"
);
}