Skip to content

Commit 50cc21e

Browse files
committed
chore: add prompt serialization test cases
1 parent bed7202 commit 50cc21e

10 files changed

Lines changed: 412 additions & 1 deletion

File tree

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Langfuse API credentials for integration tests.
2+
# Copy this file to .env and fill in your values.
3+
#
4+
# Required prompts in the Langfuse project:
5+
# - "test-chat-prompt" : chat type, at least one message with role + content
6+
# - "test-text-prompt" : text type, non-empty prompt text
7+
LANGFUSE_PUBLIC_KEY=pk-lf-...
8+
LANGFUSE_SECRET_KEY=sk-lf-...
9+
LANGFUSE_HOST=https://cloud.langfuse.com

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ build/
3636
.vscode/
3737

3838
### Mac OS ###
39-
.DS_Store
39+
.DS_Store
40+
41+
### Environment ###
42+
.env

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,41 @@ try {
4848
}
4949
```
5050

51+
## Testing
52+
53+
### Unit tests
54+
55+
Unit tests (deserialization, query string mapping) run without any credentials:
56+
57+
```bash
58+
mvn test
59+
```
60+
61+
### Integration tests
62+
63+
Integration tests connect to a real Langfuse project. They require credentials and are excluded from `mvn test`.
64+
65+
1. Copy `.env.example` to `.env` and fill in your API keys:
66+
```bash
67+
cp .env.example .env
68+
```
69+
70+
2. Ensure your Langfuse project contains the following prompts:
71+
- `test-chat-prompt` — chat type, at least one message with `role` and `content`
72+
- `test-text-prompt` — text type, non-empty prompt text
73+
74+
3. Run all tests (unit + integration):
75+
```bash
76+
mvn verify
77+
```
78+
79+
Or run only integration tests:
80+
```bash
81+
mvn failsafe:integration-test
82+
```
83+
84+
Integration tests skip gracefully when credentials are absent.
85+
5186
## Drafting a Release
5287

5388
Run `./mvnw release:prepare -DreleaseVersion=` with the version you want to create.

pom.xml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,37 @@
118118
</execution>
119119
</executions>
120120
</plugin>
121+
122+
<!-- Surefire: runs unit tests (mvn test), excludes integration -->
123+
<plugin>
124+
<groupId>org.apache.maven.plugins</groupId>
125+
<artifactId>maven-surefire-plugin</artifactId>
126+
<version>3.2.5</version>
127+
<configuration>
128+
<excludedGroups>integration</excludedGroups>
129+
</configuration>
130+
</plugin>
131+
132+
<!-- Failsafe: runs integration tests (mvn verify) -->
133+
<plugin>
134+
<groupId>org.apache.maven.plugins</groupId>
135+
<artifactId>maven-failsafe-plugin</artifactId>
136+
<version>3.2.5</version>
137+
<configuration>
138+
<groups>integration</groups>
139+
<includes>
140+
<include>**/*Test.java</include>
141+
</includes>
142+
</configuration>
143+
<executions>
144+
<execution>
145+
<goals>
146+
<goal>integration-test</goal>
147+
<goal>verify</goal>
148+
</goals>
149+
</execution>
150+
</executions>
151+
</plugin>
121152
</plugins>
122153
</build>
123154

@@ -157,5 +188,29 @@
157188
<artifactId>junit-jupiter-api</artifactId>
158189
<version>${junit.version}</version>
159190
</dependency>
191+
192+
<!-- JUnit Jupiter Engine (required to run tests) -->
193+
<dependency>
194+
<groupId>org.junit.jupiter</groupId>
195+
<artifactId>junit-jupiter-engine</artifactId>
196+
<version>${junit.version}</version>
197+
<scope>test</scope>
198+
</dependency>
199+
200+
<!-- AssertJ -->
201+
<dependency>
202+
<groupId>org.assertj</groupId>
203+
<artifactId>assertj-core</artifactId>
204+
<version>3.25.3</version>
205+
<scope>test</scope>
206+
</dependency>
207+
208+
<!-- dotenv-java for loading .env files -->
209+
<dependency>
210+
<groupId>io.github.cdimascio</groupId>
211+
<artifactId>dotenv-java</artifactId>
212+
<version>3.0.2</version>
213+
<scope>test</scope>
214+
</dependency>
160215
</dependencies>
161216
</project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.langfuse.client;
2+
3+
import io.github.cdimascio.dotenv.Dotenv;
4+
5+
/**
6+
* Utility for integration tests: loads Langfuse credentials from a {@code .env}
7+
* file (or system/environment variables) and builds a {@link LangfuseClient}.
8+
*/
9+
public final class TestClientFactory {
10+
11+
private static final Dotenv DOTENV = Dotenv.configure()
12+
.ignoreIfMissing()
13+
.load();
14+
15+
private TestClientFactory() {}
16+
17+
public static String getEnv(String key) {
18+
// dotenv-java checks .env first, then falls back to system env
19+
return DOTENV.get(key);
20+
}
21+
22+
public static boolean hasCredentials() {
23+
String publicKey = getEnv("LANGFUSE_PUBLIC_KEY");
24+
String secretKey = getEnv("LANGFUSE_SECRET_KEY");
25+
String host = getEnv("LANGFUSE_HOST");
26+
return publicKey != null && !publicKey.isEmpty()
27+
&& secretKey != null && !secretKey.isEmpty()
28+
&& host != null && !host.isEmpty();
29+
}
30+
31+
public static LangfuseClient createClient() {
32+
return LangfuseClient.builder()
33+
.credentials(getEnv("LANGFUSE_PUBLIC_KEY"), getEnv("LANGFUSE_SECRET_KEY"))
34+
.url(getEnv("LANGFUSE_HOST"))
35+
.build();
36+
}
37+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.langfuse.client.deserialization;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.langfuse.client.core.ObjectMappers;
6+
import com.langfuse.client.resources.prompts.types.ChatMessage;
7+
import com.langfuse.client.resources.prompts.types.ChatMessageWithPlaceholders;
8+
import com.langfuse.client.resources.prompts.types.ChatPrompt;
9+
import com.langfuse.client.resources.prompts.types.Prompt;
10+
import java.io.InputStream;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.List;
13+
import java.util.Optional;
14+
import org.junit.jupiter.api.Test;
15+
16+
/**
17+
* Unit tests for chat prompt deserialization through the same code path as
18+
* {@code PromptsClient.get()} — i.e. {@code ObjectMappers.JSON_MAPPER.readValue(..., Prompt.class)}.
19+
*
20+
* <p>These tests use a hardcoded JSON fixture and require no credentials or network access.
21+
* They exist primarily to catch the regression where {@code ChatMessage.getRole()} and
22+
* {@code ChatMessage.getContent()} return {@code null} after deserialization.
23+
*/
24+
class ChatPromptDeserializationTest {
25+
26+
private static final String FIXTURE = "/fixtures/chat_prompt_response.json";
27+
28+
@Test
29+
void deserializeChatPrompt_hasCorrectTopLevelFields() throws Exception {
30+
Prompt prompt = deserializeFixture();
31+
32+
assertThat(prompt.isChat()).isTrue();
33+
assertThat(prompt.isText()).isFalse();
34+
35+
ChatPrompt chatPrompt = prompt.getChat().orElseThrow();
36+
assertThat(chatPrompt.getName()).isEqualTo("test-chat-prompt");
37+
assertThat(chatPrompt.getVersion()).isEqualTo(1);
38+
assertThat(chatPrompt.getLabels()).contains("production");
39+
}
40+
41+
@Test
42+
void deserializeChatPrompt_chatMessageHasNonNullRoleAndContent() throws Exception {
43+
Prompt prompt = deserializeFixture();
44+
ChatPrompt chatPrompt = prompt.getChat().orElseThrow();
45+
List<ChatMessageWithPlaceholders> messages = chatPrompt.getPrompt();
46+
47+
assertThat(messages).hasSize(2);
48+
49+
// First entry — chat message (no "type" discriminator in real API response)
50+
assertThat(messages.get(0).isChatmessage()).isTrue();
51+
Optional<ChatMessage> system = messages.get(0).getChatmessage();
52+
assertThat(system).isPresent();
53+
assertThat(system.get().getRole()).isNotNull().isEqualTo("system");
54+
assertThat(system.get().getContent()).isNotNull().isEqualTo("Hello World");
55+
56+
// Second entry — placeholder (has "type": "placeholder")
57+
assertThat(messages.get(1).isPlaceholder()).isTrue();
58+
assertThat(messages.get(1).getPlaceholder()).isPresent();
59+
assertThat(messages.get(1).getPlaceholder().get().getName()).isEqualTo("username");
60+
}
61+
62+
@Test
63+
void chatMessageRoundTripSerialization() throws Exception {
64+
Prompt prompt = deserializeFixture();
65+
ChatPrompt chatPrompt = prompt.getChat().orElseThrow();
66+
ChatMessage original = chatPrompt.getPrompt().get(0).getChatmessage().orElseThrow();
67+
68+
// Serialize to JSON and back
69+
String json = ObjectMappers.JSON_MAPPER.writeValueAsString(original);
70+
ChatMessage roundTripped = ObjectMappers.JSON_MAPPER.readValue(json, ChatMessage.class);
71+
72+
assertThat(roundTripped.getRole()).isEqualTo(original.getRole());
73+
assertThat(roundTripped.getContent()).isEqualTo(original.getContent());
74+
}
75+
76+
private Prompt deserializeFixture() throws Exception {
77+
try (InputStream is = getClass().getResourceAsStream(FIXTURE)) {
78+
assertThat(is).as("Fixture %s must be on the classpath", FIXTURE).isNotNull();
79+
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
80+
return ObjectMappers.JSON_MAPPER.readValue(json, Prompt.class);
81+
}
82+
}
83+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.langfuse.client.deserialization;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.langfuse.client.core.ObjectMappers;
6+
import com.langfuse.client.resources.prompts.types.Prompt;
7+
import com.langfuse.client.resources.prompts.types.TextPrompt;
8+
import java.io.InputStream;
9+
import java.nio.charset.StandardCharsets;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* Unit tests for text prompt deserialization through
14+
* {@code ObjectMappers.JSON_MAPPER.readValue(..., Prompt.class)}.
15+
*
16+
* <p>Uses a hardcoded JSON fixture — no credentials or network access required.
17+
*/
18+
class TextPromptDeserializationTest {
19+
20+
private static final String FIXTURE = "/fixtures/text_prompt_response.json";
21+
22+
@Test
23+
void deserializeTextPrompt_hasCorrectTopLevelFields() throws Exception {
24+
Prompt prompt = deserializeFixture();
25+
26+
assertThat(prompt.isText()).isTrue();
27+
assertThat(prompt.isChat()).isFalse();
28+
29+
TextPrompt textPrompt = prompt.getText().orElseThrow();
30+
assertThat(textPrompt.getName()).isEqualTo("test-text-prompt");
31+
assertThat(textPrompt.getVersion()).isEqualTo(1);
32+
}
33+
34+
@Test
35+
void deserializeTextPrompt_promptIsNonNullAndNonEmpty() throws Exception {
36+
Prompt prompt = deserializeFixture();
37+
TextPrompt textPrompt = prompt.getText().orElseThrow();
38+
39+
assertThat(textPrompt.getPrompt()).isNotNull().isNotEmpty();
40+
assertThat(textPrompt.getPrompt()).isEqualTo("Hello World");
41+
}
42+
43+
private Prompt deserializeFixture() throws Exception {
44+
try (InputStream is = getClass().getResourceAsStream(FIXTURE)) {
45+
assertThat(is).as("Fixture %s must be on the classpath", FIXTURE).isNotNull();
46+
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
47+
return ObjectMappers.JSON_MAPPER.readValue(json, Prompt.class);
48+
}
49+
}
50+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.langfuse.client.integration;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.langfuse.client.LangfuseClient;
6+
import com.langfuse.client.TestClientFactory;
7+
import com.langfuse.client.resources.prompts.types.ChatMessage;
8+
import com.langfuse.client.resources.prompts.types.ChatMessageWithPlaceholders;
9+
import com.langfuse.client.resources.prompts.types.ChatPrompt;
10+
import com.langfuse.client.resources.prompts.types.Prompt;
11+
import com.langfuse.client.resources.prompts.types.TextPrompt;
12+
import java.util.List;
13+
import java.util.Optional;
14+
import org.junit.jupiter.api.Assumptions;
15+
import org.junit.jupiter.api.BeforeAll;
16+
import org.junit.jupiter.api.Tag;
17+
import org.junit.jupiter.api.Test;
18+
19+
/**
20+
* Integration tests that connect to a real Langfuse project.
21+
*
22+
* <p><b>Prerequisites:</b> The target Langfuse project must contain:
23+
* <ul>
24+
* <li>{@code test-chat-prompt} — a chat-type prompt with at least one message
25+
* that has both {@code role} and {@code content} set.</li>
26+
* <li>{@code test-text-prompt} — a text-type prompt with non-empty text.</li>
27+
* </ul>
28+
*
29+
* <p>Credentials are loaded from a {@code .env} file (or system env vars).
30+
* See {@code .env.example} for the required variables. When credentials are
31+
* absent the tests are skipped rather than failed.
32+
*/
33+
@Tag("integration")
34+
class PromptIntegrationTest {
35+
36+
private static LangfuseClient client;
37+
38+
@BeforeAll
39+
static void setUp() {
40+
Assumptions.assumeTrue(
41+
TestClientFactory.hasCredentials(),
42+
"Skipping integration tests — LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_HOST not set");
43+
client = TestClientFactory.createClient();
44+
}
45+
46+
@Test
47+
void fetchChatPrompt_messagesHaveRoleAndContent() {
48+
Prompt prompt = client.prompts().get("test-chat-prompt");
49+
50+
assertThat(prompt.isChat())
51+
.as("Expected a chat prompt")
52+
.isTrue();
53+
54+
ChatPrompt chatPrompt = prompt.getChat().orElseThrow();
55+
List<ChatMessageWithPlaceholders> messages = chatPrompt.getPrompt();
56+
57+
assertThat(messages).isNotEmpty();
58+
59+
for (ChatMessageWithPlaceholders msg : messages) {
60+
Optional<ChatMessage> chatMessage = msg.getChatmessage();
61+
assertThat(chatMessage)
62+
.as("Each prompt entry should be a chat message")
63+
.isPresent();
64+
assertThat(chatMessage.get().getRole())
65+
.as("role must not be null")
66+
.isNotNull()
67+
.isNotEmpty();
68+
assertThat(chatMessage.get().getContent())
69+
.as("content must not be null")
70+
.isNotNull()
71+
.isNotEmpty();
72+
}
73+
}
74+
75+
@Test
76+
void fetchTextPrompt_promptIsNonEmpty() {
77+
Prompt prompt = client.prompts().get("test-text-prompt");
78+
79+
assertThat(prompt.isText())
80+
.as("Expected a text prompt")
81+
.isTrue();
82+
83+
TextPrompt textPrompt = prompt.getText().orElseThrow();
84+
85+
assertThat(textPrompt.getPrompt())
86+
.as("prompt text must not be null or empty")
87+
.isNotNull()
88+
.isNotEmpty();
89+
}
90+
}

0 commit comments

Comments
 (0)