test: Add comprehensive tests for platform and utils packages

Added extensive test coverage for previously untested files:

Platform package (94.69% coverage, +47 tests):
- paths.test.ts: 22 tests for path construction and directory creation
- security.test.ts: 25 tests for path validation and security

Utils package (94.3% coverage, +109 tests):
- logger.test.ts: 23 tests for logging with levels
- fs-utils.test.ts: 20 tests for safe file operations
- conversation-utils.test.ts: 24 tests for message formatting
- image-handler.test.ts: 25 tests for image processing
- prompt-builder.test.ts: 17 tests for prompt construction

Coverage improvements:
- Platform: 63.71% → 94.69% stmts, 40% → 97.14% funcs
- Utils: 19.51% → 94.3% stmts, 18.51% → 100% funcs

Updated thresholds to enforce high quality:
- Platform: 90% lines/stmts, 95% funcs, 75% branches
- Utils: 90% lines/stmts, 95% funcs, 85% branches

Total new tests: 156 (platform: 47, utils: 109)
All tests passing with new coverage thresholds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-20 23:35:31 +01:00
parent 8cccf74ace
commit 30f4315c17
9 changed files with 1881 additions and 12 deletions

View File

@@ -0,0 +1,227 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs/promises";
import path from "path";
import os from "os";
import {
getAutomakerDir,
getFeaturesDir,
getFeatureDir,
getFeatureImagesDir,
getBoardDir,
getImagesDir,
getContextDir,
getWorktreesDir,
getAppSpecPath,
getBranchTrackingPath,
ensureAutomakerDir,
getGlobalSettingsPath,
getCredentialsPath,
getProjectSettingsPath,
ensureDataDir,
} from "../src/paths";
describe("paths.ts", () => {
let tempDir: string;
let projectPath: string;
let dataDir: string;
beforeEach(async () => {
// Create a temporary directory for testing
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "platform-paths-test-"));
projectPath = path.join(tempDir, "test-project");
dataDir = path.join(tempDir, "user-data");
await fs.mkdir(projectPath, { recursive: true });
});
afterEach(async () => {
// Clean up temporary directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe("Project-level path construction", () => {
it("should return automaker directory path", () => {
const result = getAutomakerDir(projectPath);
expect(result).toBe(path.join(projectPath, ".automaker"));
});
it("should return features directory path", () => {
const result = getFeaturesDir(projectPath);
expect(result).toBe(path.join(projectPath, ".automaker", "features"));
});
it("should return feature directory path", () => {
const featureId = "auth-feature";
const result = getFeatureDir(projectPath, featureId);
expect(result).toBe(
path.join(projectPath, ".automaker", "features", featureId)
);
});
it("should return feature images directory path", () => {
const featureId = "auth-feature";
const result = getFeatureImagesDir(projectPath, featureId);
expect(result).toBe(
path.join(projectPath, ".automaker", "features", featureId, "images")
);
});
it("should return board directory path", () => {
const result = getBoardDir(projectPath);
expect(result).toBe(path.join(projectPath, ".automaker", "board"));
});
it("should return images directory path", () => {
const result = getImagesDir(projectPath);
expect(result).toBe(path.join(projectPath, ".automaker", "images"));
});
it("should return context directory path", () => {
const result = getContextDir(projectPath);
expect(result).toBe(path.join(projectPath, ".automaker", "context"));
});
it("should return worktrees directory path", () => {
const result = getWorktreesDir(projectPath);
expect(result).toBe(path.join(projectPath, ".automaker", "worktrees"));
});
it("should return app spec file path", () => {
const result = getAppSpecPath(projectPath);
expect(result).toBe(
path.join(projectPath, ".automaker", "app_spec.txt")
);
});
it("should return branch tracking file path", () => {
const result = getBranchTrackingPath(projectPath);
expect(result).toBe(
path.join(projectPath, ".automaker", "active-branches.json")
);
});
it("should return project settings file path", () => {
const result = getProjectSettingsPath(projectPath);
expect(result).toBe(
path.join(projectPath, ".automaker", "settings.json")
);
});
});
describe("Global settings path construction", () => {
it("should return global settings path", () => {
const result = getGlobalSettingsPath(dataDir);
expect(result).toBe(path.join(dataDir, "settings.json"));
});
it("should return credentials path", () => {
const result = getCredentialsPath(dataDir);
expect(result).toBe(path.join(dataDir, "credentials.json"));
});
});
describe("Directory creation", () => {
it("should create automaker directory", async () => {
const automakerDir = await ensureAutomakerDir(projectPath);
expect(automakerDir).toBe(path.join(projectPath, ".automaker"));
const stats = await fs.stat(automakerDir);
expect(stats.isDirectory()).toBe(true);
});
it("should be idempotent when creating automaker directory", async () => {
// Create directory first time
const firstResult = await ensureAutomakerDir(projectPath);
// Create directory second time
const secondResult = await ensureAutomakerDir(projectPath);
expect(firstResult).toBe(secondResult);
const stats = await fs.stat(firstResult);
expect(stats.isDirectory()).toBe(true);
});
it("should create data directory", async () => {
const result = await ensureDataDir(dataDir);
expect(result).toBe(dataDir);
const stats = await fs.stat(dataDir);
expect(stats.isDirectory()).toBe(true);
});
it("should be idempotent when creating data directory", async () => {
// Create directory first time
const firstResult = await ensureDataDir(dataDir);
// Create directory second time
const secondResult = await ensureDataDir(dataDir);
expect(firstResult).toBe(secondResult);
const stats = await fs.stat(firstResult);
expect(stats.isDirectory()).toBe(true);
});
it("should create nested directories recursively", async () => {
const deepProjectPath = path.join(
tempDir,
"nested",
"deep",
"project"
);
await fs.mkdir(deepProjectPath, { recursive: true });
const automakerDir = await ensureAutomakerDir(deepProjectPath);
const stats = await fs.stat(automakerDir);
expect(stats.isDirectory()).toBe(true);
});
});
describe("Path handling with special characters", () => {
it("should handle feature IDs with special characters", () => {
const featureId = "feature-with-dashes_and_underscores";
const result = getFeatureDir(projectPath, featureId);
expect(result).toContain(featureId);
});
it("should handle paths with spaces", () => {
const pathWithSpaces = path.join(tempDir, "path with spaces");
const result = getAutomakerDir(pathWithSpaces);
expect(result).toBe(path.join(pathWithSpaces, ".automaker"));
});
});
describe("Path relationships", () => {
it("should have feature dir as child of features dir", () => {
const featuresDir = getFeaturesDir(projectPath);
const featureDir = getFeatureDir(projectPath, "test-feature");
expect(featureDir.startsWith(featuresDir)).toBe(true);
});
it("should have all project paths under automaker dir", () => {
const automakerDir = getAutomakerDir(projectPath);
const paths = [
getFeaturesDir(projectPath),
getBoardDir(projectPath),
getImagesDir(projectPath),
getContextDir(projectPath),
getWorktreesDir(projectPath),
getAppSpecPath(projectPath),
getBranchTrackingPath(projectPath),
getProjectSettingsPath(projectPath),
];
paths.forEach((p) => {
expect(p.startsWith(automakerDir)).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,238 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import path from "path";
import {
initAllowedPaths,
addAllowedPath,
isPathAllowed,
validatePath,
getAllowedPaths,
} from "../src/security";
describe("security.ts", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
// Save original environment
originalEnv = { ...process.env };
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
});
describe("initAllowedPaths", () => {
it("should initialize from ALLOWED_PROJECT_DIRS environment variable", () => {
process.env.ALLOWED_PROJECT_DIRS = "/path/one,/path/two,/path/three";
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve("/path/one"));
expect(allowed).toContain(path.resolve("/path/two"));
expect(allowed).toContain(path.resolve("/path/three"));
});
it("should trim whitespace from paths", () => {
process.env.ALLOWED_PROJECT_DIRS = " /path/one , /path/two , /path/three ";
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve("/path/one"));
expect(allowed).toContain(path.resolve("/path/two"));
expect(allowed).toContain(path.resolve("/path/three"));
});
it("should skip empty paths", () => {
process.env.ALLOWED_PROJECT_DIRS = "/path/one,,/path/two, ,/path/three";
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed.length).toBeLessThanOrEqual(3);
expect(allowed).toContain(path.resolve("/path/one"));
});
it("should initialize from DATA_DIR environment variable", () => {
process.env.DATA_DIR = "/data/directory";
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve("/data/directory"));
});
it("should initialize from WORKSPACE_DIR environment variable", () => {
process.env.WORKSPACE_DIR = "/workspace/directory";
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve("/workspace/directory"));
});
it("should handle all environment variables together", () => {
process.env.ALLOWED_PROJECT_DIRS = "/projects/one,/projects/two";
process.env.DATA_DIR = "/app/data";
process.env.WORKSPACE_DIR = "/app/workspace";
initAllowedPaths();
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve("/projects/one"));
expect(allowed).toContain(path.resolve("/projects/two"));
expect(allowed).toContain(path.resolve("/app/data"));
expect(allowed).toContain(path.resolve("/app/workspace"));
});
it("should handle missing environment variables gracefully", () => {
delete process.env.ALLOWED_PROJECT_DIRS;
delete process.env.DATA_DIR;
delete process.env.WORKSPACE_DIR;
expect(() => initAllowedPaths()).not.toThrow();
});
});
describe("addAllowedPath", () => {
it("should add a path to allowed list", () => {
const testPath = "/new/allowed/path";
addAllowedPath(testPath);
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve(testPath));
});
it("should resolve relative paths to absolute", () => {
const relativePath = "relative/path";
addAllowedPath(relativePath);
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve(relativePath));
});
it("should handle duplicate paths", () => {
const testPath = "/duplicate/path";
addAllowedPath(testPath);
addAllowedPath(testPath);
const allowed = getAllowedPaths();
const count = allowed.filter((p) => p === path.resolve(testPath)).length;
expect(count).toBe(1);
});
});
describe("isPathAllowed", () => {
it("should always return true (all paths allowed)", () => {
expect(isPathAllowed("/any/path")).toBe(true);
expect(isPathAllowed("/another/path")).toBe(true);
expect(isPathAllowed("relative/path")).toBe(true);
expect(isPathAllowed("/etc/passwd")).toBe(true);
expect(isPathAllowed("../../../dangerous/path")).toBe(true);
});
it("should return true even for non-existent paths", () => {
expect(isPathAllowed("/nonexistent/path/12345")).toBe(true);
});
it("should return true for empty string", () => {
expect(isPathAllowed("")).toBe(true);
});
});
describe("validatePath", () => {
it("should resolve absolute paths", () => {
const absPath = "/absolute/path/to/file.txt";
const result = validatePath(absPath);
expect(result).toBe(path.resolve(absPath));
});
it("should resolve relative paths", () => {
const relPath = "relative/path/file.txt";
const result = validatePath(relPath);
expect(result).toBe(path.resolve(relPath));
});
it("should handle current directory", () => {
const result = validatePath(".");
expect(result).toBe(path.resolve("."));
});
it("should handle parent directory", () => {
const result = validatePath("..");
expect(result).toBe(path.resolve(".."));
});
it("should handle complex relative paths", () => {
const complexPath = "../../some/nested/../path/./file.txt";
const result = validatePath(complexPath);
expect(result).toBe(path.resolve(complexPath));
});
it("should handle paths with spaces", () => {
const pathWithSpaces = "/path with spaces/file.txt";
const result = validatePath(pathWithSpaces);
expect(result).toBe(path.resolve(pathWithSpaces));
});
it("should handle home directory expansion on Unix", () => {
if (process.platform !== "win32") {
const homePath = "~/documents/file.txt";
const result = validatePath(homePath);
expect(result).toBe(path.resolve(homePath));
}
});
});
describe("getAllowedPaths", () => {
it("should return empty array initially", () => {
const allowed = getAllowedPaths();
expect(Array.isArray(allowed)).toBe(true);
});
it("should return array of added paths", () => {
addAllowedPath("/path/one");
addAllowedPath("/path/two");
const allowed = getAllowedPaths();
expect(allowed).toContain(path.resolve("/path/one"));
expect(allowed).toContain(path.resolve("/path/two"));
});
it("should return copy of internal set", () => {
addAllowedPath("/test/path");
const allowed1 = getAllowedPaths();
const allowed2 = getAllowedPaths();
expect(allowed1).not.toBe(allowed2);
expect(allowed1).toEqual(allowed2);
});
});
describe("Path security disabled behavior", () => {
it("should allow unrestricted access despite allowed paths list", () => {
process.env.ALLOWED_PROJECT_DIRS = "/only/this/path";
initAllowedPaths();
// Should return true even for paths not in allowed list
expect(isPathAllowed("/some/other/path")).toBe(true);
expect(isPathAllowed("/completely/different/path")).toBe(true);
});
it("should validate paths without permission checks", () => {
process.env.ALLOWED_PROJECT_DIRS = "/only/this/path";
initAllowedPaths();
// Should validate any path without throwing
expect(() => validatePath("/some/other/path")).not.toThrow();
expect(validatePath("/some/other/path")).toBe(
path.resolve("/some/other/path")
);
});
});
});

