fix(personal): harden pilot smoke and private storage
This commit is contained in:
+20
-1
@@ -6,8 +6,9 @@ import org.dromara.aihr.personal.domain.PersonalAssistantDto.SearchHitResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.SearchScope;
|
||||
import org.dromara.aihr.personal.support.PersonalKnowledgeProperties;
|
||||
import org.dromara.aihr.personal.support.PersonalOwner;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
@@ -58,6 +59,7 @@ public class PersonalRetrievalService {
|
||||
if (!validated.personalScope()) {
|
||||
return List.of();
|
||||
}
|
||||
requireOwnedReadyItems(owner, validated.itemIds());
|
||||
|
||||
List<SearchHitResponse> fulltext;
|
||||
try {
|
||||
@@ -235,6 +237,23 @@ public class PersonalRetrievalService {
|
||||
args.addAll(itemIds);
|
||||
}
|
||||
|
||||
private void requireOwnedReadyItems(PersonalOwner owner, List<Long> itemIds) {
|
||||
if (itemIds.isEmpty()) return;
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
select count(*) from aihr_personal_item
|
||||
where binary tenant_id = binary ? and owner_user_id = ? and status = 'READY' and id in (
|
||||
""");
|
||||
sql.append("?,".repeat(itemIds.size()));
|
||||
sql.setLength(sql.length() - 1);
|
||||
sql.append(")");
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(owner.tenantId());
|
||||
args.add(owner.userId());
|
||||
args.addAll(itemIds);
|
||||
Long count = jdbcTemplate.queryForObject(sql.toString(), Long.class, args.toArray());
|
||||
if (count == null || count != itemIds.size()) throw new ServiceException("PERSONAL_ITEM_NOT_FOUND");
|
||||
}
|
||||
|
||||
private static void validateDates(LocalDate dateFrom, LocalDate dateTo) {
|
||||
if (dateFrom != null && dateTo != null && dateFrom.isAfter(dateTo)) {
|
||||
throw new IllegalArgumentException("PERSONAL_SEARCH_DATE_INVALID");
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ public class PersonalKnowledgeProperties {
|
||||
private String qdrantApiKey = "";
|
||||
private int qdrantTimeoutSeconds = 3;
|
||||
private int retrievalLimit = 10;
|
||||
/** Optional sys_oss_config key. Blank selects the system default client. */
|
||||
private String ossConfigKey = "";
|
||||
/** Dedicated private sys_oss_config key; never fall back to the shared public bucket. */
|
||||
private String ossConfigKey = "personal-minio";
|
||||
private int chunkSize = 800;
|
||||
private int chunkOverlap = 120;
|
||||
private int parsingLeaseMinutes = 15;
|
||||
|
||||
+6
@@ -52,6 +52,12 @@ import static org.mockito.Mockito.when;
|
||||
@Tag("dev")
|
||||
class PersonalIngestionServiceTest {
|
||||
|
||||
@Test
|
||||
void personalStorageDefaultsToDedicatedPrivateConfigKey() {
|
||||
PersonalKnowledgeProperties properties = new PersonalKnowledgeProperties();
|
||||
assertEquals("personal-minio", properties.getOssConfigKey());
|
||||
}
|
||||
|
||||
private static final PersonalOwner OWNER = new PersonalOwner("000000", 101L, "ext-101");
|
||||
|
||||
@Test
|
||||
|
||||
+46
@@ -7,6 +7,7 @@ import org.dromara.aihr.personal.service.PersonalRetrievalService;
|
||||
import org.dromara.aihr.personal.service.PersonalVectorStore;
|
||||
import org.dromara.aihr.personal.support.PersonalKnowledgeProperties;
|
||||
import org.dromara.aihr.personal.support.PersonalOwner;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -31,6 +32,7 @@ class PersonalRetrievalServiceTest {
|
||||
@Test
|
||||
void fulltextSqlPreservesOwnerJoinFiltersDatesAndPreparedItemIds() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(2L);
|
||||
when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))).thenReturn(List.of());
|
||||
PersonalRetrievalService service = service(jdbc, query -> java.util.Optional.empty(), vectorStore(List.of()));
|
||||
|
||||
@@ -57,6 +59,7 @@ class PersonalRetrievalServiceTest {
|
||||
@Test
|
||||
void excludesPersonalScopeAndRejectsInvalidRequests() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(1L);
|
||||
PersonalRetrievalService service = service(jdbc, query -> java.util.Optional.empty(), vectorStore(List.of()));
|
||||
PersonalOwner owner = new PersonalOwner("t", 1, null);
|
||||
assertTrue(service.search(owner, new PersonalSearchRequest("q", List.of(SearchScope.ENTERPRISE), null, null, null, 10)).isEmpty());
|
||||
@@ -71,6 +74,7 @@ class PersonalRetrievalServiceTest {
|
||||
@Test
|
||||
void vectorHydrationRechecksOwnerAndReadyAndRrfDedupesDeterministically() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(1L);
|
||||
SearchHitResponse lexical = hit("10", "Lexical");
|
||||
SearchHitResponse vector = hit("20", "Vector");
|
||||
when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class)))
|
||||
@@ -110,6 +114,48 @@ class PersonalRetrievalServiceTest {
|
||||
verify(jdbc, times(1)).query(anyString(), any(RowMapper.class), any(Object[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void itemIdsFailClosedWhenAnyRequestedItemIsNotOwnedAndReady() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(1L);
|
||||
PersonalRetrievalService service = service(jdbc, query -> Optional.empty(), vectorStore(List.of()));
|
||||
|
||||
ServiceException error = assertThrows(ServiceException.class, () -> service.search(
|
||||
new PersonalOwner("tenant-a", 7, null),
|
||||
new PersonalSearchRequest("隔离", List.of(SearchScope.PERSONAL), null, null, List.of(10L, 11L), 10)));
|
||||
|
||||
assertEquals("PERSONAL_ITEM_NOT_FOUND", error.getMessage());
|
||||
verify(jdbc, never()).query(anyString(), any(RowMapper.class), any(Object[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameItemIdIsVisibleToOwnerAAndRejectedForOwnerB() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(1L, 0L);
|
||||
when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))).thenReturn(List.of());
|
||||
PersonalRetrievalService service = service(jdbc, query -> Optional.empty(), vectorStore(List.of()));
|
||||
PersonalSearchRequest request = new PersonalSearchRequest(
|
||||
"隔离", List.of(SearchScope.PERSONAL), null, null, List.of(10L), 10);
|
||||
|
||||
assertDoesNotThrow(() -> service.search(new PersonalOwner("tenant-a", 7, null), request));
|
||||
ServiceException error = assertThrows(ServiceException.class,
|
||||
() -> service.search(new PersonalOwner("tenant-a", 8, null), request));
|
||||
|
||||
assertEquals("PERSONAL_ITEM_NOT_FOUND", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchWithoutItemIdsRemainsOwnerScopedWithoutPreflightLookup() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))).thenReturn(List.of());
|
||||
PersonalRetrievalService service = service(jdbc, query -> Optional.empty(), vectorStore(List.of()));
|
||||
|
||||
assertDoesNotThrow(() -> service.search(new PersonalOwner("tenant-a", 7, null),
|
||||
new PersonalSearchRequest("隔离", List.of(SearchScope.PERSONAL), null, null, null, 10)));
|
||||
|
||||
verify(jdbc, never()).queryForObject(anyString(), eq(Long.class), any(Object[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fulltextFailureStillAllowsScopedVectorHydration() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ class PersonalSpaceServiceTest {
|
||||
assertEquals(1000, properties.getMaxItems());
|
||||
assertEquals(5, properties.getDownloadUrlMinutes());
|
||||
assertEquals("aihr_personal_knowledge", properties.getQdrantCollection());
|
||||
assertEquals("", properties.getOssConfigKey());
|
||||
assertEquals("personal-minio", properties.getOssConfigKey());
|
||||
assertEquals(800, properties.getChunkSize());
|
||||
assertEquals(120, properties.getChunkOverlap());
|
||||
assertEquals(15, properties.getParsingLeaseMinutes());
|
||||
|
||||
Reference in New Issue
Block a user