mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
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.
139 lines
6.0 KiB
Java
139 lines
6.0 KiB
Java
package vip.mate.agent.chatmodel;
|
|
|
|
import io.micrometer.observation.ObservationRegistry;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import org.springframework.ai.anthropic.api.AnthropicApi;
|
|
import org.springframework.beans.factory.ObjectProvider;
|
|
import org.springframework.retry.support.RetryTemplate;
|
|
import org.springframework.web.client.RestClient;
|
|
import org.springframework.web.reactive.function.client.WebClient;
|
|
import vip.mate.exception.MateClawException;
|
|
import vip.mate.llm.anthropic.oauth.ClaudeCodeApiHeaders;
|
|
import vip.mate.llm.anthropic.oauth.ClaudeCodeOAuthService;
|
|
import vip.mate.llm.anthropic.oauth.ClaudeCodeVersionDetector;
|
|
import vip.mate.llm.model.ModelProtocol;
|
|
|
|
import java.util.function.Supplier;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.lenient;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/**
|
|
* Header-construction + token-fetch coverage for the Claude Code OAuth chat
|
|
* model builder. Building a real {@link AnthropicApi} doesn't make a network
|
|
* call (Spring AI defers all I/O to {@code chatCompletionEntity}), so these
|
|
* tests can exercise the full assembly path without mocking the API client.
|
|
*/
|
|
@ExtendWith(MockitoExtension.class)
|
|
class AgentClaudeCodeChatModelBuilderTest {
|
|
|
|
@Mock
|
|
private AgentAnthropicChatModelBuilder anthropicBuilder;
|
|
|
|
@Mock
|
|
private ClaudeCodeOAuthService oauthService;
|
|
|
|
private ClaudeCodeApiHeaders apiHeaders;
|
|
|
|
private AgentClaudeCodeChatModelBuilder builder;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
// Real ApiHeaders with a stub version detector — the version string
|
|
// shows up verbatim in User-Agent assertions.
|
|
ClaudeCodeVersionDetector detector = new ClaudeCodeVersionDetector() {
|
|
@Override
|
|
public String get() { return "2.1.114"; }
|
|
};
|
|
apiHeaders = new ClaudeCodeApiHeaders(detector);
|
|
|
|
builder = new AgentClaudeCodeChatModelBuilder(
|
|
anthropicBuilder,
|
|
oauthService,
|
|
apiHeaders,
|
|
providerOf(RestClient::builder),
|
|
providerOf(WebClient::builder),
|
|
providerOf(() -> ObservationRegistry.NOOP),
|
|
new com.fasterxml.jackson.databind.ObjectMapper());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("supportedProtocol returns ANTHROPIC_CLAUDE_CODE")
|
|
void supportedProtocol() {
|
|
assertEquals(ModelProtocol.ANTHROPIC_CLAUDE_CODE, builder.supportedProtocol());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("buildOauthAnthropicApi accepts a token and produces a non-null AnthropicApi")
|
|
void buildOauthAnthropicApi_returnsClient() {
|
|
// Sanity check: the NoopApiKey path passes Spring AI's notNull assertion
|
|
// and the OAuth headers attach without throwing. If this test ever
|
|
// fails, the most likely cause is a Spring AI upgrade tightening the
|
|
// ApiKey contract — see AgentClaudeCodeChatModelBuilder javadoc.
|
|
AnthropicApi api = builder.buildOauthAnthropicApi("sk-ant-oat01-test-token");
|
|
assertNotNull(api);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("build delegates to oauthService and reuses anthropicBuilder.buildAnthropicOptions")
|
|
void build_invokesOauthAndReusesOptions() {
|
|
when(oauthService.getValidToken()).thenReturn("tok-123");
|
|
// anthropicBuilder.buildAnthropicOptions returns a real options object —
|
|
// we don't need a strict comparison, just that it gets invoked once and
|
|
// its result is fed through.
|
|
when(anthropicBuilder.buildAnthropicOptions(any()))
|
|
.thenReturn(org.springframework.ai.anthropic.AnthropicChatOptions.builder().build());
|
|
|
|
var result = builder.build(new vip.mate.llm.model.ModelConfigEntity(), null,
|
|
RetryTemplate.defaultInstance());
|
|
assertNotNull(result);
|
|
verify(oauthService, times(1)).getValidToken();
|
|
verify(anthropicBuilder, times(1)).buildAnthropicOptions(any());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("build propagates OAuth errors without calling buildAnthropicOptions")
|
|
void build_propagatesOauthErrors() {
|
|
// Simulates "no Claude Code on disk" — caller surface is the same
|
|
// MateClawException so the global handler can format the i18n message.
|
|
when(oauthService.getValidToken()).thenThrow(new MateClawException(
|
|
"err.anthropic.no_claude_code", "no creds"));
|
|
|
|
assertThrows(MateClawException.class,
|
|
() -> builder.build(new vip.mate.llm.model.ModelConfigEntity(), null, null));
|
|
// anthropicBuilder shouldn't have been touched — short-circuit before
|
|
// it would have wasted a buildAnthropicOptions call.
|
|
verify(anthropicBuilder, never()).buildAnthropicOptions(any());
|
|
}
|
|
|
|
/* ----- ObjectProvider test helper ----- */
|
|
|
|
/** Minimal {@link ObjectProvider} that defers to a {@link Supplier} for {@code getIfAvailable}. */
|
|
@SuppressWarnings("unchecked")
|
|
private static <T> ObjectProvider<T> providerOf(Supplier<T> supplier) {
|
|
ObjectProvider<T> mock = mock(ObjectProvider.class);
|
|
// Use lenient — not every test triggers a getIfAvailable call (e.g.
|
|
// the supportedProtocol test takes a short path), and the strict
|
|
// default would fail with UnnecessaryStubbingException.
|
|
lenient().when(mock.getIfAvailable(any(Supplier.class))).thenAnswer(inv -> {
|
|
Supplier<T> fallback = inv.getArgument(0);
|
|
T v = supplier.get();
|
|
return v != null ? v : fallback.get();
|
|
});
|
|
return mock;
|
|
}
|
|
}
|