From dacdab7adb66283db8aa32bbaf71681468493556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 09:01:55 +0800 Subject: [PATCH] =?UTF-8?q?task-206:=20heartbeat=20=E6=97=A7=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=A5=91=E7=BA=A6=E5=9B=9E=E6=94=BE=EF=BC=88env-gated?= =?UTF-8?q?=EF=BC=8C=E7=9C=9F=E5=AE=9E=E6=9C=8D=E5=8A=A1=EF=BC=9ARUNNING?= =?UTF-8?q?=E2=86=92alive:true=E3=80=81=E9=9D=9E=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E2=86=92alive:false=E3=80=81=E6=9C=AA=E7=9F=A5=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=20alive:false+message=E3=80=81=E6=97=A7/=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E5=AD=97=E6=AE=B5=E5=AE=B9=E5=BF=8D=EF=BC=89+=20?= =?UTF-8?q?=E5=B7=B2=E6=9C=AC=E5=9C=B0=E7=9C=9F=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TaskHeartbeatContractReplayTest.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatContractReplayTest.java 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)); + } +}