diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java new file mode 100644 index 00000000..6ef8294e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java @@ -0,0 +1,31 @@ +package com.nanri.aiimage.modules.similarasin.model; + +/** + * similarasin 结果提交语义的纯计算(task-207 提取,防业务漂移)。 + * + * done=true / error 非空 => 本批为终态回调(触发读元数据/终态处理); + * chunkIndex/chunkTotal 缺省归一(0/1)。纯函数,便于离线单测。 + */ +public final class SimilarAsinSubmitResultSemantics { + + private SimilarAsinSubmitResultSemantics() { + } + + /** 是否终态回调:done=true 或 error 非空。 */ + public static boolean isTerminalRequest(boolean done, String error) { + if (done) { + return true; + } + return error != null && !error.isBlank(); + } + + /** chunkIndex 缺省(含 <=0 之外的 null)归一为 0(与既有 submit 逻辑一致)。 */ + public static int chunkIndex(Integer chunkIndex) { + return chunkIndex == null ? 0 : chunkIndex; + } + + /** chunkTotal 缺省归一为 1。 */ + public static int chunkTotal(Integer chunkTotal) { + return chunkTotal == null ? 1 : chunkTotal; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java index 64c7ede9..809a0883 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java @@ -1017,12 +1017,15 @@ public class SimilarAsinTaskService { } ensureTaskOwnedByCurrentInstance(task, "submit result"); - int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex(); - int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal(); + int chunkIndex = com.nanri.aiimage.modules.similarasin.model.SimilarAsinSubmitResultSemantics + .chunkIndex(request.getChunkIndex()); + int chunkTotal = com.nanri.aiimage.modules.similarasin.model.SimilarAsinSubmitResultSemantics + .chunkTotal(request.getChunkTotal()); boolean done = Boolean.TRUE.equals(request.getDone()); String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId); String scopeHash = DigestUtil.sha256Hex(scopeKey); - boolean terminalCallback = done || request.getError() != null && !request.getError().isBlank(); + boolean terminalCallback = com.nanri.aiimage.modules.similarasin.model.SimilarAsinSubmitResultSemantics + .isTerminalRequest(done, request.getError()); SubmittedTaskMetadata taskMetadata = terminalCallback ? readSubmittedTaskMetadata(task) : null; diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java new file mode 100644 index 00000000..b9b5c360 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java @@ -0,0 +1,76 @@ +package com.nanri.aiimage.modules.similarasin.model; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-207(服务层部分):done=true 最后一批语义纯单测。 + * + * 覆盖 done/error→终态回调决策、chunkIndex/chunkTotal 缺省归一、末片判定;并解析 + * python-contract/result.request.json(done=true)断言终态触发。文件生成(xlsx/终态落库) + * 依赖真实 LLM 流水线,按 3+2 约定在真实运行时经 env-gated 回放验证(本类不依赖 DB)。 + */ +class SimilarAsinSubmitResultSemanticsTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void doneTrueIsTerminalEvenWithoutError() { + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(true, null)); + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(true, "")); + } + + @Test + void errorNonBlankIsTerminal() { + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, "source timeout")); + } + + @Test + void runningChunkWithoutDoneOrErrorIsNotTerminal() { + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, null)); + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, " ")); + } + + @Test + void blankOrNullErrorNotTerminalWhenNotDone() { + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, "")); + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, " ")); + } + + @Test + void chunkIndexNullDefaultsZero() { + assertTrue(SimilarAsinSubmitResultSemantics.chunkIndex(null) == 0); + assertTrue(SimilarAsinSubmitResultSemantics.chunkIndex(3) == 3); + } + + @Test + void chunkTotalNullDefaultsOne() { + assertTrue(SimilarAsinSubmitResultSemantics.chunkTotal(null) == 1); + assertTrue(SimilarAsinSubmitResultSemantics.chunkTotal(5) == 5); + } + + @Test + void explicitValuesPassedThrough() { + assertTrue(SimilarAsinSubmitResultSemantics.chunkIndex(7) == 7); + assertTrue(SimilarAsinSubmitResultSemantics.chunkTotal(2) == 2); + } + + @Test + void fixtureDoneTrueRequestIsTerminal() throws Exception { + Path fixture = Path.of(System.getProperty("user.dir"), + "src/test/resources/python-contract/result.request.json"); + JsonNode req = MAPPER.readTree(Files.readAllBytes(fixture)); + boolean done = req.path("done").asBoolean(false); + String error = req.path("error").isNull() ? null : req.path("error").asText(); + assertTrue(done, "夹具 done 应为 true"); + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(done, error), + "done=true 的提交应触发终态回调"); + } +}