21e49c6a1a
- WebSocketServer.sendMessage 里两行 System.out.println 删除。该方法被**每个 SSE 事件**调用一次(大纲单次生成实测 3062 个事件,即一次请求刷 6000+ 行),而且绕过 日志框架——既不受 logging.level 控制(生产把 com.gxwebsoft 压到 ERROR 也拦不住), 也不进日志文件,真故障时需要的几行会被顶掉。日志改到低频的 onOpen / onClose; 同时把 onOpen 里静默吞掉的握手回应异常改成 WARN(那是「连上了却收不到内容」的 第一现场)。 - 新增用例:AiCreativeRetryTest、AiCreativeRetryIntegrationTest(issue 11, 后者用本地 HTTP server 扮演 Dify,断言瞬时故障确实重跑一次、凭证失效只请求一次)、 WebSocketServerSilentPushTest(issue 12,捕获 System.out 断言推送路径零输出)。
83 lines
3.3 KiB
Java
83 lines
3.3 KiB
Java
package com.gxwebsoft.ai.config;
|
||
|
||
import org.junit.jupiter.api.Test;
|
||
|
||
import javax.websocket.RemoteEndpoint;
|
||
import javax.websocket.Session;
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.PrintStream;
|
||
import java.lang.reflect.Proxy;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.concurrent.Callable;
|
||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
|
||
/**
|
||
* WebSocket 推送是**高频路径**,不得往标准输出打印。
|
||
*
|
||
* <p>两条 AI 流式链路(创意助手、活动策划大纲)每个 SSE 事件都会调一次
|
||
* {@link WebSocketServer#sendMessage(String, String)}:实测一次大纲生成 3062 个事件,
|
||
* 即单次请求就会调用 3000+ 次。最初这里有两行 {@code System.out.println}
|
||
* ({@code userId} 与 {@code session1}),于是一次生成就在控制台刷出上万行
|
||
* 「userId:833」,既刷屏又绕过日志级别(见 .scratch/ai-assistant/issues/12)。
|
||
*
|
||
* <p>两个分支(目标已连接 / 未连接)各锁一条:改回 {@code println} 立刻失败。
|
||
*/
|
||
class WebSocketServerSilentPushTest {
|
||
|
||
@Test
|
||
void 目标未连接时推送不得打印() throws Exception {
|
||
String output = captureStdout(() -> {
|
||
// 未连接的 userId:走"查不到会话"的分支,也正是刷屏时最常见的那条
|
||
new WebSocketServer().sendMessage("silent-push-absent", "{\"answer\":\"测试\"}");
|
||
return null;
|
||
});
|
||
|
||
assertEquals("", output, "推送是高频路径,不能刷标准输出");
|
||
}
|
||
|
||
@Test
|
||
void 目标已连接时推送也不得打印() throws Exception {
|
||
WebSocketServer server = new WebSocketServer();
|
||
// onOpen 会打一条 INFO(连接日志,低频且应当保留),故放在捕获之外
|
||
server.onOpen(fakeSession(), "silent-push-open");
|
||
|
||
String output = captureStdout(() -> {
|
||
server.sendMessage("silent-push-open", "{\"answer\":\"测试\"}");
|
||
return null;
|
||
});
|
||
|
||
assertEquals("", output, "推送是高频路径,不能刷标准输出");
|
||
}
|
||
|
||
/**
|
||
* 捕获一段代码往标准输出写的内容。推送是 void 且抛受检异常,故用 {@link Callable}。
|
||
*/
|
||
private static String captureStdout(Callable<Void> action) throws Exception {
|
||
PrintStream original = System.out;
|
||
ByteArrayOutputStream captured = new ByteArrayOutputStream();
|
||
try {
|
||
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8.name()));
|
||
action.call();
|
||
} finally {
|
||
System.setOut(original);
|
||
}
|
||
return captured.toString(StandardCharsets.UTF_8.name());
|
||
}
|
||
|
||
/**
|
||
* 一个什么都不做的 {@link Session}:只为让 {@code sendMessage} 走进"已连接"分支。
|
||
*/
|
||
private static Session fakeSession() {
|
||
return (Session) Proxy.newProxyInstance(Session.class.getClassLoader(),
|
||
new Class<?>[]{Session.class}, (proxy, method, args) -> {
|
||
if ("getBasicRemote".equals(method.getName())) {
|
||
return Proxy.newProxyInstance(RemoteEndpoint.Basic.class.getClassLoader(),
|
||
new Class<?>[]{RemoteEndpoint.Basic.class},
|
||
(endpoint, call, callArgs) -> null);
|
||
}
|
||
return null;
|
||
});
|
||
}
|
||
}
|