wasm4pm 26.7.1

High-performance process mining algorithms in WebAssembly for JavaScript/TypeScript
Documentation
//! Behavioral tests for spec-compliant Choice Graph cyclic loops,
//! start/end boundaries, and complex routing paths across playout,
//! footprints, and conformance.

use wasm4pm::powl::conformance::token_replay::replay_trace;
use wasm4pm::powl::conversion::to_petri_net;
use wasm4pm::powl::extensive_playout::{extensive_playout, ExtensivePlayoutConfig};
use wasm4pm::powl::footprints;
use wasm4pm::powl_arena::PowlArena;
use wasm4pm::powl_event_log::{Event, Trace};
use wasm4pm_compat::powl::{ChoiceGraph, StandaloneChoiceGraphNode};

fn trace_of(case: &str, acts: &[&str]) -> Trace {
    Trace {
        case_id: case.to_string(),
        events: acts
            .iter()
            .map(|a| Event {
                name: (*a).to_string(),
                timestamp: None,
                lifecycle: None,
                attributes: std::collections::BTreeMap::new(),
            })
            .collect(),
    }
}

fn replay_fitness(arena: &PowlArena, root: u32, trace: &Trace) -> f64 {
    let res = to_petri_net::apply(arena, root);
    let r = replay_trace(&res.net, &res.initial_marking, &res.final_marking, trace);
    r.fitness
}

fn get_models_trace_activities(trace: &wasm4pm::models::Trace) -> Vec<String> {
    trace
        .events
        .iter()
        .map(|ev| match ev.attributes.get("concept:name").unwrap() {
            wasm4pm::models::AttributeValue::String(s) => s.clone(),
            _ => panic!("Expected string value"),
        })
        .collect()
}

// ─────────────────────────────────────────────────────────────────────────────
// Test 1: Cyclic Loop Behavior
// ─────────────────────────────────────────────────────────────────────────────
#[test]
fn test_choice_graph_cyclic_loop_behavior() {
    use StandaloneChoiceGraphNode::*;
    // Start -> A <-> B -> End
    // Nodes: 0=Start, 1=A, 2=B, 3=End
    let nodes = vec![Start, Activity("A".into()), Activity("B".into()), End];
    let edges = vec![
        (0, 1), // Start -> A
        (1, 2), // A -> B
        (2, 1), // B -> A
        (2, 3), // B -> End
    ];
    let cg = ChoiceGraph::new(nodes, edges).unwrap();

    let mut arena = PowlArena::new();
    let root = arena.add_choice_graph(&cg);

    // 1. Playout
    let config = ExtensivePlayoutConfig {
        min_length: 1,
        max_length: 10,
        max_loops: 2,
        max_traces: 20,
    };
    let playout_res = extensive_playout(&arena, root, &config);
    assert!(
        !playout_res.traces.is_empty(),
        "Playout must generate traces"
    );

    // Check that we get expected acyclic sequence
    let mut found_acyclic = false;
    for tr in &playout_res.traces {
        let acts = get_models_trace_activities(tr);
        if acts == vec!["A", "B"] {
            found_acyclic = true;
        }
    }
    assert!(found_acyclic, "Should find ['A', 'B'] trace in playout");

    // 2. Footprints
    let fp = footprints::apply(&arena, root);
    assert!(fp.start_activities.contains("A"));
    assert!(fp.end_activities.contains("B"));
    assert!(fp.parallel.contains(&("A".to_string(), "B".to_string())));
    assert!(fp.parallel.contains(&("B".to_string(), "A".to_string())));

    // 3. Conformance
    let t1 = trace_of("t1", &["A", "B"]);
    let t2 = trace_of("t2", &["A", "B", "A", "B"]);
    let t_bad = trace_of("t_bad", &["B", "A"]);

    let f1 = replay_fitness(&arena, root, &t1);
    let f2 = replay_fitness(&arena, root, &t2);
    let f_bad = replay_fitness(&arena, root, &t_bad);

    // Due to remaining token at contested choice B (exit vs loop-back),
    // valid traces ending at B will have 1 remaining token at the end of replay.
    assert!(
        f1 > 0.89 && f1 < 0.91,
        "t1 fitness should be ~0.90, got {}",
        f1
    );
    assert!(
        f2 > 0.94 && f2 < 0.95,
        "t2 fitness should be ~0.94, got {}",
        f2
    );
    assert!(f_bad < 0.75, "t_bad fitness should be low, got {}", f_bad);
}