View File

@@ -11,12 +11,12 @@ export default defineConfig({
include: ["src/**/*.ts"],
exclude: ["src/**/*.d.ts", "src/index.ts"],
thresholds: {
// Current overall coverage: ~64% (only subprocess.ts well tested)
// Set realistic thresholds until more files are tested
lines: 60,
functions: 40,
branches: 60,
statements: 60,
// Excellent coverage: 94.69% stmts, 80.48% branches, 97.14% funcs, 94.64% lines
// All files now have comprehensive tests
lines: 90,
functions: 95,
branches: 75,
statements: 90,
},
},
},

View File

@@ -0,0 +1,261 @@
import { describe, it, expect } from "vitest";
import type { ConversationMessage } from "@automaker/types";
import {
extractTextFromContent,
normalizeContentBlocks,
formatHistoryAsText,
convertHistoryToMessages,
} from "../src/conversation-utils";
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!");
});
it("should extract text from array content with text blocks", () => {
const content = [
{ type: "text", text: "First block" },
{ type: "text", text: "Second block" },
];
const result = extractTextFromContent(content);
expect(result).toBe("First block\nSecond block");
});
it("should filter out non-text blocks", () => {
const content = [
{ 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");
});
it("should handle empty text blocks", () => {
const content = [
{ type: "text", text: "First" },
{ type: "text" },
{ type: "text", text: "Third" },
];
const result = extractTextFromContent(content);
expect(result).toBe("First\n\nThird");
});
it("should return empty string for array with only non-text blocks", () => {
const content = [
{ type: "image", source: {} },
{ type: "tool_use", source: {} },
];
const result = extractTextFromContent(content);
expect(result).toBe("");
});
it("should return empty string for empty array", () => {
const content: Array<{ type: string; text?: string }> = [];
const result = extractTextFromContent(content);
expect(result).toBe("");
});
});
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" }]);
});
it("should return array as-is", () => {
const content = [
{ 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 = "";
const result = normalizeContentBlocks(content);
expect(result).toEqual([{ type: "text", text: "" }]);
});
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" }]);
});
});
describe("formatHistoryAsText", () => {
it("should format empty history as empty string", () => {
const history: ConversationMessage[] = [];
const result = formatHistoryAsText(history);
expect(result).toBe("");
});
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");
});
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: "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" +
"User: What's 2+2?\n\n" +
"Assistant: The answer is 4.\n\n" +
"User: Thanks!\n\n" +
"---\n\n"
);
});
it("should handle array content by extracting text", () => {
const history: ConversationMessage[] = [
{
role: "user",
content: [
{ 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"
);
});
it("should handle mixed string and array content", () => {
const history: ConversationMessage[] = [
{ role: "user", content: "String 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");
});
});
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!" },
];
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
type: "user",
session_id: "",
message: {
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!" },
];
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
type: "assistant",
session_id: "",
message: {
role: "assistant",
content: [{ type: "text", text: "Hi there!" }],
},
parent_tool_use_id: null,
});
});
it("should preserve array content as-is", () => {
const content = [
{ type: "text", text: "Text" },
{ type: "image", source: { data: "..." } },
];
const history: ConversationMessage[] = [{ role: "user", content }];
const result = convertHistoryToMessages(history);
expect(result[0].message.content).toEqual(content);
});
it("should convert multiple messages", () => {
const history: ConversationMessage[] = [
{ 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");
});
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("");
});
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" },
];
const result = convertHistoryToMessages(history);
expect(result[0].message.content).toEqual([
{ type: "text", text: "String content" },
]);
});
});
});

