task-180: Worker 日志字段对齐规范(TaskResultFileJobWorker 关键阶段补 stage=/errorType=,保留 [task-file-job] 前缀)+ 8 条测试

- dispatch/success/failure/orphan/heartbeat/requeue/defer/waittxllm/finalize 日志追加规范 stage 字面量
- 失败/孤儿分支追加 errorType={}(exception 类名);保留 jobId/taskId/moduleType 与既有语义,仅消息文本追加、无逻辑改动
- 审计测试锁定字段名一致、阶段字面量白名单、无敏感字段
This commit is contained in:
2026-09-04 23:44:39 +08:00
parent 5fa59680f2
commit 39fb23bd16
2 changed files with 137 additions and 11 deletions
@@ -0,0 +1,126 @@
package com.nanri.aiimage.config;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* task-180TaskResultFileJobWorker 日志字段与规范对齐(spec 11 §2)。
*
* Worker 的 [task-file-job] 日志统一携带 jobId/taskId/moduleType,并补充规范字段 stage
* (阶段字面量)与 errorType(异常类名);保留既有前缀与语义,不记录敏感内容。
* 源码锚点审计,纯本地可重复。
*/
class TaskResultFileJobWorkerLogAuditTest {
private static final String WORKER = "src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java";
private List<String> lines() throws IOException {
return Files.readAllLines(Path.of(System.getProperty("user.dir"), WORKER));
}
private static boolean hasLine(List<String> lines, String needle) {
return lines.stream().anyMatch(l -> l.contains(needle));
}
@Test
void dispatchLogHasStageAndJobFields() throws IOException {
List<String> lines = lines();
String target = lines.stream()
.filter(l -> l.contains("[task-file-job]") && l.contains("stage=SKIP_OWNER"))
.findFirst().orElseThrow();
assertTrue(target.contains("jobId={}") && target.contains("taskId={}") && target.contains("moduleType={}"),
"dispatch 日志应带 jobId/taskId/moduleType: " + target.trim());
}
@Test
void successLogHasStageAndFields() throws IOException {
List<String> lines = lines();
String target = lines.stream()
.filter(l -> l.contains("process success") && l.contains("[task-file-job]"))
.findFirst().orElseThrow();
assertTrue(target.contains("stage=SUCCESS"), "成功日志应带 stage=SUCCESS");
assertTrue(target.contains("jobId={}") && target.contains("taskId={}") && target.contains("moduleType={}"));
}
@Test
void failureLogHasStageAndErrorType() throws IOException {
List<String> lines = lines();
String target = lines.stream()
.filter(l -> l.contains("process failed") && l.contains("[task-file-job]"))
.findFirst().orElseThrow();
assertTrue(target.contains("stage=FAILED"), "失败日志应带 stage=FAILED");
assertTrue(target.contains("errorType={}"), "失败日志应带 errorType 占位");
assertTrue(hasLine(lines, "message, ex.getClass().getSimpleName());"),
"失败日志应记录异常类名");
}
@Test
void retryAndRecoverLogsHaveStage() throws IOException {
List<String> lines = lines();
assertTrue(hasLine(lines, "stage=REQUEUE"), "锁忙重排应带 stage=REQUEUE");
assertTrue(hasLine(lines, "stage=DEFER"), "延后等待应带 stage=DEFER");
assertTrue(hasLine(lines, "stage=WAIT_LLM"), "等待异步 LLM 应带 stage=WAIT_LLM");
}
@Test
void heartbeatLogHasStageAndFields() throws IOException {
List<String> lines = lines();
String target = lines.stream()
.filter(l -> l.contains("heartbeat failed") && l.contains("[task-file-job]"))
.findFirst().orElseThrow();
assertTrue(target.contains("stage=HEARTBEAT"), "心跳日志应带 stage=HEARTBEAT");
assertTrue(target.contains("jobId={}") && target.contains("taskId={}") && target.contains("moduleType={}"));
}
@Test
void stuckFinalizeLogsHaveStage() throws IOException {
List<String> lines = lines();
assertTrue(hasLine(lines, "stage=FINALIZE"), "重试耗尽终态回调应带 stage=FINALIZE");
assertTrue(hasLine(lines, "stage=ORPHAN"), "孤儿 job 中断应带 stage=ORPHAN");
}
@Test
void fieldNamesConsistentAndPrefixKept() throws IOException {
List<String> lines = lines();
List<String> perJob = lines.stream()
.filter(l -> l.contains("[task-file-job]") && l.contains("log."))
.toList();
assertFalse(perJob.isEmpty());
for (String l : perJob) {
assertFalse(l.contains("job="), "不应使用 job= 漂移字段: " + l.trim());
assertFalse(l.contains("task="), "不应使用 task= 漂移字段: " + l.trim());
assertFalse(l.contains("module="), "不应使用 module= 漂移字段: " + l.trim());
}
// 阶段字面量来自允许集合(防新增阶段拼写漂移)
List<String> allowed = List.of("SKIP_OWNER", "REQUEUE", "WAIT_LLM", "DEFER", "SUCCESS",
"ORPHAN", "FAILED", "FINALIZE", "HEARTBEAT");
for (String l : perJob) {
if (l.contains("stage=")) {
int idx = l.indexOf("stage=");
String rawValue = l.substring(idx + "stage=".length()).split("[^A-Z0-9_]")[0];
assertTrue(allowed.contains(rawValue),
"阶段字面量应来自允许集合: " + rawValue + " @ " + l.trim());
}
}
}
@Test
void noSensitiveFieldsInWorkerLogs() throws IOException {
List<String> lines = lines();
for (String l : lines) {
if (!l.contains("[task-file-job]")) {
continue;
}
assertFalse(l.contains("password=") || l.contains("secret=") || l.contains("apiKey=")
|| l.contains("cookie=") || l.contains("Bearer") || l.contains("authorization"),
"Worker 日志不应记录敏感字段: " + l.trim());
}
}
}