



























@@ -2,6 +2,7 @@
22import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
33import { describe, expect, it } from "vitest";
44import {
5+CODEX_TURN_START_TEXT_INPUT_MAX_CHARS,
56fitCodexProjectedContextForTurnStart,
67projectContextEngineAssemblyForCodex,
78resolveCodexContextEngineProjectionMaxChars,
@@ -226,6 +227,92 @@ describe("projectContextEngineAssemblyForCodex", () => {
226227expect(fitted).not.toContain("old context");
227228});
228229230+it("bounds output when the non-context text alone exceeds the turn limit", () => {
231+// A large older-context header prefix pushes before + after over maxChars
232+// while the trailing user request stays small enough to keep its label.
233+const before = `OpenClaw assembled context for this turn:\n${"prefix ".repeat(120)}`;
234+const context = "older context ".repeat(40);
235+const prompt = `urgent request ${"q".repeat(120)}`;
236+const after = `\n</conversation_context>\n\nCurrent user request:\n${prompt}`;
237+const promptText = `${before}${context}${after}`;
238+const maxChars = 420;
239+// before + after already exceed maxChars, so the context budget is non-positive.
240+expect(before.length + after.length).toBeGreaterThan(maxChars);
241+242+const fitted = fitCodexProjectedContextForTurnStart({
243+ promptText,
244+contextRange: { start: before.length, end: before.length + context.length },
245+ maxChars,
246+});
247+248+expect(fitted.length).toBeLessThanOrEqual(maxChars);
249+// The user's actual request is the priority tail and must survive truncation.
250+expect(fitted).toContain("Current user request:");
251+expect(fitted.endsWith("q".repeat(40))).toBe(true);
252+// The dropped older context is reported, not silently lost.
253+expect(fitted).toContain("[truncated ");
254+});
255+256+it("bounds output for a large request under the default Codex turn limit", () => {
257+const maxChars = CODEX_TURN_START_TEXT_INPUT_MAX_CHARS;
258+// A large assembled header prefix already over the cap forces the
259+// non-positive context budget on the real default limit (1 << 20).
260+const before = `header\n${"older history ".repeat(90_000)}`;
261+const context = "x".repeat(2_000);
262+const prompt = `urgent request ${"u".repeat(2_000)}`;
263+const after = `\n</conversation_context>\n\nCurrent user request:\n${prompt}`;
264+const promptText = `${before}${context}${after}`;
265+expect(before.length + after.length).toBeGreaterThan(maxChars);
266+267+const fitted = fitCodexProjectedContextForTurnStart({
268+ promptText,
269+contextRange: { start: before.length, end: before.length + context.length },
270+// maxChars omitted -> defaults to CODEX_TURN_START_TEXT_INPUT_MAX_CHARS.
271+});
272+273+expect(fitted.length).toBeLessThanOrEqual(maxChars);
274+// The user request is the priority tail and survives even though the older
275+// header text is truncated to satisfy the limit.
276+expect(fitted).toContain("Current user request:");
277+expect(fitted.endsWith("u".repeat(1_000))).toBe(true);
278+});
279+280+it("never splits a UTF-16 surrogate pair at the truncation boundary", () => {
281+// Drive the non-positive-budget path with an emoji (surrogate pair) sitting
282+// across the kept-tail cut. A naive code-unit slice would orphan the low
283+// surrogate into U+FFFD; the boundary must stay on a whole code point.
284+const before = `OpenClaw assembled context for this turn:\n${"H".repeat(300)}`;
285+const context = "older context ".repeat(20);
286+// Emoji immediately before the user text so the cut can fall mid-pair.
287+const prompt = `\u{1F600}${"U".repeat(60)}`;
288+const after = `\n</conversation_context>\n\nCurrent user request:\n${prompt}`;
289+const promptText = `${before}${context}${after}`;
290+const contextRange = { start: before.length, end: before.length + context.length };
291+292+// Sweep cap sizes around the cut so the test is not brittle to marker length;
293+// at least one value lands the boundary inside the surrogate pair.
294+for (let maxChars = 90; maxChars <= 140; maxChars += 1) {
295+const fitted = fitCodexProjectedContextForTurnStart({ promptText, contextRange, maxChars });
296+expect(fitted.length).toBeLessThanOrEqual(maxChars);
297+// U+FFFD only appears when a lone surrogate is rendered, i.e. a split pair.
298+expect(fitted).not.toContain("�");
299+// Any surviving emoji must be the complete pair, not a lone low surrogate.
300+for (let i = 0; i < fitted.length; i += 1) {
301+const code = fitted.charCodeAt(i);
302+const isLowSurrogate = code >= 0xdc00 && code <= 0xdfff;
303+const isHighSurrogate = code >= 0xd800 && code <= 0xdbff;
304+if (isLowSurrogate) {
305+const prev = fitted.charCodeAt(i - 1);
306+expect(prev >= 0xd800 && prev <= 0xdbff).toBe(true);
307+}
308+if (isHighSurrogate) {
309+const next = fitted.charCodeAt(i + 1);
310+expect(next >= 0xdc00 && next <= 0xdfff).toBe(true);
311+}
312+}
313+}
314+});
315+229316it("keeps the old conservative cap when no runtime budget is available", () => {
230317expect(resolveCodexContextEngineProjectionMaxChars({})).toBe(24_000);
231318expect(resolveCodexContextEngineProjectionMaxChars({ contextTokenBudget: 0 })).toBe(24_000);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。