View File

@@ -0,0 +1,254 @@
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", () => {
let tempDir: string;
beforeEach(async () => {
// Create a temporary directory for testing
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "fs-utils-test-"));
});
afterEach(async () => {
// Clean up temporary directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe("mkdirSafe", () => {
it("should create a new directory", async () => {
const newDir = path.join(tempDir, "new-directory");
await mkdirSafe(newDir);
const stats = await fs.stat(newDir);
expect(stats.isDirectory()).toBe(true);
});
it("should create nested directories recursively", async () => {
const nestedDir = path.join(tempDir, "level1", "level2", "level3");
await mkdirSafe(nestedDir);
const stats = await fs.stat(nestedDir);
expect(stats.isDirectory()).toBe(true);
});
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");
await fs.mkdir(targetDir);
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");
await expect(mkdirSafe(filePath)).rejects.toThrow(
"Path exists and is not a directory"
);
});
it("should resolve relative paths", async () => {
const originalCwd = process.cwd();
try {
process.chdir(tempDir);
await mkdirSafe("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");
const promises = [
mkdirSafe(newDir),
mkdirSafe(newDir),
mkdirSafe(newDir),
];
await expect(Promise.all(promises)).resolves.not.toThrow();
const stats = await fs.stat(newDir);
expect(stats.isDirectory()).toBe(true);
});
it("should handle paths with special characters", async () => {
const specialDir = path.join(tempDir, "dir with spaces & special-chars");
await mkdirSafe(specialDir);
const stats = await fs.stat(specialDir);
expect(stats.isDirectory()).toBe(true);
});
});
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);
expect(result).toBe(true);
});
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");
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");
await fs.writeFile(target, "content");
await fs.symlink(target, symlink);
const result = await existsSafe(symlink);
expect(result).toBe(true);
});
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);
const result = await existsSafe(symlink);
// lstat succeeds on broken symlinks
expect(result).toBe(true);
});
it("should handle relative paths", async () => {
const originalCwd = process.cwd();
try {
process.chdir(tempDir);
await fs.writeFile("test.txt", "content");
const result = await existsSafe("test.txt");
expect(result).toBe(true);
} finally {
process.chdir(originalCwd);
}
});
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");
const result = await existsSafe(nonExistent);
expect(result).toBe(false);
});
});
describe("Error handling", () => {
it("should handle permission errors in mkdirSafe", async () => {
// Skip on Windows where permissions work differently
if (process.platform === "win32") {
return;
}
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");
try {
await expect(mkdirSafe(newDir)).rejects.toThrow();
} finally {
// Restore permissions for cleanup
await fs.chmod(restrictedDir, 0o755);
}
});
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);
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");
const existsBefore = await existsSafe(dirPath);
expect(existsBefore).toBe(false);
await mkdirSafe(dirPath);
const existsAfter = await existsSafe(dirPath);
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");
await mkdirSafe(level3);
expect(await existsSafe(level1)).toBe(true);
expect(await existsSafe(level2)).toBe(true);
expect(await existsSafe(level3)).toBe(true);
});
});
});

