Files
openclaw/src/talk/fast-context-runtime.test.ts
xydt-tanshanshan f0d4d4f2fc fix(talk): use truncateUtf16Safe for LLM-prompt-facing text truncation (#102477)
* fix(talk): use truncateUtf16Safe for snippet text truncation

fast-context-runtime.ts normalizeSnippet and heartbeat-events-filter.ts
buildExecEventPrompt used raw .slice(0, N), which can produce lone
surrogates when truncation boundaries cross emoji surrogate pairs.

* test(talk): cover UTF-16 snippet boundary

---------

Co-authored-by: hailory <hailory@xydigit.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
2026-07-09 08:39:38 +01:00

123 lines
3.5 KiB
TypeScript

// Fast context runtime tests cover timeout and fast context generation behavior.
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
getActiveMemorySearchManager: vi.fn(),
}));
vi.mock("../plugins/memory-runtime.js", () => ({
getActiveMemorySearchManager: mocks.getActiveMemorySearchManager,
}));
import { resolveRealtimeVoiceFastContextConsult } from "./fast-context-runtime.js";
describe("resolveRealtimeVoiceFastContextConsult", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
mocks.getActiveMemorySearchManager.mockReset();
});
it("caps oversized fast-context timeouts before scheduling Node timers", async () => {
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
mocks.getActiveMemorySearchManager.mockResolvedValue({
manager: {
search: vi.fn().mockResolvedValue([]),
},
});
await expect(
resolveRealtimeVoiceFastContextConsult({
cfg: {},
agentId: "main",
sessionKey: "voice:15550001234",
config: {
enabled: true,
timeoutMs: Number.MAX_SAFE_INTEGER,
maxResults: 3,
sources: ["memory", "sessions"],
fallbackToConsult: true,
},
args: { question: "What do you remember?" },
logger: {},
}),
).resolves.toEqual({ handled: false });
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
it("preserves the fast-context timeout error and clears the timer", async () => {
vi.useFakeTimers();
const logger = { debug: vi.fn() };
mocks.getActiveMemorySearchManager.mockResolvedValue({
manager: {
search: vi.fn(() => new Promise<never>(() => {})),
},
});
const result = resolveRealtimeVoiceFastContextConsult({
cfg: {},
agentId: "main",
sessionKey: "voice:15550001234",
config: {
enabled: true,
timeoutMs: 25,
maxResults: 3,
sources: ["memory", "sessions"],
fallbackToConsult: true,
},
args: { question: "What do you remember?" },
logger,
});
await vi.advanceTimersByTimeAsync(25);
await expect(result).resolves.toEqual({ handled: false });
expect(logger.debug).toHaveBeenCalledWith(
"[talk] fast context lookup failed: fast context lookup timed out after 25ms",
);
expect(vi.getTimerCount()).toBe(0);
});
it("does not split a surrogate pair at the fast-context snippet limit", async () => {
const safePrefix = "x".repeat(698);
mocks.getActiveMemorySearchManager.mockResolvedValue({
manager: {
search: vi.fn().mockResolvedValue([
{
path: "memory/test.md",
startLine: 1,
endLine: 1,
snippet: `${safePrefix}🚀tail`,
source: "memory",
score: 1,
},
]),
},
});
const result = await resolveRealtimeVoiceFastContextConsult({
cfg: {},
agentId: "main",
sessionKey: "voice:15550001234",
config: {
enabled: true,
timeoutMs: 1_000,
maxResults: 1,
sources: ["memory"],
fallbackToConsult: true,
},
args: { question: "What do you remember?" },
logger: {},
});
expect(result).toEqual({
handled: true,
result: {
text: expect.stringContaining(`1. [memory] memory/test.md:1-1\n${safePrefix}...`),
},
});
});
});