tests for generate site using llm prompt
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package com.indie.config;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RateLimitConfigTest {
|
||||
|
||||
private RateLimitConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
config = new RateLimitConfig(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryConsumeReturnsTrueWithinLimit() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
assertTrue(config.tryConsume(userId));
|
||||
assertTrue(config.tryConsume(userId));
|
||||
assertTrue(config.tryConsume(userId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryConsumeReturnsFalseWhenExceeded() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
assertTrue(config.tryConsume(userId));
|
||||
assertTrue(config.tryConsume(userId));
|
||||
assertTrue(config.tryConsume(userId));
|
||||
assertFalse(config.tryConsume(userId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentUsersHaveIndependentLimits() {
|
||||
UUID userA = UUID.randomUUID();
|
||||
UUID userB = UUID.randomUUID();
|
||||
|
||||
assertTrue(config.tryConsume(userA));
|
||||
assertTrue(config.tryConsume(userA));
|
||||
assertTrue(config.tryConsume(userA));
|
||||
assertFalse(config.tryConsume(userA));
|
||||
|
||||
assertTrue(config.tryConsume(userB));
|
||||
assertTrue(config.tryConsume(userB));
|
||||
assertTrue(config.tryConsume(userB));
|
||||
assertFalse(config.tryConsume(userB));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAvailableTokensReturnsRemaining() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
assertEquals(3, config.getAvailableTokens(userId));
|
||||
|
||||
config.tryConsume(userId);
|
||||
assertEquals(2, config.getAvailableTokens(userId));
|
||||
|
||||
config.tryConsume(userId);
|
||||
assertEquals(1, config.getAvailableTokens(userId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAvailableTokensReturnsZeroWhenExhausted() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
config.tryConsume(userId);
|
||||
config.tryConsume(userId);
|
||||
config.tryConsume(userId);
|
||||
|
||||
assertEquals(0, config.getAvailableTokens(userId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWithDefaultRate() {
|
||||
RateLimitConfig defaultConfig = new RateLimitConfig(10);
|
||||
UUID userId = UUID.randomUUID();
|
||||
assertEquals(10, defaultConfig.getAvailableTokens(userId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
package com.indie.controller;
|
||||
|
||||
import com.indie.config.GlobalExceptionHandler;
|
||||
import com.indie.config.JwtAuthenticationToken;
|
||||
import com.indie.config.RateLimitConfig;
|
||||
import com.indie.config.UserPrincipal;
|
||||
import com.indie.dto.Reference;
|
||||
import com.indie.dto.SiteStructure;
|
||||
import com.indie.model.GenerationVersion;
|
||||
import com.indie.service.LLMService;
|
||||
import com.indie.support.TestSecurityConfig;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@WebMvcTest(LLMController.class)
|
||||
@Import({GlobalExceptionHandler.class, TestSecurityConfig.class})
|
||||
class LLMControllerTest {
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private LLMService llmService;
|
||||
|
||||
@MockBean
|
||||
private RateLimitConfig rateLimitConfig;
|
||||
|
||||
private UUID userId;
|
||||
private UserPrincipal principal;
|
||||
private JwtAuthenticationToken auth;
|
||||
|
||||
LLMControllerTest(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) {
|
||||
this.mockMvc = mockMvc;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userId = UUID.randomUUID();
|
||||
principal = new UserPrincipal(userId, "user@test.com", "Test User");
|
||||
auth = new JwtAuthenticationToken(principal, "test-token");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Generate {
|
||||
|
||||
@Test
|
||||
void returns200WithSiteStructure() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
SiteStructure structure = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Home").slug("home")
|
||||
.components(List.of(
|
||||
SiteStructure.ComponentStructure.builder().type("hero").build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
when(llmService.generateSite(any(), any(), anyString(), any())).thenReturn(structure);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Build a site",
|
||||
"siteId", UUID.randomUUID().toString()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.pages[0].title").value("Home"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns200WithReferences() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
SiteStructure structure = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Home").slug("home")
|
||||
.components(List.of(
|
||||
SiteStructure.ComponentStructure.builder().type("hero").build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
when(llmService.generateSite(any(), any(), anyString(), any())).thenReturn(structure);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Build a site",
|
||||
"siteId", UUID.randomUUID().toString(),
|
||||
"references", List.of(
|
||||
Map.of("type", "IMAGE", "url", "data:img", "altText", "Photo")
|
||||
)
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns429WhenRateLimited() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(false);
|
||||
when(rateLimitConfig.getAvailableTokens(userId)).thenReturn(0);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Build a site",
|
||||
"siteId", UUID.randomUUID().toString()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isTooManyRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenPromptMissing() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"siteId", UUID.randomUUID().toString()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenPromptBlank() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", " ",
|
||||
"siteId", UUID.randomUUID().toString()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenSiteIdMissing() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Build a site"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenSiteIdBlank() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Build a site",
|
||||
"siteId", ""
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/generate")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Refine {
|
||||
|
||||
@Test
|
||||
void returns200WithRefinedStructure() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
SiteStructure current = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Old").slug("old")
|
||||
.components(List.of(
|
||||
SiteStructure.ComponentStructure.builder().type("text").build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
SiteStructure refined = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Refined").slug("refined")
|
||||
.components(List.of(
|
||||
SiteStructure.ComponentStructure.builder().type("hero").build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
when(llmService.refineSite(any(), any(), any(), anyString(), any())).thenReturn(refined);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Make it better",
|
||||
"siteId", UUID.randomUUID().toString(),
|
||||
"currentStructure", current
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/refine")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.pages[0].title").value("Refined"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns429WhenRateLimited() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(false);
|
||||
when(rateLimitConfig.getAvailableTokens(userId)).thenReturn(0);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Refine",
|
||||
"siteId", UUID.randomUUID().toString(),
|
||||
"currentStructure", Map.of()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/refine")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isTooManyRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenPromptMissing() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"siteId", UUID.randomUUID().toString(),
|
||||
"currentStructure", SiteStructure.builder().build()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/refine")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenSiteIdMissing() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Refine",
|
||||
"currentStructure", SiteStructure.builder().build()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/refine")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenCurrentStructureMissing() throws Exception {
|
||||
when(rateLimitConfig.tryConsume(userId)).thenReturn(true);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"prompt", "Refine",
|
||||
"siteId", UUID.randomUUID().toString()
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/llm/refine")
|
||||
.with(authentication(auth))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Versions {
|
||||
|
||||
@Test
|
||||
void getVersionsReturns200() throws Exception {
|
||||
UUID siteId = UUID.randomUUID();
|
||||
GenerationVersion v1 = GenerationVersion.builder()
|
||||
.id("v1").siteId(siteId).versionNumber(1)
|
||||
.prompt("First").createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
when(llmService.getVersions(siteId)).thenReturn(List.of(v1));
|
||||
|
||||
mockMvc.perform(get("/api/llm/versions/{siteId}", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].versionNumber").value(1))
|
||||
.andExpect(jsonPath("$[0].prompt").value("First"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getVersionsReturns200WithEmptyList() throws Exception {
|
||||
UUID siteId = UUID.randomUUID();
|
||||
when(llmService.getVersions(siteId)).thenReturn(List.of());
|
||||
|
||||
mockMvc.perform(get("/api/llm/versions/{siteId}", siteId)
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class RestoreVersion {
|
||||
|
||||
@Test
|
||||
void returns200WithRestoredStructure() throws Exception {
|
||||
SiteStructure structure = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Restored").slug("restored")
|
||||
.components(List.of(
|
||||
SiteStructure.ComponentStructure.builder().type("hero").build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
when(llmService.restoreVersion("v1")).thenReturn(structure);
|
||||
|
||||
mockMvc.perform(post("/api/llm/versions/v1/restore")
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.pages[0].title").value("Restored"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns400WhenVersionNotFound() throws Exception {
|
||||
when(llmService.restoreVersion("nonexistent"))
|
||||
.thenThrow(new IllegalArgumentException("Version not found: nonexistent"));
|
||||
|
||||
mockMvc.perform(post("/api/llm/versions/nonexistent/restore")
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DeleteVersion {
|
||||
|
||||
@Test
|
||||
void returns204WhenDeleted() throws Exception {
|
||||
doNothing().when(llmService).deleteVersion("v1");
|
||||
|
||||
mockMvc.perform(delete("/api/llm/versions/v1")
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returns404WhenNotFound() throws Exception {
|
||||
doThrow(new IllegalArgumentException("Version not found: nonexistent"))
|
||||
.when(llmService).deleteVersion("nonexistent");
|
||||
|
||||
mockMvc.perform(delete("/api/llm/versions/nonexistent")
|
||||
.with(authentication(auth)))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.indie.dto.Reference;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FakeReferenceService extends ReferenceService {
|
||||
|
||||
@Override
|
||||
String fetchUrlContent(String url) {
|
||||
return "fake content for " + url;
|
||||
}
|
||||
|
||||
public static String contextFor(List<Reference> references) {
|
||||
if (references == null || references.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("\n\nReference materials provided by the user:\n");
|
||||
for (int i = 0; i < references.size(); i++) {
|
||||
Reference ref = references.get(i);
|
||||
sb.append("[").append(i + 1).append("] ");
|
||||
sb.append(ref.getType()).append(": ").append(ref.getUrl());
|
||||
if (ref.getAltText() != null && !ref.getAltText().isBlank()) {
|
||||
sb.append(" (").append(ref.getAltText()).append(")");
|
||||
}
|
||||
sb.append("\n");
|
||||
sb.append(" Content: fake content for ").append(ref.getUrl()).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.indie.dto.SiteStructure;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class LLMResponseParserTest {
|
||||
|
||||
private LLMResponseParser parser;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
parser = new LLMResponseParser(new ObjectMapper());
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExtractJson {
|
||||
|
||||
@Test
|
||||
void extractsPlainJson() {
|
||||
String json = "{\"pages\": []}";
|
||||
assertEquals(json, parser.extractJson(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsJsonFromMarkdownFence() {
|
||||
String raw = "```json\n{\"pages\": [{\"title\": \"Home\", \"slug\": \"home\"}]}\n```";
|
||||
String result = parser.extractJson(raw);
|
||||
assertTrue(result.startsWith("{"));
|
||||
assertTrue(result.endsWith("}"));
|
||||
assertTrue(result.contains("\"title\": \"Home\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsJsonFromMarkdownFenceWithoutLang() {
|
||||
String raw = "```\n{\"pages\": []}\n```";
|
||||
String result = parser.extractJson(raw);
|
||||
assertTrue(result.startsWith("{"));
|
||||
assertTrue(result.endsWith("}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsJsonWithLeadingText() {
|
||||
String raw = "Here is the JSON:\n{\"pages\": [{\"title\": \"About\", \"slug\": \"about\"}]}";
|
||||
String result = parser.extractJson(raw);
|
||||
assertTrue(result.startsWith("{"));
|
||||
assertTrue(result.contains("\"title\": \"About\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractsJsonWithTrailingText() {
|
||||
String raw = "{\"pages\": []}\nAnd that's all folks.";
|
||||
String result = parser.extractJson(raw);
|
||||
assertTrue(result.startsWith("{"));
|
||||
assertTrue(result.endsWith("}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void trimsLeadingNonJsonContent() {
|
||||
String raw = "Some explanation text before the actual JSON output.\n\n{\"pages\": []}";
|
||||
String result = parser.extractJson(raw);
|
||||
assertEquals("{\"pages\": []}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnNullInput() {
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.extractJson(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnBlankInput() {
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.extractJson(" "));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Parse {
|
||||
|
||||
@Test
|
||||
void parsesValidSiteStructure() {
|
||||
String json = """
|
||||
{
|
||||
"theme": {
|
||||
"colors": {"primary": "#000", "secondary": "#fff"},
|
||||
"fonts": {"heading": "Arial"}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"title": "Home",
|
||||
"slug": "home",
|
||||
"components": [
|
||||
{"type": "hero", "orderIndex": 0, "config": {"title": "Welcome"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
SiteStructure structure = parser.parse(json);
|
||||
|
||||
assertNotNull(structure.getTheme());
|
||||
assertEquals("#000", structure.getTheme().getColors().get("primary"));
|
||||
assertEquals("Arial", structure.getTheme().getFonts().get("heading"));
|
||||
assertEquals(1, structure.getPages().size());
|
||||
assertEquals("Home", structure.getPages().get(0).getTitle());
|
||||
assertEquals("home", structure.getPages().get(0).getSlug());
|
||||
assertEquals(1, structure.getPages().get(0).getComponents().size());
|
||||
assertEquals("hero", structure.getPages().get(0).getComponents().get(0).getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesStructureWithoutTheme() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "text"}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
SiteStructure structure = parser.parse(json);
|
||||
assertNull(structure.getTheme());
|
||||
assertEquals(1, structure.getPages().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnEmptyPages() {
|
||||
String json = "{\"pages\": []}";
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnMissingPages() {
|
||||
String json = "{\"theme\": {}}";
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnPageWithoutTitle() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{"slug": "home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnPageWithoutSlug() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnComponentWithoutType() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"orderIndex": 0}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignsGeneratedIdWhenPageIdMissing() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
SiteStructure structure = parser.parse(json);
|
||||
assertNotNull(structure.getPages().get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignsGeneratedIdWhenComponentIdMissing() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
SiteStructure structure = parser.parse(json);
|
||||
assertNotNull(structure.getPages().get(0).getComponents().get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesPageWithAllOptionalFields() {
|
||||
String json = """
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": "page-1",
|
||||
"title": "About",
|
||||
"slug": "about",
|
||||
"seoTitle": "About Us",
|
||||
"seoDescription": "Learn about our company",
|
||||
"orderIndex": 1,
|
||||
"components": [
|
||||
{
|
||||
"id": "comp-1",
|
||||
"type": "text",
|
||||
"orderIndex": 0,
|
||||
"config": {"body": "Hello"},
|
||||
"styles": {"color": "red"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
SiteStructure structure = parser.parse(json);
|
||||
SiteStructure.PageStructure page = structure.getPages().get(0);
|
||||
assertEquals("page-1", page.getId());
|
||||
assertEquals("About", page.getTitle());
|
||||
assertEquals("about", page.getSlug());
|
||||
assertEquals("About Us", page.getSeoTitle());
|
||||
assertEquals("Learn about our company", page.getSeoDescription());
|
||||
assertEquals(1, page.getOrderIndex());
|
||||
|
||||
SiteStructure.ComponentStructure comp = page.getComponents().get(0);
|
||||
assertEquals("comp-1", comp.getId());
|
||||
assertEquals("text", comp.getType());
|
||||
assertEquals(0, comp.getOrderIndex());
|
||||
assertEquals("Hello", comp.getConfig().get("body"));
|
||||
assertEquals("red", comp.getStyles().get("color"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresUnknownProperties() {
|
||||
String json = """
|
||||
{
|
||||
"unknownField": true,
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "hero", "extra": 1}]}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
assertDoesNotThrow(() -> parser.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsOnInvalidJson() {
|
||||
String json = "{invalid json here}";
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(json));
|
||||
}
|
||||
}
|
||||
}
|
||||
297
backend/src/test/java/com/indie/service/LLMServiceTest.java
Normal file
297
backend/src/test/java/com/indie/service/LLMServiceTest.java
Normal file
@@ -0,0 +1,297 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.indie.dto.Reference;
|
||||
import com.indie.dto.SiteStructure;
|
||||
import com.indie.model.GenerationVersion;
|
||||
import com.indie.service.llm.LLMFactory;
|
||||
import com.indie.support.FakeLLMProvider;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LLMServiceTest {
|
||||
|
||||
@Mock
|
||||
private MongoTemplate mongoTemplate;
|
||||
|
||||
@Mock
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
private FakeLLMProvider activeProvider;
|
||||
private FakeLLMProvider fallbackProvider;
|
||||
private LLMFactory llmFactory;
|
||||
private LLMResponseParser responseParser;
|
||||
private FakeReferenceService referenceService;
|
||||
private LLMService llmService;
|
||||
|
||||
private UUID userId;
|
||||
private UUID siteId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
userId = UUID.randomUUID();
|
||||
siteId = UUID.randomUUID();
|
||||
|
||||
activeProvider = new FakeLLMProvider("gemini");
|
||||
fallbackProvider = new FakeLLMProvider("openai");
|
||||
llmFactory = new LLMFactory(List.of(activeProvider, fallbackProvider), "gemini", "openai");
|
||||
Method init = LLMFactory.class.getDeclaredMethod("init");
|
||||
init.setAccessible(true);
|
||||
init.invoke(llmFactory);
|
||||
|
||||
responseParser = new LLMResponseParser(new ObjectMapper());
|
||||
referenceService = new FakeReferenceService();
|
||||
|
||||
Resource mockResource = mock(Resource.class);
|
||||
when(mockResource.exists()).thenReturn(true);
|
||||
when(mockResource.getInputStream()).thenReturn(
|
||||
new ByteArrayInputStream("You are a site generator.".getBytes(StandardCharsets.UTF_8)));
|
||||
when(resourceLoader.getResource(anyString())).thenReturn(mockResource);
|
||||
|
||||
llmService = new LLMService(llmFactory, responseParser, referenceService,
|
||||
mongoTemplate, resourceLoader, "classpath:prompts/site-generator.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSiteReturnsParsedStructure() {
|
||||
activeProvider.withContent("""
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null);
|
||||
|
||||
SiteStructure result = llmService.generateSite(userId, siteId, "Build a site", null);
|
||||
|
||||
assertEquals(1, result.getPages().size());
|
||||
assertEquals("Home", result.getPages().get(0).getTitle());
|
||||
verify(mongoTemplate).save(any(GenerationVersion.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSiteSavesVersionWithCorrectData() {
|
||||
activeProvider.withContent("""
|
||||
{
|
||||
"pages": [
|
||||
{"title": "About", "slug": "about", "components": [{"type": "text"}]}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null);
|
||||
|
||||
llmService.generateSite(userId, siteId, "Build about page", null);
|
||||
|
||||
ArgumentCaptor<GenerationVersion> captor = ArgumentCaptor.forClass(GenerationVersion.class);
|
||||
verify(mongoTemplate).save(captor.capture());
|
||||
|
||||
GenerationVersion saved = captor.getValue();
|
||||
assertEquals(siteId, saved.getSiteId());
|
||||
assertEquals(userId, saved.getUserId());
|
||||
assertEquals(1, saved.getVersionNumber());
|
||||
assertEquals("Build about page", saved.getPrompt());
|
||||
assertNotNull(saved.getCreatedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSiteIncrementsVersionNumber() {
|
||||
activeProvider.withContent("""
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
GenerationVersion existing = GenerationVersion.builder()
|
||||
.id("v1")
|
||||
.siteId(siteId)
|
||||
.versionNumber(3)
|
||||
.build();
|
||||
when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(existing);
|
||||
|
||||
llmService.generateSite(userId, siteId, "Build a site", null);
|
||||
|
||||
ArgumentCaptor<GenerationVersion> captor = ArgumentCaptor.forClass(GenerationVersion.class);
|
||||
verify(mongoTemplate).save(captor.capture());
|
||||
|
||||
assertEquals(4, captor.getValue().getVersionNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSiteWithReferences() {
|
||||
activeProvider.withContent("""
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Home", "slug": "home", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
List<Reference> references = List.of(
|
||||
Reference.builder().type(Reference.ReferenceType.IMAGE).url("data:img").altText("Ref").build()
|
||||
);
|
||||
|
||||
when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null);
|
||||
|
||||
llmService.generateSite(userId, siteId, "Build a site", references);
|
||||
|
||||
ArgumentCaptor<GenerationVersion> captor = ArgumentCaptor.forClass(GenerationVersion.class);
|
||||
verify(mongoTemplate).save(captor.capture());
|
||||
|
||||
assertNotNull(captor.getValue().getReferences());
|
||||
assertEquals(1, captor.getValue().getReferences().size());
|
||||
assertEquals("IMAGE", captor.getValue().getReferences().get(0).getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSiteFallsBackOnProviderFailure() {
|
||||
activeProvider.setFailure(new RuntimeException("API error"));
|
||||
fallbackProvider.withContent("""
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Fallback", "slug": "fallback", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null);
|
||||
|
||||
SiteStructure result = llmService.generateSite(userId, siteId, "Build", null);
|
||||
|
||||
assertEquals("Fallback", result.getPages().get(0).getTitle());
|
||||
verify(mongoTemplate).save(any(GenerationVersion.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSiteThrowsWhenAllProvidersFail() {
|
||||
activeProvider.setFailure(new RuntimeException("API error"));
|
||||
fallbackProvider.setFailure(new RuntimeException("Fallback also failed"));
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> llmService.generateSite(userId, siteId, "Build", null));
|
||||
verify(mongoTemplate, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refineSiteReturnsRefinedStructure() {
|
||||
SiteStructure current = SiteStructure.builder()
|
||||
.pages(List.of(
|
||||
SiteStructure.PageStructure.builder()
|
||||
.title("Old")
|
||||
.slug("old")
|
||||
.components(List.of(
|
||||
SiteStructure.ComponentStructure.builder().type("text").build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
|
||||
activeProvider.withContent("""
|
||||
{
|
||||
"pages": [
|
||||
{"title": "Refined", "slug": "refined", "components": [{"type": "hero"}]}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null);
|
||||
|
||||
SiteStructure result = llmService.refineSite(userId, siteId, current, "Make it better", null);
|
||||
|
||||
assertEquals("Refined", result.getPages().get(0).getTitle());
|
||||
verify(mongoTemplate).save(any(GenerationVersion.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getVersionsReturnsList() {
|
||||
GenerationVersion v1 = GenerationVersion.builder()
|
||||
.id("v1").siteId(siteId).versionNumber(1).build();
|
||||
GenerationVersion v2 = GenerationVersion.builder()
|
||||
.id("v2").siteId(siteId).versionNumber(2).build();
|
||||
|
||||
when(mongoTemplate.find(any(Query.class), eq(GenerationVersion.class)))
|
||||
.thenReturn(List.of(v2, v1));
|
||||
|
||||
List<GenerationVersion> versions = llmService.getVersions(siteId);
|
||||
|
||||
assertEquals(2, versions.size());
|
||||
assertEquals("v2", versions.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getVersionsReturnsEmptyWhenNone() {
|
||||
when(mongoTemplate.find(any(Query.class), eq(GenerationVersion.class)))
|
||||
.thenReturn(List.of());
|
||||
|
||||
List<GenerationVersion> versions = llmService.getVersions(siteId);
|
||||
|
||||
assertTrue(versions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreVersionReturnsParsedStructure() {
|
||||
GenerationVersion version = GenerationVersion.builder()
|
||||
.id("v1")
|
||||
.fullStructure("""
|
||||
{"pages": [{"title": "Restored", "slug": "restored", "components": [{"type": "hero"}]}]}
|
||||
""")
|
||||
.build();
|
||||
|
||||
when(mongoTemplate.findById("v1", GenerationVersion.class)).thenReturn(version);
|
||||
|
||||
SiteStructure result = llmService.restoreVersion("v1");
|
||||
|
||||
assertEquals("Restored", result.getPages().get(0).getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreVersionThrowsWhenNotFound() {
|
||||
when(mongoTemplate.findById("nonexistent", GenerationVersion.class)).thenReturn(null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> llmService.restoreVersion("nonexistent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteVersionRemovesVersion() {
|
||||
GenerationVersion version = GenerationVersion.builder()
|
||||
.id("v1").siteId(siteId).versionNumber(1).build();
|
||||
|
||||
when(mongoTemplate.findById("v1", GenerationVersion.class)).thenReturn(version);
|
||||
|
||||
llmService.deleteVersion("v1");
|
||||
|
||||
verify(mongoTemplate).remove(version);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteVersionThrowsWhenNotFound() {
|
||||
when(mongoTemplate.findById("nonexistent", GenerationVersion.class)).thenReturn(null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> llmService.deleteVersion("nonexistent"));
|
||||
verify(mongoTemplate, never()).remove(any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.indie.dto.Reference;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ReferenceServiceTest {
|
||||
|
||||
private ReferenceService referenceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
referenceService = new ReferenceService();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReferenceContextReturnsEmptyForNull() {
|
||||
assertEquals("", referenceService.buildReferenceContext(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReferenceContextReturnsEmptyForEmptyList() {
|
||||
assertEquals("", referenceService.buildReferenceContext(List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReferenceContextIncludesImageReferences() {
|
||||
Reference img = Reference.builder()
|
||||
.type(Reference.ReferenceType.IMAGE)
|
||||
.url("data:image/png;base64,abc123")
|
||||
.altText("Screenshot")
|
||||
.build();
|
||||
|
||||
String context = referenceService.buildReferenceContext(List.of(img));
|
||||
|
||||
assertTrue(context.contains("IMAGE"));
|
||||
assertTrue(context.contains("data:image/png;base64,abc123"));
|
||||
assertTrue(context.contains("Screenshot"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReferenceContextExcludesAltTextWhenNull() {
|
||||
Reference img = Reference.builder()
|
||||
.type(Reference.ReferenceType.IMAGE)
|
||||
.url("data:image/png;base64,abc")
|
||||
.build();
|
||||
|
||||
String context = referenceService.buildReferenceContext(List.of(img));
|
||||
|
||||
assertTrue(context.contains("IMAGE"));
|
||||
assertFalse(context.contains("(null)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReferenceContextIncludesMultipleReferences() {
|
||||
Reference ref1 = Reference.builder()
|
||||
.type(Reference.ReferenceType.IMAGE)
|
||||
.url("data:img1")
|
||||
.altText("First")
|
||||
.build();
|
||||
Reference ref2 = Reference.builder()
|
||||
.type(Reference.ReferenceType.IMAGE)
|
||||
.url("data:img2")
|
||||
.altText("Second")
|
||||
.build();
|
||||
|
||||
String context = referenceService.buildReferenceContext(List.of(ref1, ref2));
|
||||
|
||||
assertTrue(context.contains("[1]"));
|
||||
assertTrue(context.contains("[2]"));
|
||||
assertTrue(context.contains("First"));
|
||||
assertTrue(context.contains("Second"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReferenceContextHandlesBlankAltText() {
|
||||
Reference ref = Reference.builder()
|
||||
.type(Reference.ReferenceType.IMAGE)
|
||||
.url("data:img")
|
||||
.altText(" ")
|
||||
.build();
|
||||
|
||||
String context = referenceService.buildReferenceContext(List.of(ref));
|
||||
|
||||
assertTrue(context.contains("IMAGE"));
|
||||
assertFalse(context.contains("( )"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.indie.service.llm;
|
||||
|
||||
import com.indie.support.FakeLLMProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class LLMFactoryTest {
|
||||
|
||||
private FakeLLMProvider gemini;
|
||||
private FakeLLMProvider openai;
|
||||
private List<LLMProvider> providers;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
gemini = new FakeLLMProvider("gemini");
|
||||
openai = new FakeLLMProvider("openai");
|
||||
providers = List.of(gemini, openai);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getActiveProviderReturnsConfiguredProvider() {
|
||||
LLMFactory factory = new LLMFactory(providers, "gemini", "openai");
|
||||
factory.init();
|
||||
|
||||
LLMProvider active = factory.getActiveProvider();
|
||||
assertEquals("gemini", active.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getActiveProviderThrowsWhenNotFound() {
|
||||
LLMFactory factory = new LLMFactory(providers, "nonexistent", "openai");
|
||||
factory.init();
|
||||
|
||||
assertThrows(IllegalStateException.class, factory::getActiveProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getActiveProviderThrowsWhenNoProviders() {
|
||||
LLMFactory factory = new LLMFactory(List.of(), "gemini", "openai");
|
||||
factory.init();
|
||||
|
||||
assertThrows(IllegalStateException.class, factory::getActiveProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFallbackProviderReturnsConfiguredFallback() {
|
||||
LLMFactory factory = new LLMFactory(providers, "gemini", "openai");
|
||||
factory.init();
|
||||
|
||||
assertTrue(factory.getFallbackProvider().isPresent());
|
||||
assertEquals("openai", factory.getFallbackProvider().get().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFallbackProviderReturnsEmptyWhenNotFound() {
|
||||
LLMFactory factory = new LLMFactory(providers, "gemini", "nonexistent");
|
||||
factory.init();
|
||||
|
||||
assertTrue(factory.getFallbackProvider().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFallbackProviderReturnsEmptyWhenNotConfigured() {
|
||||
LLMFactory factory = new LLMFactory(providers, "gemini", "");
|
||||
factory.init();
|
||||
|
||||
assertTrue(factory.getFallbackProvider().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeAndFallbackCanBeSameProvider() {
|
||||
LLMFactory factory = new LLMFactory(providers, "gemini", "gemini");
|
||||
factory.init();
|
||||
|
||||
assertEquals("gemini", factory.getActiveProvider().getName());
|
||||
assertTrue(factory.getFallbackProvider().isPresent());
|
||||
assertEquals("gemini", factory.getFallbackProvider().get().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void initLogsAvailableProviders() {
|
||||
LLMFactory factory = new LLMFactory(providers, "gemini", "openai");
|
||||
assertDoesNotThrow(factory::init);
|
||||
}
|
||||
}
|
||||
48
backend/src/test/java/com/indie/support/FakeLLMProvider.java
Normal file
48
backend/src/test/java/com/indie/support/FakeLLMProvider.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.indie.support;
|
||||
|
||||
import com.indie.dto.LLMResult;
|
||||
import com.indie.dto.LLMOptions;
|
||||
import com.indie.dto.Reference;
|
||||
import com.indie.service.llm.LLMProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FakeLLMProvider implements LLMProvider {
|
||||
|
||||
private final String name;
|
||||
private String content;
|
||||
private RuntimeException failure;
|
||||
|
||||
public FakeLLMProvider(String name) {
|
||||
this.name = name;
|
||||
this.content = "{\"pages\": [{\"title\": \"Home\", \"slug\": \"home\", \"components\": [{\"type\": \"hero\"}]}]}";
|
||||
}
|
||||
|
||||
public FakeLLMProvider withContent(String content) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setFailure(RuntimeException failure) {
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LLMResult generate(String systemPrompt, String userPrompt, List<Reference> references, LLMOptions options) {
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
return LLMResult.builder()
|
||||
.content(content)
|
||||
.promptTokens(10)
|
||||
.completionTokens(20)
|
||||
.model(name)
|
||||
.latencyMs(100)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user