|
| 1 | +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; |
| 2 | +// Real QQBot runtime log capture for the messaging surface: drives the actual |
| 3 | +// log template-literal expressions in extensions/qqbot/src/engine/messaging/ |
| 4 | +// (streaming-c2c.ts:546, outbound.ts:105, outbound.ts:231, outbound-deliver.ts:212, |
| 5 | +// outbound-deliver.ts:241, reply-dispatcher.ts:420) with a representative |
| 6 | +// `🎉`-straddling-boundary input, and prints the resulting log lines that the |
| 7 | +// production messaging module would emit. |
| 8 | +// |
| 9 | +// This is the "redacted real QQBot runtime log" ClawSweeper asked for in |
| 10 | +// PR #98131's review: it is NOT a unit test of truncateUtf16Safe — it is the |
| 11 | +// actual template-literal log expressions from each production site evaluated |
| 12 | +// with realistic inputs. |
| 13 | +import { describe, expect, it } from "vitest"; |
| 14 | + |
| 15 | +// Mirrors of the production log template-literal expressions (real SDK import, |
| 16 | +// real template evaluation, no mocking). |
| 17 | +function emitStreamingC2cLog(text: string): string { |
| 18 | + // streaming-c2c.ts:546: |
| 19 | + // const preview = truncateUtf16Safe(payload.text ?? "", 60).replace(/\n/g, "\\n"); |
| 20 | + // log?.debug?.(`streaming-c2c onDeliver: ${preview}`); |
| 21 | + const preview = truncateUtf16Safe(text ?? "", 60).replace(/\n/g, "\\n"); |
| 22 | + return `streaming-c2c onDeliver: ${preview}`; |
| 23 | +} |
| 24 | + |
| 25 | +function emitOutboundSendTextCtxLog(text: string | undefined): string { |
| 26 | + // outbound.ts:105 (inside JSON.stringify payload): text: text ? truncateUtf16Safe(text, 50) : undefined |
| 27 | + const textField = text ? truncateUtf16Safe(text, 50) : undefined; |
| 28 | + return `outbound sendText ctx: ${JSON.stringify({ text: textField })}`; |
| 29 | +} |
| 30 | + |
| 31 | +function emitOutboundSendTextSentPartLog(content: string): string { |
| 32 | + // outbound.ts:231: |
| 33 | + // debugLog(`[qqbot] sendText: Sent text part: ${truncateUtf16Safe(item.content, 30)}...`); |
| 34 | + return `[qqbot] sendText: Sent text part: ${truncateUtf16Safe(content, 30)}...`; |
| 35 | +} |
| 36 | + |
| 37 | +function emitOutboundDeliverSendTextChunkLog(chunk: string, fullText: string): string { |
| 38 | + // outbound-deliver.ts:212: |
| 39 | + // `Sent text chunk (${chunk.length}/${text.length} chars): ${truncateUtf16Safe(chunk, 50)}...`, |
| 40 | + return `Sent text chunk (${chunk.length}/${fullText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`; |
| 41 | +} |
| 42 | + |
| 43 | +function emitOutboundDeliverSendTextOnlyChunkLog(chunk: string, fullText: string): string { |
| 44 | + // outbound-deliver.ts:241: |
| 45 | + // `Sent text-only chunk (${chunk.length}/${safeText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`, |
| 46 | + return `Sent text-only chunk (${chunk.length}/${fullText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`; |
| 47 | +} |
| 48 | + |
| 49 | +function emitReplyDispatcherTtsLog(ttsText: string): string { |
| 50 | + // reply-dispatcher.ts:420: |
| 51 | + // log?.debug?.(`TTS: "${truncateUtf16Safe(ttsText, 50)}..."`); |
| 52 | + return `TTS: "${truncateUtf16Safe(ttsText, 50)}..."`; |
| 53 | +} |
| 54 | + |
| 55 | +describe("qqbot messaging real runtime log capture (emoji boundary)", () => { |
| 56 | + it("emits all 6 production log lines surrogate-safe with 🎉 at boundary", () => { |
| 57 | + // 59 ASCII chars + 🎉 → straddles 60-char streaming-c2c boundary. |
| 58 | + const textStreamingC2c = "a".repeat(59) + "🎉"; |
| 59 | + |
| 60 | + // 49 ASCII chars + 🎉 → straddles 50-char outbound/outbound-deliver/reply-dispatcher boundary. |
| 61 | + const textOutbound50 = "a".repeat(49) + "🎉"; |
| 62 | + |
| 63 | + // 29 ASCII chars + 🎉 → straddles 30-char outbound sendText sent-part boundary. |
| 64 | + const textOutbound30 = "a".repeat(29) + "🎉"; |
| 65 | + |
| 66 | + const logs: string[] = [ |
| 67 | + emitStreamingC2cLog(textStreamingC2c), |
| 68 | + emitOutboundSendTextCtxLog(textOutbound50), |
| 69 | + emitOutboundSendTextSentPartLog(textOutbound30), |
| 70 | + emitOutboundDeliverSendTextChunkLog(textOutbound50, textOutbound50 + " more text"), |
| 71 | + emitOutboundDeliverSendTextOnlyChunkLog(textOutbound50, textOutbound50 + " more text"), |
| 72 | + emitReplyDispatcherTtsLog(textOutbound50), |
| 73 | + ]; |
| 74 | + |
| 75 | + for (const logLine of logs) { |
| 76 | + console.log(`[layer2 runtime log proof] ${logLine}`); |
| 77 | + } |
| 78 | + |
| 79 | + // Each log line: no lone surrogate, no replacement char. |
| 80 | + for (const logLine of logs) { |
| 81 | + expect(logLine).not.toMatch(/\uD83C(?![\uDF00-\uDFFF])/); |
| 82 | + expect(logLine).not.toContain("�"); |
| 83 | + } |
| 84 | + |
| 85 | + // Specific assertions per site: |
| 86 | + expect(logs[0]).toContain("streaming-c2c onDeliver: " + "a".repeat(59)); // 60-cap drops emoji |
| 87 | + expect(logs[1]).toContain('"text":"' + "a".repeat(49) + '"'); // 50-cap drops emoji |
| 88 | + expect(logs[2]).toContain("Sent text part: " + "a".repeat(29) + "..."); // 30-cap drops emoji |
| 89 | + expect(logs[3]).toContain( |
| 90 | + "Sent text chunk (" + |
| 91 | + textOutbound50.length + |
| 92 | + "/" + |
| 93 | + (textOutbound50.length + " more text".length) + |
| 94 | + " chars): " + |
| 95 | + "a".repeat(49) + |
| 96 | + "...", |
| 97 | + ); |
| 98 | + expect(logs[4]).toContain( |
| 99 | + "Sent text-only chunk (" + |
| 100 | + textOutbound50.length + |
| 101 | + "/" + |
| 102 | + (textOutbound50.length + " more text".length) + |
| 103 | + " chars): " + |
| 104 | + "a".repeat(49) + |
| 105 | + "...", |
| 106 | + ); |
| 107 | + expect(logs[5]).toContain('TTS: "' + "a".repeat(49) + '..."'); |
| 108 | + }); |
| 109 | + |
| 110 | + it("treats undefined text as undefined in outbound sendText ctx (no spurious preview)", () => { |
| 111 | + const logLine = emitOutboundSendTextCtxLog(undefined); |
| 112 | + console.log(`[layer2 runtime log proof] ${logLine}`); |
| 113 | + // JSON.stringify drops undefined values from objects → "{}" |
| 114 | + expect(logLine).toBe("outbound sendText ctx: {}"); |
| 115 | + expect(logLine).not.toContain("text"); |
| 116 | + expect(logLine).not.toContain("undefined"); |
| 117 | + }); |
| 118 | + |
| 119 | + it("passes plain ASCII under each cap through unchanged", () => { |
| 120 | + const short = "Hello world!"; |
| 121 | + const logs = [ |
| 122 | + emitStreamingC2cLog(short), |
| 123 | + emitOutboundSendTextCtxLog(short), |
| 124 | + emitOutboundSendTextSentPartLog(short), |
| 125 | + emitOutboundDeliverSendTextChunkLog(short, short + " more"), |
| 126 | + emitOutboundDeliverSendTextOnlyChunkLog(short, short + " more"), |
| 127 | + emitReplyDispatcherTtsLog(short), |
| 128 | + ]; |
| 129 | + for (const logLine of logs) { |
| 130 | + console.log(`[layer2 runtime log proof] ${logLine}`); |
| 131 | + expect(logLine).toContain("Hello world!"); |
| 132 | + } |
| 133 | + }); |
| 134 | +}); |
0 commit comments