skippy-server 0.76.1

Embedded Skippy staged runtime server
use super::support::*;
use super::*;

#[test]
fn chat_runtime_feature_guard_allows_structured_output_for_guardrails() {
    let request: ChatCompletionRequest = serde_json::from_value(json!({
        "model": "test",
        "messages": [{"role": "user", "content": "hi"}],
        "response_format": {
            "type": "json_schema",
            "json_schema": {"name": "answer", "schema": {"type": "object"}}
        }
    }))
    .unwrap();

    ensure_chat_runtime_features_supported(&request).unwrap();
}

#[derive(Default)]
struct StructuredGuardrailRecordingBackend {
    seen: Mutex<Option<ChatCompletionRequest>>,
}

#[async_trait]
impl OpenAiBackend for StructuredGuardrailRecordingBackend {
    async fn models(&self) -> OpenAiResult<Vec<ModelObject>> {
        Ok(vec![ModelObject::new("test")])
    }

    async fn chat_completion(
        &self,
        request: ChatCompletionRequest,
    ) -> OpenAiResult<ChatCompletionResponse> {
        ensure_chat_runtime_features_supported(&request)
            .expect("guarded wrapper should downgrade backend-facing structured requests");
        *self.seen.lock().unwrap() = Some(request);
        Ok(ChatCompletionResponse::from_parts(
            "chatcmpl-test".to_string(),
            123,
            "test".to_string(),
            vec![ChatCompletionChoice {
                index: 0,
                message: AssistantMessage {
                    role: "assistant",
                    content: None,
                    reasoning_content: None,
                    tool_calls: Some(json!([{
                        "id": "call_1",
                        "type": "function",
                        "function": {
                            "name": "_mesh_emit_structured",
                            "arguments": "{\"answer\":\"ok\"}"
                        }
                    }])),
                },
                logprobs: None,
                finish_reason: Some(FinishReason::ToolCalls),
            }],
            Usage::new(1, 1),
            None,
        ))
    }

    async fn chat_completion_stream(
        &self,
        _request: ChatCompletionRequest,
        _context: OpenAiRequestContext,
    ) -> OpenAiResult<ChatCompletionStream> {
        unreachable!("streaming is not used in this test")
    }

    async fn completion(&self, _request: CompletionRequest) -> OpenAiResult<CompletionResponse> {
        unreachable!("completions are not used in this test")
    }

    async fn completion_stream(
        &self,
        _request: CompletionRequest,
        _context: OpenAiRequestContext,
    ) -> OpenAiResult<CompletionStream> {
        unreachable!("completions are not used in this test")
    }
}

#[tokio::test]
async fn guarded_structured_output_is_not_rejected_by_runtime_feature_guard() {
    let backend = Arc::new(StructuredGuardrailRecordingBackend::default());
    let guardrails = OpenAiGuardrailsConfig {
        target: OpenAiGuardrailsTarget::Skippy,
        policy: GuardrailPolicy {
            mode: GuardrailMode::Enforce,
            apply_to_all_models: true,
            ..GuardrailPolicy::default()
        }
        .into(),
        compaction: None,
    };
    let guarded = guardrails.wrap_backend(backend.clone());
    let request: ChatCompletionRequest = serde_json::from_value(json!({
        "model": "test",
        "messages": [{"role": "user", "content": "hi"}],
        "response_format": {
            "type": "json_schema",
            "json_schema": {
                "name": "answer",
                "schema": {
                    "type": "object",
                    "properties": {"answer": {"type": "string"}},
                    "required": ["answer"],
                    "additionalProperties": false
                }
            }
        }
    }))
    .unwrap();

    let response = guarded.chat_completion(request).await.unwrap();
    assert_eq!(
        response.choices[0].message.content.as_deref(),
        Some("{\"answer\":\"ok\"}")
    );

    let seen = backend.seen.lock().unwrap().clone().unwrap();
    assert!(
        seen.response_format.is_none(),
        "guarded backend should clear backend-facing response_format"
    );
}

#[test]
fn standalone_guardrail_modes_have_expected_policies() {
    let metrics =
        OpenAiGuardrailsConfig::for_standalone_mode(crate::cli::OpenAiGuardrailsCliMode::Metrics)
            .status();
    assert_eq!(metrics.mode, "metrics");
    assert_eq!(metrics.retry_exhaustion, "pass_last_text");
    assert_eq!(metrics.small_model_policy, "all");

    let enforce =
        OpenAiGuardrailsConfig::for_standalone_mode(crate::cli::OpenAiGuardrailsCliMode::Enforce)
            .status();
    assert_eq!(enforce.mode, "enforce");
    assert_eq!(enforce.retry_exhaustion, "error");
    assert_eq!(enforce.small_model_policy, "all");

    let disabled =
        OpenAiGuardrailsConfig::for_standalone_mode(crate::cli::OpenAiGuardrailsCliMode::Disabled)
            .status();
    assert_eq!(disabled.mode, "disabled");
    assert_eq!(disabled.small_model_policy, "small_models_only");
}

