Merge branch 'main' of github.com:AutoMaker-Org/automaker into improve-context-page

This commit is contained in:
Test User
2025-12-22 00:50:55 -05:00
501 changed files with 17637 additions and 17437 deletions

View File

@@ -15,6 +15,7 @@ npm install @automaker/utils
## Exports
### Logger
Structured logging with context.
```typescript
@@ -27,6 +28,7 @@ logger.debug('Debug information', { data });
```
### Error Handler
Error classification and user-friendly messages.
```typescript
@@ -35,7 +37,7 @@ import {
isCancellationError,
isAuthenticationError,
classifyError,
getUserFriendlyErrorMessage
getUserFriendlyErrorMessage,
} from '@automaker/utils';
try {
@@ -51,6 +53,7 @@ try {
```
### Conversation Utils
Message formatting and conversion.
```typescript
@@ -58,7 +61,7 @@ import {
extractTextFromContent,
normalizeContentBlocks,
formatHistoryAsText,
convertHistoryToMessages
convertHistoryToMessages,
} from '@automaker/utils';
const text = extractTextFromContent(contentBlocks);
@@ -68,6 +71,7 @@ const converted = convertHistoryToMessages(history);
```
### Image Handler
Image processing for Claude prompts.
```typescript
@@ -75,7 +79,7 @@ import {
getMimeTypeForImage,
readImageAsBase64,
convertImagesToContentBlocks,
formatImagePathsForPrompt
formatImagePathsForPrompt,
} from '@automaker/utils';
const mimeType = getMimeTypeForImage('screenshot.png');
@@ -85,6 +89,7 @@ const formatted = formatImagePathsForPrompt(imagePaths);
```
### Prompt Builder
Build prompts with images for Claude.
```typescript
@@ -93,7 +98,7 @@ import { buildPromptWithImages } from '@automaker/utils';
const result = await buildPromptWithImages({
basePrompt: 'Analyze this screenshot',
imagePaths: ['/path/to/screenshot.png'],
basePath: '/project/path'
basePath: '/project/path',
});
console.log(result.prompt); // Prompt with image references
@@ -101,15 +106,11 @@ console.log(result.images); // Image data for Claude
```
### File System Utils
Common file system operations.
```typescript
import {
ensureDir,
fileExists,
readJsonFile,
writeJsonFile
} from '@automaker/utils';
import { ensureDir, fileExists, readJsonFile, writeJsonFile } from '@automaker/utils';
await ensureDir('/path/to/dir');
const exists = await fileExists('/path/to/file');
@@ -131,7 +132,7 @@ async function executeWithImages(prompt: string, images: string[]) {
const result = await buildPromptWithImages({
basePrompt: prompt,
imagePaths: images,
basePath: process.cwd()
basePath: process.cwd(),
});
logger.debug('Prompt built successfully', { imageCount: result.images.length });

View File

@@ -11,7 +11,10 @@
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": ["automaker", "utils"],
"keywords": [
"automaker",
"utils"
],
"author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {

View File

@@ -19,15 +19,15 @@ import type { ConversationMessage } from '@automaker/types';
export function extractTextFromContent(
content: string | Array<{ type: string; text?: string; source?: object }>
): string {
if (typeof content === "string") {
if (typeof content === 'string') {
return content;
}
// Extract text blocks only
return content
.filter((block) => block.type === "text")
.map((block) => block.text || "")
.join("\n");
.filter((block) => block.type === 'text')
.map((block) => block.text || '')
.join('\n');
}
/**
@@ -42,7 +42,7 @@ export function normalizeContentBlocks(
if (Array.isArray(content)) {
return content;
}
return [{ type: "text", text: content }];
return [{ type: 'text', text: content }];
}
/**
@@ -53,18 +53,18 @@ export function normalizeContentBlocks(
*/
export function formatHistoryAsText(history: ConversationMessage[]): string {
if (history.length === 0) {
return "";
return '';
}
let historyText = "Previous conversation:\n\n";
let historyText = 'Previous conversation:\n\n';
for (const msg of history) {
const contentText = extractTextFromContent(msg.content);
const role = msg.role === "user" ? "User" : "Assistant";
const role = msg.role === 'user' ? 'User' : 'Assistant';
historyText += `${role}: ${contentText}\n\n`;
}
historyText += "---\n\n";
historyText += '---\n\n';
return historyText;
}
@@ -74,20 +74,18 @@ export function formatHistoryAsText(history: ConversationMessage[]): string {
* @param history - Array of conversation messages
* @returns Array of Claude SDK formatted messages
*/
export function convertHistoryToMessages(
history: ConversationMessage[]
): Array<{
type: "user" | "assistant";
export function convertHistoryToMessages(history: ConversationMessage[]): Array<{
type: 'user' | 'assistant';
session_id: string;
message: {
role: "user" | "assistant";
role: 'user' | 'assistant';
content: Array<{ type: string; text?: string; source?: object }>;
};
parent_tool_use_id: null;
}> {
return history.map((historyMsg) => ({
type: historyMsg.role,
session_id: "",
session_id: '',
message: {
role: historyMsg.role,
content: normalizeContentBlocks(historyMsg.content),

View File

@@ -17,10 +17,7 @@ import type { ErrorType, ErrorInfo } from '@automaker/types';
* @returns True if the error is an abort error
*/
export function isAbortError(error: unknown): boolean {
return (
error instanceof Error &&
(error.name === "AbortError" || error.message.includes("abort"))
);
return error instanceof Error && (error.name === 'AbortError' || error.message.includes('abort'));
}
/**
@@ -32,10 +29,10 @@ export function isAbortError(error: unknown): boolean {
export function isCancellationError(errorMessage: string): boolean {
const lowerMessage = errorMessage.toLowerCase();
return (
lowerMessage.includes("cancelled") ||
lowerMessage.includes("canceled") ||
lowerMessage.includes("stopped") ||
lowerMessage.includes("aborted")
lowerMessage.includes('cancelled') ||
lowerMessage.includes('canceled') ||
lowerMessage.includes('stopped') ||
lowerMessage.includes('aborted')
);
}
@@ -47,10 +44,10 @@ export function isCancellationError(errorMessage: string): boolean {
*/
export function isAuthenticationError(errorMessage: string): boolean {
return (
errorMessage.includes("Authentication failed") ||
errorMessage.includes("Invalid API key") ||
errorMessage.includes("authentication_failed") ||
errorMessage.includes("Fix external API key")
errorMessage.includes('Authentication failed') ||
errorMessage.includes('Invalid API key') ||
errorMessage.includes('authentication_failed') ||
errorMessage.includes('Fix external API key')
);
}
@@ -61,22 +58,22 @@ export function isAuthenticationError(errorMessage: string): boolean {
* @returns Classified error information
*/
export function classifyError(error: unknown): ErrorInfo {
const message = error instanceof Error ? error.message : String(error || "Unknown error");
const message = error instanceof Error ? error.message : String(error || 'Unknown error');
const isAbort = isAbortError(error);
const isAuth = isAuthenticationError(message);
const isCancellation = isCancellationError(message);
let type: ErrorType;
if (isAuth) {
type = "authentication";
type = 'authentication';
} else if (isAbort) {
type = "abort";
type = 'abort';
} else if (isCancellation) {
type = "cancellation";
type = 'cancellation';
} else if (error instanceof Error) {
type = "execution";
type = 'execution';
} else {
type = "unknown";
type = 'unknown';
}
return {
@@ -99,11 +96,11 @@ export function getUserFriendlyErrorMessage(error: unknown): string {
const info = classifyError(error);
if (info.isAbort) {
return "Operation was cancelled";
return 'Operation was cancelled';
}
if (info.isAuth) {
return "Authentication failed. Please check your API key.";
return 'Authentication failed. Please check your API key.';
}
return info.message;
@@ -130,5 +127,5 @@ export function getUserFriendlyErrorMessage(error: unknown): string {
* ```
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Unknown error";
return error instanceof Error ? error.message : 'Unknown error';
}

View File

