streamweave-attractor 0.3.0

Attractor pipeline as a StreamWeave graph
Documentation
//! Tests for `runner`.

use crate::agent_run::{
  build_prompt_with_context, format_context_for_prompt, parse_agent_json_response, run_agent,
};
use std::collections::HashMap;

#[test]
fn format_context_for_prompt_empty() {
  let ctx: HashMap<String, String> = HashMap::new();
  assert_eq!(format_context_for_prompt(&ctx), "");
}

#[test]
fn format_context_for_prompt_includes_keys_sorted() {
  let mut ctx: HashMap<String, String> = HashMap::new();
  ctx.insert("b".to_string(), "B".to_string());
  ctx.insert("a".to_string(), "A".to_string());
  let out = format_context_for_prompt(&ctx);
  assert!(out.starts_with("Context from previous steps:"));
  assert!(out.find("a:\nA").unwrap_or(0) < out.find("b:\nB").unwrap_or(0));
}

#[test]
fn build_prompt_with_context_empty_context_returns_static() {
  let ctx: HashMap<String, String> = HashMap::new();
  assert_eq!(build_prompt_with_context("Do X.", &ctx), "Do X.");
}

#[test]
fn build_prompt_with_context_prepends_context_block() {
  let mut ctx: HashMap<String, String> = HashMap::new();
  ctx.insert("task".to_string(), "Implement Y.".to_string());
  let out = build_prompt_with_context("Do it.", &ctx);
  assert!(out.starts_with("Context from previous steps:"));
  assert!(out.contains("Do it."));
  assert!(out.contains("Implement Y."));
}

#[test]
fn parse_agent_json_response_success() {
  let out =
    parse_agent_json_response(r#"{"outcome":"success","context_updates":{"k":"v"}}"#).unwrap();
  assert!(matches!(out.status, crate::types::OutcomeStatus::Success));
  assert_eq!(out.context_updates.get("k").map(String::as_str), Some("v"));
}

#[test]
fn parse_agent_json_response_fail() {
  let out = parse_agent_json_response(r#"{"outcome":"fail","context_updates":{}}"#).unwrap();
  assert!(matches!(out.status, crate::types::OutcomeStatus::Error));
}

#[test]
fn parse_agent_json_response_partial_success() {
  let out =
    parse_agent_json_response(r#"{"outcome":"partial_success","context_updates":{"key":"value"}}"#)
      .unwrap();
  assert!(matches!(out.status, crate::types::OutcomeStatus::Success));
  assert_eq!(
    out.context_updates.get("key").map(String::as_str),
    Some("value")
  );
}

#[test]
fn parse_agent_json_response_invalid_json() {
  assert!(parse_agent_json_response("not json").is_err());
}

#[test]
fn parse_agent_json_response_invalid_outcome() {
  assert!(parse_agent_json_response(r#"{"outcome":"unknown","context_updates":{}}"#).is_err());
}

#[test]
fn parse_agent_json_response_empty() {
  assert!(parse_agent_json_response("").is_err());
  assert!(parse_agent_json_response("   ").is_err());
}

/// run_agent with a command that prints valid success JSON returns parsed outcome (exercises logging path).
#[test]
fn run_agent_returns_parsed_outcome_when_agent_prints_success_json() {
  let dir = tempfile::tempdir().unwrap();
  let path = dir.path().join("out.json");
  std::fs::write(
    &path,
    r#"{"outcome":"success","context_updates":{"k":"v"}}"#,
  )
  .unwrap();
  let cmd = format!("cat {}", path.display());
  let outcome = run_agent(&cmd, "prompt", Some(dir.path()));
  assert!(matches!(
    outcome.status,
    crate::types::OutcomeStatus::Success
  ));
  assert_eq!(
    outcome.context_updates.get("k").map(String::as_str),
    Some("v")
  );
}

/// run_agent with a command that prints fail JSON returns error outcome.
#[test]
fn run_agent_returns_error_outcome_when_agent_prints_fail_json() {
  let dir = tempfile::tempdir().unwrap();
  let path = dir.path().join("out.json");
  std::fs::write(&path, r#"{"outcome":"fail","context_updates":{}}"#).unwrap();
  let cmd = format!("cat {}", path.display());
  let outcome = run_agent(&cmd, "prompt", Some(dir.path()));
  assert!(matches!(outcome.status, crate::types::OutcomeStatus::Error));
}

/// run_agent when agent prints nothing but exits 0 uses exit-code fallback (success).
#[test]
fn run_agent_uses_exit_code_when_stdout_empty_and_exit_zero() {
  let outcome = run_agent("true", "prompt", None);
  assert!(matches!(
    outcome.status,
    crate::types::OutcomeStatus::Success
  ));
}

/// run_agent when agent prints nothing but exits non-zero returns error.
#[test]
fn run_agent_uses_exit_code_when_stdout_empty_and_exit_nonzero() {
  let outcome = run_agent("false", "prompt", None);
  assert!(matches!(outcome.status, crate::types::OutcomeStatus::Error));
}