| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- 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 <synthetic>", () => {
- expect(SYNTHETIC_MODEL).toBe("<synthetic>");
- });
- });
- // ─── 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("<foo>bar</foo>", "foo")).toBe("bar");
- });
- test("extracts tag with attributes", () => {
- expect(extractTag('<foo class="a">bar</foo>', "foo")).toBe("bar");
- });
- test("handles multiline content", () => {
- expect(extractTag("<foo>\nline1\nline2\n</foo>", "foo")).toBe(
- "\nline1\nline2\n"
- );
- });
- test("returns null for missing tag", () => {
- expect(extractTag("<foo>bar</foo>", "baz")).toBeNull();
- });
- test("returns null for empty html", () => {
- expect(extractTag("", "foo")).toBeNull();
- });
- test("returns null for empty tagName", () => {
- expect(extractTag("<foo>bar</foo>", "")).toBeNull();
- });
- test("is case-insensitive", () => {
- expect(extractTag("<FOO>bar</FOO>", "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);
- });
- });
|