@@ -2,8 +2,8 @@
* File system utilities that handle symlinks safely
*/
import { secureFs } from "@automaker/platform";
import path from "path";
import { secureFs } from '@automaker/platform';
import path from 'path';
/**
* Create a directory, handling symlinks safely to avoid ELOOP errors.
@@ -23,10 +23,10 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
throw new Error(`Path exists and is not a directory: ${resolvedPath}`);
} catch (error: any) {
// ENOENT means path doesn't exist - we should create it
if (error.code !== "ENOENT") {
if (error.code !== 'ENOENT') {
// Some other error (could be ELOOP in parent path)
// If it's ELOOP, the path involves symlinks - don't try to create
if (error.code === "ELOOP") {
if (error.code === 'ELOOP') {
console.warn(`[fs-utils] Symlink loop detected at ${resolvedPath}, skipping mkdir`);
return;
}
@@ -39,7 +39,7 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
await secureFs.mkdir(resolvedPath, { recursive: true });
} catch (error: any) {
// Handle race conditions and symlink issues
if (error.code === "EEXIST" || error.code === "ELOOP") {
if (error.code === 'EEXIST' || error.code === 'ELOOP') {
return;
}
throw error;
@@ -55,11 +55,11 @@ export async function existsSafe(filePath: string): Promise<boolean> {
await secureFs.lstat(filePath);
return true;
} catch (error: any) {
if (error.code === "ENOENT") {
if (error.code === 'ENOENT') {
return false;
}
// ELOOP or other errors - path exists but is problematic
if (error.code === "ELOOP") {
if (error.code === 'ELOOP') {
return true; // Symlink exists, even if looping
}
throw error;

View File

@@ -8,19 +8,19 @@
* - Path resolution (relative/absolute)
*/
import { secureFs } from "@automaker/platform";
import path from "path";
import { secureFs } from '@automaker/platform';
import path from 'path';
import type { ImageData, ImageContentBlock } from '@automaker/types';
/**
* MIME type mapping for image file extensions
*/
const IMAGE_MIME_TYPES: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
} as const;
/**
@@ -31,7 +31,7 @@ const IMAGE_MIME_TYPES: Record<string, string> = {
*/
export function getMimeTypeForImage(imagePath: string): string {
const ext = path.extname(imagePath).toLowerCase();
return IMAGE_MIME_TYPES[ext] || "image/png";
return IMAGE_MIME_TYPES[ext] || 'image/png';
}
/**
@@ -42,8 +42,8 @@ export function getMimeTypeForImage(imagePath: string): string {
* @throws Error if file cannot be read
*/
export async function readImageAsBase64(imagePath: string): Promise<ImageData> {
const imageBuffer = await secureFs.readFile(imagePath) as Buffer;
const base64Data = imageBuffer.toString("base64");
const imageBuffer = (await secureFs.readFile(imagePath)) as Buffer;
const base64Data = imageBuffer.toString('base64');
const mimeType = getMimeTypeForImage(imagePath);
return {
@@ -71,16 +71,15 @@ export async function convertImagesToContentBlocks(
for (const imagePath of imagePaths) {
try {
// Resolve to absolute path if needed
const absolutePath = workDir && !path.isAbsolute(imagePath)
? path.join(workDir, imagePath)
: imagePath;
const absolutePath =
workDir && !path.isAbsolute(imagePath) ? path.join(workDir, imagePath) : imagePath;
const imageData = await readImageAsBase64(absolutePath);
blocks.push({
type: "image",
type: 'image',
source: {
type: "base64",
type: 'base64',
media_type: imageData.mimeType,
data: imageData.base64,
},
@@ -103,10 +102,10 @@ export async function convertImagesToContentBlocks(
*/
export function formatImagePathsForPrompt(imagePaths: string[]): string {
if (imagePaths.length === 0) {
return "";
return '';
}
let text = "\n\nAttached images:\n";
let text = '\n\nAttached images:\n';
for (const imagePath of imagePaths) {
text += `- ${imagePath}\n`;
}

View File

@@ -53,7 +53,7 @@ export function createLogger(context: string) {
debug: (...args: unknown[]): void => {
if (currentLogLevel >= LogLevel.DEBUG) {
console.log(prefix, "[DEBUG]", ...args);
console.log(prefix, '[DEBUG]', ...args);
}
},
};

View File

@@ -22,7 +22,7 @@
* ```
*/
export function normalizePath(p: string): string {
return p.replace(/\\/g, "/");
return p.replace(/\\/g, '/');
}
/**
@@ -45,10 +45,7 @@ export function normalizePath(p: string): string {
* pathsEqual(null, null); // true
* ```
*/
export function pathsEqual(
p1: string | undefined | null,
p2: string | undefined | null
): boolean {
export function pathsEqual(p1: string | undefined | null, p2: string | undefined | null): boolean {
if (!p1 || !p2) return p1 === p2;
return normalizePath(p1) === normalizePath(p2);
}

View File

@@ -8,16 +8,18 @@
* - Supports both vision and non-vision models
*/
import { convertImagesToContentBlocks, formatImagePathsForPrompt } from "./image-handler.js";
import { convertImagesToContentBlocks, formatImagePathsForPrompt } from './image-handler.js';
/**
* Content that can be either simple text or structured blocks
*/
export type PromptContent = string | Array<{
type: string;
text?: string;
source?: object;
}>;
export type PromptContent =
| string
| Array<{
type: string;
text?: string;
source?: object;
}>;
/**
* Result of building a prompt with optional images
@@ -62,7 +64,7 @@ export async function buildPromptWithImages(
// Add text block if we have text
if (textContent.trim()) {
contentBlocks.push({ type: "text", text: textContent });
contentBlocks.push({ type: 'text', text: textContent });
}
// Add image blocks
@@ -71,9 +73,7 @@ export async function buildPromptWithImages(
// Return appropriate format
const content: PromptContent =
contentBlocks.length > 1 || contentBlocks[0]?.type === "image"
? contentBlocks
: textContent;
contentBlocks.length > 1 || contentBlocks[0]?.type === 'image' ? contentBlocks : textContent;
return { content, hasImages: true };
}

View File

@@ -1,261 +1,241 @@
import { describe, it, expect } from "vitest";
import type { ConversationMessage } from "@automaker/types";
import { describe, it, expect } from 'vitest';
import type { ConversationMessage } from '@automaker/types';
import {
extractTextFromContent,
normalizeContentBlocks,
formatHistoryAsText,
convertHistoryToMessages,
} from "../src/conversation-utils";
} from '../src/conversation-utils';
describe("conversation-utils.ts", () => {
describe("extractTextFromContent", () => {
it("should extract text from string content", () => {
const content = "Hello, world!";
describe('conversation-utils.ts', () => {
describe('extractTextFromContent', () => {
it('should extract text from string content', () => {
const content = 'Hello, world!';
const result = extractTextFromContent(content);
expect(result).toBe("Hello, world!");
expect(result).toBe('Hello, world!');
});
it("should extract text from array content with text blocks", () => {
it('should extract text from array content with text blocks', () => {
const content = [
{ type: "text", text: "First block" },
{ type: "text", text: "Second block" },
{ type: 'text', text: 'First block' },
{ type: 'text', text: 'Second block' },
];
const result = extractTextFromContent(content);
expect(result).toBe("First block\nSecond block");
expect(result).toBe('First block\nSecond block');
});
it("should filter out non-text blocks", () => {
it('should filter out non-text blocks', () => {
const content = [
{ type: "text", text: "Text block" },
{ type: "image", source: { data: "..." } },
{ type: "text", text: "Another text" },
{ type: 'text', text: 'Text block' },
{ type: 'image', source: { data: '...' } },
{ type: 'text', text: 'Another text' },
];
const result = extractTextFromContent(content);
expect(result).toBe("Text block\nAnother text");
expect(result).toBe('Text block\nAnother text');
});
it("should handle empty text blocks", () => {
it('should handle empty text blocks', () => {
const content = [
{ type: "text", text: "First" },
{ type: "text" },
{ type: "text", text: "Third" },
{ type: 'text', text: 'First' },
{ type: 'text' },
{ type: 'text', text: 'Third' },
];
const result = extractTextFromContent(content);
expect(result).toBe("First\n\nThird");
expect(result).toBe('First\n\nThird');
});
it("should return empty string for array with only non-text blocks", () => {
it('should return empty string for array with only non-text blocks', () => {
const content = [
{ type: "image", source: {} },
{ type: "tool_use", source: {} },
{ type: 'image', source: {} },
{ type: 'tool_use', source: {} },
];
const result = extractTextFromContent(content);
expect(result).toBe("");
expect(result).toBe('');
});
it("should return empty string for empty array", () => {
it('should return empty string for empty array', () => {
const content: Array<{ type: string; text?: string }> = [];
const result = extractTextFromContent(content);
expect(result).toBe("");
expect(result).toBe('');
});
});
describe("normalizeContentBlocks", () => {
it("should convert string to array of text blocks", () => {
const content = "Simple text";
describe('normalizeContentBlocks', () => {
it('should convert string to array of text blocks', () => {
const content = 'Simple text';
const result = normalizeContentBlocks(content);
expect(result).toEqual([{ type: "text", text: "Simple text" }]);
expect(result).toEqual([{ type: 'text', text: 'Simple text' }]);
});
it("should return array as-is", () => {
it('should return array as-is', () => {
const content = [
{ type: "text", text: "First" },
{ type: "image", source: {} },
{ type: 'text', text: 'First' },
{ type: 'image', source: {} },
];
const result = normalizeContentBlocks(content);
expect(result).toBe(content);
expect(result).toEqual(content);
});
it("should handle empty string", () => {
const content = "";
it('should handle empty string', () => {
const content = '';
const result = normalizeContentBlocks(content);
expect(result).toEqual([{ type: "text", text: "" }]);
expect(result).toEqual([{ type: 'text', text: '' }]);
});
it("should handle multiline string", () => {
const content = "Line 1\nLine 2\nLine 3";
it('should handle multiline string', () => {
const content = 'Line 1\nLine 2\nLine 3';
const result = normalizeContentBlocks(content);
expect(result).toEqual([{ type: "text", text: "Line 1\nLine 2\nLine 3" }]);
expect(result).toEqual([{ type: 'text', text: 'Line 1\nLine 2\nLine 3' }]);
});
});
describe("formatHistoryAsText", () => {
it("should format empty history as empty string", () => {
describe('formatHistoryAsText', () => {
it('should format empty history as empty string', () => {
const history: ConversationMessage[] = [];
const result = formatHistoryAsText(history);
expect(result).toBe("");
expect(result).toBe('');
});
it("should format single user message", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "Hello!" },
];
it('should format single user message', () => {
const history: ConversationMessage[] = [{ role: 'user', content: 'Hello!' }];
const result = formatHistoryAsText(history);
expect(result).toBe("Previous conversation:\n\nUser: Hello!\n\n---\n\n");
expect(result).toBe('Previous conversation:\n\nUser: Hello!\n\n---\n\n');
});
it("should format single assistant message", () => {
it('should format single assistant message', () => {
const history: ConversationMessage[] = [{ role: 'assistant', content: 'Hi there!' }];
const result = formatHistoryAsText(history);
expect(result).toBe('Previous conversation:\n\nAssistant: Hi there!\n\n---\n\n');
});
it('should format conversation with multiple messages', () => {
const history: ConversationMessage[] = [
{ role: "assistant", content: "Hi there!" },
{ role: 'user', content: "What's 2+2?" },
{ role: 'assistant', content: 'The answer is 4.' },
{ role: 'user', content: 'Thanks!' },
];
const result = formatHistoryAsText(history);
expect(result).toBe(
"Previous conversation:\n\nAssistant: Hi there!\n\n---\n\n"
);
});
it("should format conversation with multiple messages", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "What's 2+2?" },
{ role: "assistant", content: "The answer is 4." },
{ role: "user", content: "Thanks!" },
];
const result = formatHistoryAsText(history);
expect(result).toBe(
"Previous conversation:\n\n" +
'Previous conversation:\n\n' +
"User: What's 2+2?\n\n" +
"Assistant: The answer is 4.\n\n" +
"User: Thanks!\n\n" +
"---\n\n"
'Assistant: The answer is 4.\n\n' +
'User: Thanks!\n\n' +
'---\n\n'
);
});
it("should handle array content by extracting text", () => {
it('should handle array content by extracting text', () => {
const history: ConversationMessage[] = [
{
role: "user",
role: 'user',
content: [
{ type: "text", text: "First part" },
{ type: "text", text: "Second part" },
{ type: 'text', text: 'First part' },
{ type: 'text', text: 'Second part' },
],
},
];
const result = formatHistoryAsText(history);
expect(result).toBe(
"Previous conversation:\n\nUser: First part\nSecond part\n\n---\n\n"
);
expect(result).toBe('Previous conversation:\n\nUser: First part\nSecond part\n\n---\n\n');
});
it("should handle mixed string and array content", () => {
it('should handle mixed string and array content', () => {
const history: ConversationMessage[] = [
{ role: "user", content: "String message" },
{ role: 'user', content: 'String message' },
{
role: "assistant",
content: [{ type: "text", text: "Array message" }],
role: 'assistant',
content: [{ type: 'text', text: 'Array message' }],
},
];
const result = formatHistoryAsText(history);
expect(result).toContain("User: String message");
expect(result).toContain("Assistant: Array message");
expect(result).toContain('User: String message');
expect(result).toContain('Assistant: Array message');
});
});
describe("convertHistoryToMessages", () => {
it("should convert empty history", () => {
describe('convertHistoryToMessages', () => {
it('should convert empty history', () => {
const history: ConversationMessage[] = [];
const result = convertHistoryToMessages(history);
expect(result).toEqual([]);
});
it("should convert single user message", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "Hello!" },
];
it('should convert single user message', () => {
const history: ConversationMessage[] = [{ role: 'user', content: 'Hello!' }];
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
type: "user",
session_id: "",
type: 'user',
session_id: '',
message: {
role: "user",
content: [{ type: "text", text: "Hello!" }],
role: 'user',
content: [{ type: 'text', text: 'Hello!' }],
},
parent_tool_use_id: null,
});
});
it("should convert single assistant message", () => {
const history: ConversationMessage[] = [
{ role: "assistant", content: "Hi there!" },
];
it('should convert single assistant message', () => {
const history: ConversationMessage[] = [{ role: 'assistant', content: 'Hi there!' }];
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
type: "assistant",
session_id: "",
type: 'assistant',
session_id: '',
message: {
role: "assistant",
content: [{ type: "text", text: "Hi there!" }],
role: 'assistant',
content: [{ type: 'text', text: 'Hi there!' }],
},
parent_tool_use_id: null,
});
});
it("should preserve array content as-is", () => {
it('should preserve array content as-is', () => {
const content = [
{ type: "text", text: "Text" },
{ type: "image", source: { data: "..." } },
{ type: 'text', text: 'Text' },
{ type: 'image', source: { data: '...' } },
];
const history: ConversationMessage[] = [{ role: "user", content }];
const history: ConversationMessage[] = [{ role: 'user', content }];
const result = convertHistoryToMessages(history);
expect(result[0].message.content).toEqual(content);
});
it("should convert multiple messages", () => {
it('should convert multiple messages', () => {
const history: ConversationMessage[] = [
{ role: "user", content: "First" },
{ role: "assistant", content: "Second" },
{ role: "user", content: "Third" },
{ role: 'user', content: 'First' },
{ role: 'assistant', content: 'Second' },
{ role: 'user', content: 'Third' },
];
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(3);
expect(result[0].type).toBe("user");
expect(result[1].type).toBe("assistant");
expect(result[2].type).toBe("user");
expect(result[0].type).toBe('user');
expect(result[1].type).toBe('assistant');
expect(result[2].type).toBe('user');
});
it("should set session_id to empty string", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "Test" },
];
it('should set session_id to empty string', () => {
const history: ConversationMessage[] = [{ role: 'user', content: 'Test' }];
const result = convertHistoryToMessages(history);
expect(result[0].session_id).toBe("");
expect(result[0].session_id).toBe('');
});
it("should set parent_tool_use_id to null", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "Test" },
];
it('should set parent_tool_use_id to null', () => {
const history: ConversationMessage[] = [{ role: 'user', content: 'Test' }];
const result = convertHistoryToMessages(history);
expect(result[0].parent_tool_use_id).toBeNull();
});
it("should normalize string content to blocks", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "String content" },
];
it('should normalize string content to blocks', () => {
const history: ConversationMessage[] = [{ role: 'user', content: 'String content' }];
const result = convertHistoryToMessages(history);
expect(result[0].message.content).toEqual([
{ type: "text", text: "String content" },
]);
expect(result[0].message.content).toEqual([{ type: 'text', text: 'String content' }]);
});
});
});

View File

@@ -1,261 +1,261 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect } from 'vitest';
import {
isAbortError,
isCancellationError,
isAuthenticationError,
classifyError,
getUserFriendlyErrorMessage,
} from "../src/error-handler";
} from '../src/error-handler';
describe("error-handler.ts", () => {
describe("isAbortError", () => {
describe('error-handler.ts', () => {
describe('isAbortError', () => {
it("should return true for Error with name 'AbortError'", () => {
const error = new Error("Operation aborted");
error.name = "AbortError";
const error = new Error('Operation aborted');
error.name = 'AbortError';
expect(isAbortError(error)).toBe(true);
});
it("should return true for Error with message containing 'abort'", () => {
const error = new Error("Request was aborted");
const error = new Error('Request was aborted');
expect(isAbortError(error)).toBe(true);
});
it("should return false for regular Error", () => {
const error = new Error("Something went wrong");
it('should return false for regular Error', () => {
const error = new Error('Something went wrong');
expect(isAbortError(error)).toBe(false);
});
it("should return false for non-Error values", () => {
expect(isAbortError("abort")).toBe(false);
it('should return false for non-Error values', () => {
expect(isAbortError('abort')).toBe(false);
expect(isAbortError(null)).toBe(false);
expect(isAbortError(undefined)).toBe(false);
expect(isAbortError({})).toBe(false);
});
it("should handle Error with both AbortError name and abort message", () => {
const error = new Error("abort");
error.name = "AbortError";
it('should handle Error with both AbortError name and abort message', () => {
const error = new Error('abort');
error.name = 'AbortError';
expect(isAbortError(error)).toBe(true);
});
});
describe("isCancellationError", () => {
describe('isCancellationError', () => {
it("should return true for 'cancelled' message", () => {
expect(isCancellationError("Operation cancelled")).toBe(true);
expect(isCancellationError("CANCELLED")).toBe(true);
expect(isCancellationError('Operation cancelled')).toBe(true);
expect(isCancellationError('CANCELLED')).toBe(true);
});
it("should return true for 'canceled' message (US spelling)", () => {
expect(isCancellationError("Operation canceled")).toBe(true);
expect(isCancellationError("CANCELED")).toBe(true);
expect(isCancellationError('Operation canceled')).toBe(true);
expect(isCancellationError('CANCELED')).toBe(true);
});
it("should return true for 'stopped' message", () => {
expect(isCancellationError("Process stopped")).toBe(true);
expect(isCancellationError("STOPPED")).toBe(true);
expect(isCancellationError('Process stopped')).toBe(true);
expect(isCancellationError('STOPPED')).toBe(true);
});
it("should return true for 'aborted' message", () => {
expect(isCancellationError("Request aborted")).toBe(true);
expect(isCancellationError("ABORTED")).toBe(true);
expect(isCancellationError('Request aborted')).toBe(true);
expect(isCancellationError('ABORTED')).toBe(true);
});
it("should return false for non-cancellation messages", () => {
expect(isCancellationError("Something went wrong")).toBe(false);
expect(isCancellationError("Error occurred")).toBe(false);
expect(isCancellationError("")).toBe(false);
it('should return false for non-cancellation messages', () => {
expect(isCancellationError('Something went wrong')).toBe(false);
expect(isCancellationError('Error occurred')).toBe(false);
expect(isCancellationError('')).toBe(false);
});
it("should be case-insensitive", () => {
expect(isCancellationError("CaNcElLeD")).toBe(true);
expect(isCancellationError("StOpPeD")).toBe(true);
it('should be case-insensitive', () => {
expect(isCancellationError('CaNcElLeD')).toBe(true);
expect(isCancellationError('StOpPeD')).toBe(true);
});
});
describe("isAuthenticationError", () => {
describe('isAuthenticationError', () => {
it("should return true for 'Authentication failed' message", () => {
expect(isAuthenticationError("Authentication failed")).toBe(true);
expect(isAuthenticationError('Authentication failed')).toBe(true);
});
it("should return true for 'Invalid API key' message", () => {
expect(isAuthenticationError("Invalid API key provided")).toBe(true);
expect(isAuthenticationError('Invalid API key provided')).toBe(true);
});
it("should return true for 'authentication_failed' message", () => {
expect(isAuthenticationError("Error: authentication_failed")).toBe(true);
expect(isAuthenticationError('Error: authentication_failed')).toBe(true);
});
it("should return true for 'Fix external API key' message", () => {
expect(isAuthenticationError("Fix external API key configuration")).toBe(true);
expect(isAuthenticationError('Fix external API key configuration')).toBe(true);
});
it("should return false for non-authentication errors", () => {
expect(isAuthenticationError("Something went wrong")).toBe(false);
expect(isAuthenticationError("Network error")).toBe(false);
expect(isAuthenticationError("")).toBe(false);
it('should return false for non-authentication errors', () => {
expect(isAuthenticationError('Something went wrong')).toBe(false);
expect(isAuthenticationError('Network error')).toBe(false);
expect(isAuthenticationError('')).toBe(false);
});
it("should be case-sensitive", () => {
expect(isAuthenticationError("authentication failed")).toBe(false);
expect(isAuthenticationError("AUTHENTICATION FAILED")).toBe(false);
it('should be case-sensitive', () => {
expect(isAuthenticationError('authentication failed')).toBe(false);
expect(isAuthenticationError('AUTHENTICATION FAILED')).toBe(false);
});
});
describe("classifyError", () => {
it("should classify authentication errors", () => {
const error = new Error("Authentication failed");
describe('classifyError', () => {
it('should classify authentication errors', () => {
const error = new Error('Authentication failed');
const result = classifyError(error);
expect(result.type).toBe("authentication");
expect(result.type).toBe('authentication');
expect(result.isAuth).toBe(true);
expect(result.isAbort).toBe(false);
expect(result.isCancellation).toBe(false);
expect(result.message).toBe("Authentication failed");
expect(result.message).toBe('Authentication failed');
expect(result.originalError).toBe(error);
});
it("should classify abort errors", () => {
const error = new Error("aborted");
it('should classify abort errors', () => {
const error = new Error('aborted');
const result = classifyError(error);
expect(result.type).toBe("abort");
expect(result.type).toBe('abort');
expect(result.isAbort).toBe(true);
expect(result.isAuth).toBe(false);
expect(result.message).toBe("aborted");
expect(result.message).toBe('aborted');
});
it("should classify AbortError by name", () => {
const error = new Error("Request cancelled");
error.name = "AbortError";
it('should classify AbortError by name', () => {
const error = new Error('Request cancelled');
error.name = 'AbortError';
const result = classifyError(error);
expect(result.type).toBe("abort");
expect(result.type).toBe('abort');
expect(result.isAbort).toBe(true);
});
it("should classify cancellation errors", () => {
const error = new Error("Operation cancelled");
it('should classify cancellation errors', () => {
const error = new Error('Operation cancelled');
const result = classifyError(error);
expect(result.type).toBe("cancellation");
expect(result.type).toBe('cancellation');
expect(result.isCancellation).toBe(true);
expect(result.isAbort).toBe(false);
});
it("should classify execution errors (regular Error)", () => {
const error = new Error("Something went wrong");
it('should classify execution errors (regular Error)', () => {
const error = new Error('Something went wrong');
const result = classifyError(error);
expect(result.type).toBe("execution");
expect(result.type).toBe('execution');
expect(result.isAuth).toBe(false);
expect(result.isAbort).toBe(false);
expect(result.isCancellation).toBe(false);
});
it("should classify unknown errors (non-Error)", () => {
const result = classifyError("string error");
it('should classify unknown errors (non-Error)', () => {
const result = classifyError('string error');
expect(result.type).toBe("unknown");
expect(result.message).toBe("string error");
expect(result.type).toBe('unknown');
expect(result.message).toBe('string error');
});
it("should handle null/undefined errors", () => {
it('should handle null/undefined errors', () => {
const result1 = classifyError(null);
expect(result1.type).toBe("unknown");
expect(result1.message).toBe("Unknown error");
expect(result1.type).toBe('unknown');
expect(result1.message).toBe('Unknown error');
const result2 = classifyError(undefined);
expect(result2.type).toBe("unknown");
expect(result2.message).toBe("Unknown error");
expect(result2.type).toBe('unknown');
expect(result2.message).toBe('Unknown error');
});
it("should prioritize authentication over abort", () => {
const error = new Error("Authentication failed - aborted");
it('should prioritize authentication over abort', () => {
const error = new Error('Authentication failed - aborted');
const result = classifyError(error);
expect(result.type).toBe("authentication");
expect(result.type).toBe('authentication');
expect(result.isAuth).toBe(true);
expect(result.isAbort).toBe(true); // Both flags can be true
});
it("should prioritize abort over cancellation", () => {
const error = new Error("Request cancelled");
error.name = "AbortError";
it('should prioritize abort over cancellation', () => {
const error = new Error('Request cancelled');
error.name = 'AbortError';
const result = classifyError(error);
expect(result.type).toBe("abort");
expect(result.type).toBe('abort');
expect(result.isAbort).toBe(true);
expect(result.isCancellation).toBe(true); // Both flags can be true
});
it("should convert object errors to string", () => {
const result = classifyError({ code: 500, message: "Server error" });
expect(result.message).toContain("Object");
it('should convert object errors to string', () => {
const result = classifyError({ code: 500, message: 'Server error' });
expect(result.message).toContain('Object');
});
it("should convert number errors to string", () => {
it('should convert number errors to string', () => {
const result = classifyError(404);
expect(result.message).toBe("404");
expect(result.type).toBe("unknown");
expect(result.message).toBe('404');
expect(result.type).toBe('unknown');
});
});
describe("getUserFriendlyErrorMessage", () => {
it("should return friendly message for abort errors", () => {
const error = new Error("abort");
describe('getUserFriendlyErrorMessage', () => {
it('should return friendly message for abort errors', () => {
const error = new Error('abort');
const message = getUserFriendlyErrorMessage(error);
expect(message).toBe("Operation was cancelled");
expect(message).toBe('Operation was cancelled');
});
it("should return friendly message for AbortError by name", () => {
const error = new Error("Something");
error.name = "AbortError";
it('should return friendly message for AbortError by name', () => {
const error = new Error('Something');
error.name = 'AbortError';
const message = getUserFriendlyErrorMessage(error);
expect(message).toBe("Operation was cancelled");
expect(message).toBe('Operation was cancelled');
});
it("should return friendly message for authentication errors", () => {
const error = new Error("Authentication failed");
it('should return friendly message for authentication errors', () => {
const error = new Error('Authentication failed');
const message = getUserFriendlyErrorMessage(error);
expect(message).toBe("Authentication failed. Please check your API key.");
expect(message).toBe('Authentication failed. Please check your API key.');
});
it("should prioritize abort message over auth", () => {
const error = new Error("Authentication failed - abort");
it('should prioritize abort message over auth', () => {
const error = new Error('Authentication failed - abort');
const message = getUserFriendlyErrorMessage(error);
// Auth is checked first in classifyError, but abort check happens before auth in getUserFriendlyErrorMessage
expect(message).toBe("Operation was cancelled");
expect(message).toBe('Operation was cancelled');
});
it("should return original message for other errors", () => {
const error = new Error("Network timeout");
it('should return original message for other errors', () => {
const error = new Error('Network timeout');
const message = getUserFriendlyErrorMessage(error);
expect(message).toBe("Network timeout");
expect(message).toBe('Network timeout');
});
it("should handle non-Error values", () => {
expect(getUserFriendlyErrorMessage("string error")).toBe("string error");
expect(getUserFriendlyErrorMessage(null)).toBe("Unknown error");
expect(getUserFriendlyErrorMessage(undefined)).toBe("Unknown error");
it('should handle non-Error values', () => {
expect(getUserFriendlyErrorMessage('string error')).toBe('string error');
expect(getUserFriendlyErrorMessage(null)).toBe('Unknown error');
expect(getUserFriendlyErrorMessage(undefined)).toBe('Unknown error');
});
it("should return original message for cancellation errors", () => {
const error = new Error("Operation cancelled by user");
it('should return original message for cancellation errors', () => {
const error = new Error('Operation cancelled by user');
const message = getUserFriendlyErrorMessage(error);
expect(message).toBe("Operation cancelled by user");
expect(message).toBe('Operation cancelled by user');
});
it("should handle Error without message", () => {
it('should handle Error without message', () => {
const error = new Error();
const message = getUserFriendlyErrorMessage(error);
expect(message).toBe("");
expect(message).toBe('');
});
});
});

View File

@@ -1,15 +1,15 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { mkdirSafe, existsSafe } from "../src/fs-utils";
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { mkdirSafe, existsSafe } from '../src/fs-utils';
describe("fs-utils.ts", () => {
describe('fs-utils.ts', () => {
let tempDir: string;
beforeEach(async () => {
// Create a temporary directory for testing
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "fs-utils-test-"));
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-utils-test-'));
});
afterEach(async () => {
@@ -21,9 +21,9 @@ describe("fs-utils.ts", () => {
}
});
describe("mkdirSafe", () => {
it("should create a new directory", async () => {
const newDir = path.join(tempDir, "new-directory");
describe('mkdirSafe', () => {
it('should create a new directory', async () => {
const newDir = path.join(tempDir, 'new-directory');
await mkdirSafe(newDir);
@@ -31,8 +31,8 @@ describe("fs-utils.ts", () => {
expect(stats.isDirectory()).toBe(true);
});
it("should create nested directories recursively", async () => {
const nestedDir = path.join(tempDir, "level1", "level2", "level3");
it('should create nested directories recursively', async () => {
const nestedDir = path.join(tempDir, 'level1', 'level2', 'level3');
await mkdirSafe(nestedDir);
@@ -40,54 +40,48 @@ describe("fs-utils.ts", () => {
expect(stats.isDirectory()).toBe(true);
});
it("should succeed when directory already exists", async () => {
const existingDir = path.join(tempDir, "existing");
it('should succeed when directory already exists', async () => {
const existingDir = path.join(tempDir, 'existing');
await fs.mkdir(existingDir);
await expect(mkdirSafe(existingDir)).resolves.not.toThrow();
});
it("should succeed when path is a symlink to a directory", async () => {
const targetDir = path.join(tempDir, "target");
const symlinkPath = path.join(tempDir, "symlink");
it('should succeed when path is a symlink to a directory', async () => {
const targetDir = path.join(tempDir, 'target');
const symlinkPath = path.join(tempDir, 'symlink');
await fs.mkdir(targetDir);
await fs.symlink(targetDir, symlinkPath, "dir");
await fs.symlink(targetDir, symlinkPath, 'dir');
await expect(mkdirSafe(symlinkPath)).resolves.not.toThrow();
});
it("should throw when path exists as a file", async () => {
const filePath = path.join(tempDir, "existing-file.txt");
await fs.writeFile(filePath, "content");
it('should throw when path exists as a file', async () => {
const filePath = path.join(tempDir, 'existing-file.txt');
await fs.writeFile(filePath, 'content');
await expect(mkdirSafe(filePath)).rejects.toThrow(
"Path exists and is not a directory"
);
await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory');
});
it("should resolve relative paths", async () => {
it('should resolve relative paths', async () => {
const originalCwd = process.cwd();
try {
process.chdir(tempDir);
await mkdirSafe("relative-dir");
await mkdirSafe('relative-dir');
const stats = await fs.stat(path.join(tempDir, "relative-dir"));
const stats = await fs.stat(path.join(tempDir, 'relative-dir'));
expect(stats.isDirectory()).toBe(true);
} finally {
process.chdir(originalCwd);
}
});
it("should handle concurrent creation gracefully", async () => {
const newDir = path.join(tempDir, "concurrent");
it('should handle concurrent creation gracefully', async () => {
const newDir = path.join(tempDir, 'concurrent');
const promises = [
mkdirSafe(newDir),
mkdirSafe(newDir),
mkdirSafe(newDir),
];
const promises = [mkdirSafe(newDir), mkdirSafe(newDir), mkdirSafe(newDir)];
await expect(Promise.all(promises)).resolves.not.toThrow();
@@ -95,8 +89,8 @@ describe("fs-utils.ts", () => {
expect(stats.isDirectory()).toBe(true);
});
it("should handle paths with special characters", async () => {
const specialDir = path.join(tempDir, "dir with spaces & special-chars");
it('should handle paths with special characters', async () => {
const specialDir = path.join(tempDir, 'dir with spaces & special-chars');
await mkdirSafe(specialDir);
@@ -105,9 +99,9 @@ describe("fs-utils.ts", () => {
});
});
describe("existsSafe", () => {
it("should return true for existing directory", async () => {
const existingDir = path.join(tempDir, "exists");
describe('existsSafe', () => {
it('should return true for existing directory', async () => {
const existingDir = path.join(tempDir, 'exists');
await fs.mkdir(existingDir);
const result = await existsSafe(existingDir);
@@ -115,28 +109,28 @@ describe("fs-utils.ts", () => {
expect(result).toBe(true);
});
it("should return true for existing file", async () => {
const filePath = path.join(tempDir, "file.txt");
await fs.writeFile(filePath, "content");
it('should return true for existing file', async () => {
const filePath = path.join(tempDir, 'file.txt');
await fs.writeFile(filePath, 'content');
const result = await existsSafe(filePath);
expect(result).toBe(true);
});
it("should return false for non-existent path", async () => {
const nonExistent = path.join(tempDir, "does-not-exist");
it('should return false for non-existent path', async () => {
const nonExistent = path.join(tempDir, 'does-not-exist');
const result = await existsSafe(nonExistent);
expect(result).toBe(false);
});
it("should return true for symlink", async () => {
const target = path.join(tempDir, "target.txt");
const symlink = path.join(tempDir, "link.txt");
it('should return true for symlink', async () => {
const target = path.join(tempDir, 'target.txt');
const symlink = path.join(tempDir, 'link.txt');
await fs.writeFile(target, "content");
await fs.writeFile(target, 'content');
await fs.symlink(target, symlink);
const result = await existsSafe(symlink);
@@ -144,11 +138,11 @@ describe("fs-utils.ts", () => {
expect(result).toBe(true);
});
it("should return true for broken symlink", async () => {
const symlink = path.join(tempDir, "broken-link");
it('should return true for broken symlink', async () => {
const symlink = path.join(tempDir, 'broken-link');
// Create symlink to non-existent target
await fs.symlink("/non/existent/path", symlink);
await fs.symlink('/non/existent/path', symlink);
const result = await existsSafe(symlink);
@@ -156,14 +150,14 @@ describe("fs-utils.ts", () => {
expect(result).toBe(true);
});
it("should handle relative paths", async () => {
it('should handle relative paths', async () => {
const originalCwd = process.cwd();
try {
process.chdir(tempDir);
await fs.writeFile("test.txt", "content");
await fs.writeFile('test.txt', 'content');
const result = await existsSafe("test.txt");
const result = await existsSafe('test.txt');
expect(result).toBe(true);
} finally {
@@ -171,17 +165,17 @@ describe("fs-utils.ts", () => {
}
});
it("should handle paths with special characters", async () => {
const specialFile = path.join(tempDir, "file with spaces & chars.txt");
await fs.writeFile(specialFile, "content");
it('should handle paths with special characters', async () => {
const specialFile = path.join(tempDir, 'file with spaces & chars.txt');
await fs.writeFile(specialFile, 'content');
const result = await existsSafe(specialFile);
expect(result).toBe(true);
});
it("should return false for parent of non-existent nested path", async () => {
const nonExistent = path.join(tempDir, "does", "not", "exist");
it('should return false for parent of non-existent nested path', async () => {
const nonExistent = path.join(tempDir, 'does', 'not', 'exist');
const result = await existsSafe(nonExistent);
@@ -189,20 +183,20 @@ describe("fs-utils.ts", () => {
});
});
describe("Error handling", () => {
it("should handle permission errors in mkdirSafe", async () => {
describe('Error handling', () => {
it('should handle permission errors in mkdirSafe', async () => {
// Skip on Windows where permissions work differently
if (process.platform === "win32") {
if (process.platform === 'win32') {
return;
}
const restrictedDir = path.join(tempDir, "restricted");
const restrictedDir = path.join(tempDir, 'restricted');
await fs.mkdir(restrictedDir);
// Make directory read-only
await fs.chmod(restrictedDir, 0o444);
const newDir = path.join(restrictedDir, "new");
const newDir = path.join(restrictedDir, 'new');
try {
await expect(mkdirSafe(newDir)).rejects.toThrow();
@@ -212,23 +206,21 @@ describe("fs-utils.ts", () => {
}
});
it("should propagate unexpected errors in existsSafe", async () => {
const mockError = new Error("Unexpected error");
(mockError as any).code = "EACCES";
it('should propagate unexpected errors in existsSafe', async () => {
const mockError = new Error('Unexpected error');
(mockError as any).code = 'EACCES';
const spy = vi.spyOn(fs, "lstat").mockRejectedValueOnce(mockError);
const spy = vi.spyOn(fs, 'lstat').mockRejectedValueOnce(mockError);
await expect(existsSafe("/some/path")).rejects.toThrow(
"Unexpected error"
);
await expect(existsSafe('/some/path')).rejects.toThrow('Unexpected error');
spy.mockRestore();
});
});
describe("Integration scenarios", () => {
it("should work together: check existence then create if missing", async () => {
const dirPath = path.join(tempDir, "check-then-create");
describe('Integration scenarios', () => {
it('should work together: check existence then create if missing', async () => {
const dirPath = path.join(tempDir, 'check-then-create');
const existsBefore = await existsSafe(dirPath);
expect(existsBefore).toBe(false);
@@ -239,10 +231,10 @@ describe("fs-utils.ts", () => {
expect(existsAfter).toBe(true);
});
it("should handle nested directory creation with existence checks", async () => {
const level1 = path.join(tempDir, "level1");
const level2 = path.join(level1, "level2");
const level3 = path.join(level2, "level3");
it('should handle nested directory creation with existence checks', async () => {
const level1 = path.join(tempDir, 'level1');
const level2 = path.join(level1, 'level2');
const level3 = path.join(level2, 'level3');
await mkdirSafe(level3);

View File

@@ -1,19 +1,19 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import {
getMimeTypeForImage,
readImageAsBase64,
convertImagesToContentBlocks,
formatImagePathsForPrompt,
} from "../src/image-handler";
} from '../src/image-handler';
describe("image-handler.ts", () => {
describe('image-handler.ts', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "image-handler-test-"));
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'image-handler-test-'));
});
afterEach(async () => {
@@ -24,70 +24,70 @@ describe("image-handler.ts", () => {
}
});
describe("getMimeTypeForImage", () => {
it("should return correct MIME type for .jpg", () => {
expect(getMimeTypeForImage("image.jpg")).toBe("image/jpeg");
expect(getMimeTypeForImage("/path/to/image.jpg")).toBe("image/jpeg");
describe('getMimeTypeForImage', () => {
it('should return correct MIME type for .jpg', () => {
expect(getMimeTypeForImage('image.jpg')).toBe('image/jpeg');
expect(getMimeTypeForImage('/path/to/image.jpg')).toBe('image/jpeg');
});
it("should return correct MIME type for .jpeg", () => {
expect(getMimeTypeForImage("image.jpeg")).toBe("image/jpeg");
it('should return correct MIME type for .jpeg', () => {
expect(getMimeTypeForImage('image.jpeg')).toBe('image/jpeg');
});
it("should return correct MIME type for .png", () => {
expect(getMimeTypeForImage("image.png")).toBe("image/png");
it('should return correct MIME type for .png', () => {
expect(getMimeTypeForImage('image.png')).toBe('image/png');
});
it("should return correct MIME type for .gif", () => {
expect(getMimeTypeForImage("image.gif")).toBe("image/gif");
it('should return correct MIME type for .gif', () => {
expect(getMimeTypeForImage('image.gif')).toBe('image/gif');
});
it("should return correct MIME type for .webp", () => {
expect(getMimeTypeForImage("image.webp")).toBe("image/webp");
it('should return correct MIME type for .webp', () => {
expect(getMimeTypeForImage('image.webp')).toBe('image/webp');
});
it("should be case-insensitive", () => {
expect(getMimeTypeForImage("image.JPG")).toBe("image/jpeg");
expect(getMimeTypeForImage("image.PNG")).toBe("image/png");
expect(getMimeTypeForImage("image.GIF")).toBe("image/gif");
it('should be case-insensitive', () => {
expect(getMimeTypeForImage('image.JPG')).toBe('image/jpeg');
expect(getMimeTypeForImage('image.PNG')).toBe('image/png');
expect(getMimeTypeForImage('image.GIF')).toBe('image/gif');
});
it("should default to image/png for unknown extensions", () => {
expect(getMimeTypeForImage("file.xyz")).toBe("image/png");
expect(getMimeTypeForImage("file.txt")).toBe("image/png");
expect(getMimeTypeForImage("file")).toBe("image/png");
it('should default to image/png for unknown extensions', () => {
expect(getMimeTypeForImage('file.xyz')).toBe('image/png');
expect(getMimeTypeForImage('file.txt')).toBe('image/png');
expect(getMimeTypeForImage('file')).toBe('image/png');
});
it("should handle filenames with multiple dots", () => {
expect(getMimeTypeForImage("my.file.name.jpg")).toBe("image/jpeg");
it('should handle filenames with multiple dots', () => {
expect(getMimeTypeForImage('my.file.name.jpg')).toBe('image/jpeg');
});
});
describe("readImageAsBase64", () => {
it("should read image and return base64 data", async () => {
const imagePath = path.join(tempDir, "test.png");
const imageContent = Buffer.from("fake png data");
describe('readImageAsBase64', () => {
it('should read image and return base64 data', async () => {
const imagePath = path.join(tempDir, 'test.png');
const imageContent = Buffer.from('fake png data');
await fs.writeFile(imagePath, imageContent);
const result = await readImageAsBase64(imagePath);
expect(result.base64).toBe(imageContent.toString("base64"));
expect(result.mimeType).toBe("image/png");
expect(result.filename).toBe("test.png");
expect(result.base64).toBe(imageContent.toString('base64'));
expect(result.mimeType).toBe('image/png');
expect(result.filename).toBe('test.png');
expect(result.originalPath).toBe(imagePath);
});
it("should handle different image formats", async () => {
it('should handle different image formats', async () => {
const formats = [
{ ext: "jpg", mime: "image/jpeg" },
{ ext: "png", mime: "image/png" },
{ ext: "gif", mime: "image/gif" },
{ ext: "webp", mime: "image/webp" },
{ ext: 'jpg', mime: 'image/jpeg' },
{ ext: 'png', mime: 'image/png' },
{ ext: 'gif', mime: 'image/gif' },
{ ext: 'webp', mime: 'image/webp' },
];
for (const format of formats) {
const imagePath = path.join(tempDir, `image.${format.ext}`);
await fs.writeFile(imagePath, Buffer.from("data"));
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await readImageAsBase64(imagePath);
@@ -97,154 +97,148 @@ describe("image-handler.ts", () => {
});
it("should throw error if file doesn't exist", async () => {
const imagePath = path.join(tempDir, "nonexistent.png");
const imagePath = path.join(tempDir, 'nonexistent.png');
await expect(readImageAsBase64(imagePath)).rejects.toThrow();
});
it("should handle binary image data correctly", async () => {
const imagePath = path.join(tempDir, "binary.png");
it('should handle binary image data correctly', async () => {
const imagePath = path.join(tempDir, 'binary.png');
const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a]);
await fs.writeFile(imagePath, binaryData);
const result = await readImageAsBase64(imagePath);
expect(result.base64).toBe(binaryData.toString("base64"));
expect(result.base64).toBe(binaryData.toString('base64'));
});
});
describe("convertImagesToContentBlocks", () => {
it("should convert single image to content block", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("image data"));
describe('convertImagesToContentBlocks', () => {
it('should convert single image to content block', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('image data'));
const result = await convertImagesToContentBlocks([imagePath]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
type: "image",
type: 'image',
source: {
type: "base64",
media_type: "image/png",
type: 'base64',
media_type: 'image/png',
},
});
expect(result[0].source.data).toBeTruthy();
});
it("should convert multiple images", async () => {
const image1 = path.join(tempDir, "image1.jpg");
const image2 = path.join(tempDir, "image2.png");
it('should convert multiple images', async () => {
const image1 = path.join(tempDir, 'image1.jpg');
const image2 = path.join(tempDir, 'image2.png');
await fs.writeFile(image1, Buffer.from("jpg data"));
await fs.writeFile(image2, Buffer.from("png data"));
await fs.writeFile(image1, Buffer.from('jpg data'));
await fs.writeFile(image2, Buffer.from('png data'));
const result = await convertImagesToContentBlocks([image1, image2]);
expect(result).toHaveLength(2);
expect(result[0].source.media_type).toBe("image/jpeg");
expect(result[1].source.media_type).toBe("image/png");
expect(result[0].source.media_type).toBe('image/jpeg');
expect(result[1].source.media_type).toBe('image/png');
});
it("should resolve relative paths with workDir", async () => {
const image = "test.png";
it('should resolve relative paths with workDir', async () => {
const image = 'test.png';
const imagePath = path.join(tempDir, image);
await fs.writeFile(imagePath, Buffer.from("data"));
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await convertImagesToContentBlocks([image], tempDir);
expect(result).toHaveLength(1);
expect(result[0].type).toBe("image");
expect(result[0].type).toBe('image');
});
it("should handle absolute paths without workDir", async () => {
const imagePath = path.join(tempDir, "absolute.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should handle absolute paths without workDir', async () => {
const imagePath = path.join(tempDir, 'absolute.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await convertImagesToContentBlocks([imagePath]);
expect(result).toHaveLength(1);
});
it("should skip images that fail to load", async () => {
const validImage = path.join(tempDir, "valid.png");
const invalidImage = path.join(tempDir, "nonexistent.png");
it('should skip images that fail to load', async () => {
const validImage = path.join(tempDir, 'valid.png');
const invalidImage = path.join(tempDir, 'nonexistent.png');
await fs.writeFile(validImage, Buffer.from("data"));
await fs.writeFile(validImage, Buffer.from('data'));
const result = await convertImagesToContentBlocks([
validImage,
invalidImage,
]);
const result = await convertImagesToContentBlocks([validImage, invalidImage]);
expect(result).toHaveLength(1);
expect(result[0].source.media_type).toBe("image/png");
expect(result[0].source.media_type).toBe('image/png');
});
it("should return empty array for empty input", async () => {
it('should return empty array for empty input', async () => {
const result = await convertImagesToContentBlocks([]);
expect(result).toEqual([]);
});
it("should preserve order of images", async () => {
const images = ["img1.jpg", "img2.png", "img3.gif"];
it('should preserve order of images', async () => {
const images = ['img1.jpg', 'img2.png', 'img3.gif'];
for (const img of images) {
await fs.writeFile(path.join(tempDir, img), Buffer.from("data"));
await fs.writeFile(path.join(tempDir, img), Buffer.from('data'));
}
const result = await convertImagesToContentBlocks(images, tempDir);
expect(result).toHaveLength(3);
expect(result[0].source.media_type).toBe("image/jpeg");
expect(result[1].source.media_type).toBe("image/png");
expect(result[2].source.media_type).toBe("image/gif");
expect(result[0].source.media_type).toBe('image/jpeg');
expect(result[1].source.media_type).toBe('image/png');
expect(result[2].source.media_type).toBe('image/gif');
});
});
describe("formatImagePathsForPrompt", () => {
it("should return empty string for empty array", () => {
describe('formatImagePathsForPrompt', () => {
it('should return empty string for empty array', () => {
const result = formatImagePathsForPrompt([]);
expect(result).toBe("");
expect(result).toBe('');
});
it("should format single image path", () => {
const result = formatImagePathsForPrompt(["/path/to/image.png"]);
expect(result).toBe("\n\nAttached images:\n- /path/to/image.png\n");
it('should format single image path', () => {
const result = formatImagePathsForPrompt(['/path/to/image.png']);
expect(result).toBe('\n\nAttached images:\n- /path/to/image.png\n');
});
it("should format multiple image paths", () => {
it('should format multiple image paths', () => {
const result = formatImagePathsForPrompt([
"/path/image1.png",
"/path/image2.jpg",
"/path/image3.gif",
'/path/image1.png',
'/path/image2.jpg',
'/path/image3.gif',
]);
expect(result).toBe(
"\n\nAttached images:\n" +
"- /path/image1.png\n" +
"- /path/image2.jpg\n" +
"- /path/image3.gif\n"
'\n\nAttached images:\n' +
'- /path/image1.png\n' +
'- /path/image2.jpg\n' +
'- /path/image3.gif\n'
);
});
it("should handle relative paths", () => {
const result = formatImagePathsForPrompt([
"relative/path/image.png",
"another/image.jpg",
]);
it('should handle relative paths', () => {
const result = formatImagePathsForPrompt(['relative/path/image.png', 'another/image.jpg']);
expect(result).toContain("- relative/path/image.png");
expect(result).toContain("- another/image.jpg");
expect(result).toContain('- relative/path/image.png');
expect(result).toContain('- another/image.jpg');
});
it("should start with newlines", () => {
const result = formatImagePathsForPrompt(["/image.png"]);
expect(result.startsWith("\n\n")).toBe(true);
it('should start with newlines', () => {
const result = formatImagePathsForPrompt(['/image.png']);
expect(result.startsWith('\n\n')).toBe(true);
});
it("should include header text", () => {
const result = formatImagePathsForPrompt(["/image.png"]);
expect(result).toContain("Attached images:");
it('should include header text', () => {
const result = formatImagePathsForPrompt(['/image.png']);
expect(result).toContain('Attached images:');
});
});
});

View File

@@ -1,12 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
createLogger,
LogLevel,
getLogLevel,
setLogLevel,
} from "../src/logger";
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createLogger, LogLevel, getLogLevel, setLogLevel } from '../src/logger';
describe("logger.ts", () => {
describe('logger.ts', () => {
let originalConsoleError: typeof console.error;
let originalConsoleWarn: typeof console.warn;
let originalConsoleLog: typeof console.log;
@@ -33,85 +28,77 @@ describe("logger.ts", () => {
setLogLevel(originalLogLevel);
});
describe("createLogger", () => {
it("should create logger with context prefix", () => {
const logger = createLogger("TestContext");
describe('createLogger', () => {
it('should create logger with context prefix', () => {
const logger = createLogger('TestContext');
setLogLevel(LogLevel.INFO);
logger.info("test message");
logger.info('test message');
expect(console.log).toHaveBeenCalledWith(
"[TestContext]",
"test message"
);
expect(console.log).toHaveBeenCalledWith('[TestContext]', 'test message');
});
it("should handle multiple arguments", () => {
const logger = createLogger("Test");
it('should handle multiple arguments', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.INFO);
logger.info("message", { data: 123 }, [1, 2, 3]);
logger.info('message', { data: 123 }, [1, 2, 3]);
expect(console.log).toHaveBeenCalledWith(
"[Test]",
"message",
{ data: 123 },
[1, 2, 3]
);
expect(console.log).toHaveBeenCalledWith('[Test]', 'message', { data: 123 }, [1, 2, 3]);
});
});
describe("Log levels", () => {
it("should log error at ERROR level", () => {
const logger = createLogger("Test");
describe('Log levels', () => {
it('should log error at ERROR level', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.ERROR);
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.warn).not.toHaveBeenCalled();
expect(console.log).not.toHaveBeenCalled();
});
it("should log error and warn at WARN level", () => {
const logger = createLogger("Test");
it('should log error and warn at WARN level', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.WARN);
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.log).not.toHaveBeenCalled();
});
it("should log error, warn, and info at INFO level", () => {
const logger = createLogger("Test");
it('should log error, warn, and info at INFO level', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.INFO);
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledTimes(1); // Only info, not debug
});
it("should log all messages at DEBUG level", () => {
const logger = createLogger("Test");
it('should log all messages at DEBUG level', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.DEBUG);
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledTimes(1);
@@ -119,97 +106,90 @@ describe("logger.ts", () => {
});
});
describe("error method", () => {
it("should use console.error", () => {
const logger = createLogger("ErrorTest");
describe('error method', () => {
it('should use console.error', () => {
const logger = createLogger('ErrorTest');
setLogLevel(LogLevel.ERROR);
logger.error("error occurred", { code: 500 });
logger.error('error occurred', { code: 500 });
expect(console.error).toHaveBeenCalledWith(
"[ErrorTest]",
"error occurred",
{ code: 500 }
);
expect(console.error).toHaveBeenCalledWith('[ErrorTest]', 'error occurred', { code: 500 });
});
it("should not log when level is below ERROR", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.ERROR - 1 as LogLevel);
it('should not log when level is below ERROR', () => {
const logger = createLogger('Test');
setLogLevel((LogLevel.ERROR - 1) as LogLevel);
logger.error("should not appear");
logger.error('should not appear');
expect(console.error).not.toHaveBeenCalled();
});
});
describe("warn method", () => {
it("should use console.warn", () => {
const logger = createLogger("WarnTest");
describe('warn method', () => {
it('should use console.warn', () => {
const logger = createLogger('WarnTest');
setLogLevel(LogLevel.WARN);
logger.warn("warning message");
logger.warn('warning message');
expect(console.warn).toHaveBeenCalledWith("[WarnTest]", "warning message");
expect(console.warn).toHaveBeenCalledWith('[WarnTest]', 'warning message');
});
it("should not log when level is below WARN", () => {
const logger = createLogger("Test");
it('should not log when level is below WARN', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.ERROR);
logger.warn("should not appear");
logger.warn('should not appear');
expect(console.warn).not.toHaveBeenCalled();
});
});
describe("info method", () => {
it("should use console.log", () => {
const logger = createLogger("InfoTest");
describe('info method', () => {
it('should use console.log', () => {
const logger = createLogger('InfoTest');
setLogLevel(LogLevel.INFO);
logger.info("info message");
logger.info('info message');
expect(console.log).toHaveBeenCalledWith("[InfoTest]", "info message");
expect(console.log).toHaveBeenCalledWith('[InfoTest]', 'info message');
});
it("should not log when level is below INFO", () => {
const logger = createLogger("Test");
it('should not log when level is below INFO', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.WARN);
logger.info("should not appear");
logger.info('should not appear');
expect(console.log).not.toHaveBeenCalled();
});
});
describe("debug method", () => {
it("should use console.log with DEBUG prefix", () => {
const logger = createLogger("DebugTest");
describe('debug method', () => {
it('should use console.log with DEBUG prefix', () => {
const logger = createLogger('DebugTest');
setLogLevel(LogLevel.DEBUG);
logger.debug("debug details", { trace: "..." });
logger.debug('debug details', { trace: '...' });
expect(console.log).toHaveBeenCalledWith(
"[DebugTest]",
"[DEBUG]",
"debug details",
{ trace: "..." }
);
expect(console.log).toHaveBeenCalledWith('[DebugTest]', '[DEBUG]', 'debug details', {
trace: '...',
});
});
it("should not log when level is below DEBUG", () => {
const logger = createLogger("Test");
it('should not log when level is below DEBUG', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.INFO);
logger.debug("should not appear");
logger.debug('should not appear');
expect(console.log).not.toHaveBeenCalled();
});
});
describe("getLogLevel", () => {
it("should return current log level", () => {
describe('getLogLevel', () => {
it('should return current log level', () => {
setLogLevel(LogLevel.DEBUG);
expect(getLogLevel()).toBe(LogLevel.DEBUG);
@@ -218,8 +198,8 @@ describe("logger.ts", () => {
});
});
describe("setLogLevel", () => {
it("should change log level", () => {
describe('setLogLevel', () => {
it('should change log level', () => {
setLogLevel(LogLevel.WARN);
expect(getLogLevel()).toBe(LogLevel.WARN);
@@ -227,86 +207,75 @@ describe("logger.ts", () => {
expect(getLogLevel()).toBe(LogLevel.DEBUG);
});
it("should affect subsequent logging", () => {
const logger = createLogger("Test");
it('should affect subsequent logging', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.ERROR);
logger.info("should not log");
logger.info('should not log');
expect(console.log).not.toHaveBeenCalled();
setLogLevel(LogLevel.INFO);
logger.info("should log");
expect(console.log).toHaveBeenCalledWith("[Test]", "should log");
logger.info('should log');
expect(console.log).toHaveBeenCalledWith('[Test]', 'should log');
});
});
describe("Multiple logger instances", () => {
it("should maintain separate contexts", () => {
const logger1 = createLogger("Service1");
const logger2 = createLogger("Service2");
describe('Multiple logger instances', () => {
it('should maintain separate contexts', () => {
const logger1 = createLogger('Service1');
const logger2 = createLogger('Service2');
setLogLevel(LogLevel.INFO);
logger1.info("from service 1");
logger2.info("from service 2");
logger1.info('from service 1');
logger2.info('from service 2');
expect(console.log).toHaveBeenNthCalledWith(
1,
"[Service1]",
"from service 1"
);
expect(console.log).toHaveBeenNthCalledWith(
2,
"[Service2]",
"from service 2"
);
expect(console.log).toHaveBeenNthCalledWith(1, '[Service1]', 'from service 1');
expect(console.log).toHaveBeenNthCalledWith(2, '[Service2]', 'from service 2');
});
it("should share log level setting", () => {
const logger1 = createLogger("Service1");
const logger2 = createLogger("Service2");
it('should share log level setting', () => {
const logger1 = createLogger('Service1');
const logger2 = createLogger('Service2');
setLogLevel(LogLevel.ERROR);
logger1.info("should not log");
logger2.info("should not log");
logger1.info('should not log');
logger2.info('should not log');
expect(console.log).not.toHaveBeenCalled();
});
});
describe("Edge cases", () => {
it("should handle empty context string", () => {
const logger = createLogger("");
describe('Edge cases', () => {
it('should handle empty context string', () => {
const logger = createLogger('');
setLogLevel(LogLevel.INFO);
logger.info("message");
logger.info('message');
expect(console.log).toHaveBeenCalledWith("[]", "message");
expect(console.log).toHaveBeenCalledWith('[]', 'message');
});
it("should handle context with special characters", () => {
const logger = createLogger("Test-Service_v2.0");
it('should handle context with special characters', () => {
const logger = createLogger('Test-Service_v2.0');
setLogLevel(LogLevel.INFO);
logger.info("message");
logger.info('message');
expect(console.log).toHaveBeenCalledWith(
"[Test-Service_v2.0]",
"message"
);
expect(console.log).toHaveBeenCalledWith('[Test-Service_v2.0]', 'message');
});
it("should handle no arguments to log methods", () => {
const logger = createLogger("Test");
it('should handle no arguments to log methods', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.INFO);
logger.info();
expect(console.log).toHaveBeenCalledWith("[Test]");
expect(console.log).toHaveBeenCalledWith('[Test]');
});
it("should handle complex object arguments", () => {
const logger = createLogger("Test");
it('should handle complex object arguments', () => {
const logger = createLogger('Test');
setLogLevel(LogLevel.INFO);
const complexObj = {
@@ -315,9 +284,9 @@ describe("logger.ts", () => {
fn: () => {},
};
logger.info("complex", complexObj);
logger.info('complex', complexObj);
expect(console.log).toHaveBeenCalledWith("[Test]", "complex", complexObj);
expect(console.log).toHaveBeenCalledWith('[Test]', 'complex', complexObj);
});
});
});

View File

@@ -1,14 +1,14 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { buildPromptWithImages } from "../src/prompt-builder";
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { buildPromptWithImages } from '../src/prompt-builder';
describe("prompt-builder.ts", () => {
describe('prompt-builder.ts', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "prompt-builder-test-"));
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prompt-builder-test-'));
});
afterEach(async () => {
@@ -19,43 +19,40 @@ describe("prompt-builder.ts", () => {
}
});
describe("buildPromptWithImages - no images", () => {
it("should return plain text when no images provided", async () => {
const basePrompt = "Hello, world!";
describe('buildPromptWithImages - no images', () => {
it('should return plain text when no images provided', async () => {
const basePrompt = 'Hello, world!';
const result = await buildPromptWithImages(basePrompt);
expect(result.content).toBe("Hello, world!");
expect(result.content).toBe('Hello, world!');
expect(result.hasImages).toBe(false);
});
it("should return plain text when empty image array provided", async () => {
const basePrompt = "Test prompt";
it('should return plain text when empty image array provided', async () => {
const basePrompt = 'Test prompt';
const result = await buildPromptWithImages(basePrompt, []);
expect(result.content).toBe("Test prompt");
expect(result.content).toBe('Test prompt');
expect(result.hasImages).toBe(false);
});
it("should handle multiline prompts", async () => {
const basePrompt = "Line 1\nLine 2\nLine 3";
it('should handle multiline prompts', async () => {
const basePrompt = 'Line 1\nLine 2\nLine 3';
const result = await buildPromptWithImages(basePrompt);
expect(result.content).toBe("Line 1\nLine 2\nLine 3");
expect(result.content).toBe('Line 1\nLine 2\nLine 3');
});
});
describe("buildPromptWithImages - with images", () => {
it("should build content blocks with single image", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("image data"));
describe('buildPromptWithImages - with images', () => {
it('should build content blocks with single image', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('image data'));
const result = await buildPromptWithImages(
"Check this image",
[imagePath]
);
const result = await buildPromptWithImages('Check this image', [imagePath]);
expect(result.hasImages).toBe(true);
expect(Array.isArray(result.content)).toBe(true);
@@ -67,25 +64,22 @@ describe("prompt-builder.ts", () => {
}>;
expect(blocks).toHaveLength(2);
expect(blocks[0]).toMatchObject({
type: "text",
text: "Check this image",
type: 'text',
text: 'Check this image',
});
expect(blocks[1]).toMatchObject({
type: "image",
type: 'image',
});
});
it("should build content blocks with multiple images", async () => {
const image1 = path.join(tempDir, "img1.jpg");
const image2 = path.join(tempDir, "img2.png");
it('should build content blocks with multiple images', async () => {
const image1 = path.join(tempDir, 'img1.jpg');
const image2 = path.join(tempDir, 'img2.png');
await fs.writeFile(image1, Buffer.from("jpg data"));
await fs.writeFile(image2, Buffer.from("png data"));
await fs.writeFile(image1, Buffer.from('jpg data'));
await fs.writeFile(image2, Buffer.from('png data'));
const result = await buildPromptWithImages("Two images", [
image1,
image2,
]);
const result = await buildPromptWithImages('Two images', [image1, image2]);
expect(result.hasImages).toBe(true);
@@ -95,107 +89,93 @@ describe("prompt-builder.ts", () => {
source?: object;
}>;
expect(blocks).toHaveLength(3); // 1 text + 2 images
expect(blocks[0].type).toBe("text");
expect(blocks[1].type).toBe("image");
expect(blocks[2].type).toBe("image");
expect(blocks[0].type).toBe('text');
expect(blocks[1].type).toBe('image');
expect(blocks[2].type).toBe('image');
});
it("should resolve relative paths with workDir", async () => {
const imagePath = "test.png";
it('should resolve relative paths with workDir', async () => {
const imagePath = 'test.png';
const fullPath = path.join(tempDir, imagePath);
await fs.writeFile(fullPath, Buffer.from("data"));
await fs.writeFile(fullPath, Buffer.from('data'));
const result = await buildPromptWithImages(
"Test",
[imagePath],
tempDir
);
const result = await buildPromptWithImages('Test', [imagePath], tempDir);
expect(result.hasImages).toBe(true);
expect(Array.isArray(result.content)).toBe(true);
});
it("should handle absolute paths without workDir", async () => {
const imagePath = path.join(tempDir, "absolute.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should handle absolute paths without workDir', async () => {
const imagePath = path.join(tempDir, 'absolute.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages("Test", [imagePath]);
const result = await buildPromptWithImages('Test', [imagePath]);
expect(result.hasImages).toBe(true);
});
});
describe("buildPromptWithImages - includeImagePaths option", () => {
it("should not include image paths by default", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
describe('buildPromptWithImages - includeImagePaths option', () => {
it('should not include image paths by default', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages("Prompt", [imagePath]);
const result = await buildPromptWithImages('Prompt', [imagePath]);
const blocks = result.content as Array<{
type: string;
text?: string;
}>;
const textBlock = blocks.find((b) => b.type === "text");
const textBlock = blocks.find((b) => b.type === 'text');
expect(textBlock?.text).not.toContain("Attached images:");
expect(textBlock?.text).toBe("Prompt");
expect(textBlock?.text).not.toContain('Attached images:');
expect(textBlock?.text).toBe('Prompt');
});
it("should include image paths when requested", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should include image paths when requested', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages(
"Prompt",
[imagePath],
undefined,
true
);
const result = await buildPromptWithImages('Prompt', [imagePath], undefined, true);
const blocks = result.content as Array<{
type: string;
text?: string;
}>;
const textBlock = blocks.find((b) => b.type === "text");
const textBlock = blocks.find((b) => b.type === 'text');
expect(textBlock?.text).toContain("Prompt");
expect(textBlock?.text).toContain("Attached images:");
expect(textBlock?.text).toContain('Prompt');
expect(textBlock?.text).toContain('Attached images:');
expect(textBlock?.text).toContain(imagePath);
});
it("should format multiple image paths when included", async () => {
const img1 = path.join(tempDir, "img1.png");
const img2 = path.join(tempDir, "img2.jpg");
it('should format multiple image paths when included', async () => {
const img1 = path.join(tempDir, 'img1.png');
const img2 = path.join(tempDir, 'img2.jpg');
await fs.writeFile(img1, Buffer.from("data1"));
await fs.writeFile(img2, Buffer.from("data2"));
await fs.writeFile(img1, Buffer.from('data1'));
await fs.writeFile(img2, Buffer.from('data2'));
const result = await buildPromptWithImages(
"Test",
[img1, img2],
undefined,
true
);
const result = await buildPromptWithImages('Test', [img1, img2], undefined, true);
const blocks = result.content as Array<{
type: string;
text?: string;
}>;
const textBlock = blocks.find((b) => b.type === "text");
const textBlock = blocks.find((b) => b.type === 'text');
expect(textBlock?.text).toContain("Attached images:");
expect(textBlock?.text).toContain('Attached images:');
expect(textBlock?.text).toContain(img1);
expect(textBlock?.text).toContain(img2);
});
});
describe("buildPromptWithImages - edge cases", () => {
it("should handle empty prompt with images", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
describe('buildPromptWithImages - edge cases', () => {
it('should handle empty prompt with images', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages("", [imagePath]);
const result = await buildPromptWithImages('', [imagePath]);
expect(result.hasImages).toBe(true);
@@ -206,14 +186,14 @@ describe("prompt-builder.ts", () => {
}>;
// Should only have image block, no text block for empty string
expect(blocks.length).toBeGreaterThan(0);
expect(blocks.some((b) => b.type === "image")).toBe(true);
expect(blocks.some((b) => b.type === 'image')).toBe(true);
});
it("should handle whitespace-only prompt with images", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should handle whitespace-only prompt with images', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages(" ", [imagePath]);
const result = await buildPromptWithImages(' ', [imagePath]);
expect(result.hasImages).toBe(true);
@@ -223,19 +203,16 @@ describe("prompt-builder.ts", () => {
source?: object;
}>;
// Whitespace-only is trimmed, so no text block should be added
expect(blocks.every((b) => b.type !== "text")).toBe(true);
expect(blocks.every((b) => b.type !== 'text')).toBe(true);
});
it("should skip failed image loads", async () => {
const validImage = path.join(tempDir, "valid.png");
const invalidImage = path.join(tempDir, "nonexistent.png");
it('should skip failed image loads', async () => {
const validImage = path.join(tempDir, 'valid.png');
const invalidImage = path.join(tempDir, 'nonexistent.png');
await fs.writeFile(validImage, Buffer.from("data"));
await fs.writeFile(validImage, Buffer.from('data'));
const result = await buildPromptWithImages("Test", [
validImage,
invalidImage,
]);
const result = await buildPromptWithImages('Test', [validImage, invalidImage]);
expect(result.hasImages).toBe(true);
@@ -244,28 +221,18 @@ describe("prompt-builder.ts", () => {
text?: string;
source?: object;
}>;
const imageBlocks = blocks.filter((b) => b.type === "image");
const imageBlocks = blocks.filter((b) => b.type === 'image');
// Only valid image should be included
expect(imageBlocks).toHaveLength(1);
});
it("should handle mixed case in includeImagePaths parameter", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should handle mixed case in includeImagePaths parameter', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const resultFalse = await buildPromptWithImages(
"Test",
[imagePath],
undefined,
false
);
const resultTrue = await buildPromptWithImages(
"Test",
[imagePath],
undefined,
true
);
const resultFalse = await buildPromptWithImages('Test', [imagePath], undefined, false);
const resultTrue = await buildPromptWithImages('Test', [imagePath], undefined, true);
const blocksFalse = resultFalse.content as Array<{
type: string;
@@ -276,31 +243,31 @@ describe("prompt-builder.ts", () => {
text?: string;
}>;
expect(blocksFalse[0].text).not.toContain("Attached images:");
expect(blocksTrue[0].text).toContain("Attached images:");
expect(blocksFalse[0].text).not.toContain('Attached images:');
expect(blocksTrue[0].text).toContain('Attached images:');
});
});
describe("buildPromptWithImages - content format", () => {
it("should return string when only text and includeImagePaths false", async () => {
const result = await buildPromptWithImages("Just text", undefined);
describe('buildPromptWithImages - content format', () => {
it('should return string when only text and includeImagePaths false', async () => {
const result = await buildPromptWithImages('Just text', undefined);
expect(typeof result.content).toBe("string");
expect(typeof result.content).toBe('string');
});
it("should return array when has images", async () => {
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should return array when has images', async () => {
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages("Text", [imagePath]);
const result = await buildPromptWithImages('Text', [imagePath]);
expect(Array.isArray(result.content)).toBe(true);
});
it("should preserve prompt formatting", async () => {
const basePrompt = "Line 1\n\nLine 2\n Indented line";
const imagePath = path.join(tempDir, "test.png");
await fs.writeFile(imagePath, Buffer.from("data"));
it('should preserve prompt formatting', async () => {
const basePrompt = 'Line 1\n\nLine 2\n Indented line';
const imagePath = path.join(tempDir, 'test.png');
await fs.writeFile(imagePath, Buffer.from('data'));
const result = await buildPromptWithImages(basePrompt, [imagePath]);
@@ -308,7 +275,7 @@ describe("prompt-builder.ts", () => {
type: string;
text?: string;
}>;
const textBlock = blocks.find((b) => b.type === "text");
const textBlock = blocks.find((b) => b.type === 'text');
expect(textBlock?.text).toBe(basePrompt);
});

View File

@@ -1,15 +1,15 @@
import { defineConfig } from "vitest/config";
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["tests/**/*.test.ts"],
environment: 'node',
include: ['tests/**/*.test.ts'],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.ts"],
exclude: ["src/**/*.d.ts", "src/index.ts"],
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.d.ts', 'src/index.ts'],
thresholds: {
// Excellent coverage: 94.3% stmts, 89.77% branches, 100% funcs, 94.21% lines
// All files now have comprehensive tests