Files

165 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { UserError } from "@openagentpack/sdk";
import { BailianError, ExitCode } from "bailian-cli-core";
import { expect, test } from "vite-plus/test";
import {
formatAgentDiagnosticFailure,
retainAgentError,
withAgentErrors,
} from "../src/commands/managed-agent/_engine/errors.ts";
/**
* Structural stand-in for the SDK's internal `ApiError` (not exported by the
* installed SDK version): withAgentErrors matches on statusCode/responseBody
* fields, so any Error carrying them must map through mapApiError.
*/
class FakeSdkApiError extends Error {
constructor(
readonly statusCode: number,
readonly responseBody: string,
) {
super(`Bailian API ${statusCode}: ${responseBody}`);
}
}
async function catchMapped(error: unknown): Promise<BailianError> {
try {
await withAgentErrors(() => Promise.reject(error));
} catch (mapped) {
expect(mapped).toBeInstanceOf(BailianError);
return mapped as BailianError;
}
throw new Error("expected withAgentErrors to throw");
}
test("BailianError passes through untouched", async () => {
const original = new BailianError("already mapped", ExitCode.AUTH);
const mapped = await catchMapped(original);
expect(mapped).toBe(original);
});
test("SDK UserError maps to USAGE", async () => {
const mapped = await catchMapped(new UserError("bad agents.yaml"));
expect(mapped.exitCode).toBe(ExitCode.USAGE);
expect(mapped.message).toBe("bad agents.yaml");
});
test("SDK polling-timeout UserError maps to TIMEOUT (5), not USAGE", async () => {
// 消息形状来自 SDK session-runtime 的 assertNotTimedOut —— 客户端等待超时,
// 按 bl 错误边界必须归 TIMEOUT不能告诉自动化调用方“参数错误”。
const mapped = await catchMapped(
new UserError("Session did not complete within the timeout (600 seconds)."),
);
expect(mapped.exitCode).toBe(ExitCode.TIMEOUT);
expect(mapped.message).toBe("Session did not complete within the timeout (600 seconds).");
expect(mapped.hint).toMatch(/session get/);
});
test("提及 timeout 但非轮询超时句式的 UserError 仍归 USAGE", async () => {
const mapped = await catchMapped(new UserError("Invalid timeout value in agents.yaml"));
expect(mapped.exitCode).toBe(ExitCode.USAGE);
});
test("SDK ApiError with DashScope-style JSON body surfaces clean message and api metadata", async () => {
const body = JSON.stringify({
code: "InvalidParameter",
message: "agent name already exists",
request_id: "req-123",
});
const mapped = await catchMapped(new FakeSdkApiError(400, body));
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
expect(mapped.message).toBe("agent name already exists");
expect(mapped.api).toEqual({
httpStatus: 400,
apiCode: "InvalidParameter",
requestId: "req-123",
});
});
test("SDK ApiError with OpenAI-style error envelope extracts message and type", async () => {
const body = JSON.stringify({
error: { message: "model does not exist", type: "invalid_request_error" },
request_id: "req-456",
});
const mapped = await catchMapped(new FakeSdkApiError(404, body));
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
expect(mapped.message).toBe("model does not exist");
expect(mapped.api?.apiCode).toBe("invalid_request_error");
expect(mapped.api?.requestId).toBe("req-456");
});
test("SDK ApiError with non-JSON body passes raw text through as message", async () => {
const mapped = await catchMapped(new FakeSdkApiError(502, "Bad Gateway"));
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
expect(mapped.message).toBe("Bad Gateway");
expect(mapped.api?.httpStatus).toBe(502);
});
test("SDK ApiError with empty body falls back to HTTP status message", async () => {
const mapped = await catchMapped(new FakeSdkApiError(503, ""));
expect(mapped.message).toBe("HTTP 503");
expect(mapped.api?.httpStatus).toBe(503);
});
test("plain Error maps to GENERAL with message passed through", async () => {
const mapped = await catchMapped(new Error("boom"));
expect(mapped.exitCode).toBe(ExitCode.GENERAL);
expect(mapped.message).toBe("boom");
expect(mapped.api).toBeUndefined();
});
test("fetch transport TypeError is rethrown untouched for the runtime NETWORK mapping", async () => {
const transportError = new TypeError("fetch failed", {
cause: Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:1"), {
code: "ECONNREFUSED",
}),
});
try {
await withAgentErrors(() => Promise.reject(transportError));
throw new Error("expected withAgentErrors to throw");
} catch (error) {
expect(error).toBe(transportError);
}
});
test("diagnostic failure surfaces the first error and reports the remaining count", () => {
const message = formatAgentDiagnosticFailure(
[
{ severity: "warning", code: "warning.code", message: "warning" },
{ severity: "error", code: "first.code", message: "first failure" },
{ severity: "error", code: "second.code", message: "second failure" },
{ severity: "error", code: "third.code", message: "third failure" },
],
"fallback",
);
expect(message).toBe("[first.code] first failure (+2 more errors)");
});
test("diagnostic failure uses its fallback only when no error detail exists", () => {
expect(
formatAgentDiagnosticFailure(
[{ severity: "warning", code: "warning.code", message: "warning" }],
"fallback",
),
).toBe("fallback");
});
test("retaining an Agent error adds a hint without losing structured context", () => {
const cause = new Error("provider cause");
const original = new BailianError("provider failure", ExitCode.GENERAL, undefined, {
api: { httpStatus: 500, apiCode: "PROVIDER_FAILURE", requestId: "req-retain" },
rawResponse: "raw provider response",
cause,
});
const retained = retainAgentError(original, "retry the create", "fallback");
expect(retained).not.toBe(original);
expect(retained.message).toBe("provider failure");
expect(retained.exitCode).toBe(ExitCode.GENERAL);
expect(retained.hint).toBe("retry the create");
expect(retained.api).toEqual(original.api);
expect(retained.rawResponse).toBe("raw provider response");
expect(retained.cause).toBe(cause);
});