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.
This commit is contained in:
mateaix 2026-09-29 05:38:26 -04:00 committed by GitHub
parent 6f13a70709
commit 6647312162
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 25 additions and 4 deletions

View File

@ -2126,7 +2126,7 @@ public class ChatController {
List<MessageContentPart> parts) { List<MessageContentPart> parts) {
if (parts == null) return; if (parts == null) return;
for (MessageContentPart part : parts) { 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()); Path file = uploadLocationResolver.resolveExistingFile(origin, part.getStoredName());
if (file == null) { if (file == null) {
throw new IllegalArgumentException("聊天附件不存在或不属于当前会话"); throw new IllegalArgumentException("聊天附件不存在或不属于当前会话");

View File

@ -64,7 +64,7 @@ class BaseAgentImageCaptionPersistTest {
h.agent.callBuildCurrentTurn(msg, "报错的行号是多少"); h.agent.callBuildCurrentTurn(msg, "报错的行号是多少");
ArgumentCaptor<String> question = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> 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(), assertEquals("报错的行号是多少", question.getValue(),
"the user's question (text part) must drive a context-aware caption"); "the user's question (text part) must drive a context-aware caption");
} }
@ -81,7 +81,7 @@ class BaseAgentImageCaptionPersistTest {
h.agent.callBuildCurrentTurn(msg, "[图片]"); h.agent.callBuildCurrentTurn(msg, "[图片]");
ArgumentCaptor<String> question = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> 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(), assertEquals(null, question.getValue(),
"no text part → null question → caption falls back to generic description"); "no text part → null question → caption falls back to generic description");
} }
@ -115,7 +115,7 @@ class BaseAgentImageCaptionPersistTest {
MultimodalRoutingDecision.sidecar(sidecar, MultimodalRoutingDecision.sidecar(sidecar,
EnumSet.of(ModelCapabilityService.Modality.VISION), EnumSet.of(ModelCapabilityService.Modality.VISION),
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)); .thenReturn(MediaCaptionService.CaptionResult.success(DESCRIPTION, 12L, false));
TestAgent agent = new TestAgent(conv); TestAgent agent = new TestAgent(conv);

View File

@ -20,6 +20,27 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
class ChatControllerUploadPathTest { 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 @Test
@DisplayName("default root: returns chat-uploads/{convId}/{storedName}, not absolute") @DisplayName("default root: returns chat-uploads/{convId}/{storedName}, not absolute")
void defaultRootIsRelative() { void defaultRootIsRelative() {