harn-serve 0.10.121

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

#[tokio::test]
async fn round_trips_and_reaches_execution() {
    struct MockModeGuard;
    impl Drop for MockModeGuard {
        fn drop(&mut self) {
            harn_vm::llm::clear_cli_llm_mock_mode();
        }
    }

    let mock = harn_vm::llm::parse_llm_mock_value(
        &json!({"text": "ok", "model": "o3", "provider": "mock"}),
    )
    .expect("mock fixture");
    harn_vm::llm::install_cli_llm_mocks(vec![mock]);
    let _mock_mode = MockModeGuard;

    let dir = tempfile::tempdir().expect("tempdir");
    let script = dir.path().join("agent.harn");
    std::fs::write(
        &script,
        r#"pipeline main(harness: Harness) {
  harness.llm.call(prompt, nil, {provider: "mock", model: "o3"})
  const call = harness.llm.mock_calls()[0]
  assert(
    call.thinking.mode == "effort",
    "session reasoning mode reached provider: ${json_stringify(call.thinking)}",
  )
  assert(call.thinking.level == "high", "session reasoning effort reached provider")
  harness.stdio.println("session-model-policy-fired")
}
"#,
    )
    .expect("write script");
    let server = ApiServer::new(ApiServerConfig::for_pipeline(
        script.to_string_lossy().to_string(),
    ));
    let state = server.state;
    let mut events = state.events_tx.subscribe();
    let app = api_router(state);

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/v1/sessions")
                .header("content-type", "application/json")
                .body(Body::from(r#"{"workspace_id":"local"}"#))
                .expect("request"),
        )
        .await
        .expect("default session response");
    assert_eq!(response.status(), StatusCode::CREATED);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let default_session: Value = serde_json::from_slice(&body).expect("session");
    assert!(
        default_session.get("model_policy").is_none(),
        "omitting model_policy must preserve the existing session representation"
    );

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/v1/sessions")
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"workspace_id":"local","model_policy":null}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("null policy session response");
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let null_policy_session: Value = serde_json::from_slice(&body).expect("session");
    assert!(null_policy_session.get("model_policy").is_none());

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/v1/sessions")
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"workspace_id":"local","model_policy":{"provider":" MOCK ","model":" o3 ","reasoning_effort":"high"}}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("session response");
    assert_eq!(response.status(), StatusCode::CREATED);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let session: Value = serde_json::from_slice(&body).expect("session");
    assert_eq!(
        session["model_policy"],
        json!({
            "provider": "mock",
            "model": "o3",
            "reasoning_effort": "high"
        })
    );
    let session_id = session["id"].as_str().expect("session id").to_string();

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri(format!("/v1/sessions/{session_id}"))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("get session response");
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let fetched: Value = serde_json::from_slice(&body).expect("session");
    assert_eq!(fetched["model_policy"], session["model_policy"]);

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/v1/sessions")
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("list sessions response");
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let listed: Value = serde_json::from_slice(&body).expect("session list");
    let listed_session = listed["data"]
        .as_array()
        .expect("session data")
        .iter()
        .find(|candidate| candidate["id"] == session_id)
        .expect("configured session listed");
    assert_eq!(listed_session["model_policy"], session["model_policy"]);

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri(format!("/v1/sessions/{session_id}/events"))
                .body(Body::empty())
                .expect("request"),
        )
        .await
        .expect("replayed events response");
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let replayed: Value = serde_json::from_slice(&body).expect("event list");
    assert!(
        replayed["data"]
            .as_array()
            .expect("event data")
            .iter()
            .any(|event| event["event"] == "session.created"
                && event["payload"]["model_policy"] == session["model_policy"]),
        "session history replay must project the normalized policy"
    );

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri(format!("/v1/sessions/{session_id}/tasks"))
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"input":{"role":"user","parts":[{"type":"text","text":"exercise the configured route","visibility":"public"}]}}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("task response");
    assert_eq!(response.status(), StatusCode::ACCEPTED);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let task: Value = serde_json::from_slice(&body).expect("task");
    let task_id = task["id"].as_str().expect("task id");

    loop {
        let event = events.recv().await.expect("task terminal event");
        if event.task_id.as_deref() != Some(task_id) {
            continue;
        }
        match event.event.as_str() {
            "task.completed" => break,
            "task.failed" => panic!("session model policy did not reach execution"),
            _ => {}
        }
    }
}

#[tokio::test]
async fn updates_forks_clears_and_rejects_invalid_shape() {
    let dir = tempfile::tempdir().expect("tempdir");
    let script = dir.path().join("agent.harn");
    std::fs::write(
        &script,
        "pipeline main(harness: Harness) { harness.stdio.println(prompt) }\n",
    )
    .expect("write script");
    let server = ApiServer::new(ApiServerConfig::for_pipeline(
        script.to_string_lossy().to_string(),
    ));
    let app = api_router(server.state);

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/v1/sessions")
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"workspace_id":"local","model_policy":{"provider":"mock","model":"o3"}}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("session response");
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let session: Value = serde_json::from_slice(&body).expect("session");
    let session_id = session["id"].as_str().expect("session id");

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("PATCH")
                .uri(format!("/v1/sessions/{session_id}"))
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"model_policy":{"provider":"mock","model":"o3","reasoning_effort":"low"}}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("update response");
    assert_eq!(response.status(), StatusCode::OK);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let updated: Value = serde_json::from_slice(&body).expect("session");
    assert_eq!(updated["model_policy"]["reasoning_effort"], "low");

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri(format!("/v1/sessions/{session_id}/fork"))
                .header("content-type", "application/json")
                .body(Body::from("{}"))
                .expect("request"),
        )
        .await
        .expect("fork response");
    assert_eq!(response.status(), StatusCode::CREATED);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let forked: Value = serde_json::from_slice(&body).expect("forked session");
    assert_eq!(forked["model_policy"], updated["model_policy"]);

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("PATCH")
                .uri(format!("/v1/sessions/{session_id}"))
                .header("content-type", "application/json")
                .body(Body::from(r#"{"model_policy":null}"#))
                .expect("request"),
        )
        .await
        .expect("clear response");
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let cleared: Value = serde_json::from_slice(&body).expect("session");
    assert!(cleared.get("model_policy").is_none());

    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .method("PATCH")
                .uri(format!("/v1/sessions/{session_id}"))
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"model_policy":{"provider":"mock","model":"o3","preset":"friendly"}}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("invalid update response");
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let error: Value = serde_json::from_slice(&body).expect("error");
    assert_eq!(error["error"]["code"], "invalid_model_policy");
    assert_eq!(
        error["error"]["message"],
        "model_policy must contain provider, model, and optional reasoning_effort only"
    );

    let response = app
        .oneshot(
            Request::builder()
                .method("PATCH")
                .uri(format!("/v1/sessions/{session_id}"))
                .header("content-type", "application/json")
                .body(Body::from(
                    r#"{"model_policy":{"provider":"mock","model":"o3","reasoning_effort":"turbo"}}"#,
                ))
                .expect("request"),
        )
        .await
        .expect("invalid effort response");
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("body");
    let error: Value = serde_json::from_slice(&body).expect("error");
    assert_eq!(error["error"]["code"], "invalid_model_policy");
}