package cn.iocoder.yudao.module.qcreport.service.aiimport.llm; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftRespVO; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 模型回复的解析。 *
* 全部喂罐头字符串,不 mock {@code AiChatApi}——这里是「字符串 → 草稿」的纯函数, * mock 掉模型再断言解析正确,测的是 mock 的返回值而不是解析逻辑。 *
* 罐头用例挑的都是真实会遇到的回复形状:模型习惯把 JSON 包进 markdown 围栏、习惯在前后加一句
* 「好的,这是识别结果」,也习惯在输出被截断时留下半个对象。这三种必须在切片这一步就被吃掉,
* 否则用户看到的是「AI 返回的内容无法解析」——而其实回复里内容是全的。
*/
class QcReportAiDraftParserTest {
@Test
@DisplayName("裸 JSON 原样解析")
void plainJson() {
QcReportAiDraftRespVO resp = QcReportAiDraftParser.parse("""
{"summary":"来料检验报告","page":{"size":"A4","orientation":"portrait"},
"components":[{"type":"ReportHeader","props":{"title":"来料检验报告"}}]}""");
assertEquals("来料检验报告", resp.getSummary());
assertEquals("A4", resp.getPage().getSize());
assertEquals("portrait", resp.getPage().getOrientation());
assertEquals(1, resp.getComponents().size());
assertEquals("ReportHeader", resp.getComponents().get(0).getType());
assertEquals("来料检验报告", resp.getComponents().get(0).getProps().get("title"));
}
@Test
@DisplayName("markdown 围栏包裹的 JSON 照样解析(模型最常见的输出形状)")
void markdownFenced() {
QcReportAiDraftRespVO resp = QcReportAiDraftParser.parse("""
```json
{"summary":"扫描件","components":[{"type":"Heading","props":{"text":"检验项目"}}]}
```""");
assertEquals(1, resp.getComponents().size());
assertEquals("Heading", resp.getComponents().get(0).getType());
}
@Test
@DisplayName("前后带寒暄的 JSON 照样解析")
void withChatterAround() {
QcReportAiDraftRespVO resp = QcReportAiDraftParser.parse("""
好的,我已经分析了这份文件。识别结果如下:
{"summary":"识别完成","components":[{"type":"Result","props":{"conclusion":"合格"}}]}
如果需要调整请告诉我。""");
assertEquals("识别完成", resp.getSummary());
assertEquals("Result", resp.getComponents().get(0).getType());
}
@Test
@DisplayName("props 里有嵌套的数组与对象也不炸——props 的合法 key 由前端清单决定,后端不该强类型化")
void nestedPropValues() {
QcReportAiDraftRespVO resp = QcReportAiDraftParser.parse("""
{"components":[{"type":"QualityTable","props":{
"itemsPath":"inspectionItems",
"columns":[{"key":"size","label":"尺寸"}],
"meta":{"source":"iqc"}}}]}""");
Map