diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatContractReplayTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatContractReplayTest.java new file mode 100644 index 00000000..7aaa6f45 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatContractReplayTest.java @@ -0,0 +1,87 @@ +package com.nanri.aiimage.modules.task.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-206:heartbeat 旧字段契约回放(真实服务,env-gated)。 + * CONTRACT_BASE_URL + CONTRACT_DB_* 设则跑、否则跳过(CI 绿)。 + * 覆盖:RUNNING 任务 heartbeat alive:true(路径/响应字段)、非运行任务 alive:false、未知任务 + * alive:false+message、缺省/多余(旧)字段容忍。 + */ +class TaskHeartbeatContractReplayTest { + + private static final long TASK_ID = 9000004L; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void heartbeatContract() throws Exception { + String base = System.getenv("CONTRACT_BASE_URL"); + String dbUrl = System.getenv("CONTRACT_DB_URL"); + String dbUser = System.getenv("CONTRACT_DB_USER"); + String dbPass = System.getenv("CONTRACT_DB_PASSWORD"); + Assumptions.assumeTrue(base != null && dbUrl != null && dbUser != null, "未设 CONTRACT_* 跳过"); + + seed(dbUrl, dbUser, dbPass, "RUNNING"); + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + // RUNNING + 旧/额外字段 → alive:true(兼容多余字段) + String body = "{\"moduleType\":\"SIMILAR_ASIN\",\"phase\":\"search\",\"current\":1,\"total\":10," + + "\"collectStage\":\"keyword-search\",\"currentKeyword\":\"ring\",\"searchCurrentPage\":1," + + "\"searchTotalPages\":4,\"detailProcessedAsins\":1,\"detailTotalAsins\":50," + + "\"extraLegacyField\":\"ignored\"}"; + HttpResponse ok = post(client, base + "/api/tasks/" + TASK_ID + "/heartbeat", body); + assertEquals(200, ok.statusCode()); + JsonNode okBody = MAPPER.readTree(ok.body()); + assertTrue(okBody.path("success").asBoolean(), ok.body()); + assertTrue(okBody.path("data").path("alive").asBoolean(), "RUNNING 任务应 alive:true: " + ok.body()); + assertEquals("SIMILAR_ASIN", okBody.path("data").path("moduleType").asText()); + + // 非运行 → alive:false + seed(dbUrl, dbUser, dbPass, "FINISHED"); + HttpResponse finished = post(client, base + "/api/tasks/" + TASK_ID + "/heartbeat", + "{\"moduleType\":\"SIMILAR_ASIN\",\"phase\":\"search\",\"current\":1,\"total\":1}"); + JsonNode f = MAPPER.readTree(finished.body()); + assertFalse(f.path("data").path("alive").asBoolean(), "非运行任务应 alive:false: " + finished.body()); + + // 未知任务 → alive:false + message + HttpResponse unknown = post(client, base + "/api/tasks/999999999/heartbeat", + "{\"moduleType\":\"SIMILAR_ASIN\"}"); + JsonNode u = MAPPER.readTree(unknown.body()); + assertFalse(u.path("data").path("alive").asBoolean(), "未知任务应 alive:false"); + assertTrue(u.path("data").has("message"), "应带 message"); + } + + private static void seed(String dbUrl, String user, String pass, String status) throws Exception { + try (Connection c = DriverManager.getConnection(dbUrl, user, pass); + Statement s = c.createStatement()) { + s.executeUpdate("INSERT INTO biz_file_task (id,task_no,module_type,task_mode,status,created_at,updated_at,user_id,owner_instance_id) " + + "VALUES (" + TASK_ID + ",'HB-REPLAY-" + TASK_ID + "','SIMILAR_ASIN','workbook','" + status + "',NOW(),NOW(),1,'local-smoke') " + + "ON DUPLICATE KEY UPDATE module_type='SIMILAR_ASIN',status='" + status + "',user_id=1"); + } + } + + private static HttpResponse post(HttpClient client, String url, String body) throws Exception { + return client.send(HttpRequest.newBuilder(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .timeout(Duration.ofSeconds(15)).build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } +}