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")
);
});
});
});