View File

@@ -0,0 +1,250 @@
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";
describe("image-handler.ts", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "image-handler-test-"));
});
afterEach(async () => {
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
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 .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 .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 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");
});
});
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.originalPath).toBe(imagePath);
});
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" },
];
for (const format of formats) {
const imagePath = path.join(tempDir, `image.${format.ext}`);
await fs.writeFile(imagePath, Buffer.from("data"));
const result = await readImageAsBase64(imagePath);
expect(result.mimeType).toBe(format.mime);
expect(result.filename).toBe(`image.${format.ext}`);
}
});
it("should throw error if file doesn't exist", async () => {
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");
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"));
});
});
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",
source: {
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");
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");
});
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"));
const result = await convertImagesToContentBlocks([image], tempDir);
expect(result).toHaveLength(1);
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"));
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");
await fs.writeFile(validImage, Buffer.from("data"));
const result = await convertImagesToContentBlocks([
validImage,
invalidImage,
]);
expect(result).toHaveLength(1);
expect(result[0].source.media_type).toBe("image/png");
});
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"];
for (const img of images) {
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");
});
});
describe("formatImagePathsForPrompt", () => {
it("should return empty string for empty array", () => {
const result = formatImagePathsForPrompt([]);
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 multiple image paths", () => {
const result = formatImagePathsForPrompt([
"/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"
);
});
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");
});
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:");
});
});
});

