mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Implemented comprehensive error handling system according to v0.16.0 roadmap: - ERROR_MATRIX with 9 error types (redis, parse, llm, file, command, conflict, validation, timeout, unknown) - Enhanced IpuaroError with options, defaultOption, context properties - New methods: getMeta(), hasOption(), toDisplayString() - ErrorHandler service with handle(), wrap(), withRetry() methods - Utility functions: getErrorOptions(), isRecoverableError(), toIpuaroError() - 59 new tests (27 for IpuaroError, 32 for ErrorHandler) - Coverage maintained at 97.59% Breaking changes: - IpuaroError constructor signature changed to (type, message, options?) - ErrorChoice deprecated in favor of ErrorOption
328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
import {
|
|
createErrorHandler,
|
|
ErrorHandler,
|
|
getDefaultErrorOption,
|
|
getErrorOptions,
|
|
isRecoverableError,
|
|
toIpuaroError,
|
|
} from "../../../../src/shared/errors/ErrorHandler.js"
|
|
import { IpuaroError } from "../../../../src/shared/errors/IpuaroError.js"
|
|
|
|
describe("ErrorHandler", () => {
|
|
let handler: ErrorHandler
|
|
|
|
beforeEach(() => {
|
|
handler = new ErrorHandler()
|
|
})
|
|
|
|
describe("handle", () => {
|
|
it("should abort non-recoverable errors", async () => {
|
|
const error = IpuaroError.redis("Connection failed")
|
|
|
|
const result = await handler.handle(error)
|
|
|
|
expect(result.action).toBe("abort")
|
|
expect(result.shouldContinue).toBe(false)
|
|
})
|
|
|
|
it("should use default option for recoverable errors without callback", async () => {
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
const result = await handler.handle(error)
|
|
|
|
expect(result.action).toBe("retry")
|
|
expect(result.shouldContinue).toBe(true)
|
|
})
|
|
|
|
it("should call onError callback when provided", async () => {
|
|
const onError = vi.fn().mockResolvedValue("skip")
|
|
const handler = new ErrorHandler({ onError })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
const result = await handler.handle(error)
|
|
|
|
expect(onError).toHaveBeenCalledWith(error, error.options, error.defaultOption)
|
|
expect(result.action).toBe("skip")
|
|
})
|
|
|
|
it("should auto-skip parse errors when enabled", async () => {
|
|
const handler = new ErrorHandler({ autoSkipParseErrors: true })
|
|
const error = IpuaroError.parse("Syntax error")
|
|
|
|
const result = await handler.handle(error)
|
|
|
|
expect(result.action).toBe("skip")
|
|
expect(result.shouldContinue).toBe(true)
|
|
})
|
|
|
|
it("should auto-retry LLM errors when enabled", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
const result = await handler.handle(error, "test-key")
|
|
|
|
expect(result.action).toBe("retry")
|
|
expect(result.shouldContinue).toBe(true)
|
|
expect(result.retryCount).toBe(1)
|
|
})
|
|
|
|
it("should track retry count", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
await handler.handle(error, "test-key")
|
|
await handler.handle(error, "test-key")
|
|
const result = await handler.handle(error, "test-key")
|
|
|
|
expect(result.retryCount).toBe(3)
|
|
})
|
|
|
|
it("should abort after max retries", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true, maxRetries: 2 })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
await handler.handle(error, "test-key")
|
|
await handler.handle(error, "test-key")
|
|
const result = await handler.handle(error, "test-key")
|
|
|
|
expect(result.action).toBe("abort")
|
|
expect(result.shouldContinue).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("handleSync", () => {
|
|
it("should abort non-recoverable errors", () => {
|
|
const error = IpuaroError.redis("Connection failed")
|
|
|
|
const result = handler.handleSync(error)
|
|
|
|
expect(result.action).toBe("abort")
|
|
expect(result.shouldContinue).toBe(false)
|
|
})
|
|
|
|
it("should use default option for recoverable errors", () => {
|
|
const error = IpuaroError.file("Not found")
|
|
|
|
const result = handler.handleSync(error)
|
|
|
|
expect(result.action).toBe("skip")
|
|
expect(result.shouldContinue).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("resetRetries", () => {
|
|
it("should reset specific context", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
await handler.handle(error, "key1")
|
|
await handler.handle(error, "key1")
|
|
handler.resetRetries("key1")
|
|
|
|
expect(handler.getRetryCount("key1")).toBe(0)
|
|
})
|
|
|
|
it("should reset all contexts when no key provided", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
await handler.handle(error, "key1")
|
|
await handler.handle(error, "key2")
|
|
handler.resetRetries()
|
|
|
|
expect(handler.getRetryCount("key1")).toBe(0)
|
|
expect(handler.getRetryCount("key2")).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("getRetryCount", () => {
|
|
it("should return 0 for unknown context", () => {
|
|
expect(handler.getRetryCount("unknown")).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("isMaxRetriesExceeded", () => {
|
|
it("should return false when retries not exceeded", () => {
|
|
expect(handler.isMaxRetriesExceeded("test")).toBe(false)
|
|
})
|
|
|
|
it("should return true when retries exceeded", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true, maxRetries: 1 })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
await handler.handle(error, "test")
|
|
|
|
expect(handler.isMaxRetriesExceeded("test")).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("wrap", () => {
|
|
it("should return success result on success", async () => {
|
|
const fn = vi.fn().mockResolvedValue("result")
|
|
|
|
const result = await handler.wrap(fn, "llm")
|
|
|
|
expect(result.success).toBe(true)
|
|
if (result.success) {
|
|
expect(result.data).toBe("result")
|
|
}
|
|
})
|
|
|
|
it("should return error result on failure", async () => {
|
|
const fn = vi.fn().mockRejectedValue(new Error("Failed"))
|
|
|
|
const result = await handler.wrap(fn, "llm")
|
|
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
expect(result.result.action).toBe("retry")
|
|
}
|
|
})
|
|
|
|
it("should handle IpuaroError directly", async () => {
|
|
const fn = vi.fn().mockRejectedValue(IpuaroError.file("Not found"))
|
|
|
|
const result = await handler.wrap(fn, "llm")
|
|
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
expect(result.result.action).toBe("skip")
|
|
}
|
|
})
|
|
|
|
it("should reset retries on success", async () => {
|
|
const handler = new ErrorHandler({ autoRetryLLMErrors: true })
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
await handler.handle(error, "test-key")
|
|
await handler.wrap(() => Promise.resolve("ok"), "llm", "test-key")
|
|
|
|
expect(handler.getRetryCount("test-key")).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("withRetry", () => {
|
|
it("should return result on success", async () => {
|
|
const fn = vi.fn().mockResolvedValue("result")
|
|
|
|
const result = await handler.withRetry(fn, "llm", "test")
|
|
|
|
expect(result).toBe("result")
|
|
})
|
|
|
|
it("should retry on failure", async () => {
|
|
const fn = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error("Fail 1"))
|
|
.mockResolvedValueOnce("success")
|
|
const handler = new ErrorHandler({
|
|
onError: vi.fn().mockResolvedValue("retry"),
|
|
})
|
|
|
|
const result = await handler.withRetry(fn, "llm", "test")
|
|
|
|
expect(result).toBe("success")
|
|
expect(fn).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it("should throw after max retries", async () => {
|
|
const fn = vi.fn().mockRejectedValue(new Error("Always fails"))
|
|
const handler = new ErrorHandler({
|
|
maxRetries: 2,
|
|
onError: vi.fn().mockResolvedValue("retry"),
|
|
})
|
|
|
|
await expect(handler.withRetry(fn, "llm", "test")).rejects.toThrow("Max retries")
|
|
})
|
|
|
|
it("should throw immediately when skip is chosen", async () => {
|
|
const fn = vi.fn().mockRejectedValue(new Error("Fail"))
|
|
const handler = new ErrorHandler({
|
|
onError: vi.fn().mockResolvedValue("skip"),
|
|
})
|
|
|
|
await expect(handler.withRetry(fn, "llm", "test")).rejects.toThrow("Fail")
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("utility functions", () => {
|
|
describe("getErrorOptions", () => {
|
|
it("should return options for error type", () => {
|
|
const options = getErrorOptions("llm")
|
|
|
|
expect(options).toEqual(["retry", "skip", "abort"])
|
|
})
|
|
})
|
|
|
|
describe("getDefaultErrorOption", () => {
|
|
it("should return default option for error type", () => {
|
|
expect(getDefaultErrorOption("llm")).toBe("retry")
|
|
expect(getDefaultErrorOption("parse")).toBe("skip")
|
|
expect(getDefaultErrorOption("redis")).toBe("abort")
|
|
})
|
|
})
|
|
|
|
describe("isRecoverableError", () => {
|
|
it("should return true for recoverable errors", () => {
|
|
expect(isRecoverableError("llm")).toBe(true)
|
|
expect(isRecoverableError("parse")).toBe(true)
|
|
expect(isRecoverableError("file")).toBe(true)
|
|
})
|
|
|
|
it("should return false for non-recoverable errors", () => {
|
|
expect(isRecoverableError("redis")).toBe(false)
|
|
expect(isRecoverableError("unknown")).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("toIpuaroError", () => {
|
|
it("should return IpuaroError as-is", () => {
|
|
const error = IpuaroError.llm("Timeout")
|
|
|
|
const result = toIpuaroError(error)
|
|
|
|
expect(result).toBe(error)
|
|
})
|
|
|
|
it("should convert Error to IpuaroError", () => {
|
|
const error = new Error("Something went wrong")
|
|
|
|
const result = toIpuaroError(error, "llm")
|
|
|
|
expect(result).toBeInstanceOf(IpuaroError)
|
|
expect(result.type).toBe("llm")
|
|
expect(result.message).toBe("Something went wrong")
|
|
})
|
|
|
|
it("should convert string to IpuaroError", () => {
|
|
const result = toIpuaroError("Error message", "file")
|
|
|
|
expect(result).toBeInstanceOf(IpuaroError)
|
|
expect(result.type).toBe("file")
|
|
expect(result.message).toBe("Error message")
|
|
})
|
|
|
|
it("should use unknown type by default", () => {
|
|
const result = toIpuaroError("Error")
|
|
|
|
expect(result.type).toBe("unknown")
|
|
})
|
|
})
|
|
|
|
describe("createErrorHandler", () => {
|
|
it("should create handler with default options", () => {
|
|
const handler = createErrorHandler()
|
|
|
|
expect(handler).toBeInstanceOf(ErrorHandler)
|
|
})
|
|
|
|
it("should create handler with custom options", () => {
|
|
const handler = createErrorHandler({ maxRetries: 5 })
|
|
|
|
expect(handler).toBeInstanceOf(ErrorHandler)
|
|
})
|
|
})
|
|
})
|