mateclaw/mateclaw-server/src/test/java/vip/mate/agent/ReasoningEffortSanitizerTest.java
matevip 0ffc224623 chore(test): backfill mateclaw-server/src/test/ that earlier PRIVATE_ITEMS pattern accidentally excluded
The PRIVATE_ITEMS list contained the bare 'test' entry, which rsync
interprets as 'any directory named test at any depth' — so it caught
the root-level /test/ scratch directory (intended) AND every src/test/
under each module (not intended).

Pattern is already anchored to /test (root-only). This commit rsyncs
the accumulated src/test/ tree forward so opensource has the unit tests
that have been written / updated against existing src/main/ code since
the pattern regression. Going forward each per-commit sync will carry
src/test/ files along with the main change.
2026-05-12 17:38:08 +08:00

213 lines
9.7 KiB
Java

package vip.mate.agent;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.openai.api.OpenAiApi;
import vip.mate.llm.model.ModelProviderEntity;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* RFC-049 PR-1.3 verification — covers §5.2 Case E3.1 / E3.2 / E3.3 plus the
* whitelist positive path.
*
* <p>The sanitizer is provider-first with default-deny: only providerId in
* {@code {openai, azure-openai}} is allowed to carry {@code reasoning_effort}.
* All other providers (including unknown ones) must strip regardless of what
* {@code request.model()} says, because the model name may have leaked from a
* failover primary.
*/
class ReasoningEffortSanitizerTest {
private static ModelProviderEntity provider(String id) {
ModelProviderEntity p = new ModelProviderEntity();
p.setProviderId(id);
return p;
}
/**
* Construct a minimal {@link OpenAiApi.ChatCompletionRequest} via its record canonical
* constructor with only {@code messages}, {@code model}, and {@code reasoningEffort} set
* — everything else is null. {@code ChatCompletionRequest} in Spring AI 1.1.4 has no
* public builder.
*/
private static OpenAiApi.ChatCompletionRequest request(String model, String reasoningEffort) {
return new OpenAiApi.ChatCompletionRequest(
List.of(), // messages
model, // model
null, // store
null, // metadata
null, // frequencyPenalty
null, // logitBias
null, // logprobs
null, // topLogprobs
null, // maxTokens
null, // maxCompletionTokens
null, // n
null, // outputModalities
null, // audioParameters
null, // presencePenalty
null, // responseFormat
null, // seed
null, // serviceTier
null, // stop
null, // stream
null, // streamOptions
null, // temperature
null, // topP
null, // tools
null, // toolChoice
null, // parallelToolCalls
null, // user
reasoningEffort, // reasoningEffort
null, // webSearchOptions
null, // verbosity
null, // promptCacheKey
null, // safetyIdentifier
null // extraBody
);
}
@Test
@DisplayName("Whitelist: openai is allowed")
void whitelist_openai() {
assertTrue(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("openai")));
}
@Test
@DisplayName("Whitelist: azure-openai is allowed")
void whitelist_azureOpenai() {
assertTrue(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("azure-openai")));
}
@Test
@DisplayName("Whitelist: case-insensitive (Azure-OpenAI)")
void whitelist_caseInsensitive() {
assertTrue(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("Azure-OpenAI")));
}
@Test
@DisplayName("Whitelist: deepseek is denied")
void denylist_deepseek() {
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("deepseek")));
}
@Test
@DisplayName("Whitelist: kimi family denied")
void denylist_kimi() {
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("kimi-cn")));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("kimi-intl")));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("kimi-code")));
}
@Test
@DisplayName("Whitelist: dashscope / ollama / anthropic denied")
void denylist_misc() {
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("dashscope")));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("ollama")));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(provider("anthropic")));
}
@Test
@DisplayName("Whitelist: unknown providerId denied (default-deny — §5.2 Case E3.3)")
void denylist_unknownProvider() {
// This is the critical regression guard: if anyone re-adds a default-allow
// branch to isReasoningEffortWhitelistedProvider, this case fails first.
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(
provider("my-custom-openai-compat-gateway")));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(
provider("openrouter")));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(
provider("together")));
}
@Test
@DisplayName("Whitelist: null provider / null providerId denied")
void denylist_nulls() {
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(null));
assertFalse(AgentGraphBuilder.isReasoningEffortWhitelistedProvider(new ModelProviderEntity()));
}
// ---------- sanitizeReasoningEffortForProvider ----------
@Test
@DisplayName("Sanitize no-op: request has no reasoning_effort")
void sanitize_noop_noReasoningEffort() {
OpenAiApi.ChatCompletionRequest req = request("gpt-5", null);
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, provider("deepseek"));
assertSame(req, out, "should return same instance when reasoning_effort is already null");
}
@Test
@DisplayName("§5.2 Case E3.1: primary=gpt-5 → fallback=deepseek strips reasoning_effort")
void sanitize_failover_deepseek_strips() {
// Simulate failover: OpenAiChatOptions.model still leaked as "gpt-5" on the deepseek request.
OpenAiApi.ChatCompletionRequest req = request("gpt-5", "high");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, provider("deepseek"));
assertNull(out.reasoningEffort(), "deepseek is not on the whitelist — strip regardless of model name");
// Other fields preserved
assertEquals("gpt-5", out.model());
}
@Test
@DisplayName("§5.2 Case E3.2: kimi / dashscope / ollama also strip")
void sanitize_failover_otherDenied_strips() {
for (String pid : List.of("kimi-cn", "kimi-intl", "kimi-code", "dashscope", "ollama", "anthropic")) {
OpenAiApi.ChatCompletionRequest req = request("gpt-5", "medium");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, provider(pid));
assertNull(out.reasoningEffort(), "provider=" + pid + " must strip");
}
}
@Test
@DisplayName("§5.2 Case E3.3: unknown provider strips (default-deny regression guard)")
void sanitize_unknownProvider_strips() {
OpenAiApi.ChatCompletionRequest req = request("gpt-5", "high");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(
req, provider("my-custom-openai-compat-gateway"));
assertNull(out.reasoningEffort(),
"unknown provider must strip (default-deny) — if this fails, someone re-added default-allow");
}
@Test
@DisplayName("Whitelist + supporting model: keep reasoning_effort (gpt-5 on openai)")
void sanitize_whitelisted_supportingModel_keeps() {
OpenAiApi.ChatCompletionRequest req = request("gpt-5", "high");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, provider("openai"));
assertSame(req, out, "gpt-5 on openai should pass through unchanged");
assertEquals("high", out.reasoningEffort());
}
@Test
@DisplayName("Whitelist + non-supporting model: strip (gpt-4 on openai)")
void sanitize_whitelisted_nonSupportingModel_strips() {
// gpt-4 is NOT OPENAI_REASONING family — reasoning_effort is not applicable there.
OpenAiApi.ChatCompletionRequest req = request("gpt-4", "medium");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, provider("openai"));
assertNull(out.reasoningEffort(),
"gpt-4 is whitelisted-provider but non-supporting-family — family gate should strip");
}
@Test
@DisplayName("Azure OpenAI with supporting model: keep reasoning_effort")
void sanitize_azureOpenai_supporting_keeps() {
OpenAiApi.ChatCompletionRequest req = request("gpt-5", "low");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, provider("azure-openai"));
assertEquals("low", out.reasoningEffort());
}
@Test
@DisplayName("Null provider: strip (defensive)")
void sanitize_nullProvider_strips() {
OpenAiApi.ChatCompletionRequest req = request("gpt-5", "high");
OpenAiApi.ChatCompletionRequest out = AgentGraphBuilder.sanitizeReasoningEffortForProvider(req, null);
assertNull(out.reasoningEffort());
}
}