View File

@@ -0,0 +1,323 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
createLogger,
LogLevel,
getLogLevel,
setLogLevel,
} from "../src/logger";
describe("logger.ts", () => {
let originalConsoleError: typeof console.error;
let originalConsoleWarn: typeof console.warn;
let originalConsoleLog: typeof console.log;
let originalLogLevel: LogLevel;
beforeEach(() => {
// Save original console methods and log level
originalConsoleError = console.error;
originalConsoleWarn = console.warn;
originalConsoleLog = console.log;
originalLogLevel = getLogLevel();
// Mock console methods
console.error = vi.fn();
console.warn = vi.fn();
console.log = vi.fn();
});
afterEach(() => {
// Restore original console methods and log level
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
console.log = originalConsoleLog;
setLogLevel(originalLogLevel);
});
describe("createLogger", () => {
it("should create logger with context prefix", () => {
const logger = createLogger("TestContext");
setLogLevel(LogLevel.INFO);
logger.info("test message");
expect(console.log).toHaveBeenCalledWith(
"[TestContext]",
"test message"
);
});
it("should handle multiple arguments", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.INFO);
logger.info("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");
setLogLevel(LogLevel.ERROR);
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");
setLogLevel(LogLevel.WARN);
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");
setLogLevel(LogLevel.INFO);
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");
setLogLevel(LogLevel.DEBUG);
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(2); // info + debug
});
});
describe("error method", () => {
it("should use console.error", () => {
const logger = createLogger("ErrorTest");
setLogLevel(LogLevel.ERROR);
logger.error("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);
logger.error("should not appear");
expect(console.error).not.toHaveBeenCalled();
});
});
describe("warn method", () => {
it("should use console.warn", () => {
const logger = createLogger("WarnTest");
setLogLevel(LogLevel.WARN);
logger.warn("warning message");
expect(console.warn).toHaveBeenCalledWith("[WarnTest]", "warning message");
});
it("should not log when level is below WARN", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.ERROR);
logger.warn("should not appear");
expect(console.warn).not.toHaveBeenCalled();
});
});
describe("info method", () => {
it("should use console.log", () => {
const logger = createLogger("InfoTest");
setLogLevel(LogLevel.INFO);
logger.info("info message");
expect(console.log).toHaveBeenCalledWith("[InfoTest]", "info message");
});
it("should not log when level is below INFO", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.WARN);
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");
setLogLevel(LogLevel.DEBUG);
logger.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");
setLogLevel(LogLevel.INFO);
logger.debug("should not appear");
expect(console.log).not.toHaveBeenCalled();
});
});
describe("getLogLevel", () => {
it("should return current log level", () => {
setLogLevel(LogLevel.DEBUG);
expect(getLogLevel()).toBe(LogLevel.DEBUG);
setLogLevel(LogLevel.ERROR);
expect(getLogLevel()).toBe(LogLevel.ERROR);
});
});
describe("setLogLevel", () => {
it("should change log level", () => {
setLogLevel(LogLevel.WARN);
expect(getLogLevel()).toBe(LogLevel.WARN);
setLogLevel(LogLevel.DEBUG);
expect(getLogLevel()).toBe(LogLevel.DEBUG);
});
it("should affect subsequent logging", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.ERROR);
logger.info("should not log");
expect(console.log).not.toHaveBeenCalled();
setLogLevel(LogLevel.INFO);
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");
setLogLevel(LogLevel.INFO);
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"
);
});
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");
expect(console.log).not.toHaveBeenCalled();
});
});
describe("Edge cases", () => {
it("should handle empty context string", () => {
const logger = createLogger("");
setLogLevel(LogLevel.INFO);
logger.info("message");
expect(console.log).toHaveBeenCalledWith("[]", "message");
});
it("should handle context with special characters", () => {
const logger = createLogger("Test-Service_v2.0");
setLogLevel(LogLevel.INFO);
logger.info("message");
expect(console.log).toHaveBeenCalledWith(
"[Test-Service_v2.0]",
"message"
);
});
it("should handle no arguments to log methods", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.INFO);
logger.info();
expect(console.log).toHaveBeenCalledWith("[Test]");
});
it("should handle complex object arguments", () => {
const logger = createLogger("Test");
setLogLevel(LogLevel.INFO);
const complexObj = {
nested: { deep: { value: 123 } },
array: [1, 2, 3],
fn: () => {},
};
logger.info("complex", complexObj);
expect(console.log).toHaveBeenCalledWith("[Test]", "complex", complexObj);
});
});
});

