harn-serve 0.10.129

Shared outbound workflow server core for Harn adapters
use super::*;

struct InvalidContractVmConfigurator {
    calls: Arc<AtomicUsize>,
}

impl VmConfigurator for InvalidContractVmConfigurator {
    fn configure(&self, vm: &mut Vm) -> Result<(), DispatchError> {
        let calls = self.calls.clone();
        vm.register_builtin("test_invalid_contract_value", move |_args, _output| {
            calls.fetch_add(1, Ordering::SeqCst);
            Ok(VmValue::String("not-an-integer".into()))
        });
        Ok(())
    }
}

#[tokio::test]
async fn dispatch_validates_input_before_handler_and_output_before_success() {
    let dir = tempfile::tempdir().expect("tempdir");
    let script = dir.path().join("server.harn");
    std::fs::write(
        &script,
        r"
pub fn inspect(count: int) -> int {
  return test_invalid_contract_value()
}
",
    )
    .expect("write script");
    let calls = Arc::new(AtomicUsize::new(0));
    let mut config = DispatchCoreConfig::for_script(&script);
    config.vm_configurator = Arc::new(InvalidContractVmConfigurator {
        calls: calls.clone(),
    });
    let core = DispatchCore::new(config).expect("core");

    let mut invalid_input = replay_test_request(None);
    invalid_input.function = "inspect".to_string();
    invalid_input.arguments = CallArguments::Named(BTreeMap::from([(
        "count".to_string(),
        serde_json::json!("not-an-integer"),
    )]));
    let input_error = core
        .dispatch(invalid_input)
        .await
        .expect_err("invalid input must fail");
    assert!(matches!(input_error, DispatchError::Validation(_)));
    assert_eq!(calls.load(Ordering::SeqCst), 0, "handler must not run");

    let mut invalid_output = replay_test_request(None);
    invalid_output.function = "inspect".to_string();
    invalid_output.arguments = CallArguments::Named(BTreeMap::from([(
        "count".to_string(),
        serde_json::json!(1),
    )]));
    let output_error = core
        .dispatch(invalid_output)
        .await
        .expect_err("invalid output must fail");
    assert!(matches!(output_error, DispatchError::Contract(_)));
    assert_eq!(
        calls.load(Ordering::SeqCst),
        1,
        "valid input reaches handler"
    );
}

#[tokio::test]
async fn module_initialization_throw_is_value_free_at_the_dispatch_boundary() {
    let dir = tempfile::tempdir().expect("tempdir");
    let script = dir.path().join("server.harn");
    std::fs::write(
        &script,
        r#"
fn initialize() {
  throw {message: "PRIVATE-CUSTOMER-DIAGNOSTIC-123456"}
}

let initialized = initialize()

pub fn inspect() -> int {
  return if initialized == nil { 1 } else { 2 }
}
"#,
    )
    .expect("write script");
    let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
    let mut request = replay_test_request(None);
    request.function = "inspect".to_string();

    let error = core
        .dispatch(request)
        .await
        .expect_err("module initialization must fail");
    assert_eq!(error.message(), "tool threw an undeclared value");
    assert!(!error.message().contains("PRIVATE-CUSTOMER-DIAGNOSTIC"));
}

#[tokio::test]
async fn dispatch_projects_variadic_parameters_as_arrays_for_every_argument_form() {
    let dir = tempfile::tempdir().expect("tempdir");
    let script = dir.path().join("server.harn");
    std::fs::write(
        &script,
        r"
pub fn collect(prefix: string, ...values: int) -> dict {
  return {prefix: prefix, values: values}
}
",
    )
    .expect("write script");
    let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
    assert!(
        core.catalog().function("collect").unwrap().params[1].rest,
        "export discovery preserves the variadic marker"
    );
    let mut vm = core
        .generation
        .instantiate(Arc::new(AtomicBool::new(false)));
    let exports = vm
        .load_prepared_module_exports_from_source(&script, core.generation.source())
        .await
        .expect("load compiled exports");
    assert!(
        exports["collect"].func.has_rest_param,
        "compiled export preserves the variadic marker"
    );
    assert_eq!(
        core.tool_catalog().tools[0].input_schema["properties"]["values"],
        serde_json::json!({"type": "array", "items": {"type": "integer"}})
    );

    let mut named = replay_test_request(None);
    named.function = "collect".to_string();
    named.arguments = CallArguments::Named(BTreeMap::from([
        ("prefix".to_string(), serde_json::json!("named")),
        ("values".to_string(), serde_json::json!([1, 2])),
    ]));
    assert_eq!(
        core.dispatch(named).await.expect("named dispatch").value,
        serde_json::json!({"prefix": "named", "values": [1, 2]})
    );

    let mut positional = replay_test_request(None);
    positional.function = "collect".to_string();
    positional.arguments = CallArguments::Positional(vec![
        serde_json::json!("positional"),
        serde_json::json!(3),
        serde_json::json!(4),
    ]);
    assert_eq!(
        core.dispatch(positional)
            .await
            .expect("positional dispatch")
            .value,
        serde_json::json!({"prefix": "positional", "values": [3, 4]})
    );
}

#[tokio::test]
async fn dispatch_rejects_an_unknown_named_argument_instead_of_dropping_it() {
    let dir = tempfile::tempdir().expect("tempdir");
    let script = dir.path().join("server.harn");
    std::fs::write(
        &script,
        r"
pub fn inspect(count: int) -> int {
  return count
}
",
    )
    .expect("write script");
    let core = DispatchCore::new(DispatchCoreConfig::for_script(&script)).expect("core");
    assert_eq!(
        core.tool_catalog().tools[0].input_schema["additionalProperties"],
        serde_json::json!(false),
        "the advertised export schema must close the caller-owned object"
    );

    let mut unknown = replay_test_request(None);
    unknown.function = "inspect".to_string();
    unknown.arguments = CallArguments::Named(BTreeMap::from([
        ("count".to_string(), serde_json::json!(1)),
        ("mispelled".to_string(), serde_json::json!(2)),
    ]));
    let error = core
        .dispatch(unknown)
        .await
        .expect_err("an unknown named argument must not reach the handler");
    assert!(matches!(error, DispatchError::Validation(_)));
    assert!(error.message().contains("additionalProperties"));
}