af-workflow 0.5.0

Spec-driven workflow chassis: typed node expressions composed into a branched DAG. Port of agent_core/workflow.
Documentation
//! End-to-end: compile the real `hello_world.json` builtin and run a cron tick
//! through it, asserting the branch-scoped state and terminal are correct.

use std::sync::Arc;

use af_workflow::{Event, MemoryState, NodeRegistry, Spec, State, Terminal, WorkflowHost};
use serde_json::json;

#[tokio::test]
async fn runs_hello_world_tick_end_to_end() {
    let raw = include_str!("fixtures/hello_world.json");
    let spec = Spec::from_json(raw).unwrap();
    spec.validate_structure().unwrap();

    let registry = NodeRegistry::with_builtins();
    let host = WorkflowHost::from_spec(&spec, &registry).expect("hello_world should compile");

    let state = Arc::new(MemoryState::new());
    let ctx = host.context(state.clone());

    // Simulate two cron firings.
    for ts in [1_000_u64, 2_000] {
        let event = Event::from_json(json!({ "fired_at": ts }));
        let outcome = host.run_event(&ctx, event).await.unwrap();
        // Both steps run and the sink completes the branch.
        assert_eq!(outcome.steps_run, 2);
        assert_eq!(outcome.terminal, Terminal::Completed);
        assert!(outcome.matched);
        assert!(outcome.succeeded);
    }

    // state_append wrote a branch-scoped sliding window of fired_at values.
    let ticks = state
        .get("__root__.ticks")
        .await
        .expect("ticks key should exist");
    assert_eq!(ticks, json!([1_000, 2_000]));
}

#[tokio::test]
async fn state_append_respects_max_len_over_many_ticks() {
    // hello_world caps the ring at max_len: 1000; assert the window logic with a
    // small inline spec so the test stays fast.
    let spec = Spec::from_json(
        r#"{
          "spec_id": "ring", "version": "1.0",
          "branches": [{ "branch_id": "__root__",
            "nodes": [
              {"id": "t", "type": "ingress.cron", "config": {}},
              {"id": "rec", "type": "transform.state_append",
               "config": {"path": "n", "key": "win", "max_len": 3}}
            ],
            "edges": [{"source": "t", "target": "rec"}]
          }]
        }"#,
    )
    .unwrap();

    let registry = NodeRegistry::with_builtins();
    let host = WorkflowHost::from_spec(&spec, &registry).unwrap();
    let state = Arc::new(MemoryState::new());
    let ctx = host.context(state.clone());

    for n in 0..5u64 {
        let outcome = host
            .run_event(&ctx, Event::from_json(json!({ "n": n })))
            .await;
        assert_eq!(outcome.unwrap().terminal, Terminal::Completed);
    }

    assert_eq!(state.get("__root__.win").await.unwrap(), json!([2, 3, 4]));
}