roder-dynamic-workflows 0.1.1

Agentic software development tools and SDKs for Roder.
Documentation
use std::fs;

use roder_api::dynamic_workflows::WorkflowRunLimits;
use roder_dynamic_workflows::{
    WorkflowCheckpoint, WorkflowCheckpointStore, WorkflowRunInput, WorkflowRuntimeErrorKind,
    WorkflowRuntimeOptions, WorkflowScriptRuntime, parse_workflow_definition,
};

fn runtime_with_limits(limits: WorkflowRunLimits) -> WorkflowScriptRuntime {
    WorkflowScriptRuntime::new(WorkflowRuntimeOptions {
        limits,
        ..WorkflowRuntimeOptions::default()
    })
}

#[test]
fn script_fixture_produces_report_with_ordered_fake_host_agents() {
    let runtime = WorkflowScriptRuntime::default();
    let execution = runtime
        .run(
            r#"
workflow.define({
  name: "audit",
  description: "Audit scoped files",
  argumentsSchema: { type: "object" },
  phases: ["Scout"],
  limits: { maxAgentsPerRun: 4 }
}, async (ctx) => {
  ctx.phase.start("Scout");
  const findings = await ctx.agents.reduce("scout", ["api", "core"], (target) => ({
    lane: "scout",
    description: `inspect ${target}`,
    prompt: `Inspect ${target}`,
    output: `finding:${target}`
  }), (acc, item) => acc.concat([item.output]), []);
  const vote = ctx.results.vote(findings);
  ctx.checkpoint.save("findings", findings);
  return ctx.report.markdown([vote.winner].concat(findings));
});
"#,
            WorkflowRunInput::new("run-1"),
        )
        .unwrap();

    assert_eq!(execution.definition.name, "audit");
    assert_eq!(execution.definition.phases, vec!["Scout"]);
    assert_eq!(execution.phases, vec!["Scout"]);
    assert_eq!(execution.agent_launches.len(), 2);
    assert_eq!(execution.agent_launches[0].input, "api");
    assert_eq!(execution.agent_launches[1].input, "core");
    assert_eq!(execution.report, "finding:api\nfinding:api\nfinding:core");
    assert_eq!(execution.checkpoints[0].key, "findings");
}

#[test]
fn script_parser_accepts_partial_limits_and_defaults_the_rest() {
    let options = WorkflowRuntimeOptions::default();
    let definition = parse_workflow_definition(
        r#"
workflow.define({
  name: "review",
  hostApiVersion: 1,
  limits: { maxAgentsPerRun: 2 }
}, () => "ok");
"#,
        &options,
    )
    .unwrap();

    assert_eq!(definition.name, "review");
    assert_eq!(definition.limits.max_agents_per_run, 2);
    assert_eq!(
        definition.limits.default_agent_timeout_seconds,
        WorkflowRunLimits::default().default_agent_timeout_seconds
    );
}

#[test]
fn ambient_api_usage_is_denied_before_execution() {
    let runtime = WorkflowScriptRuntime::default();
    let error = runtime
        .run(
            r#"
workflow.define({ name: "network" }, () => fetch("https://example.com"));
"#,
            WorkflowRunInput::new("run-ambient"),
        )
        .unwrap_err();

    assert_eq!(error.kind(), WorkflowRuntimeErrorKind::DeniedAmbientApi);
    assert!(error.message().contains("network access"));
}

#[test]
fn runtime_limit_stops_agent_launches_beyond_configured_cap() {
    let limits = WorkflowRunLimits {
        max_agents_per_run: 1,
        ..WorkflowRunLimits::default()
    };
    let runtime = runtime_with_limits(limits);
    let error = runtime
        .run(
            r#"
workflow.define({ name: "too-many" }, async (ctx) => {
  await ctx.agents.map("scout", ["one", "two"], (target) => ({ prompt: target }));
  return "unreachable";
});
"#,
            WorkflowRunInput::new("run-limit"),
        )
        .unwrap_err();

    assert_eq!(error.kind(), WorkflowRuntimeErrorKind::LimitExceeded);
    assert!(error.message().contains("maxAgentsPerRun"));
}

#[test]
fn abort_signal_stops_before_first_host_launch() {
    let runtime = WorkflowScriptRuntime::default();
    let mut input = WorkflowRunInput::new("run-abort");
    input.abort_before_start = true;

    let error = runtime
        .run(
            r#"
workflow.define({ name: "abortable" }, async (ctx) => {
  await ctx.agents.run("scout", { prompt: "should not launch" });
  return "unreachable";
});
"#,
            input,
        )
        .unwrap_err();

    assert_eq!(error.kind(), WorkflowRuntimeErrorKind::Aborted);
}

#[test]
fn checkpoint_store_persists_jsonl_records() {
    let root = std::env::temp_dir().join(format!(
        "roder-dynamic-workflow-store-{}",
        std::process::id()
    ));
    let _ = fs::remove_dir_all(&root);
    let store = WorkflowCheckpointStore::new(&root);
    let checkpoint = WorkflowCheckpoint {
        key: "phase-1".to_string(),
        value: serde_json::json!({ "complete": true }),
        byte_count: 17,
    };

    store.append_checkpoint("run-store", &checkpoint).unwrap();
    let loaded = store.read_checkpoints("run-store").unwrap();

    assert_eq!(loaded, vec![checkpoint]);
    fs::remove_dir_all(root).unwrap();
}