mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
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);
|
||
});
|