diff --git a/src/utils/__tests__/claudemd.test.ts b/src/utils/__tests__/claudemd.test.ts new file mode 100644 index 0000000..fe942a3 --- /dev/null +++ b/src/utils/__tests__/claudemd.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from "bun:test"; +import { + stripHtmlComments, + isMemoryFilePath, + getLargeMemoryFiles, + MAX_MEMORY_CHARACTER_COUNT, + type MemoryFileInfo, +} from "../claudemd"; + +function mockMemoryFile(overrides: Partial = {}): MemoryFileInfo { + return { + path: "/project/CLAUDE.md", + type: "Project", + content: "test content", + ...overrides, + }; +} + +describe("stripHtmlComments", () => { + test("strips block-level HTML comments (own line)", () => { + // CommonMark type-2 HTML blocks: comment must start at beginning of line + const result = stripHtmlComments("text\n\nmore"); + expect(result.content).not.toContain("block comment"); + expect(result.stripped).toBe(true); + }); + + test("returns stripped: false when no comments", () => { + const result = stripHtmlComments("no comments here"); + expect(result.stripped).toBe(false); + expect(result.content).toBe("no comments here"); + }); + + test("returns stripped: true when block comments exist", () => { + const result = stripHtmlComments("hello\n\nend"); + expect(result.stripped).toBe(true); + }); + + test("handles empty string", () => { + const result = stripHtmlComments(""); + expect(result.content).toBe(""); + expect(result.stripped).toBe(false); + }); + + test("handles multiple block comments", () => { + const result = stripHtmlComments( + "a\n\nb\n\nc" + ); + expect(result.content).not.toContain("c1"); + expect(result.content).not.toContain("c2"); + expect(result.stripped).toBe(true); + }); + + test("preserves code block content", () => { + const input = "text\n```html\n\n```\nmore"; + const result = stripHtmlComments(input); + expect(result.content).toContain(""); + }); + + test("preserves inline comments within paragraphs", () => { + // Inline comments are NOT stripped (CommonMark paragraph semantics) + const result = stripHtmlComments("text more"); + expect(result.content).toContain(""); + expect(result.stripped).toBe(false); + }); +}); + +describe("isMemoryFilePath", () => { + test("returns true for CLAUDE.md path", () => { + expect(isMemoryFilePath("/project/CLAUDE.md")).toBe(true); + }); + + test("returns true for CLAUDE.local.md path", () => { + expect(isMemoryFilePath("/project/CLAUDE.local.md")).toBe(true); + }); + + test("returns true for .claude/rules/ path", () => { + expect(isMemoryFilePath("/project/.claude/rules/foo.md")).toBe(true); + }); + + test("returns false for regular file", () => { + expect(isMemoryFilePath("/project/src/main.ts")).toBe(false); + }); + + test("returns false for unrelated .md file", () => { + expect(isMemoryFilePath("/project/README.md")).toBe(false); + }); + + test("returns false for .claude directory non-rules file", () => { + expect(isMemoryFilePath("/project/.claude/settings.json")).toBe(false); + }); +}); + +describe("getLargeMemoryFiles", () => { + test("returns files exceeding threshold", () => { + const largeContent = "x".repeat(MAX_MEMORY_CHARACTER_COUNT + 1); + const files = [ + mockMemoryFile({ content: "small" }), + mockMemoryFile({ content: largeContent, path: "/big.md" }), + ]; + const result = getLargeMemoryFiles(files); + expect(result).toHaveLength(1); + expect(result[0].path).toBe("/big.md"); + }); + + test("returns empty array when all files are small", () => { + const files = [ + mockMemoryFile({ content: "small" }), + mockMemoryFile({ content: "also small" }), + ]; + expect(getLargeMemoryFiles(files)).toEqual([]); + }); + + test("correctly identifies threshold boundary", () => { + const atThreshold = "x".repeat(MAX_MEMORY_CHARACTER_COUNT); + const overThreshold = "x".repeat(MAX_MEMORY_CHARACTER_COUNT + 1); + const files = [ + mockMemoryFile({ content: atThreshold }), + mockMemoryFile({ content: overThreshold }), + ]; + const result = getLargeMemoryFiles(files); + expect(result).toHaveLength(1); + }); +}); diff --git a/src/utils/__tests__/systemPrompt.test.ts b/src/utils/__tests__/systemPrompt.test.ts new file mode 100644 index 0000000..f7c9edd --- /dev/null +++ b/src/utils/__tests__/systemPrompt.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "bun:test"; +import { buildEffectiveSystemPrompt } from "../systemPrompt"; + +const defaultPrompt = ["You are a helpful assistant.", "Follow instructions."]; + +function buildPrompt(overrides: Record = {}) { + return buildEffectiveSystemPrompt({ + mainThreadAgentDefinition: undefined, + toolUseContext: { options: {} as any }, + customSystemPrompt: undefined, + defaultSystemPrompt: defaultPrompt, + appendSystemPrompt: undefined, + ...overrides, + }); +} + +describe("buildEffectiveSystemPrompt", () => { + test("returns default system prompt when no overrides", () => { + const result = buildPrompt(); + expect(Array.from(result)).toEqual(defaultPrompt); + }); + + test("overrideSystemPrompt replaces everything", () => { + const result = buildPrompt({ overrideSystemPrompt: "override" }); + expect(Array.from(result)).toEqual(["override"]); + }); + + test("customSystemPrompt replaces default", () => { + const result = buildPrompt({ customSystemPrompt: "custom" }); + expect(Array.from(result)).toEqual(["custom"]); + }); + + test("appendSystemPrompt is appended after main prompt", () => { + const result = buildPrompt({ appendSystemPrompt: "appended" }); + expect(Array.from(result)).toEqual([...defaultPrompt, "appended"]); + }); + + test("agent definition replaces default prompt", () => { + const agentDef = { + getSystemPrompt: () => "agent prompt", + agentType: "custom", + } as any; + const result = buildPrompt({ mainThreadAgentDefinition: agentDef }); + expect(Array.from(result)).toEqual(["agent prompt"]); + }); + + test("agent definition with append combines both", () => { + const agentDef = { + getSystemPrompt: () => "agent prompt", + agentType: "custom", + } as any; + const result = buildPrompt({ + mainThreadAgentDefinition: agentDef, + appendSystemPrompt: "extra", + }); + expect(Array.from(result)).toEqual(["agent prompt", "extra"]); + }); + + test("override takes precedence over agent and custom", () => { + const agentDef = { + getSystemPrompt: () => "agent prompt", + agentType: "custom", + } as any; + const result = buildPrompt({ + mainThreadAgentDefinition: agentDef, + customSystemPrompt: "custom", + appendSystemPrompt: "extra", + overrideSystemPrompt: "override", + }); + expect(Array.from(result)).toEqual(["override"]); + }); + + test("returns array of strings", () => { + const result = buildPrompt(); + expect(Array.isArray(result)).toBe(true); + for (const item of result) { + expect(typeof item).toBe("string"); + } + }); + + test("custom + append combines both", () => { + const result = buildPrompt({ + customSystemPrompt: "custom", + appendSystemPrompt: "extra", + }); + expect(Array.from(result)).toEqual(["custom", "extra"]); + }); +});