mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
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:
227
libs/platform/tests/paths.test.ts
Normal file
227
libs/platform/tests/paths.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
238
libs/platform/tests/security.test.ts
Normal file
238
libs/platform/tests/security.test.ts
Normal 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")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
261
libs/utils/tests/conversation-utils.test.ts
Normal file
261
libs/utils/tests/conversation-utils.test.ts
Normal 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" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
254
libs/utils/tests/fs-utils.test.ts
Normal file
254
libs/utils/tests/fs-utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
250
libs/utils/tests/image-handler.test.ts
Normal file
250
libs/utils/tests/image-handler.test.ts
Normal 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:");
|
||||
});
|
||||
});
|
||||
});
|
||||
323
libs/utils/tests/logger.test.ts
Normal file
323
libs/utils/tests/logger.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
316
libs/utils/tests/prompt-builder.test.ts
Normal file
316
libs/utils/tests/prompt-builder.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user