chore: end to end test coverage for ts sdk (#4357)

This commit is contained in:
Kartik
2026-03-17 21:13:52 +05:30
committed by GitHub
parent 336fbce60a
commit 3cdcb6564c
36 changed files with 4583 additions and 778 deletions
+246
View File
@@ -0,0 +1,246 @@
import {
MemoryError,
AuthenticationError,
RateLimitError,
ValidationError,
MemoryNotFoundError,
NetworkError,
ConfigurationError,
MemoryQuotaExceededError,
createExceptionFromResponse,
HTTP_STATUS_TO_EXCEPTION,
} from "./exceptions";
describe("MemoryError", () => {
const error = new MemoryError("test error", "MEM_001", {
details: { operation: "add" },
suggestion: "Try again",
debugInfo: { requestId: "req_123" },
});
test("is an instance of Error", () => {
expect(error).toBeInstanceOf(Error);
});
test("has correct message", () => {
expect(error.message).toBe("test error");
});
test("has correct errorCode", () => {
expect(error.errorCode).toBe("MEM_001");
});
test("has correct details", () => {
expect(error.details).toEqual({ operation: "add" });
});
test("has correct suggestion", () => {
expect(error.suggestion).toBe("Try again");
});
test("has correct debugInfo", () => {
expect(error.debugInfo).toEqual({ requestId: "req_123" });
});
test("defaults details to empty object", () => {
const err = new MemoryError("test error", "MEM_001");
expect(err.details).toEqual({});
});
test("defaults suggestion to undefined", () => {
const err = new MemoryError("test error", "MEM_001");
expect(err.suggestion).toBeUndefined();
});
test("defaults debugInfo to empty object", () => {
const err = new MemoryError("test error", "MEM_001");
expect(err.debugInfo).toEqual({});
});
test("is throwable and catchable", () => {
expect(() => {
throw new MemoryError("fail", "MEM_001");
}).toThrow("fail");
});
});
describe("Exception subclasses", () => {
const subclasses = [
{ Class: AuthenticationError, name: "AuthenticationError" },
{ Class: RateLimitError, name: "RateLimitError" },
{ Class: ValidationError, name: "ValidationError" },
{ Class: MemoryNotFoundError, name: "MemoryNotFoundError" },
{ Class: NetworkError, name: "NetworkError" },
{ Class: ConfigurationError, name: "ConfigurationError" },
{ Class: MemoryQuotaExceededError, name: "MemoryQuotaExceededError" },
] as const;
test.each(subclasses)("$name extends MemoryError", ({ Class }) => {
const error = new Class("test", "CODE_001");
expect(error).toBeInstanceOf(MemoryError);
});
test.each(subclasses)("$name extends Error", ({ Class }) => {
const error = new Class("test", "CODE_001");
expect(error).toBeInstanceOf(Error);
});
test.each(subclasses)("$name has correct name", ({ Class, name }) => {
const error = new Class("test", "CODE_001");
expect(error.name).toBe(name);
});
test.each(subclasses)("$name supports instanceof checks", ({ Class }) => {
const error = new Class("test", "CODE_001");
expect(error instanceof Class).toBe(true);
});
});
describe("createExceptionFromResponse", () => {
test("maps 401 to AuthenticationError", () => {
const error = createExceptionFromResponse(401, "Unauthorized");
expect(error).toBeInstanceOf(AuthenticationError);
});
test("maps 401 to errorCode HTTP_401", () => {
const error = createExceptionFromResponse(401, "Unauthorized");
expect(error.errorCode).toBe("HTTP_401");
});
test("maps 401 to authentication suggestion", () => {
const error = createExceptionFromResponse(401, "Unauthorized");
expect(error.suggestion).toBe(
"Please check your API key and authentication credentials",
);
});
test("maps 429 to RateLimitError", () => {
const error = createExceptionFromResponse(429, "Too many requests", {
debugInfo: { retryAfter: 60 },
});
expect(error).toBeInstanceOf(RateLimitError);
});
test("maps 429 passes debugInfo through", () => {
const error = createExceptionFromResponse(429, "Too many requests", {
debugInfo: { retryAfter: 60 },
});
expect(error.debugInfo).toEqual({ retryAfter: 60 });
});
test("maps 404 to MemoryNotFoundError", () => {
const error = createExceptionFromResponse(404, "Not found");
expect(error).toBeInstanceOf(MemoryNotFoundError);
});
test("maps 400 to ValidationError", () => {
const error = createExceptionFromResponse(400, "Bad request");
expect(error).toBeInstanceOf(ValidationError);
});
test("maps 413 to MemoryQuotaExceededError", () => {
const error = createExceptionFromResponse(413, "Quota exceeded");
expect(error).toBeInstanceOf(MemoryQuotaExceededError);
});
test.each([502, 503, 504])("maps %i to NetworkError", (code) => {
const error = createExceptionFromResponse(code, "Service unavailable");
expect(error).toBeInstanceOf(NetworkError);
});
test("maps 500 to MemoryError", () => {
const error = createExceptionFromResponse(500, "Internal error");
expect(error).toBeInstanceOf(MemoryError);
});
test("maps 500 to errorCode HTTP_500", () => {
const error = createExceptionFromResponse(500, "Internal error");
expect(error.errorCode).toBe("HTTP_500");
});
test("maps unknown status to MemoryError", () => {
const error = createExceptionFromResponse(418, "I am a teapot");
expect(error).toBeInstanceOf(MemoryError);
});
test("maps unknown status to correct errorCode", () => {
const error = createExceptionFromResponse(418, "I am a teapot");
expect(error.errorCode).toBe("HTTP_418");
});
test("maps unknown status to retry suggestion", () => {
const error = createExceptionFromResponse(418, "I am a teapot");
expect(error.suggestion).toBe("Please try again later");
});
test("uses response text as message", () => {
const error = createExceptionFromResponse(400, "Invalid user_id format");
expect(error.message).toBe("Invalid user_id format");
});
test("falls back to generic message when response text is empty", () => {
const error = createExceptionFromResponse(500, "");
expect(error.message).toBe("HTTP 500 error");
});
test("passes details through", () => {
const error = createExceptionFromResponse(400, "Bad request", {
details: { field: "user_id", value: "" },
});
expect(error.details).toEqual({ field: "user_id", value: "" });
});
});
describe("HTTP_STATUS_TO_EXCEPTION", () => {
test("maps 400 to ValidationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[400]).toBe(ValidationError);
});
test("maps 401 to AuthenticationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[401]).toBe(AuthenticationError);
});
test("maps 403 to AuthenticationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[403]).toBe(AuthenticationError);
});
test("maps 404 to MemoryNotFoundError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[404]).toBe(MemoryNotFoundError);
});
test("maps 408 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[408]).toBe(NetworkError);
});
test("maps 409 to ValidationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[409]).toBe(ValidationError);
});
test("maps 413 to MemoryQuotaExceededError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[413]).toBe(MemoryQuotaExceededError);
});
test("maps 422 to ValidationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[422]).toBe(ValidationError);
});
test("maps 429 to RateLimitError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[429]).toBe(RateLimitError);
});
test("maps 500 to MemoryError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[500]).toBe(MemoryError);
});
test("maps 502 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[502]).toBe(NetworkError);
});
test("maps 503 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[503]).toBe(NetworkError);
});
test("maps 504 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[504]).toBe(NetworkError);
});
});
+205
View File
@@ -0,0 +1,205 @@
/**
* Structured exception classes for mem0 TypeScript SDK.
*
* Provides specific, actionable exceptions with error codes, suggestions,
* and debug information. Maps HTTP status codes to appropriate exception types.
*
* @example
* ```typescript
* import { RateLimitError, MemoryNotFoundError } from 'mem0ai'
*
* try {
* await client.get(memoryId)
* } catch (e) {
* if (e instanceof MemoryNotFoundError) {
* console.log(e.suggestion) // "The requested resource was not found"
* } else if (e instanceof RateLimitError) {
* await sleep(e.debugInfo.retryAfter ?? 60)
* }
* }
* ```
*/
export interface MemoryErrorOptions {
details?: Record<string, unknown>;
suggestion?: string;
debugInfo?: Record<string, unknown>;
}
/**
* Base exception for all memory-related errors.
*
* Every mem0 exception includes an error code for programmatic handling,
* optional details, a user-friendly suggestion, and debug information.
*/
export class MemoryError extends Error {
readonly errorCode: string;
readonly details: Record<string, unknown>;
readonly suggestion?: string;
readonly debugInfo: Record<string, unknown>;
constructor(
message: string,
errorCode: string,
options: MemoryErrorOptions = {},
) {
super(message);
this.name = "MemoryError";
this.errorCode = errorCode;
this.details = options.details ?? {};
this.suggestion = options.suggestion;
this.debugInfo = options.debugInfo ?? {};
// Fix prototype chain for instanceof checks
Object.setPrototypeOf(this, new.target.prototype);
}
}
/** Raised when authentication fails (401, 403). */
export class AuthenticationError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "AuthenticationError";
}
}
/** Raised when rate limits are exceeded (429). */
export class RateLimitError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "RateLimitError";
}
}
/** Raised when input validation fails (400, 409, 422). */
export class ValidationError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "ValidationError";
}
}
/** Raised when a memory is not found (404). */
export class MemoryNotFoundError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "MemoryNotFoundError";
}
}
/** Raised when network connectivity issues occur (408, 502, 503, 504). */
export class NetworkError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "NetworkError";
}
}
/** Raised when client configuration is invalid. */
export class ConfigurationError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "ConfigurationError";
}
}
/** Raised when memory quota is exceeded (413). */
export class MemoryQuotaExceededError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "MemoryQuotaExceededError";
}
}
// ─── HTTP Status → Exception Mapping ─────────────────────
type MemoryErrorConstructor = new (
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) => MemoryError;
export const HTTP_STATUS_TO_EXCEPTION: Record<number, MemoryErrorConstructor> =
{
400: ValidationError,
401: AuthenticationError,
403: AuthenticationError,
404: MemoryNotFoundError,
408: NetworkError,
409: ValidationError,
413: MemoryQuotaExceededError,
422: ValidationError,
429: RateLimitError,
500: MemoryError,
502: NetworkError,
503: NetworkError,
504: NetworkError,
};
const HTTP_SUGGESTIONS: Record<number, string> = {
400: "Please check your request parameters and try again",
401: "Please check your API key and authentication credentials",
403: "You don't have permission to perform this operation",
404: "The requested resource was not found",
408: "Request timed out. Please try again",
409: "Resource conflict. Please check your request",
413: "Request too large. Please reduce the size of your request",
422: "Invalid request data. Please check your input",
429: "Rate limit exceeded. Please wait before making more requests",
500: "Internal server error. Please try again later",
502: "Service temporarily unavailable. Please try again later",
503: "Service unavailable. Please try again later",
504: "Gateway timeout. Please try again later",
};
/**
* Create an appropriate exception based on HTTP response status code.
*
* @param statusCode - HTTP status code from the response
* @param responseText - Response body text
* @param options - Additional error context (details, debugInfo)
* @returns An instance of the appropriate MemoryError subclass
*/
export function createExceptionFromResponse(
statusCode: number,
responseText: string,
options: Omit<MemoryErrorOptions, "suggestion"> = {},
): MemoryError {
const ExceptionClass = HTTP_STATUS_TO_EXCEPTION[statusCode] ?? MemoryError;
const errorCode = `HTTP_${statusCode}`;
const suggestion = HTTP_SUGGESTIONS[statusCode] ?? "Please try again later";
return new ExceptionClass(
responseText || `HTTP ${statusCode} error`,
errorCode,
{ ...options, suggestion },
);
}