Files
modelstudioai__cli/packages/core/tests/index.test.ts

334 lines
10 KiB
TypeScript

import { expect, test } from "vite-plus/test";
import type { Identity, Settings } from "../src/index.ts";
import {
BailianError,
ExitCode,
McpClient,
callConsoleGateway,
mapApiError,
request,
} from "../src/index.ts";
import { parseConfigFile } from "../src/config/schema.ts";
import {
parseBooleanValue,
resolveBooleanFlag,
resolveWatermark,
} from "../src/utils/boolean-flag.ts";
function testDeps(identity: Partial<Identity> = {}): {
identity: Identity;
settings: Settings;
} {
return {
identity: {
binName: "bl",
version: "0.0.0-test",
npmPackage: "bailian-cli",
clientName: "bailian-cli",
...identity,
},
settings: {
output: "json",
outputExplicit: true,
timeout: 30,
verbose: false,
quiet: true,
dryRun: false,
telemetry: true,
},
};
}
test("BailianError carries exitCode and hint", () => {
const err = new BailianError("nope", ExitCode.AUTH, "do this");
expect(err.name).toBe("BailianError");
expect(err.exitCode).toBe(ExitCode.AUTH);
expect(err.hint).toBe("do this");
expect(err.toJSON()).toEqual({
error: { code: ExitCode.AUTH, message: "nope", hint: "do this" },
});
});
test("mapApiError keeps server message verbatim and surfaces metadata via err.api", () => {
const err = mapApiError(401, { error: { message: "bad key" } });
expect(err).toBeInstanceOf(BailianError);
expect(err.exitCode).toBe(ExitCode.GENERAL);
expect(err.message).toBe("bad key");
expect(err.api?.httpStatus).toBe(401);
expect(err.api?.apiCode).toBeUndefined();
expect(err.api?.requestId).toBeUndefined();
});
test("mapApiError captures apiCode and request_id when present", () => {
const err = mapApiError(429, {
error: { message: "too many", type: "Throttling" },
request_id: "req-abc-123",
});
expect(err.exitCode).toBe(ExitCode.GENERAL);
expect(err.message).toBe("too many");
expect(err.api).toEqual({
httpStatus: 429,
apiCode: "Throttling",
requestId: "req-abc-123",
});
});
test("BailianError propagates cause via options-bag and exposes it in toJSON", () => {
const root = Object.assign(new Error("getaddrinfo ENOTFOUND example.invalid"), {
code: "ENOTFOUND",
});
const err = new BailianError("Network request failed: ENOTFOUND", ExitCode.NETWORK, "hint", {
cause: root,
});
expect(err.cause).toBe(root);
expect(err.toJSON()).toEqual({
error: {
code: ExitCode.NETWORK,
message: "Network request failed: ENOTFOUND",
hint: "hint",
cause: { message: root.message, code: "ENOTFOUND" },
},
});
});
test("toJSON splits service-error metadata into structured fields", () => {
const err = mapApiError(404, {
error: {
message: "The model `qwen3.7` does not exist",
type: "invalid_request_error",
},
request_id: "c55e1acc",
});
expect(err.toJSON()).toEqual({
error: {
code: ExitCode.GENERAL,
message: "The model `qwen3.7` does not exist",
http_status: 404,
api_code: "invalid_request_error",
request_id: "c55e1acc",
},
});
});
test("callConsoleGateway verbose prints structured request payload", async () => {
const originalFetch = globalThis.fetch;
const originalWrite = process.stderr.write.bind(process.stderr);
let stderr = "";
let requestBody: string | undefined;
globalThis.fetch = async (_url, init) => {
requestBody = init?.body as string | undefined;
return new Response(JSON.stringify({ data: { success: true, value: "response-body" } }), {
status: 200,
statusText: "OK",
headers: { "Content-Type": "application/json" },
});
};
process.stderr.write = ((chunk: string | Uint8Array) => {
stderr += String(chunk);
return true;
}) as typeof process.stderr.write;
try {
await callConsoleGateway(
{
region: "ap-southeast-1",
site: "international",
switchAgent: 123,
token: "token",
},
30,
{
api: "test.api",
data: { workspaceId: "ws-1", cornerstoneParam: { custom: "value" } },
},
{ verbose: true },
);
} finally {
globalThis.fetch = originalFetch;
process.stderr.write = originalWrite;
}
expect(requestBody).toBeDefined();
expect(stderr).toContain('> payload {\n "params": {');
expect(stderr).toContain(' "region": "ap-southeast-1"');
expect(stderr).toContain(' "Api": "test.api"');
expect(stderr).toContain(' "workspaceId": "ws-1"');
expect(stderr).toContain(' "switchUserType": 3');
expect(stderr).toContain(' "switchAgent": 123');
expect(stderr).toContain(' "custom": "value"');
expect(stderr).toContain("< 200 OK");
expect(stderr).not.toContain("response-body");
});
test("callConsoleGateway keeps readable message and raw gateway response separately", async () => {
const originalFetch = globalThis.fetch;
const originalWrite = process.stderr.write.bind(process.stderr);
const responseBody = {
data: {
success: false,
errorCode: "BailianGateway.Team.NotAuthorised",
errorMsg: "team not authorised",
},
};
globalThis.fetch = async () =>
new Response(JSON.stringify(responseBody), {
status: 200,
statusText: "OK",
headers: { "Content-Type": "application/json" },
});
process.stderr.write = (() => true) as typeof process.stderr.write;
try {
await expect(
callConsoleGateway(
{ region: "cn-beijing", site: "domestic", token: "token" },
30,
{ api: "test.api", data: {} },
{ verbose: true },
),
).rejects.toMatchObject({
message: "Console gateway error: BailianGateway.Team.NotAuthorised",
rawResponse: JSON.stringify(responseBody),
exitCode: ExitCode.GENERAL,
});
} finally {
globalThis.fetch = originalFetch;
process.stderr.write = originalWrite;
}
});
test("request uses injected client identity for User-Agent", async () => {
const originalFetch = globalThis.fetch;
let userAgent: string | undefined;
globalThis.fetch = async (_url, init) => {
const headers = init?.headers as Record<string, string> | undefined;
userAgent = headers?.["User-Agent"];
return new Response("{}", { status: 200 });
};
try {
await request(testDeps({ clientName: "test-client", version: "9.8.7" }), {
url: "https://example.test",
});
} finally {
globalThis.fetch = originalFetch;
}
expect(userAgent).toBe("test-client/9.8.7");
});
test("request propagates caller AbortSignal to fetch", async () => {
const originalFetch = globalThis.fetch;
const controller = new AbortController();
let fetchSignal: AbortSignal | undefined;
let resolveFetch: ((response: Response) => void) | undefined;
const fetchStarted = new Promise<void>((resolve) => {
globalThis.fetch = async (_url, init) => {
fetchSignal = init?.signal as AbortSignal | undefined;
resolve();
return await new Promise<Response>((resolveResponse) => {
resolveFetch = resolveResponse;
});
};
});
const requestPromise = request(testDeps(), {
url: "https://example.test",
signal: controller.signal,
});
try {
await fetchStarted;
expect(fetchSignal).toBeDefined();
expect(fetchSignal?.aborted).toBe(false);
controller.abort();
expect(fetchSignal?.aborted).toBe(true);
resolveFetch?.(new Response("{}", { status: 200 }));
await requestPromise;
} finally {
globalThis.fetch = originalFetch;
}
});
test("McpClient uses injected client identity for initialize and User-Agent", async () => {
const originalFetch = globalThis.fetch;
const bodies: unknown[] = [];
const userAgents: string[] = [];
globalThis.fetch = async (_url, init) => {
const headers = init?.headers as Record<string, string> | undefined;
userAgents.push(headers?.["User-Agent"] ?? "");
const body = init?.body;
if (typeof body === "string") bodies.push(JSON.parse(body));
return new Response(JSON.stringify({ jsonrpc: "2.0", id: 1, result: {} }), {
status: 200,
});
};
try {
const client = new McpClient(
testDeps({ clientName: "test-client", version: "9.8.7" }),
"https://mcp.example.test",
"sk-test",
);
await client.initialize();
} finally {
globalThis.fetch = originalFetch;
}
expect(userAgents).toEqual(["test-client/9.8.7", "test-client/9.8.7"]);
expect(bodies[0]).toMatchObject({
method: "initialize",
params: { clientInfo: { name: "test-client", version: "9.8.7" } },
});
});
test("resolveWatermark uses flag or defaults to true", () => {
expect(resolveWatermark("false")).toBe(false);
expect(resolveWatermark("true")).toBe(true);
expect(resolveWatermark(undefined)).toBe(true);
});
test("resolveBooleanFlag uses flag or defaultWhenUnset", () => {
expect(resolveBooleanFlag("false", true, "prompt-extend")).toBe(false);
expect(resolveBooleanFlag(undefined, true, "prompt-extend")).toBe(true);
expect(resolveBooleanFlag(undefined, undefined, "prompt-extend")).toBeUndefined();
});
test("parseBooleanValue accepts only true and false strings (case-insensitive)", () => {
expect(parseBooleanValue("true")).toBe(true);
expect(parseBooleanValue("FALSE")).toBe(false);
expect(() => parseBooleanValue("1")).toThrow(BailianError);
expect(() => parseBooleanValue("yes")).toThrow(BailianError);
expect(() => parseBooleanValue("maybe")).toThrow(BailianError);
});
test("parseConfigFile ignores obsolete region field", () => {
const f = parseConfigFile({ region: "intl" });
expect("region" in f).toBe(false);
});
test("parseConfigFile accepts only supported languages", () => {
expect(parseConfigFile({ language: "zh-CN" }).language).toBe("zh-CN");
expect(parseConfigFile({ language: "fr-FR" }).language).toBeUndefined();
});
test("parseConfigFile accepts only well-formed http(s) base_url", () => {
expect(parseConfigFile({ base_url: "https://dashscope.aliyuncs.com" }).base_url).toBe(
"https://dashscope.aliyuncs.com",
);
expect(parseConfigFile({ base_url: "http://localhost:8080" }).base_url).toBe(
"http://localhost:8080",
);
expect(
parseConfigFile({ base_url: "https://proxy.example.com/team/compatible-mode/v1?x=1#y" })
.base_url,
).toBe("https://proxy.example.com");
// Previously accepted because the value merely "starts with http".
expect(parseConfigFile({ base_url: "httpfoo://evil" }).base_url).toBeUndefined();
expect(parseConfigFile({ base_url: "not a url" }).base_url).toBeUndefined();
});