task-19: Coze 请求/响应与 Python 回传日志改采样、截断、DEBUG
- 新增 SimilarAsinLogSupport 纯函数:truncate(2000 前缀+长度后缀,代理对安全) 与 shouldLog(每 N 次采样,计数 0 恒采样) - Coze 请求 body 与 submit 响应正文日志降 DEBUG + 截断; history 轮询响应降 DEBUG + 每 20 次记一次完整正文,其余只记状态 - Python 回传逐行日志降 DEBUG + 每 20 行采样一行,大任务日志量降到 5%
This commit is contained in:
+156
@@ -0,0 +1,156 @@
|
||||
package com.nanri.aiimage.modules.similarasin.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.config.SimilarAsinProperties;
|
||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
|
||||
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinLogSupport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 19:Coze 请求/响应及 Python 回传日志改为采样、截断和 DEBUG 级别。
|
||||
* 新工具 SimilarAsinLogSupport 提供两条纯函数:
|
||||
* - truncate:正文超限截断为 maxChars + 后缀,长文本不占满日志;
|
||||
* - shouldLog:每 everyN 次采样一次(counter % everyN == 0),控制轮询/逐行日志量。
|
||||
* Coze 客户端正文日志与 Python 回传逐行日志经该工具后输出有界、可识别。
|
||||
*/
|
||||
class SimilarAsinCozeClientLoggingTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_normal_default_path() {
|
||||
// 正常输入:短文本不截断;每次采样(everyN=1)恒记录。
|
||||
assertEquals("hello", SimilarAsinLogSupport.truncate("hello", 100));
|
||||
assertEquals("", SimilarAsinLogSupport.truncate(null, 100), "null 文本返回空串");
|
||||
assertTrue(SimilarAsinLogSupport.shouldLog(0, 1), "everyN=1 恒采样");
|
||||
assertTrue(SimilarAsinLogSupport.shouldLog(7, 1), "everyN=1 不抑制任何计数");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_normal_multiple_items() throws Exception {
|
||||
// 批量场景:多个长文本各自截断、结果互不影响;采样按每 everyN 次一次。
|
||||
String longA = "A".repeat(3000);
|
||||
String longB = "B".repeat(5000);
|
||||
String truncatedA = SimilarAsinLogSupport.truncate(longA, 100);
|
||||
String truncatedB = SimilarAsinLogSupport.truncate(longB, 100);
|
||||
assertTrue(truncatedA.startsWith("A".repeat(100)));
|
||||
assertTrue(truncatedB.startsWith("B".repeat(100)));
|
||||
assertTrue(truncatedA.length() < longA.length(), "截断后必须短于原文");
|
||||
|
||||
int sampled = 0;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (SimilarAsinLogSupport.shouldLog(i, 10)) {
|
||||
sampled++;
|
||||
}
|
||||
}
|
||||
assertEquals(3, sampled, "everyN=10 在 0..29 内应采样 0/10/20 共 3 次");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_normal_repeated_operation_is_idempotent() throws Exception {
|
||||
// 重复执行:同一文本多次截断结果一致;同一计数采样判定一致。
|
||||
String text = "x".repeat(1234);
|
||||
String first = SimilarAsinLogSupport.truncate(text, 500);
|
||||
String second = SimilarAsinLogSupport.truncate(text, 500);
|
||||
assertEquals(first, second, "重复截断必须产生相同输出");
|
||||
assertEquals(first, SimilarAsinLogSupport.truncate(text, 500), "截断幂等");
|
||||
assertEquals(SimilarAsinLogSupport.shouldLog(20, 10), SimilarAsinLogSupport.shouldLog(20, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_boundary_empty_input() throws Exception {
|
||||
// 空输入:null/空串安全返回空串;空白串按长度截断语义原样保留。
|
||||
assertEquals("", SimilarAsinLogSupport.truncate(null, 100));
|
||||
assertEquals("", SimilarAsinLogSupport.truncate("", 100));
|
||||
assertEquals(" ", SimilarAsinLogSupport.truncate(" ", 100), "空白串不做 trim,按原样返回");
|
||||
assertTrue(SimilarAsinLogSupport.shouldLog(0, 10), "计数 0 必须采样(首条不丢)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_boundary_single_item() throws Exception {
|
||||
// 单元素边界:恰好等于上限不截断;超 1 字符截断并带长度后缀。
|
||||
String exact = "y".repeat(100);
|
||||
assertEquals(exact, SimilarAsinLogSupport.truncate(exact, 100), "恰好等于上限不截断");
|
||||
String over = "y".repeat(101);
|
||||
String truncated = SimilarAsinLogSupport.truncate(over, 100);
|
||||
assertEquals(over.substring(0, 100), truncated.substring(0, 100), "截断保留前缀");
|
||||
assertTrue(truncated.contains("101"), "截断输出应携带原文长度");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_boundary_limit_and_overflow() throws Exception {
|
||||
// 上限/超限:10 万字符文本截断后有界、不再无界增长;采样 everyN 超限不抑制。
|
||||
String huge = "z".repeat(100_000);
|
||||
String truncated = SimilarAsinLogSupport.truncate(huge, 2000);
|
||||
assertTrue(truncated.length() < 2200, "截断输出必须有界,实际=" + truncated.length());
|
||||
assertTrue(truncated.length() > 2000, "应保留 2000 前缀 + 后缀");
|
||||
assertTrue(truncated.endsWith("]"), "截断输出带可识别后缀");
|
||||
assertTrue(SimilarAsinLogSupport.shouldLog(0, Integer.MAX_VALUE), "计数 0 在超大 everyN 下仍采样");
|
||||
assertFalse(SimilarAsinLogSupport.shouldLog(1, Integer.MAX_VALUE), "非零计数在超大 everyN 下抑制");
|
||||
assertFalse(SimilarAsinLogSupport.shouldLog(31, 10), "非采样点必须被抑制");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_invalid_input_rejected() throws Exception {
|
||||
// 非法参数:maxChars ≤ 0 时原样返回(不截断);null 文本始终空串。
|
||||
String text = "invalid-max";
|
||||
assertEquals(text, SimilarAsinLogSupport.truncate(text, 0), "maxChars=0 不截断");
|
||||
assertEquals(text, SimilarAsinLogSupport.truncate(text, -1), "负上限不截断");
|
||||
assertEquals("", SimilarAsinLogSupport.truncate(null, -5));
|
||||
assertTrue(SimilarAsinLogSupport.shouldLog(5, 0), "everyN=0 视为恒采样");
|
||||
assertTrue(SimilarAsinLogSupport.shouldLog(5, -3), "负 everyN 视为恒采样");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_019_logging_dependency_failure_releases_resources() throws Exception {
|
||||
// 依赖失败:含代理对(emoji)的长文本截断不抛异常、不产生孤立代理项;
|
||||
// 计数接近 Long.MAX_VALUE 不溢出;掩码后的请求体经截断管线输出有界且不泄漏密钥。
|
||||
String emoji = "🚀".repeat(3000);
|
||||
String truncatedEmoji = SimilarAsinLogSupport.truncate(emoji, 100);
|
||||
assertNotNull(truncatedEmoji);
|
||||
assertTrue(truncatedEmoji.length() < emoji.length(), "代理对文本必须被截断");
|
||||
assertFalse(SimilarAsinLogSupport.shouldLog(Long.MAX_VALUE, 10), "极大计数采样判定不抛异常");
|
||||
|
||||
List<SimilarAsinResultRowDto> rows = new java.util.ArrayList<>();
|
||||
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
|
||||
row.setAsin("B0SECRET1");
|
||||
row.setUrl("https://m.media-amazon.com/images/I/" + "U".repeat(500) + ".jpg");
|
||||
row.setTitle("T".repeat(5000));
|
||||
row.setSku("SKU-SECRET");
|
||||
rows.add(row);
|
||||
SimilarAsinCozeClient client = new SimilarAsinCozeClient(new SimilarAsinProperties(), objectMapper, null);
|
||||
Method maskMethod = SimilarAsinCozeClient.class.getDeclaredMethod(
|
||||
"maskCozeRequestBody", Map.class);
|
||||
maskMethod.setAccessible(true);
|
||||
Method buildMethod = SimilarAsinCozeClient.class.getDeclaredMethod(
|
||||
"buildParameters", List.class, String.class, String.class, boolean.class);
|
||||
buildMethod.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> parameters = (Map<String, Object>) buildMethod.invoke(client, rows, "", "supersecretkey", true);
|
||||
Map<String, Object> body = new java.util.LinkedHashMap<>();
|
||||
body.put("workflow_id", "wf-1");
|
||||
body.put("parameters", parameters);
|
||||
body.put("api_key", "supersecretkey");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> masked = (Map<String, Object>) maskMethod.invoke(client, body);
|
||||
Method writeMethod = SimilarAsinCozeClient.class.getDeclaredMethod("writeJson", Object.class);
|
||||
writeMethod.setAccessible(true);
|
||||
String maskedJson = (String) writeMethod.invoke(client, masked);
|
||||
String logged = SimilarAsinLogSupport.truncate(maskedJson, 2000);
|
||||
|
||||
assertTrue(logged.length() < maskedJson.length(), "超长掩码 body 必须截断");
|
||||
assertFalse(logged.contains("supersecretkey"), "日志不得泄漏完整 api_key");
|
||||
assertTrue(logged.contains("B0SECRET1"), "截断保留正文关键字段");
|
||||
assertTrue(logged.length() < 2500, "截断输出必须有界");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user