diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java index ddce36886..be277f48c 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; /** * OpenAI Reasoning Detail DTO (OpenRouter specific for Gemini). @@ -90,4 +91,26 @@ public Integer getIndex() { public void setIndex(Integer index) { this.index = index; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpenAIReasoningDetail)) { + return false; + } + OpenAIReasoningDetail that = (OpenAIReasoningDetail) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.type, that.type) + && Objects.equals(this.data, that.data) + && Objects.equals(this.text, that.text) + && Objects.equals(this.format, that.format) + && Objects.equals(this.index, that.index); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.type, this.data, this.text, this.format, this.index); + } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java index 02f102b5a..0d8f8cf78 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java @@ -56,6 +56,23 @@ public Source getSource() { return source; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AudioBlock)) { + return false; + } + AudioBlock that = (AudioBlock) o; + return Objects.equals(this.source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(this.source); + } + /** * Creates a new builder for constructing AudioBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java b/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java index 35408aec3..beafd3c15 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java @@ -75,6 +75,24 @@ public String getData() { return data; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Base64Source)) { + return false; + } + Base64Source that = (Base64Source) o; + return Objects.equals(this.mediaType, that.mediaType) + && Objects.equals(this.data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(this.mediaType, this.data); + } + /** * Creates a new builder for constructing Base64Source instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java index b57866f02..c1f41ba07 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java @@ -97,6 +97,25 @@ public Integer getMaxPixels() { return maxPixels; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ImageBlock)) { + return false; + } + ImageBlock that = (ImageBlock) o; + return Objects.equals(this.source, that.source) + && Objects.equals(this.minPixels, that.minPixels) + && Objects.equals(this.maxPixels, that.maxPixels); + } + + @Override + public int hashCode() { + return Objects.hash(this.source, this.minPixels, this.maxPixels); + } + /** * Creates a new builder for constructing ImageBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java b/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java index f5c7e291c..3924bfc40 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java @@ -109,6 +109,29 @@ private Msg( this.timestamp = timestamp; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof Msg)) { + return false; + } else { + Msg that = (Msg) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && this.role == that.role + && Objects.equals(this.content, that.content) + && Objects.equals(this.metadata, that.metadata) + && Objects.equals(this.timestamp, that.timestamp); + } + } + + @Override + public int hashCode() { + return Objects.hash( + this.id, this.name, this.role, this.content, this.metadata, this.timestamp); + } + /** * Creates a new message builder with a randomly generated ID. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java index 622be0492..85d9e837d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; /** * Represents plain text content in a message. @@ -56,6 +57,23 @@ public String toString() { return text; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof TextBlock)) { + return false; + } else { + TextBlock that = (TextBlock) o; + return Objects.equals(this.text, that.text); + } + } + + @Override + public int hashCode() { + return Objects.hash(this.text); + } + /** * Creates a new builder for constructing TextBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java index fc337fdb3..71699695c 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Represents reasoning or thinking content in a message. @@ -81,6 +82,24 @@ public Map getMetadata() { return metadata; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ThinkingBlock)) { + return false; + } + ThinkingBlock that = (ThinkingBlock) o; + return Objects.equals(this.thinking, that.thinking) + && Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.thinking, this.metadata); + } + /** * Creates a new builder for constructing ThinkingBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java index f1caf26c3..fe85b4ce1 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java @@ -22,6 +22,7 @@ import java.beans.Transient; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Represents the result of a tool execution. @@ -289,6 +290,26 @@ public ToolResultBlock withIdAndName(String id, String name) { return new ToolResultBlock(id, name, this.output, this.metadata); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ToolResultBlock)) { + return false; + } + ToolResultBlock that = (ToolResultBlock) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && Objects.equals(this.output, that.output) + && Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.output, this.metadata); + } + /** * Creates a new builder for constructing ToolResultBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java index f83d79249..051600b2b 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Represents a tool use request within a message. @@ -144,6 +145,27 @@ public Map getMetadata() { return metadata; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ToolUseBlock)) { + return false; + } + ToolUseBlock that = (ToolUseBlock) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && Objects.equals(this.input, that.input) + && Objects.equals(this.content, that.content) + && Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.input, this.content, this.metadata); + } + /** * Creates a new builder for constructing a ToolUseBlock. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java b/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java index 35d25e2b2..00c0a48b5 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java @@ -60,6 +60,23 @@ public String getUrl() { return url; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof URLSource)) { + return false; + } + URLSource that = (URLSource) o; + return Objects.equals(this.url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(this.url); + } + /** * Creates a new builder for constructing URLSource instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java index 09dc51cc9..a3733e112 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; /** * Represents video content in a message. @@ -135,6 +136,34 @@ public Integer getTotalPixels() { return totalPixels; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VideoBlock)) { + return false; + } + VideoBlock that = (VideoBlock) o; + return Objects.equals(this.source, that.source) + && Objects.equals(this.fps, that.fps) + && Objects.equals(this.maxFrames, that.maxFrames) + && Objects.equals(this.minPixels, that.minPixels) + && Objects.equals(this.maxPixels, that.maxPixels) + && Objects.equals(this.totalPixels, that.totalPixels); + } + + @Override + public int hashCode() { + return Objects.hash( + this.source, + this.fps, + this.maxFrames, + this.minPixels, + this.maxPixels, + this.totalPixels); + } + /** * Creates a new builder for constructing VideoBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java b/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java index 556eedde7..ee37dfed0 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java @@ -15,6 +15,8 @@ */ package io.agentscope.core.model; +import java.util.Objects; + /** * Represents token usage information for chat completion responses. * @@ -76,6 +78,25 @@ public double getTime() { return time; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ChatUsage)) { + return false; + } + ChatUsage that = (ChatUsage) o; + return this.inputTokens == that.inputTokens + && this.outputTokens == that.outputTokens + && Double.compare(this.time, that.time) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(this.inputTokens, this.outputTokens, this.time); + } + /** * Creates a new builder for ChatUsage. * diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java index d40226e16..f56fbdaf1 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java @@ -16,6 +16,7 @@ package io.agentscope.core.formatter.openai.dto; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -168,4 +169,31 @@ void testDeserializeVllmStreamingChunk() { assertEquals("Step 1: parse the input...", delta.getReasoningContent()); } + + @Test + @DisplayName("Should compare reasoning details by value") + void testReasoningDetailEqualsAndHashCodeUseValues() { + OpenAIReasoningDetail first = reasoningDetail("reasoning.text"); + OpenAIReasoningDetail second = reasoningDetail("reasoning.text"); + OpenAIReasoningDetail different = reasoningDetail("reasoning.summary"); + + assertEquals(first, first); + assertNotEquals(first, null); + assertNotEquals(first, "other"); + assertEquals(first, second); + assertEquals(second, first); + assertEquals(first.hashCode(), second.hashCode()); + assertNotEquals(first, different); + } + + private OpenAIReasoningDetail reasoningDetail(String type) { + OpenAIReasoningDetail detail = new OpenAIReasoningDetail(); + detail.setId("reasoning-1"); + detail.setType(type); + detail.setData("encrypted-data"); + detail.setText("visible reasoning"); + detail.setFormat("openai-responses-v1"); + detail.setIndex(0); + return detail; + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java b/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java index 23fafec95..6a6cb5dbd 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java @@ -16,9 +16,11 @@ package io.agentscope.core.message; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -205,4 +207,167 @@ void testTextContentConvenienceMethod() { assertTrue(msg.getFirstContentBlock() instanceof TextBlock); assertEquals("Hello World", ((TextBlock) msg.getFirstContentBlock()).getText()); } + + @Test + void testContentBlocksEqualsAndHashCodeUseValues() { + assertValueEquality( + TextBlock.builder().text("hello").build(), + TextBlock.builder().text("hello").build(), + TextBlock.builder().text("different").build()); + + assertValueEquality( + URLSource.builder().url("https://example.com/image.png").build(), + URLSource.builder().url("https://example.com/image.png").build(), + URLSource.builder().url("https://example.com/other.png").build()); + + assertValueEquality( + Base64Source.builder().mediaType("image/png").data("abc").build(), + Base64Source.builder().mediaType("image/png").data("abc").build(), + Base64Source.builder().mediaType("image/png").data("def").build()); + + assertValueEquality( + ImageBlock.builder() + .source(URLSource.builder().url("https://example.com/image.png").build()) + .minPixels(128) + .maxPixels(512) + .build(), + ImageBlock.builder() + .source(URLSource.builder().url("https://example.com/image.png").build()) + .minPixels(128) + .maxPixels(512) + .build(), + ImageBlock.builder() + .source(URLSource.builder().url("https://example.com/image.png").build()) + .minPixels(128) + .maxPixels(1024) + .build()); + + assertValueEquality( + AudioBlock.builder() + .source(Base64Source.builder().mediaType("audio/mp3").data("abc").build()) + .build(), + AudioBlock.builder() + .source(Base64Source.builder().mediaType("audio/mp3").data("abc").build()) + .build(), + AudioBlock.builder() + .source(Base64Source.builder().mediaType("audio/mp3").data("def").build()) + .build()); + + assertValueEquality( + VideoBlock.builder() + .source(URLSource.builder().url("https://example.com/video.mp4").build()) + .fps(2.0f) + .maxFrames(12) + .minPixels(128) + .maxPixels(512) + .totalPixels(2048) + .build(), + VideoBlock.builder() + .source(URLSource.builder().url("https://example.com/video.mp4").build()) + .fps(2.0f) + .maxFrames(12) + .minPixels(128) + .maxPixels(512) + .totalPixels(2048) + .build(), + VideoBlock.builder() + .source(URLSource.builder().url("https://example.com/video.mp4").build()) + .fps(2.0f) + .maxFrames(12) + .minPixels(128) + .maxPixels(512) + .totalPixels(4096) + .build()); + + assertValueEquality( + ThinkingBlock.builder().thinking("thinking").metadata(Map.of("k", "v")).build(), + ThinkingBlock.builder().thinking("thinking").metadata(Map.of("k", "v")).build(), + ThinkingBlock.builder() + .thinking("thinking") + .metadata(Map.of("k", "different")) + .build()); + + assertValueEquality( + ToolUseBlock.builder() + .id("call-1") + .name("search") + .input(Map.of("query", "agent")) + .content("{\"query\":\"agent\"}") + .metadata(Map.of("signature", "sig-1")) + .build(), + ToolUseBlock.builder() + .id("call-1") + .name("search") + .input(Map.of("query", "agent")) + .content("{\"query\":\"agent\"}") + .metadata(Map.of("signature", "sig-1")) + .build(), + ToolUseBlock.builder() + .id("call-1") + .name("search") + .input(Map.of("query", "agent")) + .content("{\"query\":\"agent\"}") + .metadata(Map.of("signature", "sig-2")) + .build()); + + assertValueEquality( + ToolResultBlock.of( + "call-1", + "search", + List.of(TextBlock.builder().text("result").build()), + Map.of("status", "ok")), + ToolResultBlock.of( + "call-1", + "search", + List.of(TextBlock.builder().text("result").build()), + Map.of("status", "ok")), + ToolResultBlock.of( + "call-1", + "search", + List.of(TextBlock.builder().text("result").build()), + Map.of("status", "different"))); + } + + @Test + void testMsgEqualsAndHashCodeUseValues() { + Msg first = + Msg.builder() + .id("msg-1") + .name("assistant") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata(Map.of("k", "v")) + .timestamp("2026-05-13 11:00:00.000") + .build(); + Msg second = + Msg.builder() + .id("msg-1") + .name("assistant") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata(Map.of("k", "v")) + .timestamp("2026-05-13 11:00:00.000") + .build(); + Msg different = + Msg.builder() + .id("msg-1") + .name("assistant") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata(Map.of("k", "v")) + .timestamp("2026-05-13 11:00:01.000") + .build(); + + assertValueEquality(first, second, different); + } + + private void assertValueEquality(Object first, Object second, Object different) { + assertEquals(first, first); + assertNotEquals(first, null); + assertNotEquals(first, "other"); + assertEquals(first, second); + assertEquals(second, first); + assertEquals(first.hashCode(), second.hashCode()); + assertNotEquals(first, different); + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java index 617d77890..79ae13185 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -248,6 +249,21 @@ void testGetModelName() { assertEquals("gpt-4", model.getModelName()); } + @Test + @DisplayName("Should compare chat usage by value") + void testChatUsageEqualsAndHashCodeUseValues() { + ChatUsage first = ChatUsage.builder().inputTokens(10).outputTokens(20).time(1.5).build(); + ChatUsage second = ChatUsage.builder().inputTokens(10).outputTokens(20).time(1.5).build(); + ChatUsage different = + ChatUsage.builder().inputTokens(10).outputTokens(21).time(1.5).build(); + + assertEquals(first, first); + assertNotEquals(first, null); + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + assertNotEquals(first, different); + } + @Test @DisplayName("Should build model with custom endpoint path") void testBuildModelWithEndpointPath() throws Exception { diff --git a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java index dfd10e20b..030a8cc83 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java @@ -20,11 +20,16 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.agentscope.core.formatter.openai.dto.OpenAIReasoningDetail; +import io.agentscope.core.message.MessageMetadataKeys; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ThinkingBlock; +import io.agentscope.core.model.ChatUsage; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; /** @@ -55,6 +60,133 @@ void testComputeHashSameListSameHash() { assertEquals(hash1, hash2); } + @Test + void testComputeHashEquivalentMessageLists() { + List first = + List.of( + Msg.builder() + .id("m-user-1") + .timestamp("2026-05-08 14:00:00.000") + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(), + Msg.builder() + .id("m-assistant-1") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-user-1") + .timestamp("2026-05-08 14:00:00.000") + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(), + Msg.builder() + .id("m-assistant-1") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .build()); + + String h1 = ListHashUtil.computeHash(first); + String h2 = ListHashUtil.computeHash(second); + + assertEquals(h1, h2); + } + + @Test + void testComputeHashListWithNullItem() { + List list = new ArrayList<>(); + list.add(null); + + String hash1 = ListHashUtil.computeHash(list); + String hash2 = ListHashUtil.computeHash(list); + + assertEquals(hash1, hash2); + } + + @Test + void testComputeHashEquivalentMessagesWithChatUsage() { + List first = + List.of( + Msg.builder() + .id("m-assistant-usage") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata( + Map.of( + MessageMetadataKeys.CHAT_USAGE, + ChatUsage.builder() + .inputTokens(10) + .outputTokens(20) + .time(1.5) + .build())) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-assistant-usage") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata( + Map.of( + MessageMetadataKeys.CHAT_USAGE, + ChatUsage.builder() + .inputTokens(10) + .outputTokens(20) + .time(1.5) + .build())) + .build()); + + assertEquals(ListHashUtil.computeHash(first), ListHashUtil.computeHash(second)); + } + + @Test + void testComputeHashEquivalentThinkingBlocksWithReasoningDetails() { + List first = + List.of( + Msg.builder() + .id("m-assistant-thinking") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content( + ThinkingBlock.builder() + .thinking("thinking") + .metadata( + Map.of( + ThinkingBlock + .METADATA_REASONING_DETAILS, + List.of(createReasoningDetail()))) + .build()) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-assistant-thinking") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content( + ThinkingBlock.builder() + .thinking("thinking") + .metadata( + Map.of( + ThinkingBlock + .METADATA_REASONING_DETAILS, + List.of(createReasoningDetail()))) + .build()) + .build()); + + assertEquals(ListHashUtil.computeHash(first), ListHashUtil.computeHash(second)); + } + @Test void testComputeHashListModifiedDifferentHash() { List list = createMsgList(5); @@ -205,4 +337,15 @@ private List createMsgList(int size) { } return list; } + + private OpenAIReasoningDetail createReasoningDetail() { + OpenAIReasoningDetail detail = new OpenAIReasoningDetail(); + detail.setId("reasoning-1"); + detail.setType("reasoning.text"); + detail.setData("encrypted-data"); + detail.setText("visible reasoning"); + detail.setFormat("openai-responses-v1"); + detail.setIndex(0); + return detail; + } }