View File

@@ -0,0 +1,316 @@
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", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "prompt-builder-test-"));
});
afterEach(async () => {
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
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.hasImages).toBe(false);
});
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.hasImages).toBe(false);
});
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");
});
});
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]
);
expect(result.hasImages).toBe(true);
expect(Array.isArray(result.content)).toBe(true);
const blocks = result.content as Array<{
type: string;
text?: string;
source?: object;
}>;
expect(blocks).toHaveLength(2);
expect(blocks[0]).toMatchObject({
type: "text",
text: "Check this image",
});
expect(blocks[1]).toMatchObject({
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");
await fs.writeFile(image1, Buffer.from("jpg data"));
await fs.writeFile(image2, Buffer.from("png data"));
const result = await buildPromptWithImages("Two images", [
image1,
image2,
]);
expect(result.hasImages).toBe(true);
const blocks = result.content as Array<{
type: string;
text?: string;
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");
});
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"));
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"));
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"));
const result = await buildPromptWithImages("Prompt", [imagePath]);
const blocks = result.content as Array<{
type: string;
text?: string;
}>;
const textBlock = blocks.find((b) => b.type === "text");
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"));
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");
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");
await fs.writeFile(img1, Buffer.from("data1"));
await fs.writeFile(img2, Buffer.from("data2"));
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");
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"));
const result = await buildPromptWithImages("", [imagePath]);
expect(result.hasImages).toBe(true);
const blocks = result.content as Array<{
type: string;
text?: string;
source?: object;
}>;
// 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);
});
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]);
expect(result.hasImages).toBe(true);
const blocks = result.content as Array<{
type: string;
text?: string;
source?: object;
}>;
// Whitespace-only is trimmed, so no text block should be added
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");
await fs.writeFile(validImage, Buffer.from("data"));
const result = await buildPromptWithImages("Test", [
validImage,
invalidImage,
]);
expect(result.hasImages).toBe(true);
const blocks = result.content as Array<{
type: string;
text?: string;
source?: object;
}>;
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"));
const resultFalse = await buildPromptWithImages(
"Test",
[imagePath],
undefined,
false
);
const resultTrue = await buildPromptWithImages(
"Test",
[imagePath],
undefined,
true
);
const blocksFalse = resultFalse.content as Array<{
type: string;
text?: string;
}>;
const blocksTrue = resultTrue.content as Array<{
type: string;
text?: string;
}>;
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);
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"));
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"));
const result = await buildPromptWithImages(basePrompt, [imagePath]);
const blocks = result.content as Array<{
type: string;
text?: string;
}>;
const textBlock = blocks.find((b) => b.type === "text");
expect(textBlock?.text).toBe(basePrompt);
});
});
});

View File

@@ -11,12 +11,12 @@ export default defineConfig({
include: ["src/**/*.ts"],
exclude: ["src/**/*.d.ts", "src/index.ts"],
thresholds: {
// Current overall coverage: ~19% (only error-handler.ts tested)
// Set modest thresholds until more files are tested
lines: 15,
functions: 15,
branches: 25,
statements: 15,
// Excellent coverage: 94.3% stmts, 89.77% branches, 100% funcs, 94.21% lines
// All files now have comprehensive tests
lines: 90,
functions: 95,
branches: 85,
statements: 90,
},
},
},