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 推送是**高频路径**,不得往标准输出打印。 * *

两条 AI 流式链路(创意助手、活动策划大纲)每个 SSE 事件都会调一次 * {@link WebSocketServer#sendMessage(String, String)}:实测一次大纲生成 3062 个事件, * 即单次请求就会调用 3000+ 次。最初这里有两行 {@code System.out.println} * ({@code userId} 与 {@code session1}),于是一次生成就在控制台刷出上万行 * 「userId:833」,既刷屏又绕过日志级别(见 .scratch/ai-assistant/issues/12)。 * *

两个分支(目标已连接 / 未连接)各锁一条:改回 {@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 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; }); } }