Add last inference to RecipeDraftView.

This commit is contained in:
Jesse Brault 2026-01-28 14:00:48 -06:00
parent d61fb97dbb
commit 77b94e6988
4 changed files with 84 additions and 11 deletions

View File

@ -6,6 +6,8 @@ import app.mealsmadeeasy.api.ai.OcrService;
import app.mealsmadeeasy.api.auth.AuthService;
import app.mealsmadeeasy.api.auth.LoginException;
import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
import app.mealsmadeeasy.api.recipe.job.RecipeInferJobHandler;
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService;
@ -14,17 +16,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.MimeTypeUtils;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThrows;
@ -52,6 +58,12 @@ public class RecipeDraftsControllerIntegrationTests {
@Autowired
private ObjectMapper objectMapper;
@MockitoBean
private OcrService ocrService;
@MockitoBean
private InferenceService inferenceService;
private static final String TEST_PASSWORD = "test";
private User seedUser() {
@ -177,12 +189,6 @@ public class RecipeDraftsControllerIntegrationTests {
@Nested
public class AiDraftTestsWithMocks {
@MockitoBean
private OcrService ocrService;
@MockitoBean
private InferenceService inferenceService;
@Test
public void whenAiDraft_returnsDraft() throws Exception {
final User owner = seedUser();
@ -190,7 +196,7 @@ public class RecipeDraftsControllerIntegrationTests {
"sourceFile",
"recipe.jpeg",
MimeTypeUtils.IMAGE_JPEG_VALUE,
AiDraftTestsWithMocks.class.getResourceAsStream("recipe.jpeg")
this.getClass().getResourceAsStream("/recipe.jpeg")
);
mockMvc.perform(
multipart("/recipe-drafts/ai")
@ -205,6 +211,46 @@ public class RecipeDraftsControllerIntegrationTests {
.andExpect(jsonPath("$.owner.username", is(owner.getUsername())));
}
@Test
public void pollingAfterAiDraft_returnsDraftWithInference() throws Exception {
Mockito.when(ocrService.readMarkdown(Mockito.any())).thenReturn("# Recipe");
Mockito.when(inferenceService.extract(
Mockito.any(),
Mockito.any(),
Mockito.any()
)).thenReturn(new RecipeInferJobHandler.RecipeExtraction("Recipe", List.of()));
final User owner = seedUser();
final MockMultipartFile sourceFile = new MockMultipartFile(
"sourceFile",
"recipe.jpeg",
MimeTypeUtils.IMAGE_JPEG_VALUE,
this.getClass().getResourceAsStream("/recipe.jpeg")
);
final MvcResult result = mockMvc.perform(
multipart("/recipe-drafts/ai")
.file(sourceFile)
.param("sourceFileName", sourceFile.getOriginalFilename())
.header("Authorization", "Bearer " + getAccessToken(owner))
).andReturn();
final UUID draftId = objectMapper.readValue(
result.getResponse().getContentAsString(),
RecipeDraftView.class
).id();
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
mockMvc.perform(
get("/recipe-drafts/{id}", draftId)
.header("Authorization", "Bearer " + getAccessToken(owner))
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.state", is(RecipeDraft.State.ENTER_DATA.toString())))
.andExpect(jsonPath("$.lastInference", is(notNullValue())))
.andExpect(jsonPath("$.lastInference.inferredAt", is(notNullValue())))
.andExpect(jsonPath("$.lastInference.title", is("Recipe")))
.andExpect(jsonPath("$.lastInference.rawText", is("# Recipe")));
});
}
}
private static RecipeDraftUpdateBody getTestRecipeDraftUpdateBody() {

View File

@ -10,6 +10,8 @@ import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import java.util.Comparator;
@Component
@RequiredArgsConstructor
public class RecipeDraftToViewConverter {
@ -20,6 +22,17 @@ public class RecipeDraftToViewConverter {
final @Nullable ImageView mainImageView = recipeDraft.getMainImage() != null
? this.imageToViewConverter.convert(recipeDraft.getMainImage(), viewer, false)
: null;
final @Nullable RecipeDraftView.RecipeDraftInferenceView lastInference = recipeDraft.getInferences() != null
? recipeDraft.getInferences().stream()
.max(Comparator.comparing(RecipeDraft.RecipeDraftInference::getInferredAt))
.map(inference -> RecipeDraftView.RecipeDraftInferenceView.builder()
.inferredAt(inference.getInferredAt())
.title(inference.getTitle())
.rawText(inference.getRawText())
.build()
)
.orElse(null)
: null;
return RecipeDraftView.builder()
.id(recipeDraft.getId())
.created(recipeDraft.getCreated())
@ -34,6 +47,7 @@ public class RecipeDraftToViewConverter {
.ingredients(recipeDraft.getIngredients())
.owner(UserInfoView.from(recipeDraft.getOwner()))
.mainImage(mainImageView)
.lastInference(lastInference)
.build();
}

View File

@ -10,6 +10,7 @@ import app.mealsmadeeasy.api.recipe.RecipeDraft;
import app.mealsmadeeasy.api.recipe.RecipeService;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,12 +33,14 @@ import java.util.List;
@RequiredArgsConstructor
public class RecipeInferJobHandler implements JobHandler<RecipeInferJobPayload> {
private record RecipeExtraction(
@ApiStatus.Internal
public record RecipeExtraction(
@JsonProperty(required = true) String title,
@JsonProperty(required = true) List<IngredientExtraction> ingredients
) {}
private record IngredientExtraction(
@ApiStatus.Internal
public record IngredientExtraction(
String amount,
@JsonProperty(required = true) String name,
String notes

View File

@ -24,5 +24,15 @@ public record RecipeDraftView(
@Nullable String rawText,
@Nullable List<RecipeDraft.IngredientDraft> ingredients,
UserInfoView owner,
@Nullable ImageView mainImage
) {}
@Nullable ImageView mainImage,
@Nullable RecipeDraftInferenceView lastInference
) {
@Builder
public record RecipeDraftInferenceView(
OffsetDateTime inferredAt,
String title,
String rawText
) {}
}