aion-cli 0.31.0

The `aion` command line: operate Aion durable workflows over gRPC and run the Aion server.
//! The cross-crate pins for the workloop seam — the names and shapes the
//! COMPILER and the ENGINE must both know, asserted from the one crate that
//! depends on both.
//!
//! # 🔴 WHY HERE AND NOT IN EITHER HALF
//!
//! This whole feature was built as two halves that each reported "nothing to
//! reconcile", because a sweep run inside one half cannot see the other. The
//! compiler and the engine each carry their own copy of the retire entry's
//! name and arity — the compiler because `aion-awl` must not depend on the
//! engine, the engine because `aion-rs` must not depend on the compiler — and
//! nothing forced them to agree. A name known in two places with nothing
//! forcing agreement has already drifted; this is the crate where both are in
//! scope, so this is where the agreement is a test.

use aion_package::{
    CarryContract, InvariantContract, ReportContract, ToleranceContract, WorkloopContract,
};

type TestResult = Result<(), Box<dyn std::error::Error>>;

/// The compiler's exported retire entry IS the entry the engine probes for.
///
/// The world this comes out differently in: either side renaming its constant,
/// or the compiler emitting a different arity. The engine's existence probe
/// (`RuntimeHandle::module_exports_function`) matches BY NAME AT ANY ARITY, so
/// an arity drift would pass that probe and then fail typed at spawn,
/// mid-retirement, after the resident generation had already been stood down.
#[test]
fn the_compilers_retire_entry_is_the_entry_the_engine_probes_for() {
    assert_eq!(
        aion_awl::RETIRE_ENTRY,
        aion::workloop::RETIRE_ENTRY,
        "the compiler emits `{}` and the engine looks for `{}`; a workloop \
         compiled by this toolchain could not be retired by this engine",
        aion_awl::RETIRE_ENTRY,
        aion::workloop::RETIRE_ENTRY
    );
    assert_eq!(
        aion_awl::RETIRE_ARITY,
        aion::workloop::RETIRE_ARITY,
        "the retire entry's arity is load-bearing: the engine spawns it with \
         exactly one term (the loop's carry), and its existence probe matches \
         by name at ANY arity"
    );
}

/// A compiled workloop declaration builds an armable runtime spec.
///
/// The two records look alike and are not the same thing: the contract is a
/// transport shape whose fields are optional on the wire, and the spec is an
/// executable authority whose constructors refuse every shape that cannot be
/// acted on. This is the conversion between them, and it is the step that
/// makes a deployed `.awl` workloop start AS a workloop.
#[test]
fn a_compiled_workloop_declaration_converts_to_an_armable_spec() -> TestResult {
    let spec = aion::workloop::spec_from_contract("tick_watch", &contract())?;
    assert_eq!(
        spec.arming().cadence_period(),
        Some(std::time::Duration::from_secs(1_500)),
        "the declared cadence must reach the arming the sweep fires on"
    );
    assert_eq!(spec.arming().signals(), ["poke"]);
    assert_eq!(spec.retention(), std::time::Duration::from_secs(1_209_600));
    let invariant = spec
        .invariants()
        .first()
        .ok_or("the spec carries no invariant")?;
    assert_eq!(invariant.name, "serving");
    assert_eq!(invariant.record_type, "Board");
    assert_eq!(
        invariant.confirms,
        vec![String::from("start")],
        "the declared confirming route is what turns a taken route into a green \
         health sample; an invariant with none alarms at every window forever"
    );
    assert_eq!(invariant.tolerance.consecutive_windows(), Some(3));
    assert_eq!(
        spec.carry().defaults().get("seen"),
        Some(&serde_json::json!([])),
        "the declared carry default is what generation 1 is seeded from"
    );
    Ok(())
}

/// Every gap between the wire's optionality and the runtime's demands is
/// REFUSED with the declaration named, never defaulted.
///
/// Each of these is a shape the language itself refuses, so an archive
/// carrying one was built by a toolchain that did not check it — and the
/// engine must not paper over that by inventing a cadence nobody wrote or a
/// threshold nobody declared.
#[test]
fn a_declaration_the_engine_cannot_act_on_is_refused_by_name() -> TestResult {
    let mut unarmed = contract();
    unarmed.cadence_seconds = None;
    unarmed.arms.clear();
    let refusal = aion::workloop::spec_from_contract("tick_watch", &unarmed)
        .err()
        .ok_or("a loop nothing wakes was accepted")?
        .to_string();
    assert!(
        refusal.contains("tick_watch") && refusal.contains("neither a cadence nor a signal"),
        "the refusal must name the declaration and what is missing: {refusal}"
    );

    let mut untolerated = contract();
    untolerated.invariants[0].tolerances.clear();
    let refusal = aion::workloop::spec_from_contract("tick_watch", &untolerated)
        .err()
        .ok_or("an invariant with no tolerance was accepted")?
        .to_string();
    assert!(
        refusal.contains("serving") && refusal.contains("no tolerance"),
        "the refusal must name the INVARIANT, not just the loop: {refusal}"
    );

    let mut unconfirmable = contract();
    unconfirmable.invariants[0].confirms = None;
    let refusal = aion::workloop::spec_from_contract("tick_watch", &unconfirmable)
        .err()
        .ok_or("an invariant nothing can confirm was accepted")?
        .to_string();
    assert!(
        refusal.contains("serving") && refusal.contains("confirming route"),
        "the refusal must name the invariant and the missing clause: {refusal}"
    );

    let mut unretained = contract();
    unretained.retention_seconds = 0;
    assert!(
        aion::workloop::spec_from_contract("tick_watch", &unretained).is_err(),
        "a zero retention window prunes everything and must be refused"
    );

    // THE CONTROL, on the same reader: the unmutated declaration is accepted,
    // so the four refusals above are measuring the mutations rather than a
    // conversion that refuses everything.
    aion::workloop::spec_from_contract("tick_watch", &contract())?;
    Ok(())
}

/// The declaration the mutations start from: what `tick_watch.awl` compiles to.
fn contract() -> WorkloopContract {
    WorkloopContract {
        cadence_seconds: Some(1_500),
        arms: vec![String::from("poke")],
        carries: vec![CarryContract {
            name: String::from("seen"),
            schema: serde_json::json!({"type":"array","items":{"type":"string"}}),
            default: serde_json::json!([]),
        }],
        invariants: vec![InvariantContract {
            name: String::from("serving"),
            record_type: String::from("Board"),
            schema: serde_json::json!({"type":"object"}),
            tolerances: vec![ToleranceContract::Windows { count: 3 }],
            confirms: Some(String::from("start")),
        }],
        retention_seconds: 1_209_600,
        detached: Vec::new(),
        reports: vec![ReportContract {
            name: String::from("board"),
            schema: serde_json::json!({"type":"object"}),
        }],
        has_retire_body: true,
    }
}