From c57950e15e52dd975f99342e6814eb686e8f6026 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 1 Apr 2026 22:43:31 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=20(?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=AE=A1=E5=88=92=2006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为消息创建、查询、文本提取、规范化等函数添加 56 个测试用例, 覆盖 createAssistantMessage、createUserMessage、isSyntheticMessage、 extractTag、isNotEmptyMessage、normalizeMessages 等核心功能。 Co-Authored-By: Claude Opus 4.6 --- src/utils/__tests__/messages.test.ts | 473 +++++++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 src/utils/__tests__/messages.test.ts diff --git a/src/utils/__tests__/messages.test.ts b/src/utils/__tests__/messages.test.ts new file mode 100644 index 0000000..500dc1f --- /dev/null +++ b/src/utils/__tests__/messages.test.ts @@ -0,0 +1,473 @@ +import { describe, expect, test } from "bun:test"; +import { + deriveShortMessageId, + INTERRUPT_MESSAGE, + INTERRUPT_MESSAGE_FOR_TOOL_USE, + CANCEL_MESSAGE, + REJECT_MESSAGE, + NO_RESPONSE_REQUESTED, + SYNTHETIC_MESSAGES, + isSyntheticMessage, + getLastAssistantMessage, + hasToolCallsInLastAssistantTurn, + createAssistantMessage, + createAssistantAPIErrorMessage, + createUserMessage, + createUserInterruptionMessage, + prepareUserContent, + createToolResultStopMessage, + extractTag, + isNotEmptyMessage, + deriveUUID, + normalizeMessages, + isClassifierDenial, + buildYoloRejectionMessage, + buildClassifierUnavailableMessage, + AUTO_REJECT_MESSAGE, + DONT_ASK_REJECT_MESSAGE, + SYNTHETIC_MODEL, +} from "../messages"; +import type { Message, AssistantMessage, UserMessage } from "../../types/message"; + +// ─── Helpers ───────────────────────────────────────────────────────────── + +function makeAssistantMsg( + contentBlocks: Array<{ type: string; text?: string; [key: string]: any }> +): AssistantMessage { + return createAssistantMessage({ + content: contentBlocks as any, + }); +} + +function makeUserMsg(text: string): UserMessage { + return createUserMessage({ content: text }); +} + +// ─── deriveShortMessageId ─────────────────────────────────────────────── + +describe("deriveShortMessageId", () => { + test("returns 6-char string", () => { + const id = deriveShortMessageId("550e8400-e29b-41d4-a716-446655440000"); + expect(id).toHaveLength(6); + }); + + test("is deterministic for same input", () => { + const uuid = "a0b1c2d3-e4f5-6789-abcd-ef0123456789"; + expect(deriveShortMessageId(uuid)).toBe(deriveShortMessageId(uuid)); + }); + + test("produces different IDs for different UUIDs", () => { + const id1 = deriveShortMessageId("00000000-0000-0000-0000-000000000001"); + const id2 = deriveShortMessageId("ffffffff-ffff-ffff-ffff-ffffffffffff"); + expect(id1).not.toBe(id2); + }); +}); + +// ─── Constants ────────────────────────────────────────────────────────── + +describe("message constants", () => { + test("SYNTHETIC_MESSAGES contains expected messages", () => { + expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE)).toBe(true); + expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE_FOR_TOOL_USE)).toBe(true); + expect(SYNTHETIC_MESSAGES.has(CANCEL_MESSAGE)).toBe(true); + expect(SYNTHETIC_MESSAGES.has(REJECT_MESSAGE)).toBe(true); + expect(SYNTHETIC_MESSAGES.has(NO_RESPONSE_REQUESTED)).toBe(true); + }); + + test("SYNTHETIC_MODEL is ", () => { + expect(SYNTHETIC_MODEL).toBe(""); + }); +}); + +// ─── Message factories ────────────────────────────────────────────────── + +describe("createAssistantMessage", () => { + test("creates assistant message with string content", () => { + const msg = createAssistantMessage({ content: "hello" }); + expect(msg.type).toBe("assistant"); + expect(msg.message.role).toBe("assistant"); + expect(msg.message.content).toHaveLength(1); + expect((msg.message.content[0] as any).text).toBe("hello"); + }); + + test("creates assistant message with content blocks", () => { + const blocks = [{ type: "text" as const, text: "hello" }]; + const msg = createAssistantMessage({ content: blocks as any }); + expect(msg.type).toBe("assistant"); + expect(msg.message.content).toHaveLength(1); + }); + + test("generates unique uuid per call", () => { + const msg1 = createAssistantMessage({ content: "a" }); + const msg2 = createAssistantMessage({ content: "b" }); + expect(msg1.uuid).not.toBe(msg2.uuid); + }); + + test("has isApiErrorMessage false", () => { + const msg = createAssistantMessage({ content: "test" }); + expect(msg.isApiErrorMessage).toBe(false); + }); +}); + +describe("createAssistantAPIErrorMessage", () => { + test("sets isApiErrorMessage to true", () => { + const msg = createAssistantAPIErrorMessage({ content: "error" }); + expect(msg.isApiErrorMessage).toBe(true); + }); + + test("includes error details", () => { + const msg = createAssistantAPIErrorMessage({ + content: "fail", + errorDetails: "rate limited", + }); + expect(msg.errorDetails).toBe("rate limited"); + }); +}); + +describe("createUserMessage", () => { + test("creates user message with string content", () => { + const msg = createUserMessage({ content: "hello" }); + expect(msg.type).toBe("user"); + expect(msg.message.role).toBe("user"); + expect(msg.message.content).toBe("hello"); + }); + + test("generates unique uuid", () => { + const msg1 = createUserMessage({ content: "a" }); + const msg2 = createUserMessage({ content: "b" }); + expect(msg1.uuid).not.toBe(msg2.uuid); + }); + + test("uses provided uuid when given", () => { + const msg = createUserMessage({ + content: "test", + uuid: "custom-uuid-1234-5678-abcd-ef0123456789", + }); + expect(msg.uuid).toBe("custom-uuid-1234-5678-abcd-ef0123456789"); + }); + + test("sets isMeta flag", () => { + const msg = createUserMessage({ content: "test", isMeta: true }); + expect(msg.isMeta).toBe(true); + }); +}); + +describe("createUserInterruptionMessage", () => { + test("creates interrupt message without tool use", () => { + const msg = createUserInterruptionMessage({}); + expect(msg.type).toBe("user"); + expect((msg.message.content as any)[0].text).toBe(INTERRUPT_MESSAGE); + }); + + test("creates interrupt message with tool use", () => { + const msg = createUserInterruptionMessage({ toolUse: true }); + expect((msg.message.content as any)[0].text).toBe( + INTERRUPT_MESSAGE_FOR_TOOL_USE + ); + }); +}); + +describe("prepareUserContent", () => { + test("returns string when no preceding blocks", () => { + const result = prepareUserContent({ + inputString: "hello", + precedingInputBlocks: [], + }); + expect(result).toBe("hello"); + }); + + test("returns array when preceding blocks exist", () => { + const blocks = [{ type: "image" as const, source: {} } as any]; + const result = prepareUserContent({ + inputString: "describe this", + precedingInputBlocks: blocks, + }); + expect(Array.isArray(result)).toBe(true); + expect((result as any[]).length).toBe(2); + expect((result as any[])[1].text).toBe("describe this"); + }); +}); + +describe("createToolResultStopMessage", () => { + test("creates tool result with error flag", () => { + const result = createToolResultStopMessage("tool-123"); + expect(result.type).toBe("tool_result"); + expect(result.is_error).toBe(true); + expect(result.tool_use_id).toBe("tool-123"); + expect(result.content).toBe(CANCEL_MESSAGE); + }); +}); + +// ─── isSyntheticMessage ───────────────────────────────────────────────── + +describe("isSyntheticMessage", () => { + test("identifies interrupt message as synthetic", () => { + const msg: any = { + type: "user", + message: { content: [{ type: "text", text: INTERRUPT_MESSAGE }] }, + }; + expect(isSyntheticMessage(msg)).toBe(true); + }); + + test("identifies cancel message as synthetic", () => { + const msg: any = { + type: "user", + message: { content: [{ type: "text", text: CANCEL_MESSAGE }] }, + }; + expect(isSyntheticMessage(msg)).toBe(true); + }); + + test("returns false for normal user message", () => { + const msg: any = { + type: "user", + message: { content: [{ type: "text", text: "hello" }] }, + }; + expect(isSyntheticMessage(msg)).toBe(false); + }); + + test("returns false for progress message", () => { + const msg: any = { + type: "progress", + message: { content: [{ type: "text", text: INTERRUPT_MESSAGE }] }, + }; + expect(isSyntheticMessage(msg)).toBe(false); + }); + + test("returns false for string content", () => { + const msg: any = { + type: "user", + message: { content: INTERRUPT_MESSAGE }, + }; + expect(isSyntheticMessage(msg)).toBe(false); + }); +}); + +// ─── getLastAssistantMessage ──────────────────────────────────────────── + +describe("getLastAssistantMessage", () => { + test("returns last assistant message", () => { + const a1 = makeAssistantMsg([{ type: "text", text: "first" }]); + const u = makeUserMsg("mid"); + const a2 = makeAssistantMsg([{ type: "text", text: "last" }]); + const result = getLastAssistantMessage([a1, u, a2]); + expect(result).toBe(a2); + }); + + test("returns undefined for empty array", () => { + expect(getLastAssistantMessage([])).toBeUndefined(); + }); + + test("returns undefined when no assistant messages", () => { + const u = makeUserMsg("hello"); + expect(getLastAssistantMessage([u])).toBeUndefined(); + }); +}); + +// ─── hasToolCallsInLastAssistantTurn ──────────────────────────────────── + +describe("hasToolCallsInLastAssistantTurn", () => { + test("returns true when last assistant has tool_use", () => { + const msg = makeAssistantMsg([ + { type: "text", text: "let me check" }, + { type: "tool_use", id: "t1", name: "Bash", input: {} }, + ]); + expect(hasToolCallsInLastAssistantTurn([msg])).toBe(true); + }); + + test("returns false when last assistant has only text", () => { + const msg = makeAssistantMsg([{ type: "text", text: "done" }]); + expect(hasToolCallsInLastAssistantTurn([msg])).toBe(false); + }); + + test("returns false for empty messages", () => { + expect(hasToolCallsInLastAssistantTurn([])).toBe(false); + }); +}); + +// ─── extractTag ───────────────────────────────────────────────────────── + +describe("extractTag", () => { + test("extracts simple tag content", () => { + expect(extractTag("bar", "foo")).toBe("bar"); + }); + + test("extracts tag with attributes", () => { + expect(extractTag('bar', "foo")).toBe("bar"); + }); + + test("handles multiline content", () => { + expect(extractTag("\nline1\nline2\n", "foo")).toBe( + "\nline1\nline2\n" + ); + }); + + test("returns null for missing tag", () => { + expect(extractTag("bar", "baz")).toBeNull(); + }); + + test("returns null for empty html", () => { + expect(extractTag("", "foo")).toBeNull(); + }); + + test("returns null for empty tagName", () => { + expect(extractTag("bar", "")).toBeNull(); + }); + + test("is case-insensitive", () => { + expect(extractTag("bar", "foo")).toBe("bar"); + }); +}); + +// ─── isNotEmptyMessage ────────────────────────────────────────────────── + +describe("isNotEmptyMessage", () => { + test("returns true for message with text content", () => { + const msg: any = { + type: "user", + message: { content: "hello" }, + }; + expect(isNotEmptyMessage(msg)).toBe(true); + }); + + test("returns false for empty string content", () => { + const msg: any = { + type: "user", + message: { content: " " }, + }; + expect(isNotEmptyMessage(msg)).toBe(false); + }); + + test("returns false for empty content array", () => { + const msg: any = { + type: "user", + message: { content: [] }, + }; + expect(isNotEmptyMessage(msg)).toBe(false); + }); + + test("returns true for progress message", () => { + const msg: any = { + type: "progress", + message: { content: [] }, + }; + expect(isNotEmptyMessage(msg)).toBe(true); + }); + + test("returns true for multi-block content", () => { + const msg: any = { + type: "user", + message: { + content: [ + { type: "text", text: "a" }, + { type: "text", text: "b" }, + ], + }, + }; + expect(isNotEmptyMessage(msg)).toBe(true); + }); + + test("returns true for non-text block", () => { + const msg: any = { + type: "user", + message: { + content: [{ type: "tool_use", id: "t1", name: "Bash", input: {} }], + }, + }; + expect(isNotEmptyMessage(msg)).toBe(true); + }); +}); + +// ─── deriveUUID ───────────────────────────────────────────────────────── + +describe("deriveUUID", () => { + test("produces deterministic output", () => { + const parent = "550e8400-e29b-41d4-a716-446655440000" as any; + expect(deriveUUID(parent, 0)).toBe(deriveUUID(parent, 0)); + }); + + test("produces different output for different indices", () => { + const parent = "550e8400-e29b-41d4-a716-446655440000" as any; + expect(deriveUUID(parent, 0)).not.toBe(deriveUUID(parent, 1)); + }); + + test("preserves UUID-like length", () => { + const parent = "550e8400-e29b-41d4-a716-446655440000" as any; + const derived = deriveUUID(parent, 5); + expect(derived.length).toBe(parent.length); + }); +}); + +// ─── isClassifierDenial ───────────────────────────────────────────────── + +describe("isClassifierDenial", () => { + test("returns true for classifier denial prefix", () => { + expect( + isClassifierDenial( + "Permission for this action has been denied. Reason: unsafe" + ) + ).toBe(true); + }); + + test("returns false for normal content", () => { + expect(isClassifierDenial("hello world")).toBe(false); + }); +}); + +// ─── Message builder functions ────────────────────────────────────────── + +describe("AUTO_REJECT_MESSAGE", () => { + test("includes tool name", () => { + const msg = AUTO_REJECT_MESSAGE("Bash"); + expect(msg).toContain("Bash"); + expect(msg).toContain("denied"); + }); +}); + +describe("DONT_ASK_REJECT_MESSAGE", () => { + test("includes tool name and dont ask mode", () => { + const msg = DONT_ASK_REJECT_MESSAGE("Write"); + expect(msg).toContain("Write"); + expect(msg).toContain("don't ask mode"); + }); +}); + +describe("buildYoloRejectionMessage", () => { + test("includes reason", () => { + const msg = buildYoloRejectionMessage("potentially destructive"); + expect(msg).toContain("potentially destructive"); + expect(msg).toContain("denied"); + }); +}); + +describe("buildClassifierUnavailableMessage", () => { + test("includes tool name and model", () => { + const msg = buildClassifierUnavailableMessage("Bash", "classifier-v1"); + expect(msg).toContain("Bash"); + expect(msg).toContain("classifier-v1"); + expect(msg).toContain("unavailable"); + }); +}); + +// ─── normalizeMessages ────────────────────────────────────────────────── + +describe("normalizeMessages", () => { + test("splits multi-block assistant message into individual messages", () => { + const msg = makeAssistantMsg([ + { type: "text", text: "first" }, + { type: "text", text: "second" }, + ]); + const normalized = normalizeMessages([msg]); + expect(normalized.length).toBe(2); + }); + + test("handles empty array", () => { + const result = normalizeMessages([] as AssistantMessage[]); + expect(result).toEqual([]); + }); + + test("preserves single-block message", () => { + const msg = makeAssistantMsg([{ type: "text", text: "hello" }]); + const normalized = normalizeMessages([msg]); + expect(normalized.length).toBe(1); + }); +});