#[tokio::test]
async fn compaction_wraps_skippy_backend_even_when_guardrails_are_disabled() {
    let backend = Arc::new(StructuredGuardrailRecordingBackend::default());
    let guardrails = OpenAiGuardrailsConfig {
        target: OpenAiGuardrailsTarget::Skippy,
        policy: GuardrailPolicy::default().into(),
        compaction: Some(CompactionConfig::default()),
    };
    let wrapped = guardrails.wrap_backend(backend.clone());
    let request: ChatCompletionRequest = serde_json::from_value(json!({
        "model": "test",
        "messages": [
            {"role": "tool", "content": "stale tool result"},
            {"role": "user", "content": "continue"}
        ],
        "mesh_compact": true
    }))
    .unwrap();

    let _ = wrapped.chat_completion(request).await;

    let seen = backend.seen.lock().unwrap().clone().unwrap();
    assert_eq!(seen.messages[0].role, "system");
    assert!(seen.messages.iter().all(|message| message.role != "tool"));
}

#[tokio::test]
async fn disabled_skippy_guardrail_wrapper_can_be_enabled_live() {
    let backend = Arc::new(StructuredGuardrailRecordingBackend::default());
    let policy: openai_frontend::GuardrailPolicyHandle = GuardrailPolicy::default().into();
    let guardrails = OpenAiGuardrailsConfig {
        target: OpenAiGuardrailsTarget::Skippy,
        policy: policy.clone(),
        compaction: None,
    };
    let wrapped = guardrails.wrap_backend(backend.clone());
    let request = tool_request();

    wrapped.chat_completion(request.clone()).await.unwrap();
    assert_eq!(
        backend.seen.lock().unwrap().clone().unwrap().tools,
        request.tools
    );

    policy.update(GuardrailPolicy {
        mode: GuardrailMode::Enforce,
        apply_to_all_models: true,
        ..GuardrailPolicy::default()
    });
    let _ = wrapped.chat_completion(request).await;

    let seen = backend.seen.lock().unwrap().clone().unwrap();
    let tool_names = seen
        .tools
        .as_ref()
        .and_then(|tools| tools.as_array())
        .unwrap()
        .iter()
        .filter_map(|tool| tool.get("function"))
        .filter_map(|function| function.get("name"))
        .filter_map(serde_json::Value::as_str)
        .collect::<Vec<_>>();
    assert!(tool_names.contains(&"_mesh_respond"));
    assert_eq!(guardrails.status().mode, "enforce");
}

#[tokio::test]
async fn compaction_wraps_skippy_backend_with_runtime_context_limit() {
    let backend = Arc::new(StructuredGuardrailRecordingBackend::default());
    let guardrails = OpenAiGuardrailsConfig {
        target: OpenAiGuardrailsTarget::Skippy,
        policy: GuardrailPolicy::default().into(),
        compaction: Some(CompactionConfig {
            enabled: true,
            ..CompactionConfig::default()
        }),
    };
    let wrapped = guardrails.wrap_backend_with_context_limit(backend.clone(), Some(1));
    let request: ChatCompletionRequest = serde_json::from_value(json!({
        "model": "test",
        "messages": [
            {"role": "tool", "content": "stale tool result"},
            {"role": "user", "content": "continue"}
        ]
    }))
    .unwrap();

    wrapped.chat_completion(request).await.unwrap();

    let seen = backend.seen.lock().unwrap().clone().unwrap();
    assert_eq!(seen.messages[0].role, "system");
    assert!(seen.messages.iter().all(|message| message.role != "tool"));
}

#[tokio::test]
async fn compaction_and_guardrails_can_stack() {
    let backend = Arc::new(StructuredGuardrailRecordingBackend::default());
    let guardrails = OpenAiGuardrailsConfig {
        target: OpenAiGuardrailsTarget::Skippy,
        policy: GuardrailPolicy {
            mode: GuardrailMode::Enforce,
            apply_to_all_models: true,
            ..GuardrailPolicy::default()
        }
        .into(),
        compaction: Some(CompactionConfig::default()),
    };
    let wrapped = guardrails.wrap_backend(backend.clone());
    let request: ChatCompletionRequest = serde_json::from_value(json!({
        "model": "test",
        "messages": [
            {"role": "tool", "content": "stale tool result"},
            {"role": "user", "content": "continue"}
        ],
        "response_format": {
            "type": "json_schema",
            "json_schema": {
                "name": "answer",
                "schema": {
                    "type": "object",
                    "properties": {"answer": {"type": "string"}},
                    "required": ["answer"],
                    "additionalProperties": false
                }
            }
        },
        "mesh_compact": true
    }))
    .unwrap();

    let response = wrapped.chat_completion(request).await.unwrap();
    assert_eq!(
        response.choices[0].message.content.as_deref(),
        Some("{\"answer\":\"ok\"}")
    );

    let seen = backend.seen.lock().unwrap().clone().unwrap();
    assert!(seen.response_format.is_none());
    assert_eq!(seen.messages[0].role, "system");
    assert!(seen.messages.iter().all(|message| message.role != "tool"));
}