257 lines
7.4 KiB
TypeScript
257 lines
7.4 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test";
|
|
import { APIError } from "@letta-ai/letta-client/core/error";
|
|
import {
|
|
clearErrorContext,
|
|
setErrorContext,
|
|
} from "../../cli/helpers/errorContext";
|
|
import { formatErrorDetails } from "../../cli/helpers/errorFormatter";
|
|
|
|
describe("formatErrorDetails", () => {
|
|
beforeEach(() => {
|
|
clearErrorContext();
|
|
});
|
|
|
|
describe("encrypted content org mismatch", () => {
|
|
const chatGptDetail =
|
|
'INTERNAL_SERVER_ERROR: ChatGPT request failed (400): {\n "error": {\n "message": "The encrypted content for item rs_0dd1c85f779f9f0301698a7e40a0508193ba9a669d32159bf0 could not be verified. Reason: Encrypted content organization_id did not match the target organization.",\n "type": "invalid_request_error",\n "param": null,\n "code": "invalid_encrypted_content"\n }\n}';
|
|
|
|
test("handles nested error object from run metadata", () => {
|
|
// This is the errorObject shape constructed in App.tsx from run.metadata.error
|
|
const errorObject = {
|
|
error: {
|
|
error: {
|
|
message_type: "error_message",
|
|
run_id: "run-cb408f59-f901-4bde-ad1f-ed58a1f13482",
|
|
error_type: "internal_error",
|
|
message: "An error occurred during agent execution.",
|
|
detail: chatGptDetail,
|
|
seq_id: null,
|
|
},
|
|
run_id: "run-cb408f59-f901-4bde-ad1f-ed58a1f13482",
|
|
},
|
|
};
|
|
|
|
const result = formatErrorDetails(errorObject);
|
|
|
|
expect(result).toContain("OpenAI error:");
|
|
expect(result).toContain("invalid_encrypted_content");
|
|
expect(result).toContain("/clear to start a new conversation.");
|
|
expect(result).toContain("different OpenAI authentication scope");
|
|
// Should NOT be raw JSON
|
|
expect(result).not.toContain('"message_type"');
|
|
expect(result).not.toContain('"run_id"');
|
|
});
|
|
|
|
test("formats inner error as JSON-like block", () => {
|
|
const errorObject = {
|
|
error: {
|
|
error: {
|
|
detail: chatGptDetail,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = formatErrorDetails(errorObject);
|
|
|
|
// JSON-like structured format
|
|
expect(result).toContain('type: "invalid_request_error"');
|
|
expect(result).toContain('code: "invalid_encrypted_content"');
|
|
expect(result).toContain("organization_id did not match");
|
|
expect(result).toContain(" {");
|
|
expect(result).toContain(" }");
|
|
});
|
|
|
|
test("handles error with direct detail field", () => {
|
|
const errorObject = {
|
|
detail: chatGptDetail,
|
|
};
|
|
|
|
const result = formatErrorDetails(errorObject);
|
|
|
|
expect(result).toContain("OpenAI error:");
|
|
expect(result).toContain("/clear to start a new conversation.");
|
|
});
|
|
|
|
test("falls back gracefully when detail JSON is malformed", () => {
|
|
const errorObject = {
|
|
error: {
|
|
error: {
|
|
detail:
|
|
"INTERNAL_SERVER_ERROR: ChatGPT request failed (400): invalid_encrypted_content garbled",
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = formatErrorDetails(errorObject);
|
|
|
|
expect(result).toContain("OpenAI error:");
|
|
expect(result).toContain("/clear to start a new conversation.");
|
|
});
|
|
});
|
|
|
|
test("uses neutral credit exhaustion copy for free tier not-enough-credits", () => {
|
|
setErrorContext({ billingTier: "free", modelDisplayName: "Kimi K2.5" });
|
|
|
|
const error = new APIError(
|
|
402,
|
|
{
|
|
error: "Rate limited",
|
|
reasons: ["not-enough-credits"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("out of credits");
|
|
expect(message).toContain("/connect");
|
|
expect(message).not.toContain("not available on Free plan");
|
|
expect(message).not.toContain("Selected hosted model");
|
|
});
|
|
|
|
test("handles nested reasons for credit exhaustion", () => {
|
|
const error = new APIError(
|
|
402,
|
|
{
|
|
error: {
|
|
reasons: ["not-enough-credits"],
|
|
},
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
expect(message).toContain("out of credits");
|
|
});
|
|
|
|
test("shows explicit model availability guidance for model-unknown", () => {
|
|
const error = new APIError(
|
|
429,
|
|
{
|
|
error: "Rate limited",
|
|
reasons: ["model-unknown"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("not currently available");
|
|
expect(message).toContain("Run /model");
|
|
expect(message).toContain("press R");
|
|
});
|
|
|
|
test("keeps canonical free model pair for byok-not-available-on-free-tier", () => {
|
|
setErrorContext({ modelDisplayName: "GPT-5" });
|
|
|
|
const error = new APIError(
|
|
403,
|
|
{
|
|
error: "Forbidden",
|
|
reasons: ["byok-not-available-on-free-tier"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("glm-4.7");
|
|
expect(message).toContain("minimax-m2.1");
|
|
expect(message).toContain("Free plan");
|
|
});
|
|
|
|
test("keeps canonical free model pair for free-usage-exceeded", () => {
|
|
const error = new APIError(
|
|
429,
|
|
{
|
|
error: "Rate limited",
|
|
reasons: ["free-usage-exceeded"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("glm-4.7");
|
|
expect(message).toContain("minimax-m2.1");
|
|
expect(message).toContain("/model");
|
|
});
|
|
|
|
test("uses premium-specific guidance for premium-usage-exceeded", () => {
|
|
const error = new APIError(
|
|
429,
|
|
{
|
|
error: "Rate limited",
|
|
reasons: ["premium-usage-exceeded"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("Premium model usage limit");
|
|
expect(message).toContain("Standard or Basic hosted models");
|
|
expect(message).toContain("/model");
|
|
expect(message).not.toContain("hosted model usage limit");
|
|
});
|
|
|
|
test("uses standard-specific guidance for standard-usage-exceeded", () => {
|
|
const error = new APIError(
|
|
429,
|
|
{
|
|
error: "Rate limited",
|
|
reasons: ["standard-usage-exceeded"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("Standard model usage limit");
|
|
expect(message).toContain("Basic hosted models");
|
|
expect(message).toContain("/model");
|
|
});
|
|
|
|
test("uses basic-specific guidance for basic-usage-exceeded", () => {
|
|
const error = new APIError(
|
|
429,
|
|
{
|
|
error: "Rate limited",
|
|
reasons: ["basic-usage-exceeded"],
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("Basic model usage limit");
|
|
expect(message).toContain("/model");
|
|
});
|
|
|
|
test("formats Z.ai error from APIError with embedded error code", () => {
|
|
const error = new APIError(
|
|
429,
|
|
{
|
|
error:
|
|
"Rate limited by OpenAI: Error code: 429 - {'error': {'code': 1302, 'message': 'High concurrency usage exceeds limits'}}",
|
|
},
|
|
undefined,
|
|
new Headers(),
|
|
);
|
|
|
|
const message = formatErrorDetails(error);
|
|
|
|
expect(message).toContain("Z.ai rate limit");
|
|
expect(message).toContain("High concurrency usage exceeds limits");
|
|
expect(message).not.toContain("OpenAI");
|
|
});
|
|
});
|