test: Add comprehensive unit tests for shared packages

Add 88 new unit tests covering critical business logic in shared packages:

- libs/git-utils/tests/diff.test.ts (22 tests)
  * Synthetic diff generation for new files
  * Binary file handling
  * Large file handling
  * Untracked file diff appending
  * Directory file listing with exclusions
  * Non-git directory handling

- libs/dependency-resolver/tests/resolver.test.ts (30 tests)
  * Topological sorting with dependencies
  * Priority-aware ordering
  * Circular dependency detection
  * Missing dependency tracking
  * Blocked feature detection
  * Complex dependency graphs

- libs/utils/tests/error-handler.test.ts (36 tests)
  * Abort error detection
  * Cancellation error detection
  * Authentication error detection
  * Error classification logic
  * User-friendly error messages

All tests use vitest and follow best practices with proper setup/teardown.

Resolves PR review issue #1 (HIGH PRIORITY)

🤖 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 22:48:43 +01:00
parent 46994bea34
commit 0cef537a3d
8 changed files with 958 additions and 17 deletions

View File

@@ -343,13 +343,8 @@ Understanding the dependency chain helps prevent circular dependencies:
All packages must be built before use:
```bash
# Build all packages
cd libs/types && npm run build
cd libs/utils && npm run build
cd libs/platform && npm run build
cd libs/model-resolver && npm run build
cd libs/dependency-resolver && npm run build
cd libs/git-utils && npm run build
# Build all packages from workspace
npm run build:packages
# Or from root
npm install # Installs and links workspace packages

View File

@@ -14,7 +14,9 @@
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
"watch": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": ["automaker", "dependency", "resolver"],
"author": "AutoMaker Team",
@@ -24,6 +26,7 @@
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
}

View File

@@ -0,0 +1,360 @@
import { describe, it, expect } from "vitest";
import {
resolveDependencies,
areDependenciesSatisfied,
getBlockingDependencies,
} from "../src/resolver";
import type { Feature } from "@automaker/types";
// Helper to create test features
function createFeature(
id: string,
options: {
dependencies?: string[];
status?: string;
priority?: number;
} = {}
): Feature {
return {
id,
category: "test",
description: `Feature ${id}`,
dependencies: options.dependencies,
status: options.status || "pending",
priority: options.priority,
};
}
describe("resolver.ts", () => {
describe("resolveDependencies", () => {
it("should handle features with no dependencies", () => {
const features = [
createFeature("A"),
createFeature("B"),
createFeature("C"),
];
const result = resolveDependencies(features);
expect(result.orderedFeatures).toHaveLength(3);
expect(result.circularDependencies).toEqual([]);
expect(result.missingDependencies.size).toBe(0);
expect(result.blockedFeatures.size).toBe(0);
});
it("should order features with linear dependencies", () => {
const features = [
createFeature("C", { dependencies: ["B"] }),
createFeature("A"),
createFeature("B", { dependencies: ["A"] }),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map(f => f.id);
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("B"));
expect(ids.indexOf("B")).toBeLessThan(ids.indexOf("C"));
expect(result.circularDependencies).toEqual([]);
});
it("should respect priority within same dependency level", () => {
const features = [
createFeature("Low", { priority: 3 }),
createFeature("High", { priority: 1 }),
createFeature("Medium", { priority: 2 }),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map(f => f.id);
expect(ids).toEqual(["High", "Medium", "Low"]);
});
it("should use default priority 2 when not specified", () => {
const features = [
createFeature("NoPriority"),
createFeature("HighPriority", { priority: 1 }),
createFeature("LowPriority", { priority: 3 }),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map(f => f.id);
expect(ids.indexOf("HighPriority")).toBeLessThan(ids.indexOf("NoPriority"));
expect(ids.indexOf("NoPriority")).toBeLessThan(ids.indexOf("LowPriority"));
});
it("should respect dependencies over priority", () => {
const features = [
createFeature("B", { dependencies: ["A"], priority: 1 }), // High priority but depends on A
createFeature("A", { priority: 3 }), // Low priority but no dependencies
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map(f => f.id);
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("B"));
});
it("should detect circular dependencies (simple cycle)", () => {
const features = [
createFeature("A", { dependencies: ["B"] }),
createFeature("B", { dependencies: ["A"] }),
];
const result = resolveDependencies(features);
expect(result.circularDependencies).toHaveLength(1);
expect(result.circularDependencies[0]).toContain("A");
expect(result.circularDependencies[0]).toContain("B");
expect(result.orderedFeatures).toHaveLength(2); // All features still included
});
it("should detect circular dependencies (3-way cycle)", () => {
const features = [
createFeature("A", { dependencies: ["C"] }),
createFeature("B", { dependencies: ["A"] }),
createFeature("C", { dependencies: ["B"] }),
];
const result = resolveDependencies(features);
expect(result.circularDependencies.length).toBeGreaterThan(0);
const allCycleIds = result.circularDependencies.flat();
expect(allCycleIds).toContain("A");
expect(allCycleIds).toContain("B");
expect(allCycleIds).toContain("C");
});
it("should detect missing dependencies", () => {
const features = [
createFeature("A", { dependencies: ["NonExistent"] }),
createFeature("B"),
];
const result = resolveDependencies(features);
expect(result.missingDependencies.has("A")).toBe(true);
expect(result.missingDependencies.get("A")).toContain("NonExistent");
});
it("should detect blocked features (incomplete dependencies)", () => {
const features = [
createFeature("A", { status: "pending" }),
createFeature("B", { dependencies: ["A"], status: "pending" }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.has("B")).toBe(true);
expect(result.blockedFeatures.get("B")).toContain("A");
});
it("should not mark features as blocked if dependencies are completed", () => {
const features = [
createFeature("A", { status: "completed" }),
createFeature("B", { dependencies: ["A"], status: "pending" }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.has("B")).toBe(false);
});
it("should not mark features as blocked if dependencies are verified", () => {
const features = [
createFeature("A", { status: "verified" }),
createFeature("B", { dependencies: ["A"], status: "pending" }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.has("B")).toBe(false);
});
it("should handle complex dependency graph", () => {
const features = [
createFeature("E", { dependencies: ["C", "D"] }),
createFeature("D", { dependencies: ["B"] }),
createFeature("C", { dependencies: ["A", "B"] }),
createFeature("B"),
createFeature("A"),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map(f => f.id);
// A and B have no dependencies - can be first or second
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("C"));
expect(ids.indexOf("B")).toBeLessThan(ids.indexOf("C"));
expect(ids.indexOf("B")).toBeLessThan(ids.indexOf("D"));
// C depends on A and B
expect(ids.indexOf("C")).toBeLessThan(ids.indexOf("E"));
// D depends on B
expect(ids.indexOf("D")).toBeLessThan(ids.indexOf("E"));
expect(result.circularDependencies).toEqual([]);
});
it("should handle multiple missing dependencies", () => {
const features = [
createFeature("A", { dependencies: ["X", "Y", "Z"] }),
];
const result = resolveDependencies(features);
expect(result.missingDependencies.get("A")).toEqual(["X", "Y", "Z"]);
});
it("should handle empty feature list", () => {
const result = resolveDependencies([]);
expect(result.orderedFeatures).toEqual([]);
expect(result.circularDependencies).toEqual([]);
expect(result.missingDependencies.size).toBe(0);
expect(result.blockedFeatures.size).toBe(0);
});
it("should handle features with both missing and existing dependencies", () => {
const features = [
createFeature("A"),
createFeature("B", { dependencies: ["A", "NonExistent"] }),
];
const result = resolveDependencies(features);
expect(result.missingDependencies.get("B")).toContain("NonExistent");
const ids = result.orderedFeatures.map(f => f.id);
expect(ids.indexOf("A")).toBeLessThan(ids.indexOf("B"));
});
});
describe("areDependenciesSatisfied", () => {
it("should return true for feature with no dependencies", () => {
const feature = createFeature("A");
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it("should return true for feature with empty dependencies array", () => {
const feature = createFeature("A", { dependencies: [] });
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it("should return true when all dependencies are completed", () => {
const dep = createFeature("Dep", { status: "completed" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it("should return true when all dependencies are verified", () => {
const dep = createFeature("Dep", { status: "verified" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it("should return false when any dependency is pending", () => {
const dep = createFeature("Dep", { status: "pending" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
it("should return false when any dependency is running", () => {
const dep = createFeature("Dep", { status: "running" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
it("should return false when dependency is missing", () => {
const feature = createFeature("A", { dependencies: ["NonExistent"] });
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
it("should check all dependencies", () => {
const dep1 = createFeature("Dep1", { status: "completed" });
const dep2 = createFeature("Dep2", { status: "pending" });
const feature = createFeature("A", { dependencies: ["Dep1", "Dep2"] });
const allFeatures = [dep1, dep2, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
});
describe("getBlockingDependencies", () => {
it("should return empty array for feature with no dependencies", () => {
const feature = createFeature("A");
const allFeatures = [feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
it("should return empty array when all dependencies are completed", () => {
const dep = createFeature("Dep", { status: "completed" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
it("should return empty array when all dependencies are verified", () => {
const dep = createFeature("Dep", { status: "verified" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
it("should return pending dependencies", () => {
const dep = createFeature("Dep", { status: "pending" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual(["Dep"]);
});
it("should return running dependencies", () => {
const dep = createFeature("Dep", { status: "running" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual(["Dep"]);
});
it("should return failed dependencies", () => {
const dep = createFeature("Dep", { status: "failed" });
const feature = createFeature("A", { dependencies: ["Dep"] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual(["Dep"]);
});
it("should return all incomplete dependencies", () => {
const dep1 = createFeature("Dep1", { status: "pending" });
const dep2 = createFeature("Dep2", { status: "completed" });
const dep3 = createFeature("Dep3", { status: "running" });
const feature = createFeature("A", { dependencies: ["Dep1", "Dep2", "Dep3"] });
const allFeatures = [dep1, dep2, dep3, feature];
const blocking = getBlockingDependencies(feature, allFeatures);
expect(blocking).toContain("Dep1");
expect(blocking).toContain("Dep3");
expect(blocking).not.toContain("Dep2");
});
});
});

View File

@@ -6,7 +6,9 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
"watch": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": ["automaker", "git", "utils"],
"author": "AutoMaker Team",
@@ -17,6 +19,7 @@
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
}

View File

@@ -0,0 +1,306 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
generateSyntheticDiffForNewFile,
appendUntrackedFileDiffs,
listAllFilesInDirectory,
generateDiffsForNonGitDirectory,
getGitRepositoryDiffs,
} from "../src/diff";
import fs from "fs/promises";
import path from "path";
import os from "os";
describe("diff.ts", () => {
let tempDir: string;
beforeEach(async () => {
// Create a temporary directory for each test
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "git-utils-test-"));
});
afterEach(async () => {
// Clean up temporary directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe("generateSyntheticDiffForNewFile", () => {
it("should generate diff for binary file", async () => {
const fileName = "test.png";
const filePath = path.join(tempDir, fileName);
await fs.writeFile(filePath, Buffer.from([0x89, 0x50, 0x4e, 0x47]));
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain(`diff --git a/${fileName} b/${fileName}`);
expect(diff).toContain("new file mode 100644");
expect(diff).toContain(`Binary file ${fileName} added`);
});
it("should generate diff for large text file", async () => {
const fileName = "large.txt";
const filePath = path.join(tempDir, fileName);
// Create a file > 1MB
const largeContent = "x".repeat(1024 * 1024 + 100);
await fs.writeFile(filePath, largeContent);
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain(`diff --git a/${fileName} b/${fileName}`);
expect(diff).toContain("[File too large to display:");
expect(diff).toMatch(/\d+KB\]/);
});
it("should generate diff for small text file with trailing newline", async () => {
const fileName = "test.txt";
const filePath = path.join(tempDir, fileName);
const content = "line 1\nline 2\nline 3\n";
await fs.writeFile(filePath, content);
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain(`diff --git a/${fileName} b/${fileName}`);
expect(diff).toContain("new file mode 100644");
expect(diff).toContain("--- /dev/null");
expect(diff).toContain(`+++ b/${fileName}`);
expect(diff).toContain("@@ -0,0 +1,3 @@");
expect(diff).toContain("+line 1");
expect(diff).toContain("+line 2");
expect(diff).toContain("+line 3");
expect(diff).not.toContain("\\ No newline at end of file");
});
it("should generate diff for text file without trailing newline", async () => {
const fileName = "no-newline.txt";
const filePath = path.join(tempDir, fileName);
const content = "line 1\nline 2";
await fs.writeFile(filePath, content);
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain(`diff --git a/${fileName} b/${fileName}`);
expect(diff).toContain("+line 1");
expect(diff).toContain("+line 2");
expect(diff).toContain("\\ No newline at end of file");
});
it("should generate diff for empty file", async () => {
const fileName = "empty.txt";
const filePath = path.join(tempDir, fileName);
await fs.writeFile(filePath, "");
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain(`diff --git a/${fileName} b/${fileName}`);
expect(diff).toContain("@@ -0,0 +1,0 @@");
});
it("should generate diff for single line file", async () => {
const fileName = "single.txt";
const filePath = path.join(tempDir, fileName);
await fs.writeFile(filePath, "single line\n");
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain("@@ -0,0 +1,1 @@");
expect(diff).toContain("+single line");
});
it("should handle file not found error", async () => {
const fileName = "nonexistent.txt";
const diff = await generateSyntheticDiffForNewFile(tempDir, fileName);
expect(diff).toContain(`diff --git a/${fileName} b/${fileName}`);
expect(diff).toContain("[Unable to read file content]");
});
});
describe("appendUntrackedFileDiffs", () => {
it("should return existing diff when no untracked files", async () => {
const existingDiff = "diff --git a/test.txt b/test.txt\n";
const files = [
{ status: "M", path: "test.txt" },
{ status: "A", path: "new.txt" },
];
const result = await appendUntrackedFileDiffs(tempDir, existingDiff, files);
expect(result).toBe(existingDiff);
});
it("should append synthetic diffs for untracked files", async () => {
const existingDiff = "existing diff\n";
const untrackedFile = "untracked.txt";
const filePath = path.join(tempDir, untrackedFile);
await fs.writeFile(filePath, "content\n");
const files = [
{ status: "M", path: "modified.txt" },
{ status: "?", path: untrackedFile },
];
const result = await appendUntrackedFileDiffs(tempDir, existingDiff, files);
expect(result).toContain("existing diff");
expect(result).toContain(`diff --git a/${untrackedFile} b/${untrackedFile}`);
expect(result).toContain("+content");
});
it("should handle multiple untracked files", async () => {
const file1 = "file1.txt";
const file2 = "file2.txt";
await fs.writeFile(path.join(tempDir, file1), "file1\n");
await fs.writeFile(path.join(tempDir, file2), "file2\n");
const files = [
{ status: "?", path: file1 },
{ status: "?", path: file2 },
];
const result = await appendUntrackedFileDiffs(tempDir, "", files);
expect(result).toContain(`diff --git a/${file1} b/${file1}`);
expect(result).toContain(`diff --git a/${file2} b/${file2}`);
expect(result).toContain("+file1");
expect(result).toContain("+file2");
});
});
describe("listAllFilesInDirectory", () => {
it("should list files in empty directory", async () => {
const files = await listAllFilesInDirectory(tempDir);
expect(files).toEqual([]);
});
it("should list files in flat directory", async () => {
await fs.writeFile(path.join(tempDir, "file1.txt"), "content");
await fs.writeFile(path.join(tempDir, "file2.js"), "code");
const files = await listAllFilesInDirectory(tempDir);
expect(files).toHaveLength(2);
expect(files).toContain("file1.txt");
expect(files).toContain("file2.js");
});
it("should list files in nested directories", async () => {
await fs.mkdir(path.join(tempDir, "subdir"));
await fs.writeFile(path.join(tempDir, "root.txt"), "");
await fs.writeFile(path.join(tempDir, "subdir", "nested.txt"), "");
const files = await listAllFilesInDirectory(tempDir);
expect(files).toHaveLength(2);
expect(files).toContain("root.txt");
expect(files).toContain("subdir/nested.txt");
});
it("should skip node_modules directory", async () => {
await fs.mkdir(path.join(tempDir, "node_modules"));
await fs.writeFile(path.join(tempDir, "app.js"), "");
await fs.writeFile(path.join(tempDir, "node_modules", "package.js"), "");
const files = await listAllFilesInDirectory(tempDir);
expect(files).toHaveLength(1);
expect(files).toContain("app.js");
expect(files).not.toContain("node_modules/package.js");
});
it("should skip common build directories", async () => {
await fs.mkdir(path.join(tempDir, "dist"));
await fs.mkdir(path.join(tempDir, "build"));
await fs.mkdir(path.join(tempDir, ".next"));
await fs.writeFile(path.join(tempDir, "source.ts"), "");
await fs.writeFile(path.join(tempDir, "dist", "output.js"), "");
await fs.writeFile(path.join(tempDir, "build", "output.js"), "");
const files = await listAllFilesInDirectory(tempDir);
expect(files).toHaveLength(1);
expect(files).toContain("source.ts");
});
it("should skip hidden files except .env", async () => {
await fs.writeFile(path.join(tempDir, ".hidden"), "");
await fs.writeFile(path.join(tempDir, ".env"), "");
await fs.writeFile(path.join(tempDir, "visible.txt"), "");
const files = await listAllFilesInDirectory(tempDir);
expect(files).toHaveLength(2);
expect(files).toContain(".env");
expect(files).toContain("visible.txt");
expect(files).not.toContain(".hidden");
});
it("should skip .git directory", async () => {
await fs.mkdir(path.join(tempDir, ".git"));
await fs.writeFile(path.join(tempDir, ".git", "config"), "");
await fs.writeFile(path.join(tempDir, "README.md"), "");
const files = await listAllFilesInDirectory(tempDir);
expect(files).toHaveLength(1);
expect(files).toContain("README.md");
});
});
describe("generateDiffsForNonGitDirectory", () => {
it("should generate diffs for all files in directory", async () => {
await fs.writeFile(path.join(tempDir, "file1.txt"), "content1\n");
await fs.writeFile(path.join(tempDir, "file2.js"), "console.log('hi');\n");
const result = await generateDiffsForNonGitDirectory(tempDir);
expect(result.files).toHaveLength(2);
expect(result.files.every(f => f.status === "?")).toBe(true);
expect(result.diff).toContain("diff --git a/file1.txt b/file1.txt");
expect(result.diff).toContain("diff --git a/file2.js b/file2.js");
expect(result.diff).toContain("+content1");
expect(result.diff).toContain("+console.log('hi');");
});
it("should return empty result for empty directory", async () => {
const result = await generateDiffsForNonGitDirectory(tempDir);
expect(result.files).toEqual([]);
expect(result.diff).toBe("");
});
it("should mark all files as untracked", async () => {
await fs.writeFile(path.join(tempDir, "test.txt"), "test");
const result = await generateDiffsForNonGitDirectory(tempDir);
expect(result.files).toHaveLength(1);
expect(result.files[0].status).toBe("?");
expect(result.files[0].statusText).toBe("New");
});
});
describe("getGitRepositoryDiffs", () => {
it("should treat non-git directory as all new files", async () => {
await fs.writeFile(path.join(tempDir, "file.txt"), "content\n");
const result = await getGitRepositoryDiffs(tempDir);
expect(result.hasChanges).toBe(true);
expect(result.files).toHaveLength(1);
expect(result.files[0].status).toBe("?");
expect(result.diff).toContain("diff --git a/file.txt b/file.txt");
});
it("should return no changes for empty non-git directory", async () => {
const result = await getGitRepositoryDiffs(tempDir);
expect(result.hasChanges).toBe(false);
expect(result.files).toEqual([]);
expect(result.diff).toBe("");
});
});
});

View File

@@ -6,7 +6,9 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
"watch": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": ["automaker", "utils"],
"author": "AutoMaker Team",
@@ -16,6 +18,7 @@
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
}

View File

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

18
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"apps/server": {
"name": "@automaker/server",
"version": "0.1.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.72",
"@automaker/dependency-resolver": "^1.0.0",
@@ -62,7 +63,7 @@
"name": "@automaker/ui",
"version": "0.1.0",
"hasInstallScript": true,
"license": "Unlicense",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/dependency-resolver": "^1.0.0",
"@automaker/types": "^1.0.0",
@@ -162,12 +163,14 @@
"libs/dependency-resolver": {
"name": "@automaker/dependency-resolver",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/types": "^1.0.0"
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
},
"libs/dependency-resolver/node_modules/@types/node": {
@@ -183,13 +186,15 @@
"libs/git-utils": {
"name": "@automaker/git-utils",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/types": "^1.0.0",
"@automaker/utils": "^1.0.0"
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
},
"libs/git-utils/node_modules/@types/node": {
@@ -205,6 +210,7 @@
"libs/model-resolver": {
"name": "@automaker/model-resolver",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/types": "^1.0.0"
},
@@ -226,6 +232,7 @@
"libs/platform": {
"name": "@automaker/platform",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/types": "^1.0.0"
},
@@ -507,6 +514,7 @@
"libs/types": {
"name": "@automaker/types",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE",
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
@@ -525,12 +533,14 @@
"libs/utils": {
"name": "@automaker/utils",
"version": "1.0.0",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/types": "^1.0.0"
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
},
"libs/utils/node_modules/@types/node": {