






















1+// Real-behavior proof that the chat.history budget pipeline emits the
2+// `payload.large` / `truncated` diagnostic whenever older history is omitted,
3+// and that the omitted count reflects unique source messages (a message that is
4+// first replaced and then trimmed is not double-counted). These run the real
5+// production helpers and capture the real diagnostic event bus output.
6+import { describe, expect, it } from "vitest";
7+import { onDiagnosticEvent } from "../../infra/diagnostic-events.js";
8+import type { DiagnosticPayloadLargeEvent } from "../../infra/diagnostic-events.js";
9+import { capArrayByJsonBytes } from "../session-utils.js";
10+import {
11+enforceChatHistoryFinalBudget,
12+replaceOversizedChatHistoryMessages,
13+reportOmittedChatHistory,
14+} from "./chat.js";
15+16+type Captured = DiagnosticPayloadLargeEvent[];
17+18+// Mirrors the production sequence in handleChatHistoryRequest: replace oversized
19+// messages, cap the array by byte budget, enforce the final budget, then report
20+// omissions. Captures any emitted `payload.large` diagnostic event.
21+function runHistoryBudgetPipeline(params: {
22+messages: unknown[];
23+maxHistoryBytes: number;
24+perMessageHardCap: number;
25+}): { emittedCount: number; events: Captured; replacedCount: number; frontCapDropped: number } {
26+const { messages, maxHistoryBytes, perMessageHardCap } = params;
27+const events: Captured = [];
28+const unsubscribe = onDiagnosticEvent((evt) => {
29+if (evt.type === "payload.large") {
30+events.push(evt);
31+}
32+});
33+try {
34+const replaced = replaceOversizedChatHistoryMessages({
35+ messages,
36+maxSingleMessageBytes: perMessageHardCap,
37+});
38+const capped = capArrayByJsonBytes(replaced.messages, maxHistoryBytes).items;
39+const bounded = enforceChatHistoryFinalBudget({ messages: capped, maxBytes: maxHistoryBytes });
40+const emittedCount = reportOmittedChatHistory({
41+originalMessages: messages,
42+finalMessages: bounded.messages,
43+normalizedBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
44+ maxHistoryBytes,
45+logDebug: () => {},
46+});
47+return {
48+ emittedCount,
49+ events,
50+replacedCount: replaced.replacedCount,
51+frontCapDropped: replaced.messages.length - capped.length,
52+};
53+} finally {
54+unsubscribe();
55+}
56+}
57+58+function textMessage(role: string, text: string): Record<string, unknown> {
59+return { role, content: [{ type: "text", text }] };
60+}
61+62+describe("chat.history truncation logging (real diagnostic bus)", () => {
63+it("emits a truncated diagnostic when history is trimmed to the last message", () => {
64+const big = textMessage("user", "x".repeat(8000));
65+const last = textMessage("assistant", "ok");
66+const result = runHistoryBudgetPipeline({
67+messages: [big, last],
68+maxHistoryBytes: 2_000,
69+perMessageHardCap: 2_000,
70+});
71+72+expect(result.events).toHaveLength(1);
73+const event = result.events[0];
74+expect(event.surface).toBe("gateway.chat.history");
75+expect(event.action).toBe("truncated");
76+expect(event.reason).toBe("chat_history_budget");
77+expect(event.count).toBe(1);
78+expect(result.emittedCount).toBe(1);
79+});
80+81+it("emits no diagnostic when nothing is omitted", () => {
82+const result = runHistoryBudgetPipeline({
83+messages: [textMessage("user", "hello"), textMessage("assistant", "hi")],
84+maxHistoryBytes: 1_000_000,
85+perMessageHardCap: 1_000_000,
86+});
87+88+expect(result.events).toHaveLength(0);
89+expect(result.emittedCount).toBe(0);
90+});
91+92+it("counts a replaced-then-trimmed message once, not twice", () => {
93+// `huge` is oversized so it is replaced with a small placeholder, then the
94+// placeholder sits at the front and is dropped by the byte cap. The naive
95+// sum of replacedCount + front-cap drops would count `huge` twice.
96+const huge = textMessage("user", "h".repeat(8000));
97+const big1 = textMessage("assistant", "a".repeat(2000));
98+const big2 = textMessage("user", "b".repeat(2000));
99+const last = textMessage("assistant", "ok");
100+const messages = [huge, big1, big2, last];
101+102+const result = runHistoryBudgetPipeline({
103+ messages,
104+maxHistoryBytes: 4_000,
105+perMessageHardCap: 3_000,
106+});
107+108+// Scenario preconditions: a message was replaced AND front-capped, so the
109+// old additive count would have over-reported.
110+expect(result.replacedCount).toBeGreaterThan(0);
111+expect(result.frontCapDropped).toBeGreaterThan(0);
112+const naiveAdditive = result.replacedCount + result.frontCapDropped;
113+114+// The emitted count equals the number of original messages that lost their
115+// verbatim representation, and is strictly less than the double-counting sum.
116+expect(result.events).toHaveLength(1);
117+expect(result.events[0].count).toBe(result.emittedCount);
118+expect(result.emittedCount).toBe(2);
119+expect(naiveAdditive).toBeGreaterThan(result.emittedCount);
120+});
121+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。