From f24e1617640df958f68a090e2e857ccd9134dd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Sch=C3=B6dl?= Date: Thu, 18 Jun 2026 10:25:40 +0200 Subject: [PATCH 1/4] Regenerate clients for RecipeRequest language field - Add optional `language` enum (EN/DE/HU) to RecipeRequest spec - Spring API forwards client UI language to GenAI service - All clients regenerated: TS api.ts, Kotlin RecipeRequest, Python clients - Web client sends active UI language with each recipe request - Add i18n test suite and update GeneratePage/ProfilePage tests Co-Authored-By: Claude Opus 4.8 (1M context) --- api/openapi.yaml | 4 ++ openapitools.json | 2 +- .../models/__init__.py | 2 + .../models/recipe_request.py | 19 ++++++++ .../models/recipe_request_language.py | 10 +++++ .../py-help-service/client/pyproject.toml | 5 +-- .../models/__init__.py | 2 + .../models/recipe_request.py | 19 ++++++++ .../models/recipe_request_language.py | 10 +++++ .../py-recipe-service/client/pyproject.toml | 5 +-- services/spring-api/.openapi-generator/FILES | 1 - .../org/openapitools/api/AIApiController.kt | 9 +++- .../org/openapitools/model/RecipeRequest.kt | 28 +++++++++++- .../src/main/resources/openapi.yaml | 4 ++ .../kotlin/org/openapitools/api/AIApiTest.kt | 40 +++++++++++++++++ web-client/src/api.ts | 5 +++ web-client/src/i18n.ts | 7 ++- web-client/src/locales/de.ts | 1 + web-client/src/locales/en.ts | 1 + web-client/src/locales/hu.ts | 1 + web-client/src/pages/GeneratePage.tsx | 3 +- web-client/src/pages/ProfilePage.tsx | 17 ++++--- web-client/tests/i18n.test.ts | 22 +++++++++ web-client/tests/pages/GeneratePage.test.tsx | 16 +++++++ web-client/tests/pages/ProfilePage.test.tsx | 45 +++++++++++++++++++ 25 files changed, 260 insertions(+), 18 deletions(-) create mode 100644 services/py-help-service/client/cooking_assistant_api_client/models/recipe_request_language.py create mode 100644 services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request_language.py create mode 100644 web-client/tests/i18n.test.ts diff --git a/api/openapi.yaml b/api/openapi.yaml index af4c2ef..063da0c 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -662,6 +662,10 @@ components: type: string minLength: 1 maxLength: 4096 + language: + type: string + enum: [EN, DE, HU] + description: Active UI language; generated recipe content is written in it RecipeRequestForwarded: type: object diff --git a/openapitools.json b/openapitools.json index c27f4c1..6707fd3 100644 --- a/openapitools.json +++ b/openapitools.json @@ -1,3 +1,3 @@ { - "generator-cli": { "version": "7.21.0" } + "generator-cli": { "version": "7.22.0" } } diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/__init__.py b/services/py-help-service/client/cooking_assistant_api_client/models/__init__.py index de49581..3976dfb 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/models/__init__.py +++ b/services/py-help-service/client/cooking_assistant_api_client/models/__init__.py @@ -12,6 +12,7 @@ from .recipe_nutrients import RecipeNutrients from .recipe_request import RecipeRequest from .recipe_request_forwarded import RecipeRequestForwarded +from .recipe_request_language import RecipeRequestLanguage from .recipe_update import RecipeUpdate from .user_credentials import UserCredentials from .user_preferences import UserPreferences @@ -32,6 +33,7 @@ "RecipeNutrients", "RecipeRequest", "RecipeRequestForwarded", + "RecipeRequestLanguage", "RecipeUpdate", "UserCredentials", "UserPreferences", diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request.py b/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request.py index 729b48e..09652ea 100644 --- a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request.py +++ b/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request.py @@ -5,6 +5,9 @@ from attrs import define as _attrs_define +from ..models.recipe_request_language import RecipeRequestLanguage +from ..types import UNSET, Unset + T = TypeVar("T", bound="RecipeRequest") @@ -13,13 +16,19 @@ class RecipeRequest: """ Attributes: prompt (str): + language (RecipeRequestLanguage | Unset): Active UI language; generated recipe content is written in it """ prompt: str + language: RecipeRequestLanguage | Unset = UNSET def to_dict(self) -> dict[str, Any]: prompt = self.prompt + language: str | Unset = UNSET + if not isinstance(self.language, Unset): + language = self.language.value + field_dict: dict[str, Any] = {} field_dict.update( @@ -27,6 +36,8 @@ def to_dict(self) -> dict[str, Any]: "prompt": prompt, } ) + if language is not UNSET: + field_dict["language"] = language return field_dict @@ -35,8 +46,16 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) prompt = d.pop("prompt") + _language = d.pop("language", UNSET) + language: RecipeRequestLanguage | Unset + if isinstance(_language, Unset): + language = UNSET + else: + language = RecipeRequestLanguage(_language) + recipe_request = cls( prompt=prompt, + language=language, ) return recipe_request diff --git a/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request_language.py b/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request_language.py new file mode 100644 index 0000000..1a4b904 --- /dev/null +++ b/services/py-help-service/client/cooking_assistant_api_client/models/recipe_request_language.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class RecipeRequestLanguage(str, Enum): + DE = "DE" + EN = "EN" + HU = "HU" + + def __str__(self) -> str: + return str(self.value) diff --git a/services/py-help-service/client/pyproject.toml b/services/py-help-service/client/pyproject.toml index a3c918b..a2f2f62 100644 --- a/services/py-help-service/client/pyproject.toml +++ b/services/py-help-service/client/pyproject.toml @@ -10,10 +10,9 @@ packages = [ include = ["cooking_assistant_api_client/py.typed"] [tool.poetry.dependencies] -python = "^3.10" -httpx = ">=0.23.0,<0.29.0" +python = "^3.11" +httpx = ">=0.23.1,<0.29.0" attrs = ">=22.2.0" -python-dateutil = "^2.8.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py b/services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py index de49581..3976dfb 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py +++ b/services/py-recipe-service/client/cooking_assistant_api_client/models/__init__.py @@ -12,6 +12,7 @@ from .recipe_nutrients import RecipeNutrients from .recipe_request import RecipeRequest from .recipe_request_forwarded import RecipeRequestForwarded +from .recipe_request_language import RecipeRequestLanguage from .recipe_update import RecipeUpdate from .user_credentials import UserCredentials from .user_preferences import UserPreferences @@ -32,6 +33,7 @@ "RecipeNutrients", "RecipeRequest", "RecipeRequestForwarded", + "RecipeRequestLanguage", "RecipeUpdate", "UserCredentials", "UserPreferences", diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request.py b/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request.py index 729b48e..09652ea 100644 --- a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request.py +++ b/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request.py @@ -5,6 +5,9 @@ from attrs import define as _attrs_define +from ..models.recipe_request_language import RecipeRequestLanguage +from ..types import UNSET, Unset + T = TypeVar("T", bound="RecipeRequest") @@ -13,13 +16,19 @@ class RecipeRequest: """ Attributes: prompt (str): + language (RecipeRequestLanguage | Unset): Active UI language; generated recipe content is written in it """ prompt: str + language: RecipeRequestLanguage | Unset = UNSET def to_dict(self) -> dict[str, Any]: prompt = self.prompt + language: str | Unset = UNSET + if not isinstance(self.language, Unset): + language = self.language.value + field_dict: dict[str, Any] = {} field_dict.update( @@ -27,6 +36,8 @@ def to_dict(self) -> dict[str, Any]: "prompt": prompt, } ) + if language is not UNSET: + field_dict["language"] = language return field_dict @@ -35,8 +46,16 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) prompt = d.pop("prompt") + _language = d.pop("language", UNSET) + language: RecipeRequestLanguage | Unset + if isinstance(_language, Unset): + language = UNSET + else: + language = RecipeRequestLanguage(_language) + recipe_request = cls( prompt=prompt, + language=language, ) return recipe_request diff --git a/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request_language.py b/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request_language.py new file mode 100644 index 0000000..1a4b904 --- /dev/null +++ b/services/py-recipe-service/client/cooking_assistant_api_client/models/recipe_request_language.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class RecipeRequestLanguage(str, Enum): + DE = "DE" + EN = "EN" + HU = "HU" + + def __str__(self) -> str: + return str(self.value) diff --git a/services/py-recipe-service/client/pyproject.toml b/services/py-recipe-service/client/pyproject.toml index a3c918b..a2f2f62 100644 --- a/services/py-recipe-service/client/pyproject.toml +++ b/services/py-recipe-service/client/pyproject.toml @@ -10,10 +10,9 @@ packages = [ include = ["cooking_assistant_api_client/py.typed"] [tool.poetry.dependencies] -python = "^3.10" -httpx = ">=0.23.0,<0.29.0" +python = "^3.11" +httpx = ">=0.23.1,<0.29.0" attrs = ">=22.2.0" -python-dateutil = "^2.8.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/services/spring-api/.openapi-generator/FILES b/services/spring-api/.openapi-generator/FILES index a333abc..aec68ff 100644 --- a/services/spring-api/.openapi-generator/FILES +++ b/services/spring-api/.openapi-generator/FILES @@ -3,7 +3,6 @@ gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties gradlew gradlew.bat -pom.xml settings.gradle src/main/kotlin/org/openapitools/SpringDocConfiguration.kt src/main/kotlin/org/openapitools/api/AIApi.kt diff --git a/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt b/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt index 68b7a43..c8359b3 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/api/AIApiController.kt @@ -55,12 +55,19 @@ class AIApiController( @Valid recipeRequest: RecipeRequest, ): ResponseEntity> { val user = userRepository.findByUsername(currentUsername()).orElseThrow() + // the client sends the active UI language so generated recipes match what the user sees, + // even when no language is stored in their preferences + val profile = user.toProfile() + val profileWithLanguage = + recipeRequest.language?.let { + profile.copy(preferences = profile.preferences.copy(language = it)) + } ?: profile val recipes = aiRecipeWebClient .post() .uri("/ai/recipes") .contentType(MediaType.APPLICATION_JSON) - .bodyValue(mapOf("profile" to user.toProfile(), "prompt" to recipeRequest.prompt)) + .bodyValue(mapOf("profile" to profileWithLanguage, "prompt" to recipeRequest.prompt)) .retrieve() .bodyToMono(object : ParameterizedTypeReference>() {}) .timeout(aiTimeout) diff --git a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeRequest.kt b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeRequest.kt index 3cb45f0..52c750a 100644 --- a/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeRequest.kt +++ b/services/spring-api/src/main/kotlin/org/openapitools/model/RecipeRequest.kt @@ -1,6 +1,8 @@ package org.openapitools.model +import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.Valid import jakarta.validation.constraints.DecimalMax @@ -16,9 +18,33 @@ import java.util.Objects /** * * @param prompt + * @param language Active UI language; generated recipe content is written in it */ data class RecipeRequest( @get:Size(min = 1, max = 4096) @Schema(example = "null", required = true, description = "") @get:JsonProperty("prompt", required = true) val prompt: kotlin.String, -) + @Schema(example = "null", description = "Active UI language; generated recipe content is written in it") + @get:JsonProperty("language") val language: RecipeRequest.Language? = null, +) { + /** + * Active UI language; generated recipe content is written in it + * Values: EN,DE,HU + */ + enum class Language( + @get:JsonValue val value: kotlin.String, + ) { + EN("EN"), + DE("DE"), + HU("HU"), + ; + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Language = + values().firstOrNull { it -> it.value == value } + ?: throw IllegalArgumentException("Unexpected value '$value' for enum 'Language'") + } + } +} diff --git a/services/spring-api/src/main/resources/openapi.yaml b/services/spring-api/src/main/resources/openapi.yaml index 1bd4eca..50e3e87 100644 --- a/services/spring-api/src/main/resources/openapi.yaml +++ b/services/spring-api/src/main/resources/openapi.yaml @@ -568,6 +568,10 @@ components: type: string minLength: 1 maxLength: 2000 + language: + type: string + enum: [EN, DE, HU] + description: Active UI language; generated recipe content is written in it RecipeRequestForwarded: type: object diff --git a/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt b/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt index 57d360b..e5f610e 100644 --- a/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt +++ b/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt @@ -7,11 +7,15 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mockito import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever import org.openapitools.model.HelpResponse import org.openapitools.model.RecipeIngredient import org.openapitools.model.RecipeInput import org.openapitools.model.RecipeNutrients +import org.openapitools.model.UserPreferences +import org.openapitools.model.UserProfile +import kotlin.test.assertEquals import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean @@ -184,6 +188,42 @@ class AIApiTest : ApiTestBase() { .andExpect(jsonPath("$[0].title").value("AI Pasta")) } + // Stubs the recipe client and captures the body forwarded to the GenAI service. + private fun captureForwardedRecipeBody(): org.mockito.kotlin.KArgumentCaptor { + val bodyCaptor = argumentCaptor() + whenever( + mockWebClients.recipeClient + .post() + .uri("/ai/recipes") + .contentType(any()) + .bodyValue(bodyCaptor.capture()) + .retrieve() + .bodyToMono(any>>()), + ).thenReturn(Mono.just(listOf(sampleRecipeInput))) + return bodyCaptor + } + + private fun forwardedProfile(bodyCaptor: org.mockito.kotlin.KArgumentCaptor): UserProfile { + @Suppress("UNCHECKED_CAST") + val forwarded = bodyCaptor.firstValue as Map + return forwarded["profile"] as UserProfile + } + + @Test + fun `ai recipes - forwards the request language so recipes match the UI`() { + val token = register() + val bodyCaptor = captureForwardedRecipeBody() + mockMvc + .perform( + post("/api/v1/ai/recipes") + .header("Authorization", "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + .content("""{"prompt":"Give me a pasta recipe","language":"DE"}"""), + ).andExpect(status().isOk) + + assertEquals(UserPreferences.Language.DE, forwardedProfile(bodyCaptor).preferences.language) + } + @Test fun `ai recipes - service returns null returns 502`() { val token = register() diff --git a/web-client/src/api.ts b/web-client/src/api.ts index c529fcd..3c793df 100644 --- a/web-client/src/api.ts +++ b/web-client/src/api.ts @@ -784,6 +784,11 @@ export interface components { }; RecipeRequest: { prompt: string; + /** + * @description Active UI language; generated recipe content is written in it + * @enum {string} + */ + language?: "EN" | "DE" | "HU"; }; RecipeRequestForwarded: { profile: components["schemas"]["UserProfile"]; diff --git a/web-client/src/i18n.ts b/web-client/src/i18n.ts index 77286aa..ecdde75 100644 --- a/web-client/src/i18n.ts +++ b/web-client/src/i18n.ts @@ -14,7 +14,7 @@ const FALLBACK_LANGUAGE: Language = 'EN' const isSupported = (lang: string): lang is Language => (SUPPORTED_LANGUAGES as readonly string[]).includes(lang) -function detectInitialLanguage(): Language { +export function detectInitialLanguage(): Language { for (const tag of navigator.languages ?? [navigator.language]) { const base = tag.toUpperCase().split('-')[0] if (isSupported(base)) return base @@ -31,6 +31,11 @@ i18n.use(initReactI18next).init({ returnNull: false, }) +export function currentLanguage(): Language { + const lang = i18n.resolvedLanguage + return lang && isSupported(lang) ? lang : FALLBACK_LANGUAGE +} + export function applyUserLanguage(lang: string | undefined | null) { if (lang && isSupported(lang) && i18n.resolvedLanguage !== lang) { void i18n.changeLanguage(lang) diff --git a/web-client/src/locales/de.ts b/web-client/src/locales/de.ts index be9adac..60e99a1 100644 --- a/web-client/src/locales/de.ts +++ b/web-client/src/locales/de.ts @@ -15,6 +15,7 @@ export const DE = { logout: 'Abmelden', preferences: 'Einstellungen', language: 'Sprache', + detectLanguage: 'Automatisch', english: 'English', german: 'Deutsch', hungarian: 'Magyar', diff --git a/web-client/src/locales/en.ts b/web-client/src/locales/en.ts index 5d8a75c..c042c5a 100644 --- a/web-client/src/locales/en.ts +++ b/web-client/src/locales/en.ts @@ -15,6 +15,7 @@ export const EN = { logout: 'Log out', preferences: 'Preferences', language: 'Language', + detectLanguage: 'Auto', english: 'English', german: 'Deutsch', hungarian: 'Magyar', diff --git a/web-client/src/locales/hu.ts b/web-client/src/locales/hu.ts index d7f0e74..b905be3 100644 --- a/web-client/src/locales/hu.ts +++ b/web-client/src/locales/hu.ts @@ -15,6 +15,7 @@ export const HU = { logout: 'Kijelentkezés', preferences: 'Beállítások', language: 'Nyelv', + detectLanguage: 'Auto', english: 'English', german: 'Deutsch', hungarian: 'Magyar', diff --git a/web-client/src/pages/GeneratePage.tsx b/web-client/src/pages/GeneratePage.tsx index c376a52..d3ccd49 100644 --- a/web-client/src/pages/GeneratePage.tsx +++ b/web-client/src/pages/GeneratePage.tsx @@ -12,6 +12,7 @@ import {localizeTagLabel} from '../locales/recipeTagLabels' import {usePressPulse} from '../usePressPulse' import {errorMessage} from '../apiError' import {SessionExpiredError, useApi} from '../useApi' +import {currentLanguage} from '../i18n' // id is stored on the recipe once it is saved type Recipe = components['schemas']['RecipeInput'] & { id?: number } @@ -76,7 +77,7 @@ export function GenerateFlow() { try { const tagLabels = selectedTags.map((id) => tagsById.get(id)?.label).filter(Boolean) const fullPrompt = tagLabels.length > 0 ? `${prompt}\n\nPreferences: ${tagLabels.join(', ')}` : prompt - const body: RecipeRequest = {prompt: fullPrompt} + const body: RecipeRequest = {prompt: fullPrompt, language: currentLanguage()} const response = await apiFetch('/ai/recipes', { method: 'POST', headers: {'content-type': 'application/json'}, diff --git a/web-client/src/pages/ProfilePage.tsx b/web-client/src/pages/ProfilePage.tsx index 473f0fc..02914ea 100644 --- a/web-client/src/pages/ProfilePage.tsx +++ b/web-client/src/pages/ProfilePage.tsx @@ -15,20 +15,24 @@ import { usePressPulse } from '../usePressPulse' import { usePrefsAutosave } from '../usePrefsAutosave' import { errorMessage } from '../apiError' import { SessionExpiredError, useApi } from '../useApi' +import { detectInitialLanguage } from '../i18n' type UserProfile = components['schemas']['UserProfile'] type UserProfileUpdate = components['schemas']['UserProfileUpdate'] type Language = NonNullable +type LanguageSetting = Language | 'detect' + const LANGUAGE_OPTIONS = [ + { code: 'detect', labelKey: 'profile.detectLanguage' }, { code: 'EN', labelKey: 'profile.english' }, { code: 'DE', labelKey: 'profile.german' }, { code: 'HU', labelKey: 'profile.hungarian' }, -] as const satisfies readonly { code: Language; labelKey: string }[] +] as const satisfies readonly { code: LanguageSetting; labelKey: string }[] // un-trimmed preferences as held by the inputs -type PrefsDraft = { language: Language; aboutMe: string[]; diet: string[]; allergies: string[] } +type PrefsDraft = { language: LanguageSetting; aboutMe: string[]; diet: string[]; allergies: string[] } const trimList = (xs: string[]) => xs.map((x) => x.trim()).filter((x) => x !== '') @@ -53,7 +57,7 @@ export function ProfilePage() { const newUsername = usernameDraft ?? username ?? '' const [newPassword, setNewPassword] = useState('') const [repeatNewPassword, setRepeatNewPassword] = useState('') - const [language, setLanguage] = useState('EN') + const [language, setLanguage] = useState('detect') const [aboutMe, setAboutMe] = useState([]) const [diet, setDiet] = useState(['']) const [allergies, setAllergies] = useState(['', '']) @@ -78,7 +82,7 @@ export function ProfilePage() { const data = (await res.json()) as UserProfile if (cancelled) return const prefs = data.preferences ?? {} - setLanguage(prefs.language ?? 'EN') + setLanguage(prefs.language ?? 'detect') setAboutMe(prefs.aboutMe ?? []) setDiet(prefs.diet?.length ? prefs.diet : ['']) setAllergies(prefs.allergies?.length ? prefs.allergies : ['', '']) @@ -117,7 +121,7 @@ export function ProfilePage() { updateProfile( { preferences: { - language: draft.language, + language: draft.language === 'detect' ? undefined : draft.language, diet: trimList(draft.diet), allergies: trimList(draft.allergies), aboutMe: trimList(draft.aboutMe), @@ -289,12 +293,13 @@ export function ProfilePage() { From 933866757bee0b8acac6391ba420d9fc70b91778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Sch=C3=B6dl?= Date: Thu, 18 Jun 2026 17:04:46 +0200 Subject: [PATCH 4/4] Fix AIApiTest to parse forwarded body as JSON string The recipe controller serializes the GenAI request body to a JSON string via objectMapper.writeValueAsString, so the test helper must deserialize it rather than casting the captured value to a Map. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/test/kotlin/org/openapitools/api/AIApiTest.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt b/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt index ccd928c..7b0055a 100644 --- a/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt +++ b/services/spring-api/src/test/kotlin/org/openapitools/api/AIApiTest.kt @@ -14,6 +14,7 @@ import org.openapitools.model.Language import org.openapitools.model.RecipeIngredient import org.openapitools.model.RecipeInput import org.openapitools.model.RecipeNutrients +import org.openapitools.model.RecipeRequestForwarded import org.openapitools.model.UserProfile import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.TestConfiguration @@ -26,6 +27,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPat import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.reactive.function.client.WebClient import reactor.core.publisher.Mono +import tools.jackson.databind.ObjectMapper import java.math.BigDecimal import kotlin.test.assertEquals @@ -44,6 +46,8 @@ class AIApiTest : ApiTestBase() { @Autowired lateinit var mockWebClients: MockWebClients + @Autowired lateinit var objectMapper: ObjectMapper + @BeforeEach fun resetMocks() { Mockito.reset(mockWebClients.helpClient, mockWebClients.recipeClient) @@ -203,11 +207,8 @@ class AIApiTest : ApiTestBase() { return bodyCaptor } - private fun forwardedProfile(bodyCaptor: org.mockito.kotlin.KArgumentCaptor): UserProfile { - @Suppress("UNCHECKED_CAST") - val forwarded = bodyCaptor.firstValue as Map - return forwarded["profile"] as UserProfile - } + private fun forwardedProfile(bodyCaptor: org.mockito.kotlin.KArgumentCaptor): UserProfile = + objectMapper.readValue(bodyCaptor.firstValue as String, RecipeRequestForwarded::class.java).profile @Test fun `ai recipes - forwards the request language so recipes match the UI`() {