feat: 完成管理端聊天工作台收口
- 新增管理端聊天工作台与会话级额外知识库持久化 - 补齐发布态聊天、历史会话只读判断与答案版本切换 - 新增 chat_round 热数据与主线消息读取支撑
This commit is contained in:
@@ -1,18 +1,29 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionSummaryCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEvent;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEventType;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatRoundRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.YearMonth;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatPersistMySqlApplyServiceTest {
|
||||
|
||||
private final ChatPersistMySqlApplyService service =
|
||||
new ChatPersistMySqlApplyService(null, null, null, null);
|
||||
new ChatPersistMySqlApplyService(null, null, null, null, new ChatJsonSupport(new ObjectMapper()));
|
||||
|
||||
@Test
|
||||
public void shouldBuildMissingSessionUpsertFromMessageMetadata() {
|
||||
@@ -69,4 +80,101 @@ public class ChatPersistMySqlApplyServiceTest {
|
||||
Assert.assertEquals("会话-202", upsert.getTitle());
|
||||
Assert.assertEquals(BigInteger.valueOf(7), upsert.getOperatorId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotDoubleCountSummaryWhenMessageEventReplayed() {
|
||||
ChatJsonSupport jsonSupport = new ChatJsonSupport(new ObjectMapper());
|
||||
FakeSessionRepository sessionRepository = new FakeSessionRepository();
|
||||
FakeLogRepository logRepository = new FakeLogRepository(jsonSupport);
|
||||
ChatPersistMySqlApplyService applyService = new ChatPersistMySqlApplyService(
|
||||
sessionRepository,
|
||||
logRepository,
|
||||
new FakeRoundRepository(),
|
||||
new FakeTableManager(),
|
||||
jsonSupport
|
||||
);
|
||||
ChatAppendMessageCommand command = new ChatAppendMessageCommand();
|
||||
command.setMessageId(BigInteger.valueOf(301));
|
||||
command.setSessionId(BigInteger.valueOf(401));
|
||||
command.setTenantId(BigInteger.ONE);
|
||||
command.setDeptId(BigInteger.ONE);
|
||||
command.setUserId(BigInteger.valueOf(7));
|
||||
command.setAssistantId(BigInteger.valueOf(8));
|
||||
command.setSenderId(BigInteger.valueOf(7));
|
||||
command.setSenderName("admin");
|
||||
command.setSenderRole("user");
|
||||
command.setContentText("第一条消息");
|
||||
command.setCreatedBy(BigInteger.valueOf(7));
|
||||
command.setCreated(new Date(4_000L));
|
||||
|
||||
ChatPersistEvent event = new ChatPersistEvent();
|
||||
event.setEventId("message-301");
|
||||
event.setEventType(ChatPersistEventType.USER_MESSAGE_APPENDED);
|
||||
event.setSessionId(command.getSessionId());
|
||||
event.setPayload(jsonSupport.toJson(command));
|
||||
|
||||
applyService.apply(List.of(event));
|
||||
applyService.apply(List.of(event));
|
||||
|
||||
Assert.assertEquals(1, sessionRepository.summaryCommands.size());
|
||||
ChatSessionSummaryCommand summaryCommand = sessionRepository.summaryCommands.get(0);
|
||||
Assert.assertEquals(1, summaryCommand.getMessageIncrement());
|
||||
Assert.assertEquals("第一条消息", summaryCommand.getLastMessagePreview());
|
||||
Assert.assertEquals(new Date(4_000L), summaryCommand.getLastMessageAt());
|
||||
Assert.assertEquals(new Date(4_000L), summaryCommand.getAccessAt());
|
||||
}
|
||||
|
||||
private static final class FakeSessionRepository extends MySqlChatSessionRepository {
|
||||
|
||||
private final List<ChatSessionSummaryCommand> summaryCommands = new ArrayList<>();
|
||||
|
||||
private FakeSessionRepository() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createOrTouchBatch(List<ChatSessionUpsertCommand> commands) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSummaries(List<ChatSessionSummaryCommand> commands) {
|
||||
summaryCommands.addAll(commands);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeLogRepository extends MySqlChatLogRepository {
|
||||
|
||||
private boolean inserted;
|
||||
|
||||
private FakeLogRepository(ChatJsonSupport jsonSupport) {
|
||||
super(null, null, jsonSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatAppendMessageCommand> appendMessages(List<ChatAppendMessageCommand> commands) {
|
||||
if (inserted) {
|
||||
return List.of();
|
||||
}
|
||||
inserted = true;
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeRoundRepository extends MySqlChatRoundRepository {
|
||||
|
||||
private FakeRoundRepository() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeTableManager extends MySqlChatLogTableManager {
|
||||
|
||||
private FakeTableManager() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureMonthTable(YearMonth month) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.chatlog.domain.command.ChatRoundSelectCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatRoundUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatRoundRecord;
|
||||
import tech.easyflow.chatlog.service.ChatRoundCommandService;
|
||||
import tech.easyflow.chatlog.service.ChatRoundQueryService;
|
||||
import tech.easyflow.chatlog.support.ChatConstants;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link ChatRoundOperateServiceImpl} 单元测试。
|
||||
*/
|
||||
public class ChatRoundOperateServiceImplTest {
|
||||
|
||||
/**
|
||||
* 切换答案版本时应精准查询目标版本,避免先加载全部版本再过滤。
|
||||
*/
|
||||
@Test
|
||||
public void selectVariantShouldReadTargetVariantDirectly() {
|
||||
FakeRoundQueryService queryService = new FakeRoundQueryService();
|
||||
queryService.round = round(BigInteger.valueOf(1001), BigInteger.valueOf(2001), 2, ChatConstants.ROUND_STATUS_READY);
|
||||
queryService.latestRound = queryService.round;
|
||||
queryService.targetVariant = message(BigInteger.valueOf(3002), 2);
|
||||
FakeRoundCommandService commandService = new FakeRoundCommandService();
|
||||
ChatRoundOperateServiceImpl service = new ChatRoundOperateServiceImpl(queryService, commandService);
|
||||
|
||||
ChatMessageRecord selected = service.selectVariant(
|
||||
BigInteger.valueOf(1001),
|
||||
BigInteger.valueOf(2001),
|
||||
2,
|
||||
BigInteger.valueOf(7)
|
||||
);
|
||||
|
||||
Assert.assertEquals(BigInteger.valueOf(3002), selected.getId());
|
||||
Assert.assertEquals(Integer.valueOf(2), selected.getSelectedVariantIndex());
|
||||
Assert.assertEquals(Integer.valueOf(2), selected.getVariantCount());
|
||||
Assert.assertEquals(Boolean.TRUE, selected.getSwitchable());
|
||||
Assert.assertEquals(1, queryService.getRoundVariantCalls);
|
||||
Assert.assertEquals(0, queryService.listRoundVariantsCalls);
|
||||
Assert.assertNotNull(commandService.selectedCommand);
|
||||
Assert.assertEquals(BigInteger.valueOf(3002), commandService.selectedCommand.getSelectedAssistantMessageId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出答案版本时应由业务层统一补齐当前选中态和可切换状态。
|
||||
*/
|
||||
@Test
|
||||
public void listVariantsShouldFillVariantMetadata() {
|
||||
FakeRoundQueryService queryService = new FakeRoundQueryService();
|
||||
queryService.round = round(BigInteger.valueOf(1001), BigInteger.valueOf(2001), 2, ChatConstants.ROUND_STATUS_READY);
|
||||
queryService.latestRound = queryService.round;
|
||||
queryService.variants = List.of(message(BigInteger.valueOf(3001), 1), message(BigInteger.valueOf(3002), 2));
|
||||
ChatRoundOperateServiceImpl service = new ChatRoundOperateServiceImpl(queryService, new FakeRoundCommandService());
|
||||
|
||||
List<ChatMessageRecord> variants = service.listVariants(BigInteger.valueOf(1001), BigInteger.valueOf(2001));
|
||||
|
||||
Assert.assertEquals(2, variants.size());
|
||||
for (ChatMessageRecord variant : variants) {
|
||||
Assert.assertEquals(Integer.valueOf(2), variant.getVariantCount());
|
||||
Assert.assertEquals(Integer.valueOf(2), variant.getSelectedVariantIndex());
|
||||
Assert.assertEquals(Boolean.TRUE, variant.getSwitchable());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已锁定轮次禁止切换,避免改变已有后续上下文。
|
||||
*/
|
||||
@Test(expected = BusinessException.class)
|
||||
public void selectVariantShouldRejectLockedRound() {
|
||||
FakeRoundQueryService queryService = new FakeRoundQueryService();
|
||||
queryService.round = round(BigInteger.valueOf(1001), BigInteger.valueOf(2001), 2, ChatConstants.ROUND_STATUS_LOCKED);
|
||||
queryService.latestRound = queryService.round;
|
||||
ChatRoundOperateServiceImpl service = new ChatRoundOperateServiceImpl(queryService, new FakeRoundCommandService());
|
||||
|
||||
service.selectVariant(BigInteger.valueOf(1001), BigInteger.valueOf(2001), 1, BigInteger.valueOf(7));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增迁移必须为热表版本切换查询补齐索引。
|
||||
*
|
||||
* @throws Exception 读取迁移文件失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void migrationShouldCreateRoundVariantIndex() throws Exception {
|
||||
String sql = Files.readString(
|
||||
resolveMigrationPath("V18__mysql_chat_round_variant_index.sql"),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
Assert.assertTrue(sql.contains("idx_chat_log_round_variant"));
|
||||
Assert.assertTrue(sql.contains("`session_id`, `round_id`, `message_kind`, `variant_index`, `created`, `id`"));
|
||||
Assert.assertFalse(sql.contains("V16__mysql_chat_round_variant"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前测试工作目录向上查找迁移文件,兼容根工程与模块工程两种运行方式。
|
||||
*
|
||||
* @param fileName 迁移文件名
|
||||
* @return 迁移文件路径
|
||||
* @throws Exception 未找到迁移文件时抛出
|
||||
*/
|
||||
private static Path resolveMigrationPath(String fileName) throws Exception {
|
||||
Path current = Path.of("").toAbsolutePath();
|
||||
while (current != null) {
|
||||
Path candidate = current.resolve(
|
||||
"easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/" + fileName
|
||||
);
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
throw new java.nio.file.NoSuchFileException(fileName);
|
||||
}
|
||||
|
||||
private static ChatRoundRecord round(BigInteger sessionId, BigInteger roundId, int selectedVariantIndex, String status) {
|
||||
ChatRoundRecord round = new ChatRoundRecord();
|
||||
round.setId(roundId);
|
||||
round.setSessionId(sessionId);
|
||||
round.setRoundNo(1);
|
||||
round.setSelectedVariantIndex(selectedVariantIndex);
|
||||
round.setVariantCount(2);
|
||||
round.setStatus(status);
|
||||
return round;
|
||||
}
|
||||
|
||||
private static ChatMessageRecord message(BigInteger id, int variantIndex) {
|
||||
ChatMessageRecord record = new ChatMessageRecord();
|
||||
record.setId(id);
|
||||
record.setSessionId(BigInteger.valueOf(1001));
|
||||
record.setRoundId(BigInteger.valueOf(2001));
|
||||
record.setVariantIndex(variantIndex);
|
||||
record.setSenderRole("assistant");
|
||||
record.setMessageKind(ChatConstants.MESSAGE_KIND_ASSISTANT_VARIANT);
|
||||
record.setContentText("答案 " + variantIndex);
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮次读服务测试替身。
|
||||
*/
|
||||
private static final class FakeRoundQueryService implements ChatRoundQueryService {
|
||||
|
||||
private ChatRoundRecord round;
|
||||
private ChatRoundRecord latestRound;
|
||||
private ChatMessageRecord targetVariant;
|
||||
private List<ChatMessageRecord> variants = List.of();
|
||||
private int getRoundVariantCalls;
|
||||
private int listRoundVariantsCalls;
|
||||
|
||||
@Override
|
||||
public ChatRoundRecord getLatestRound(BigInteger sessionId) {
|
||||
return latestRound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatRoundRecord getRound(BigInteger sessionId, BigInteger roundId) {
|
||||
return round;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessageRecord> listRoundVariants(BigInteger sessionId, BigInteger roundId) {
|
||||
listRoundVariantsCalls += 1;
|
||||
return variants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatMessageRecord getRoundVariant(BigInteger sessionId, BigInteger roundId, Integer variantIndex) {
|
||||
getRoundVariantCalls += 1;
|
||||
return targetVariant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRounds(BigInteger sessionId) {
|
||||
return round != null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮次写服务测试替身。
|
||||
*/
|
||||
private static final class FakeRoundCommandService implements ChatRoundCommandService {
|
||||
|
||||
private ChatRoundSelectCommand selectedCommand;
|
||||
|
||||
@Override
|
||||
public ChatRoundRecord createOrTouchRound(ChatRoundUpsertCommand command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectVariant(ChatRoundSelectCommand command) {
|
||||
selectedCommand = command;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.chatlog.cache.ChatHotStateService;
|
||||
import tech.easyflow.chatlog.config.ChatCacheProperties;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.YearMonth;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link ChatSessionQueryServiceImpl} 单元测试。
|
||||
*/
|
||||
public class ChatSessionQueryServiceImplTest {
|
||||
|
||||
/**
|
||||
* 会话列表必须以 MySQL 会话表为唯一权威来源,不再使用 Redis 列表索引。
|
||||
*/
|
||||
@Test
|
||||
public void pageSessionsShouldUseMysqlRepositoryAsAuthority() {
|
||||
FakeSessionRepository sessionRepository = new FakeSessionRepository();
|
||||
sessionRepository.sessions = List.of(session(BigInteger.valueOf(1001), 4));
|
||||
sessionRepository.count = 1;
|
||||
ChatSessionQueryServiceImpl service = new ChatSessionQueryServiceImpl(
|
||||
sessionRepository,
|
||||
new FakeLogRepository(),
|
||||
new FakeTableManager(List.of()),
|
||||
new FakeHotStateService()
|
||||
);
|
||||
|
||||
ChatSessionPage page = service.pageSessions(BigInteger.valueOf(7), null, new ChatPageQuery());
|
||||
|
||||
Assert.assertEquals(1, page.getTotal());
|
||||
Assert.assertEquals(1, page.getRecords().size());
|
||||
Assert.assertEquals(1, sessionRepository.countSessionsCalls);
|
||||
Assert.assertEquals(1, sessionRepository.listSessionsCalls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台消息分页必须走 MySQL 热表主线查询,并保持分页参数语义。
|
||||
*/
|
||||
@Test
|
||||
public void pageMainlineMessagesShouldReadMysqlHotTables() {
|
||||
FakeSessionRepository sessionRepository = new FakeSessionRepository();
|
||||
sessionRepository.summary = session(BigInteger.valueOf(2001), 6);
|
||||
FakeLogRepository logRepository = new FakeLogRepository();
|
||||
logRepository.records = List.of(message(3001), message(3002));
|
||||
List<YearMonth> months = List.of(YearMonth.of(2026, 5));
|
||||
ChatSessionQueryServiceImpl service = new ChatSessionQueryServiceImpl(
|
||||
sessionRepository,
|
||||
logRepository,
|
||||
new FakeTableManager(months),
|
||||
new FakeHotStateService()
|
||||
);
|
||||
ChatPageQuery query = new ChatPageQuery();
|
||||
query.setPageNumber(2);
|
||||
query.setPageSize(2);
|
||||
|
||||
ChatHistoryPage page = service.pageMainlineMessages(BigInteger.valueOf(2001), query);
|
||||
|
||||
Assert.assertEquals(6, page.getTotal());
|
||||
Assert.assertEquals(2, page.getRecords().size());
|
||||
Assert.assertEquals(BigInteger.valueOf(2001), logRepository.capturedSessionId);
|
||||
Assert.assertEquals(months, logRepository.capturedMonths);
|
||||
Assert.assertEquals(2, logRepository.capturedOffset);
|
||||
Assert.assertEquals(2, logRepository.capturedLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当 MySQL 摘要计数滞后时,分页 total 至少覆盖当前已返回的数据范围。
|
||||
*/
|
||||
@Test
|
||||
public void pageMainlineMessagesShouldNotReturnTotalSmallerThanCurrentPage() {
|
||||
FakeSessionRepository sessionRepository = new FakeSessionRepository();
|
||||
sessionRepository.summary = session(BigInteger.valueOf(2002), 1);
|
||||
FakeLogRepository logRepository = new FakeLogRepository();
|
||||
logRepository.records = List.of(message(4001), message(4002));
|
||||
ChatSessionQueryServiceImpl service = new ChatSessionQueryServiceImpl(
|
||||
sessionRepository,
|
||||
logRepository,
|
||||
new FakeTableManager(List.of(YearMonth.of(2026, 5))),
|
||||
new FakeHotStateService()
|
||||
);
|
||||
ChatPageQuery query = new ChatPageQuery();
|
||||
query.setPageNumber(2);
|
||||
query.setPageSize(2);
|
||||
|
||||
ChatHistoryPage page = service.pageMainlineMessages(BigInteger.valueOf(2002), query);
|
||||
|
||||
Assert.assertEquals(4, page.getTotal());
|
||||
}
|
||||
|
||||
private static ChatSessionSummary session(BigInteger id, int messageCount) {
|
||||
ChatSessionSummary summary = new ChatSessionSummary();
|
||||
summary.setId(id);
|
||||
summary.setUserId(BigInteger.valueOf(7));
|
||||
summary.setMessageCount(messageCount);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private static ChatMessageRecord message(long id) {
|
||||
ChatMessageRecord record = new ChatMessageRecord();
|
||||
record.setId(BigInteger.valueOf(id));
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL 会话仓储测试替身。
|
||||
*/
|
||||
private static final class FakeSessionRepository extends MySqlChatSessionRepository {
|
||||
|
||||
private long count;
|
||||
private int countSessionsCalls;
|
||||
private int listSessionsCalls;
|
||||
private ChatSessionSummary summary;
|
||||
private List<ChatSessionSummary> sessions = new ArrayList<>();
|
||||
|
||||
private FakeSessionRepository() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
|
||||
listSessionsCalls += 1;
|
||||
return sessions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countSessions(BigInteger userId, BigInteger assistantId) {
|
||||
countSessionsCalls += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary findBySessionId(BigInteger sessionId) {
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL 消息仓储测试替身。
|
||||
*/
|
||||
private static final class FakeLogRepository extends MySqlChatLogRepository {
|
||||
|
||||
private BigInteger capturedSessionId;
|
||||
private List<YearMonth> capturedMonths;
|
||||
private long capturedOffset;
|
||||
private int capturedLimit;
|
||||
private List<ChatMessageRecord> records = new ArrayList<>();
|
||||
|
||||
private FakeLogRepository() {
|
||||
super(null, null, new ChatJsonSupport(new ObjectMapper()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessageRecord> listMainlineMessages(BigInteger sessionId, List<YearMonth> months, long offset, int limit) {
|
||||
capturedSessionId = sessionId;
|
||||
capturedMonths = months;
|
||||
capturedOffset = offset;
|
||||
capturedLimit = limit;
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL 热表管理器测试替身。
|
||||
*/
|
||||
private static final class FakeTableManager extends MySqlChatLogTableManager {
|
||||
|
||||
private final List<YearMonth> months;
|
||||
|
||||
private FakeTableManager(List<YearMonth> months) {
|
||||
super(null, null);
|
||||
this.months = months;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<YearMonth> listRecentExistingMonths(int retentionMonths) {
|
||||
return months;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 热态测试替身,避免单测依赖真实 Redis。
|
||||
*/
|
||||
private static final class FakeHotStateService extends ChatHotStateService {
|
||||
|
||||
private FakeHotStateService() {
|
||||
super(null, new ObjectMapper(), new ChatCacheProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary getSessionSummary(BigInteger sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheSessionSummary(ChatSessionSummary summary) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessageRecord> getSessionTail(BigInteger sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSessionTail(BigInteger sessionId, List<ChatMessageRecord> records) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatRoundRecord;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionExtPayload;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.service.ChatPersistDispatcher;
|
||||
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||
import tech.easyflow.chatlog.service.ChatRoundQueryService;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link ChatlogRuntimeListener} 单元测试。
|
||||
*/
|
||||
public class ChatlogRuntimeListenerTest {
|
||||
|
||||
/**
|
||||
* 会话准备阶段应把额外知识库选择写入 ext_json。
|
||||
*/
|
||||
@Test
|
||||
public void onSessionPreparedShouldWriteExtraKnowledgeIdsToExtJson() {
|
||||
CapturingChatPersistDispatcher dispatcher = new CapturingChatPersistDispatcher();
|
||||
ChatlogRuntimeListener listener = new ChatlogRuntimeListener(
|
||||
dispatcher,
|
||||
new NoopChatRoundOperateService(),
|
||||
new NoopChatRoundQueryService(),
|
||||
new NoopChatSessionQueryService(),
|
||||
new ChatJsonSupport(new ObjectMapper())
|
||||
);
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setSessionId(BigInteger.valueOf(1001));
|
||||
context.setTenantId(BigInteger.ONE);
|
||||
context.setDeptId(BigInteger.TEN);
|
||||
context.setUserId(BigInteger.valueOf(7));
|
||||
context.setUserAccount("admin");
|
||||
context.setAssistantId(BigInteger.valueOf(88));
|
||||
context.setAssistantCode("bot-88");
|
||||
context.setAssistantName("测试助手");
|
||||
context.setSessionTitle("你好");
|
||||
context.getExt().put(
|
||||
ChatRuntimeExtKeys.EXTRA_KNOWLEDGE_IDS,
|
||||
List.of(BigInteger.valueOf(11), BigInteger.valueOf(22))
|
||||
);
|
||||
|
||||
listener.onSessionPrepared(context);
|
||||
|
||||
Assert.assertNotNull(dispatcher.captured);
|
||||
ChatSessionExtPayload payload = new ChatJsonSupport(new ObjectMapper())
|
||||
.fromJson(dispatcher.captured.getExtJson(), ChatSessionExtPayload.class);
|
||||
Assert.assertEquals(
|
||||
List.of(BigInteger.valueOf(11), BigInteger.valueOf(22)),
|
||||
payload.getExtraKnowledgeIds()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成时历史上下文应排除当前轮旧问题和旧答案。
|
||||
*/
|
||||
@Test
|
||||
public void loadMessagesShouldExcludeRegenerateRoundHistory() {
|
||||
ChatlogRuntimeListener listener = new ChatlogRuntimeListener(
|
||||
null,
|
||||
new NoopChatRoundOperateService(),
|
||||
new NoopChatRoundQueryService(),
|
||||
new TailChatSessionQueryService(List.of(
|
||||
record(4, 2, "assistant", "旧答案"),
|
||||
record(3, 2, "user", "当前问题"),
|
||||
record(2, 1, "assistant", "上一轮答案"),
|
||||
record(1, 1, "user", "上一轮问题")
|
||||
)),
|
||||
new ChatJsonSupport(new ObjectMapper())
|
||||
);
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setSessionId(BigInteger.valueOf(1001));
|
||||
context.getExt().put(ChatRuntimeExtKeys.REGENERATE_ROUND_ID, BigInteger.valueOf(2));
|
||||
|
||||
List<tech.easyflow.core.runtime.ChatRuntimeMessage> messages = listener.loadMessages(context, 10);
|
||||
|
||||
Assert.assertEquals(2, messages.size());
|
||||
Assert.assertEquals("上一轮问题", messages.get(0).getContentText());
|
||||
Assert.assertEquals("上一轮答案", messages.get(1).getContentText());
|
||||
}
|
||||
|
||||
private static ChatMessageRecord record(long id, int roundId, String role, String text) {
|
||||
ChatMessageRecord record = new ChatMessageRecord();
|
||||
record.setId(BigInteger.valueOf(id));
|
||||
record.setSessionId(BigInteger.valueOf(1001));
|
||||
record.setRoundId(BigInteger.valueOf(roundId));
|
||||
record.setSenderRole(role);
|
||||
record.setContentType("TEXT");
|
||||
record.setContentText(text);
|
||||
record.setCreated(new Date(id));
|
||||
return record;
|
||||
}
|
||||
|
||||
private static class CapturingChatPersistDispatcher extends ChatPersistDispatcher {
|
||||
|
||||
private ChatSessionUpsertCommand captured;
|
||||
|
||||
private CapturingChatPersistDispatcher() {
|
||||
super(null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command) {
|
||||
this.captured = command;
|
||||
return new ChatSessionSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopChatRoundOperateService implements ChatRoundOperateService {
|
||||
|
||||
@Override
|
||||
public ChatRoundRecord requireRegeneratableRound(BigInteger sessionId, BigInteger roundId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<tech.easyflow.chatlog.domain.dto.ChatMessageRecord> listVariants(BigInteger sessionId, BigInteger roundId) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public tech.easyflow.chatlog.domain.dto.ChatMessageRecord selectVariant(BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopChatRoundQueryService implements ChatRoundQueryService {
|
||||
|
||||
@Override
|
||||
public ChatRoundRecord getLatestRound(BigInteger sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatRoundRecord getRound(BigInteger sessionId, BigInteger roundId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<tech.easyflow.chatlog.domain.dto.ChatMessageRecord> listRoundVariants(BigInteger sessionId, BigInteger roundId) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public tech.easyflow.chatlog.domain.dto.ChatMessageRecord getRoundVariant(BigInteger sessionId, BigInteger roundId, Integer variantIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRounds(BigInteger sessionId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopChatSessionQueryService implements ChatSessionQueryService {
|
||||
|
||||
@Override
|
||||
public List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, tech.easyflow.chatlog.domain.query.ChatPageQuery query) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countSessions(BigInteger userId, BigInteger assistantId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public tech.easyflow.chatlog.domain.dto.ChatSessionPage pageSessions(BigInteger userId, BigInteger assistantId, tech.easyflow.chatlog.domain.query.ChatPageQuery query) {
|
||||
return new tech.easyflow.chatlog.domain.dto.ChatSessionPage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary getSessionSummary(BigInteger sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public tech.easyflow.chatlog.domain.dto.ChatHistoryPage pageMainlineMessages(BigInteger sessionId, tech.easyflow.chatlog.domain.query.ChatPageQuery query) {
|
||||
return new tech.easyflow.chatlog.domain.dto.ChatHistoryPage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<tech.easyflow.chatlog.domain.dto.ChatMessageRecord> listMainlineMessages(BigInteger sessionId) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<tech.easyflow.chatlog.domain.dto.ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TailChatSessionQueryService extends NoopChatSessionQueryService {
|
||||
|
||||
private final List<ChatMessageRecord> records;
|
||||
|
||||
private TailChatSessionQueryService(List<ChatMessageRecord> records) {
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit) {
|
||||
return records.subList(0, Math.min(records.size(), limit));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user