mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge branch 'main' of github.com:AutoMaker-Org/automaker into improve-context-page
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user