// ─────────────────────────────────────────────────────────────────────────────
// Test 2: Start / End Boundaries
// ─────────────────────────────────────────────────────────────────────────────
#[test]
fn test_choice_graph_start_end_boundaries() {
    use StandaloneChoiceGraphNode::*;
    let nodes = vec![Start, Activity("A".into()), Activity("B".into()), End];
    let edges = vec![
        (0, 1), // Start -> A
        (0, 2), // Start -> B
        (1, 3), // A -> End
        (2, 3), // B -> End
    ];
    let cg = ChoiceGraph::new(nodes, edges).unwrap();

    let mut arena = PowlArena::new();
    let root = arena.add_choice_graph(&cg);

    // 1. Playout
    let config = ExtensivePlayoutConfig {
        min_length: 1,
        max_length: 5,
        max_loops: 1,
        max_traces: 10,
    };
    let playout_res = extensive_playout(&arena, root, &config);
    let mut trace_acts: Vec<Vec<String>> = playout_res
        .traces
        .iter()
        .map(get_models_trace_activities)
        .collect();
    trace_acts.sort();
    assert_eq!(
        trace_acts,
        vec![vec!["A".to_string()], vec!["B".to_string()]]
    );

    // 2. Footprints
    let fp = footprints::apply(&arena, root);
    assert!(fp.start_activities.contains("A"));
    assert!(fp.start_activities.contains("B"));
    assert!(fp.end_activities.contains("A"));
    assert!(fp.end_activities.contains("B"));

    // 3. Conformance
    let t_a = trace_of("t_a", &["A"]);
    let t_b = trace_of("t_b", &["B"]);
    let t_ab = trace_of("t_ab", &["A", "B"]);

    assert!(replay_fitness(&arena, root, &t_a) >= 0.999);
    assert!(replay_fitness(&arena, root, &t_b) >= 0.999);
    assert!(replay_fitness(&arena, root, &t_ab) < 0.999);
}

// ─────────────────────────────────────────────────────────────────────────────
// Test 3: Complex Routing Paths
// ─────────────────────────────────────────────────────────────────────────────
#[test]
fn test_choice_graph_complex_routing_paths() {
    use StandaloneChoiceGraphNode::*;
    let nodes = vec![
        Start,
        Activity("Check".into()),
        Activity("Approve".into()),
        Activity("Reject".into()),
        Activity("Review".into()),
        End,
    ];
    let edges = vec![
        (0, 1), // Start -> Check
        (1, 2), // Check -> Approve
        (1, 3), // Check -> Reject
        (2, 5), // Approve -> End
        (3, 4), // Reject -> Review
        (4, 1), // Review -> Check (loop back)
        (4, 5), // Review -> End (direct exit)
    ];
    let cg = ChoiceGraph::new(nodes, edges).unwrap();

    let mut arena = PowlArena::new();
    let root = arena.add_choice_graph(&cg);

    // 1. Playout
    let config = ExtensivePlayoutConfig {
        min_length: 1,
        max_length: 15,
        max_loops: 2,
        max_traces: 50,
    };
    let playout_res = extensive_playout(&arena, root, &config);
    assert!(!playout_res.traces.is_empty());

    let mut found_simple = false;
    let mut found_reject_exit = false;

    for tr in &playout_res.traces {
        let acts = get_models_trace_activities(tr);
        if acts == vec!["Check", "Approve"] {
            found_simple = true;
        } else if acts == vec!["Check", "Reject", "Review"] {
            found_reject_exit = true;
        }
    }

    assert!(found_simple, "Should play out ['Check', 'Approve']");
    assert!(
        found_reject_exit,
        "Should play out ['Check', 'Reject', 'Review']"
    );

    // 2. Footprints
    let fp = footprints::apply(&arena, root);
    assert!(fp.start_activities.contains("Check"));
    assert!(fp.end_activities.contains("Approve"));
    assert!(fp.end_activities.contains("Review"));

    // 3. Conformance
    let t_simple = trace_of("t_simple", &["Check", "Approve"]);
    let t_reject_exit = trace_of("t_reject", &["Check", "Reject", "Review"]);
    let t_loop = trace_of("t_loop", &["Check", "Reject", "Review", "Check", "Approve"]);
    let t_bad = trace_of("t_bad", &["Check", "Review"]);

    let f_simple = replay_fitness(&arena, root, &t_simple);
    let f_reject = replay_fitness(&arena, root, &t_reject_exit);
    let f_loop = replay_fitness(&arena, root, &t_loop);
    let f_bad = replay_fitness(&arena, root, &t_bad);

    assert!(f_simple >= 0.999);
    // Ends with Review which has contested choice check vs end -> fitness ~0.928
    assert!(
        f_reject > 0.92 && f_reject < 0.93,
        "t_reject fitness should be ~0.928, got {}",
        f_reject
    );
    // Ends with Approve which has no contested exits -> fitness 1.0
    assert!(f_loop >= 0.999);
    assert!(f_bad < 0.85);
}