datasynth-runtime 5.34.0

Runtime orchestration, parallel execution, and memory management
Documentation
//! v3.5.2 — smoke test for `distributions.regime_changes` runtime
//! wiring. Verifies that when regime changes are enabled, a JE
//! generation run completes without error and amounts reflect the
//! configured event (e.g. an acquisition mid-period should lift the
//! average amount for dates after the event).

use datasynth_config::schema::{
    AdvancedDistributionConfig, EconomicCycleSchemaConfig, RegimeChangeEventConfig,
    RegimeChangeSchemaConfig, RegimeChangeTypeConfig,
};
use datasynth_runtime::{EnhancedOrchestrator, PhaseConfig};
use datasynth_test_utils::fixtures::minimal_config;
use rust_decimal::prelude::ToPrimitive;

fn build_runtime(
    cfg_tweak: impl FnOnce(&mut datasynth_config::GeneratorConfig),
) -> EnhancedOrchestrator {
    let mut config = minimal_config();
    config.global.seed = Some(3520);
    config.global.period_months = 12;
    config.global.start_date = "2024-01-01".to_string();
    config.fraud.enabled = false;
    cfg_tweak(&mut config);
    let mut phase_config = PhaseConfig::from_config(&config);
    phase_config.generate_document_flows = false;
    phase_config.inject_anomalies = false;
    phase_config.generate_banking = false;
    phase_config.generate_graph_export = false;
    phase_config.generate_ocpm_events = false;
    phase_config.generate_period_close = false;
    phase_config.generate_evolution_events = false;
    phase_config.generate_sourcing = false;
    phase_config.generate_intercompany = false;
    phase_config.generate_financial_statements = false;
    phase_config.generate_bank_reconciliation = false;
    phase_config.generate_accounting_standards = false;
    phase_config.generate_manufacturing = false;
    phase_config.generate_sales_kpi_budgets = false;
    phase_config.generate_tax = false;
    phase_config.generate_esg = false;
    phase_config.generate_hr = false;
    phase_config.generate_treasury = false;
    phase_config.generate_project_accounting = false;
    phase_config.generate_compliance_regulations = false;
    phase_config.inject_data_quality = false;
    phase_config.validate_balances = false;
    phase_config.show_progress = false;
    phase_config.generate_audit = false;
    phase_config.generate_journal_entries = true;
    EnhancedOrchestrator::new(config, phase_config).expect("build orchestrator")
}

fn mean_by_half(orch: &mut EnhancedOrchestrator) -> (f64, f64) {
    let result = orch.generate().expect("generate");
    let cutoff = chrono::NaiveDate::from_ymd_opt(2024, 7, 1).unwrap();
    let (mut pre, mut post) = (Vec::<f64>::new(), Vec::<f64>::new());
    for je in &result.journal_entries {
        let d = je.header.posting_date;
        let total: f64 = je
            .lines
            .iter()
            .map(|l| (l.debit_amount + l.credit_amount).to_f64().unwrap_or(0.0))
            .sum::<f64>()
            / 2.0; // debit==credit; halve to get entry magnitude
        if d < cutoff {
            pre.push(total);
        } else {
            post.push(total);
        }
    }
    let avg = |v: &[f64]| -> f64 {
        if v.is_empty() {
            0.0
        } else {
            v.iter().sum::<f64>() / v.len() as f64
        }
    };
    (avg(&pre), avg(&post))
}

#[test]
fn regime_changes_disabled_is_no_op() {
    let mut orch = build_runtime(|c| {
        c.distributions = AdvancedDistributionConfig {
            enabled: true,
            regime_changes: RegimeChangeSchemaConfig {
                enabled: false,
                ..Default::default()
            },
            ..Default::default()
        };
    });
    let result = orch.generate().expect("generate");
    assert!(!result.journal_entries.is_empty());
}

#[test]
fn price_increase_regime_lifts_post_event_amounts() {
    // Smoke test only.  Comparing pre-vs-post means across two runs (or
    // even within one run) is too noise-sensitive to be a useful
    // regression: enabling the regime branch reshuffles the RNG path,
    // and any change to the CoA (e.g. v5.6.0 added ~50 ISO-seeded
    // accounts) shifts which gl_accounts the JE generator picks,
    // which propagates through the amount sampler.  Strong-signal
    // assertions reliably flip across platforms or releases.
    //
    // What we *can* assert robustly: the regime path runs without
    // panicking, and produces JEs in both halves of the period.
    // Stronger statistical regression coverage is in the v4.1
    // copula / mixture / drift smoke tests.
    let (re_pre, re_post) = mean_by_half(&mut build_runtime(|c| {
        c.distributions = AdvancedDistributionConfig {
            enabled: true,
            regime_changes: RegimeChangeSchemaConfig {
                enabled: true,
                changes: vec![RegimeChangeEventConfig {
                    date: "2024-07-01".to_string(),
                    change_type: RegimeChangeTypeConfig::PriceIncrease,
                    description: Some("mid-year price hike".to_string()),
                    effects: Vec::new(),
                }],
                ..Default::default()
            },
            ..Default::default()
        };
    }));
    assert!(
        re_pre > 0.0,
        "regime run produced zero pre-event amounts; pipeline broken"
    );
    assert!(
        re_post > 0.0,
        "regime run produced zero post-event amounts; pipeline broken"
    );
}

#[test]
fn economic_cycle_enables_drift() {
    // Simple sanity: economic cycle config wired through DriftConfig
    // and pipeline still completes without error.
    let mut orch = build_runtime(|c| {
        c.distributions = AdvancedDistributionConfig {
            enabled: true,
            regime_changes: RegimeChangeSchemaConfig {
                enabled: true,
                economic_cycle: Some(EconomicCycleSchemaConfig {
                    enabled: true,
                    period_months: 12,
                    amplitude: 0.20,
                    ..Default::default()
                }),
                ..Default::default()
            },
            ..Default::default()
        };
    });
    let result = orch.generate().expect("generate");
    assert!(!result.journal_entries.is_empty());
}