//! Tests that verify pattern analysis using the real catalog. use std::collections::HashSet; use std::fmt::Write; use brk_bindgen::ClientMetadata; use brk_types::TreeNode; /// Load the catalog from the JSON file. fn load_catalog() -> TreeNode { let path = concat!(env!("CARGO_MANIFEST_DIR"), "/catalog.json"); let catalog_json = std::fs::read_to_string(path).expect("Failed to read catalog.json"); serde_json::from_str(&catalog_json).expect("Failed to parse catalog.json") } /// Load OpenAPI spec from openapi.json. fn load_openapi_json() -> String { let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json"); std::fs::read_to_string(path).expect("Failed to read openapi.json") } /// Load metadata from the catalog. #[allow(unused)] fn load_metadata() -> ClientMetadata { ClientMetadata::from_catalog(load_catalog()) } /// Collect all leaf metric names from a tree. fn collect_leaf_names(node: &TreeNode, names: &mut HashSet) { match node { TreeNode::Leaf(leaf) => { names.insert(leaf.name().to_string()); } TreeNode::Branch(children) => { for child in children.values() { collect_leaf_names(child, names); } } } } #[test] fn test_catalog_loads() { let catalog = load_catalog(); // Should be a branch with top-level categories let TreeNode::Branch(categories) = &catalog else { panic!("Expected catalog to be a branch"); }; // Check some expected top-level categories exist assert!( categories.contains_key("addresses"), "Missing addresses category" ); assert!(categories.contains_key("blocks"), "Missing blocks category"); assert!(categories.contains_key("market"), "Missing market category"); assert!(categories.contains_key("supply"), "Missing supply category"); println!("Catalog has {} top-level categories", categories.len()); } #[test] fn test_all_leaves_have_names() { let catalog = load_catalog(); let mut names = HashSet::new(); collect_leaf_names(&catalog, &mut names); println!("Catalog has {} unique metric names", names.len()); assert!(!names.is_empty(), "Should have at least some metrics"); // All names should be non-empty for name in &names { assert!(!name.is_empty(), "Found empty metric name"); } } #[test] fn test_pattern_detection() { let catalog = load_catalog(); let (patterns, concrete_to_pattern, concrete_to_type_param, _node_bases) = brk_bindgen::detect_structural_patterns(&catalog); println!("Detected {} structural patterns", patterns.len()); println!( "Concrete to pattern mappings: {}", concrete_to_pattern.len() ); println!("Type parameter mappings: {}", concrete_to_type_param.len()); // Print pattern details for pattern in &patterns { let mode_str = match &pattern.mode { Some(brk_bindgen::PatternMode::Suffix { relatives }) => { format!("Suffix({})", relatives.len()) } Some(brk_bindgen::PatternMode::Prefix { prefixes }) => { format!("Prefix({})", prefixes.len()) } None => "None".to_string(), }; println!( " {} (fields: {}, generic: {}, mode: {})", pattern.name, pattern.fields.len(), pattern.is_generic, mode_str ); } // Should have detected some patterns assert!(!patterns.is_empty(), "Should detect at least some patterns"); // Check that parameterizable patterns have valid modes for pattern in &patterns { if pattern.is_parameterizable() { let mode = pattern.mode.as_ref().unwrap(); match mode { brk_bindgen::PatternMode::Suffix { relatives } => { assert_eq!( relatives.len(), pattern.fields.len(), "Pattern {} should have relative for each field", pattern.name ); } brk_bindgen::PatternMode::Prefix { prefixes } => { assert_eq!( prefixes.len(), pattern.fields.len(), "Pattern {} should have prefix for each field", pattern.name ); } } } } } #[test] fn test_cost_basis_pattern() { let catalog = load_catalog(); let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog); // Find CostBasisPattern2 and inspect it let cost_basis = patterns .iter() .find(|p| p.name == "CostBasisPattern2") .expect("CostBasisPattern2 should exist"); println!("CostBasisPattern2:"); println!( " Fields: {:?}", cost_basis .fields .iter() .map(|f| &f.name) .collect::>() ); println!(" Mode: {:?}", cost_basis.mode); println!(" Is generic: {}", cost_basis.is_generic); // With suffix naming convention (cost_basis_max, cost_basis_min, cost_basis): // // At root level: common prefix is "cost_basis_" -> suffix mode // max -> "max" // min -> "min" // percentiles -> "" (identity) // // At lth_ level: common prefix is "lth_cost_basis_" -> suffix mode // max -> "max" // min -> "min" // percentiles -> "" (identity) // // Both use suffix mode with same relatives, so pattern IS parameterizable! assert!( cost_basis.is_parameterizable(), "CostBasisPattern2 should be parameterizable with consistent suffix mode" ); } #[test] fn test_realized_pattern3_fields() { let catalog = load_catalog(); let metadata = ClientMetadata::from_catalog(catalog); let pattern = metadata .find_pattern("RealizedPattern3") .expect("RealizedPattern3 should exist"); println!("RealizedPattern3 fields:"); for field in &pattern.fields { let is_branch = field.is_branch(); let is_pattern = metadata.find_pattern(&field.rust_type).is_some(); let is_param = metadata.is_parameterizable(&field.rust_type); println!( " {} -> {} (branch={}, pattern={}, param={})", field.name, field.rust_type, is_branch, is_pattern, is_param ); } // Check if RealizedPattern3 is considered parameterizable println!( "\nRealizedPattern3 is_parameterizable (metadata): {}", metadata.is_parameterizable("RealizedPattern3") ); } #[test] fn test_parameterizable_patterns_have_mode() { let catalog = load_catalog(); let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog); // All patterns that appear 2+ times should either: // 1. Be parameterizable (have a mode) // 2. Or have inconsistent instances (mode = None) // // Patterns with mode = None should be inlined, not generate factories let parameterizable: Vec<_> = patterns.iter().filter(|p| p.is_parameterizable()).collect(); let non_parameterizable: Vec<_> = patterns .iter() .filter(|p| !p.is_parameterizable()) .collect(); println!("\nParameterizable patterns ({}):", parameterizable.len()); for p in ¶meterizable { let mode = p.mode.as_ref().unwrap(); let mode_type = match mode { brk_bindgen::PatternMode::Suffix { .. } => "Suffix", brk_bindgen::PatternMode::Prefix { .. } => "Prefix", }; println!(" {} ({} fields, {})", p.name, p.fields.len(), mode_type); } println!( "\nNon-parameterizable patterns ({}):", non_parameterizable.len() ); for p in &non_parameterizable { println!(" {} ({} fields)", p.name, p.fields.len()); } // Verify all parameterizable patterns have valid modes with all fields for pattern in ¶meterizable { let mode = pattern.mode.as_ref().unwrap(); let field_names: HashSet<_> = pattern.fields.iter().map(|f| f.name.clone()).collect(); match mode { brk_bindgen::PatternMode::Suffix { relatives } => { let mode_fields: HashSet<_> = relatives.keys().cloned().collect(); assert_eq!( field_names, mode_fields, "Pattern {} suffix mode should have all fields", pattern.name ); } brk_bindgen::PatternMode::Prefix { prefixes } => { let mode_fields: HashSet<_> = prefixes.keys().cloned().collect(); assert_eq!( field_names, mode_fields, "Pattern {} prefix mode should have all fields", pattern.name ); } } } } #[test] fn test_fee_rate_pattern_relatives() { let catalog = load_catalog(); let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog); let fee_rate_pattern = patterns .iter() .find(|p| p.name == "FeeRatePattern") .expect("FeeRatePattern should exist"); println!("FeeRatePattern mode:"); if let Some(mode) = &fee_rate_pattern.mode { match mode { brk_bindgen::PatternMode::Suffix { relatives } => { println!(" Suffix mode:"); for (field, relative) in relatives { println!(" {} -> '{}'", field, relative); } } brk_bindgen::PatternMode::Prefix { prefixes } => { println!(" Prefix mode:"); for (field, prefix) in prefixes { println!(" {} -> '{}'", field, prefix); } } } } else { println!(" No mode (not parameterizable)"); } // Check that relatives are correct - should be "average", "max", etc. // NOT "tx_weight_average", "tx_weight_max", etc. if let Some(brk_bindgen::PatternMode::Suffix { relatives }) = &fee_rate_pattern.mode { assert_eq!( relatives.get("average"), Some(&"average".to_string()), "average relative should be 'average', not 'tx_weight_average'" ); } } #[test] fn test_index_patterns() { let catalog = load_catalog(); let index_patterns = brk_bindgen::detect_index_patterns(&catalog); // println!("Used indexes: {:?}", used_indexes); println!("Index set patterns: {}", index_patterns.len()); for pattern in &index_patterns { println!(" {} -> {:?}", pattern.name, pattern.indexes); } // Should have detected some index patterns assert!(!index_patterns.is_empty(), "Should detect index patterns"); } #[test] fn test_generated_rust_output() { let catalog = load_catalog(); let metadata = ClientMetadata::from_catalog(catalog.clone()); // Collect all metric names from the catalog let mut all_metrics = HashSet::new(); collect_leaf_names(&catalog, &mut all_metrics); // Generate Rust client output let mut rust_output = String::new(); brk_bindgen::rust::client::generate_imports(&mut rust_output); brk_bindgen::rust::client::generate_base_client(&mut rust_output); brk_bindgen::rust::client::generate_metric_pattern_trait(&mut rust_output); brk_bindgen::rust::client::generate_endpoint(&mut rust_output); brk_bindgen::rust::client::generate_index_accessors( &mut rust_output, &metadata.index_set_patterns, ); brk_bindgen::rust::client::generate_pattern_structs( &mut rust_output, &metadata.structural_patterns, &metadata, ); brk_bindgen::rust::tree::generate_tree(&mut rust_output, &metadata.catalog, &metadata); brk_bindgen::rust::api::generate_main_client(&mut rust_output, &[]); // Count metrics that appear as direct string literals let mut direct_metrics = 0; for metric in &all_metrics { if rust_output.contains(&format!("\"{}\"", metric)) { direct_metrics += 1; } } println!("\nGenerated Rust output stats:"); println!(" Total metrics in catalog: {}", all_metrics.len()); println!(" Direct string literals: {}", direct_metrics); println!( " Via pattern factories: {}", all_metrics.len() - direct_metrics ); println!(" Output size: {} bytes", rust_output.len()); // Write output to test directory (not actual client) let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output"); std::fs::create_dir_all(output_dir).ok(); let output_path = format!("{}/rust_client.rs", output_dir); std::fs::write(&output_path, &rust_output).expect("Failed to write client output"); println!(" Wrote output to: {}", output_path); // Verify the output contains the key components assert!(rust_output.contains("fn _m("), "Should define _m helper"); assert!( rust_output.contains("pub struct MetricsTree"), "Should have MetricsTree" ); assert!( rust_output.contains("impl MetricsTree"), "Should have MetricsTree impl" ); // Count parameterizable patterns (these use _m for dynamic metric names) // Use metadata.is_parameterizable() for full recursive check let parameterizable_count = metadata .structural_patterns .iter() .filter(|p| metadata.is_parameterizable(&p.name)) .count(); println!(" Parameterizable patterns: {}", parameterizable_count); // Verify all pattern structs are generated (parameterizable and non) for pattern in &metadata.structural_patterns { assert!( rust_output.contains(&format!("pub struct {}", pattern.name)), "Missing pattern struct: {}", pattern.name ); } println!("\nGenerated Rust client is complete!"); } #[test] fn test_generated_javascript_output() { let catalog = load_catalog(); let metadata = ClientMetadata::from_catalog(catalog.clone()); // Collect all metric names from the catalog let mut all_metrics = HashSet::new(); collect_leaf_names(&catalog, &mut all_metrics); // Load schemas from OpenAPI spec only (catalog schemas require runtime data) let openapi_json = load_openapi_json(); let schemas = brk_bindgen::extract_schemas(&openapi_json); // Generate JavaScript client output let mut js_output = String::new(); writeln!(js_output, "// Auto-generated BRK JavaScript client").unwrap(); writeln!(js_output, "// Do not edit manually\n").unwrap(); brk_bindgen::javascript::types::generate_type_definitions(&mut js_output, &schemas); brk_bindgen::javascript::client::generate_base_client(&mut js_output); brk_bindgen::javascript::client::generate_index_accessors( &mut js_output, &metadata.index_set_patterns, ); brk_bindgen::javascript::client::generate_structural_patterns( &mut js_output, &metadata.structural_patterns, &metadata, ); brk_bindgen::javascript::tree::generate_tree_typedefs( &mut js_output, &metadata.catalog, &metadata, ); brk_bindgen::javascript::tree::generate_main_client( &mut js_output, &metadata.catalog, &metadata, &[], ); // Count metrics that appear as direct string literals let mut direct_metrics = 0; for metric in &all_metrics { if js_output.contains(&format!("'{}'", metric)) || js_output.contains(&format!("\"{}\"", metric)) { direct_metrics += 1; } } println!("\nGenerated JavaScript output stats:"); println!(" Total metrics in catalog: {}", all_metrics.len()); println!(" Direct string literals: {}", direct_metrics); println!( " Via pattern factories: {}", all_metrics.len() - direct_metrics ); println!(" Output size: {} bytes", js_output.len()); println!(" Output lines: {}", js_output.lines().count()); // Write output to test directory (not actual client) let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output"); std::fs::create_dir_all(output_dir).ok(); let output_path = format!("{}/js_client.js", output_dir); std::fs::write(&output_path, &js_output).expect("Failed to write JS client output"); println!(" Wrote output to: {}", output_path); // Verify the output contains key components assert!(js_output.contains("const _m ="), "Should define _m helper"); assert!(js_output.contains("const _p ="), "Should define _p helper"); assert!( js_output.contains("@typedef {Object} MetricsTree"), "Should have MetricsTree typedef" ); assert!( js_output.contains("class BrkClient"), "Should have BrkClient class" ); // Verify all pattern factories are generated for pattern in &metadata.structural_patterns { assert!( js_output.contains(&format!("function create{}(", pattern.name)), "Missing pattern factory: {}", pattern.name ); } println!("\nGenerated JavaScript client is complete!"); } #[test] fn test_generated_python_output() { let catalog = load_catalog(); let metadata = ClientMetadata::from_catalog(catalog.clone()); // Collect all metric names from the catalog let mut all_metrics = HashSet::new(); collect_leaf_names(&catalog, &mut all_metrics); // Load schemas from OpenAPI spec only (catalog schemas require runtime data) let openapi_json = load_openapi_json(); let schemas = brk_bindgen::extract_schemas(&openapi_json); // Generate Python client output let mut py_output = String::new(); writeln!(py_output, "# Auto-generated BRK Python client").unwrap(); writeln!(py_output, "# Do not edit manually\n").unwrap(); writeln!(py_output, "from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload").unwrap(); writeln!( py_output, "from http.client import HTTPSConnection, HTTPConnection" ) .unwrap(); writeln!(py_output, "from urllib.parse import urlparse").unwrap(); writeln!(py_output, "import json\n").unwrap(); writeln!(py_output, "T = TypeVar('T')\n").unwrap(); brk_bindgen::python::types::generate_type_definitions(&mut py_output, &schemas); brk_bindgen::python::client::generate_base_client(&mut py_output); brk_bindgen::python::client::generate_endpoint_class(&mut py_output); brk_bindgen::python::client::generate_index_accessors( &mut py_output, &metadata.index_set_patterns, ); brk_bindgen::python::client::generate_structural_patterns( &mut py_output, &metadata.structural_patterns, &metadata, ); brk_bindgen::python::tree::generate_tree_classes(&mut py_output, &metadata.catalog, &metadata); brk_bindgen::python::api::generate_main_client(&mut py_output, &[]); // Count metrics that appear as direct string literals let mut direct_metrics = 0; for metric in &all_metrics { if py_output.contains(&format!("'{}'", metric)) || py_output.contains(&format!("\"{}\"", metric)) { direct_metrics += 1; } } println!("\nGenerated Python output stats:"); println!(" Total metrics in catalog: {}", all_metrics.len()); println!(" Direct string literals: {}", direct_metrics); println!( " Via pattern factories: {}", all_metrics.len() - direct_metrics ); println!(" Output size: {} bytes", py_output.len()); println!(" Output lines: {}", py_output.lines().count()); // Write output to test directory (not actual client) let output_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/output"); std::fs::create_dir_all(output_dir).ok(); let output_path = format!("{}/python_client.py", output_dir); std::fs::write(&output_path, &py_output).expect("Failed to write Python client output"); println!(" Wrote output to: {}", output_path); // Verify the output contains key components assert!(py_output.contains("def _m("), "Should define _m helper"); assert!(py_output.contains("def _p("), "Should define _p helper"); assert!( py_output.contains("class MetricsTree:"), "Should have MetricsTree class" ); assert!( py_output.contains("class BrkClient"), "Should have BrkClient class" ); // Verify all pattern classes have constructors for pattern in &metadata.structural_patterns { assert!( py_output.contains(&format!("class {}:", pattern.name)) || py_output.contains(&format!("class {}(", pattern.name)), "Missing pattern class: {}", pattern.name ); } println!("\nGenerated Python client is complete!"); } #[test] fn test_cost_basis_relatives() { let catalog = load_catalog(); // Find cost_basis branches that have 3 direct children (max, min, percentiles) fn find_cost_basis_with_percentiles( node: &TreeNode, path: &str, ) -> Vec<(String, Vec<(String, String)>)> { let mut results = Vec::new(); if let TreeNode::Branch(children) = node { for (name, child) in children { let child_path = if path.is_empty() { name.clone() } else { format!("{}.{}", path, name) }; if name == "cost_basis" && let TreeNode::Branch(cb_children) = child && cb_children.contains_key("percentiles") { // Found a cost_basis with percentiles let mut metrics = Vec::new(); for (field_name, field_node) in cb_children { match field_node { TreeNode::Leaf(leaf) => { metrics.push((field_name.clone(), leaf.name().to_string())); } TreeNode::Branch(pct_children) => { // Get first percentile as example if let Some((_, TreeNode::Leaf(first))) = pct_children.iter().next() { metrics.push(( format!("{}.first", field_name), first.name().to_string(), )); } } } } results.push((child_path.clone(), metrics)); } results.extend(find_cost_basis_with_percentiles(child, &child_path)); } } results } let instances = find_cost_basis_with_percentiles(&catalog, ""); println!("\nCostBasisPattern2 instances (with percentiles):"); for (path, metrics) in instances.iter().take(10) { println!(" {}:", path); for (field, metric) in metrics { println!(" {} -> {}", field, metric); } } // Now compute what relatives the pattern detection would see // The key is: percentiles returns its BASE (common prefix of pct05, pct10, etc.) // not the individual percentile metrics use brk_bindgen::find_common_prefix; println!("\nComputing relatives (simulating branch base returns):"); for (path, metrics) in instances.iter().take(5) { println!(" Instance: {}", path); // For leaves (max, min), the base is the metric name // For branches (percentiles), the base is the common prefix of its children let mut child_bases: std::collections::HashMap = std::collections::HashMap::new(); for (field, metric) in metrics { if field.starts_with("percentiles.") { // This is a percentile metric - compute what the percentiles branch would return // The base is the metric name with the pct suffix stripped let base = metric .strip_suffix("_pct05") .or_else(|| metric.strip_suffix("_pct10")) .unwrap_or(metric) .to_string(); child_bases.insert("percentiles".to_string(), base); } else { child_bases.insert(field.clone(), metric.clone()); } } let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect(); println!(" Child bases:"); for (field, base) in &child_bases { println!(" {} -> {}", field, base); } if let Some(prefix) = find_common_prefix(&bases) { println!(" Common prefix: '{}'", prefix); for (field, base) in &child_bases { let relative = base.strip_prefix(&prefix).unwrap_or(base); println!(" {} -> relative '{}'", field, relative); } } else { println!(" No common prefix found!"); } } } #[test] fn test_debug_cost_basis_pattern2_mode() { // Debug why CostBasisPattern2 has mode=None let catalog = load_catalog(); let metadata = brk_bindgen::ClientMetadata::from_catalog(catalog.clone()); let pattern_lookup = metadata.pattern_lookup(); let pattern = metadata .find_pattern("CostBasisPattern2") .expect("CostBasisPattern2 should exist"); println!("\nCostBasisPattern2 fields:"); for field in &pattern.fields { println!(" {} (type: {})", field.name, field.rust_type); } println!("Mode: {:?}", pattern.mode); // Now debug the instance collection #[derive(Debug, Clone)] struct DebugInstanceAnalysis { base: String, field_parts: std::collections::HashMap, is_suffix_mode: bool, } fn collect_debug( node: &TreeNode, pattern_lookup: &std::collections::HashMap, String>, all_analyses: &mut std::collections::HashMap>, ) -> Option { match node { TreeNode::Leaf(leaf) => Some(leaf.name().to_string()), TreeNode::Branch(children) => { let mut child_bases: std::collections::HashMap = std::collections::HashMap::new(); for (field_name, child_node) in children { if let Some(base) = collect_debug(child_node, pattern_lookup, all_analyses) { child_bases.insert(field_name.clone(), base); } } if child_bases.is_empty() { return None; } // Analyze this instance let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect(); let (base, field_parts, is_suffix_mode) = if let Some(common_prefix) = brk_bindgen::find_common_prefix(&bases) { let base = common_prefix.trim_end_matches('_').to_string(); let mut parts = std::collections::HashMap::new(); for (field_name, child_base) in &child_bases { let relative = if *child_base == base { String::new() } else { child_base .strip_prefix(&common_prefix) .unwrap_or(child_base) .to_string() }; parts.insert(field_name.clone(), relative); } (base, parts, true) } else { let base = child_bases.values().next().cloned().unwrap_or_default(); let parts = child_bases .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); (base, parts, true) }; let analysis = DebugInstanceAnalysis { base: base.clone(), field_parts, is_suffix_mode, }; // Get the pattern name for this node let fields = brk_bindgen::get_node_fields(children, pattern_lookup); if let Some(pattern_name) = pattern_lookup.get(&fields) { all_analyses .entry(pattern_name.clone()) .or_default() .push(analysis); } Some(base) } } } let mut all_analyses: std::collections::HashMap> = std::collections::HashMap::new(); collect_debug(&catalog, &pattern_lookup, &mut all_analyses); if let Some(analyses) = all_analyses.get("CostBasisPattern2") { println!( "\nCollected {} instances of CostBasisPattern2:", analyses.len() ); for (i, a) in analyses.iter().enumerate() { println!(" Instance {}:", i); println!(" base: {}", a.base); println!(" is_suffix: {}", a.is_suffix_mode); println!(" field_parts:"); for (f, p) in &a.field_parts { println!(" {} -> '{}'", f, p); } } // Check consistency if analyses.len() >= 2 { let first = &analyses[0]; for (i, a) in analyses.iter().enumerate().skip(1) { if a.is_suffix_mode != first.is_suffix_mode { println!(" INCONSISTENT: Instance {} has different mode", i); } for (field, part) in &a.field_parts { if first.field_parts.get(field) != Some(part) { println!( " INCONSISTENT: Instance {} field '{}' has part '{}' vs '{}'", i, field, part, first .field_parts .get(field) .unwrap_or(&"".to_string()) ); } } } } } else { println!("\nNo instances collected for CostBasisPattern2!"); } } #[test] fn test_root_cost_basis_prefix() { use brk_bindgen::find_common_prefix; // Root-level cost_basis has: // max -> "max_cost_basis" // min -> "min_cost_basis" // percentiles -> "cost_basis" (base of pct05, pct10, etc.) let bases = vec!["max_cost_basis", "min_cost_basis", "cost_basis"]; let prefix = find_common_prefix(&bases); println!("Root cost_basis prefix: {:?}", prefix); // Compare with nested cost_basis let nested_bases = vec![ "utxos_at_least_15y_old_max_cost_basis", "utxos_at_least_15y_old_min_cost_basis", "utxos_at_least_15y_old_cost_basis", ]; let nested_prefix = find_common_prefix(&nested_bases); println!("Nested cost_basis prefix: {:?}", nested_prefix); } #[test] fn test_utxo_cohorts_all_activity_base() { // Test that distribution.utxo_cohorts.all.activity uses empty base // because its children (coinblocks_destroyed, coindays_destroyed, etc.) // have no common prefix or suffix. let catalog = load_catalog(); let metadata = ClientMetadata::from_catalog(catalog.clone()); // Generate JavaScript output let mut js_output = String::new(); writeln!(js_output, "// Test output").unwrap(); brk_bindgen::javascript::client::generate_base_client(&mut js_output); brk_bindgen::javascript::client::generate_index_accessors( &mut js_output, &metadata.index_set_patterns, ); brk_bindgen::javascript::client::generate_structural_patterns( &mut js_output, &metadata.structural_patterns, &metadata, ); brk_bindgen::javascript::tree::generate_tree_typedefs( &mut js_output, &metadata.catalog, &metadata, ); brk_bindgen::javascript::tree::generate_main_client( &mut js_output, &metadata.catalog, &metadata, &[], ); // The all.activity should use empty base, so metrics don't get duplicated // Look for: activity: createActivityPattern2(this, '') // NOT: activity: createActivityPattern2(this, 'coinblocks_destroyed') assert!( !js_output.contains("createActivityPattern2(this, 'coinblocks_destroyed')"), "all.activity should NOT use 'coinblocks_destroyed' as base (causes duplication)" ); // Check that it uses empty string as base assert!( js_output.contains("activity: createActivityPattern2(this, '')"), "all.activity should use empty base" ); println!("utxo_cohorts.all.activity base test passed!"); }