From 6647312162a5233a239fff57e6d52ab2be43c21e Mon Sep 17 00:00:00 2001 From: mateaix <57164338+mateaix@users.noreply.github.com> Date: Tue, 29 Sep 2026 05:38:26 -0400 Subject: [PATCH] fix(chat): preserve directory attachments during upload validation Follow up #652 by excluding blank stored names from upload validation and covering directory attachments with a regression test. Update caption persistence mocks for the origin-aware method. Verified with 148 targeted tests. --- .../vip/mate/channel/web/ChatController.java | 2 +- .../BaseAgentImageCaptionPersistTest.java | 6 +++--- .../web/ChatControllerUploadPathTest.java | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java index 3abe79ed..bc4936f6 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java @@ -2126,7 +2126,7 @@ public class ChatController { List parts) { if (parts == null) return; for (MessageContentPart part : parts) { - if (part == null || part.getStoredName() == null) continue; + if (part == null || part.getStoredName() == null || part.getStoredName().isBlank()) continue; Path file = uploadLocationResolver.resolveExistingFile(origin, part.getStoredName()); if (file == null) { throw new IllegalArgumentException("聊天附件不存在或不属于当前会话"); diff --git a/mateclaw-server/src/test/java/vip/mate/agent/BaseAgentImageCaptionPersistTest.java b/mateclaw-server/src/test/java/vip/mate/agent/BaseAgentImageCaptionPersistTest.java index 50a92988..79d7b599 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/BaseAgentImageCaptionPersistTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/BaseAgentImageCaptionPersistTest.java @@ -64,7 +64,7 @@ class BaseAgentImageCaptionPersistTest { h.agent.callBuildCurrentTurn(msg, "报错的行号是多少"); ArgumentCaptor question = ArgumentCaptor.forClass(String.class); - verify(h.caption).caption(any(), any(), any(), question.capture()); + verify(h.caption).caption(any(), any(), any(), question.capture(), any()); assertEquals("报错的行号是多少", question.getValue(), "the user's question (text part) must drive a context-aware caption"); } @@ -81,7 +81,7 @@ class BaseAgentImageCaptionPersistTest { h.agent.callBuildCurrentTurn(msg, "[图片]"); ArgumentCaptor question = ArgumentCaptor.forClass(String.class); - verify(h.caption).caption(any(), any(), any(), question.capture()); + verify(h.caption).caption(any(), any(), any(), question.capture(), any()); assertEquals(null, question.getValue(), "no text part → null question → caption falls back to generic description"); } @@ -115,7 +115,7 @@ class BaseAgentImageCaptionPersistTest { MultimodalRoutingDecision.sidecar(sidecar, EnumSet.of(ModelCapabilityService.Modality.VISION), EnumSet.of(ModelCapabilityService.Modality.VISION))); - when(caption.caption(any(), any(), any(), any())) + when(caption.caption(any(), any(), any(), any(), any())) .thenReturn(MediaCaptionService.CaptionResult.success(DESCRIPTION, 12L, false)); TestAgent agent = new TestAgent(conv); diff --git a/mateclaw-server/src/test/java/vip/mate/channel/web/ChatControllerUploadPathTest.java b/mateclaw-server/src/test/java/vip/mate/channel/web/ChatControllerUploadPathTest.java index d8dc6b9c..e31a95bb 100644 --- a/mateclaw-server/src/test/java/vip/mate/channel/web/ChatControllerUploadPathTest.java +++ b/mateclaw-server/src/test/java/vip/mate/channel/web/ChatControllerUploadPathTest.java @@ -20,6 +20,27 @@ import static org.assertj.core.api.Assertions.assertThat; */ class ChatControllerUploadPathTest { + @Test + void directoryAttachmentWithEmptyStoredNameIsNotAnUpload() throws Exception { + ChatController controller = org.mockito.Mockito.mock(ChatController.class, + org.mockito.Mockito.CALLS_REAL_METHODS); + var resolver = org.mockito.Mockito.mock( + vip.mate.workspace.core.service.ChatUploadLocationResolver.class); + org.springframework.test.util.ReflectionTestUtils.setField(controller, "uploadLocationResolver", resolver); + var part = new vip.mate.workspace.conversation.model.MessageContentPart(); + part.setType("file"); + part.setContentType("inode/directory"); + part.setStoredName(""); + part.setPath("/workspace/project"); + var origin = vip.mate.agent.context.ChatOrigin.web("conv", "alice", 7L, null); + + org.junit.jupiter.api.Assertions.assertDoesNotThrow(() -> + org.springframework.test.util.ReflectionTestUtils.invokeMethod(controller, + "validateUploadedParts", "conv", origin, java.util.List.of(part))); + assertThat(part.getPath()).isEqualTo("/workspace/project"); + org.mockito.Mockito.verifyNoInteractions(resolver); + } + @Test @DisplayName("default root: returns chat-uploads/{convId}/{storedName}, not absolute") void defaultRootIsRelative() {