181 lines
5.8 KiB
TypeScript
181 lines
5.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type {
|
|
ErrorMessage,
|
|
ResultMessage,
|
|
ResultSubtype,
|
|
} from "../types/protocol";
|
|
|
|
/**
|
|
* Tests for error handling in headless mode.
|
|
*
|
|
* These tests document and verify the expected wire format for errors.
|
|
* See GitHub issue #813 for background.
|
|
*
|
|
* Expected behavior:
|
|
* 1. When an error occurs, ResultMessage.subtype should be "error" (not "success")
|
|
* 2. ErrorMessage should contain detailed API error info when available
|
|
* 3. Both one-shot and bidirectional modes should surface errors properly
|
|
*/
|
|
|
|
describe("headless error format types", () => {
|
|
test("ResultSubtype includes 'error' option", () => {
|
|
// This is a compile-time check - if ResultSubtype doesn't include "error",
|
|
// this would fail to compile.
|
|
const errorSubtype: ResultSubtype = "error";
|
|
expect(errorSubtype).toBe("error");
|
|
|
|
const successSubtype: ResultSubtype = "success";
|
|
expect(successSubtype).toBe("success");
|
|
|
|
const interruptedSubtype: ResultSubtype = "interrupted";
|
|
expect(interruptedSubtype).toBe("interrupted");
|
|
});
|
|
|
|
test("ResultMessage type supports stop_reason field", () => {
|
|
// Verify the ResultMessage type accepts stop_reason for error cases
|
|
const errorResult: ResultMessage = {
|
|
type: "result",
|
|
subtype: "error",
|
|
session_id: "test-session",
|
|
uuid: "test-uuid",
|
|
agent_id: "agent-123",
|
|
conversation_id: "conv-123",
|
|
duration_ms: 1000,
|
|
duration_api_ms: 500,
|
|
num_turns: 1,
|
|
result: null,
|
|
run_ids: ["run-123"],
|
|
usage: null,
|
|
stop_reason: "error", // This field should be present for errors
|
|
};
|
|
|
|
expect(errorResult.subtype).toBe("error");
|
|
expect(errorResult.stop_reason).toBe("error");
|
|
});
|
|
|
|
test("ErrorMessage type supports api_error field", () => {
|
|
// Verify ErrorMessage can include nested API error details
|
|
const errorMsg: ErrorMessage = {
|
|
type: "error",
|
|
message: "CONFLICT: Another request is being processed",
|
|
stop_reason: "error",
|
|
session_id: "test-session",
|
|
uuid: "test-uuid",
|
|
run_id: "run-123",
|
|
api_error: {
|
|
message_type: "error_message",
|
|
message: "CONFLICT: Another request is being processed",
|
|
error_type: "internal_error",
|
|
detail:
|
|
"Cannot send a new message: Another request is currently being processed for this conversation.",
|
|
run_id: "run-123",
|
|
},
|
|
};
|
|
|
|
expect(errorMsg.type).toBe("error");
|
|
expect(errorMsg.api_error).toBeDefined();
|
|
expect(errorMsg.api_error?.detail).toContain("Another request");
|
|
});
|
|
});
|
|
|
|
describe("headless error format expectations", () => {
|
|
/**
|
|
* These tests document the EXPECTED behavior for error handling.
|
|
* They verify the wire format contracts that the SDK depends on.
|
|
*/
|
|
|
|
test("error result should have subtype 'error', not 'success'", () => {
|
|
// When an error occurs (stop_reason !== "end_turn"), the result
|
|
// should indicate failure, not success.
|
|
//
|
|
// Bug (issue #813): Bidirectional mode was returning subtype: "success"
|
|
// even when stop_reason was "error".
|
|
//
|
|
// Expected: subtype should be "error" so SDK can detect failure
|
|
|
|
// This is a contract test - verifying the expected structure
|
|
const mockErrorResult: ResultMessage = {
|
|
type: "result",
|
|
subtype: "error", // NOT "success"
|
|
session_id: "test",
|
|
uuid: "test",
|
|
agent_id: "agent-123",
|
|
conversation_id: "conv-123",
|
|
duration_ms: 1000,
|
|
duration_api_ms: 500,
|
|
num_turns: 1,
|
|
result: null,
|
|
run_ids: [],
|
|
usage: null,
|
|
stop_reason: "error",
|
|
};
|
|
|
|
// SDK transforms this to { success: false } based on subtype
|
|
const sdkSuccess = mockErrorResult.subtype === "success";
|
|
expect(sdkSuccess).toBe(false);
|
|
});
|
|
|
|
test("409 conflict error should include detail in message", () => {
|
|
// When API returns 409 with a detail field, that detail should be
|
|
// surfaced in the error message, not lost.
|
|
//
|
|
// Bug (issue #813): The detail was being lost, making debugging hard.
|
|
//
|
|
// Expected: ErrorMessage.message or api_error.detail contains the info
|
|
|
|
// Example 409 error detail
|
|
const conflictDetail =
|
|
"CONFLICT: Cannot send a new message: Another request is currently being processed for this conversation.";
|
|
|
|
// The error message should include this detail
|
|
const mockError: ErrorMessage = {
|
|
type: "error",
|
|
message: conflictDetail, // Detail should be in message
|
|
stop_reason: "error",
|
|
session_id: "test",
|
|
uuid: "test",
|
|
run_id: "run-123",
|
|
};
|
|
|
|
expect(mockError.message).toContain("CONFLICT");
|
|
expect(mockError.message).toContain("Another request");
|
|
});
|
|
|
|
test("approval pending error should include detail", () => {
|
|
// When conversation has a stuck approval, the error should explain this.
|
|
//
|
|
// Example error: "The agent is waiting for approval on a tool call.
|
|
// Please approve or deny the pending request before continuing."
|
|
|
|
const approvalDetail =
|
|
"CONFLICT: Cannot send a new message: The agent is waiting for approval on a tool call.";
|
|
|
|
const mockError: ErrorMessage = {
|
|
type: "error",
|
|
message: approvalDetail,
|
|
stop_reason: "error",
|
|
session_id: "test",
|
|
uuid: "test",
|
|
};
|
|
|
|
expect(mockError.message).toContain("waiting for approval");
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Note for SDK team:
|
|
*
|
|
* The SDK (letta-code-sdk) transforms ResultMessage as follows:
|
|
*
|
|
* success: msg.subtype === "success"
|
|
* error: msg.subtype !== "success" ? msg.subtype : undefined
|
|
*
|
|
* With this fix:
|
|
* - Error results will have subtype: "error", so success will be false
|
|
* - The error field will be "error" (the subtype string)
|
|
*
|
|
* For more detailed error info, SDK could be updated to:
|
|
* 1. Parse ErrorMessage events (currently ignored)
|
|
* 2. Use stop_reason from ResultMessage for specific error types
|
|
*/
|