import { expect, test } from "vite-plus/test"; import type { Identity, Settings } from "../src/index.ts"; import { BailianError, Client, ExitCode, McpClient, callConsoleGateway, mapApiError, request, sandboxEndpoint, sandboxInstanceActionPath, sandboxInstancePath, sandboxTemplateBuildStatusPath, sandboxTemplatePath, } 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; 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, watermark: true, 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("mapApiError captures Sandbox numeric code and requestID", () => { const err = mapApiError(400, { code: 100004, message: "参数缺失", requestID: "sandbox-request-id", }); expect(err.message).toBe("参数缺失"); expect(err.api).toEqual({ httpStatus: 400, apiCode: "100004", requestId: "sandbox-request-id", }); }); test("Sandbox endpoint builders fix cn-beijing and encode path identifiers", () => { expect(sandboxEndpoint("ws-test", "/sandboxes")).toBe( "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes", ); expect(sandboxInstancePath("sandbox/a b")).toBe("/sandboxes/sandbox%2Fa%20b"); expect(sandboxInstanceActionPath("sandbox/a b", "connect")).toBe( "/sandboxes/sandbox%2Fa%20b/connect", ); expect(sandboxTemplatePath("template/a b")).toBe("/templates/template%2Fa%20b"); expect(sandboxTemplateBuildStatusPath("template/a", "build b")).toBe( "/templates/template%2Fa/builds/build%20b/status", ); }); test("Client sends Sandbox REST requests with Bearer auth and no E2B API key", async () => { const originalFetch = globalThis.fetch; let requestHeaders: RequestInit["headers"]; globalThis.fetch = async (_input, init) => { requestHeaders = init?.headers; return new Response(JSON.stringify({ sandboxID: "sandbox-test" }), { status: 200, headers: { "content-type": "application/json" }, }); }; try { const deps = testDeps(); const client = new Client({ ...deps, baseUrl: "https://dashscope.aliyuncs.com", apiCred: { token: "bailian-api-key", baseUrl: "https://dashscope.aliyuncs.com", source: "flag", }, }); await client.requestJson({ path: sandboxEndpoint("ws-test", "/sandboxes/sandbox-test"), method: "GET", }); const headers = new Headers(requestHeaders); expect(headers.get("Authorization")).toBe("Bearer bailian-api-key"); expect(headers.has("X-API-Key")).toBe(false); } finally { globalThis.fetch = originalFetch; } }); test("Client.url evaluates a service default only when the shared origin was not configured", () => { const deps = testDeps(); const defaultClient = new Client({ ...deps, baseUrl: "https://dashscope.aliyuncs.com", baseUrlIsDefault: true, }); const configuredClient = new Client({ ...deps, baseUrl: "https://dashscope.aliyuncs.com", baseUrlIsDefault: false, }); expect(defaultClient.url("/service")).toBe("https://dashscope.aliyuncs.com/service"); expect(defaultClient.url("/service", () => "https://workspace.example.test")).toBe( "https://workspace.example.test/service", ); expect( configuredClient.url("/service", () => { throw new Error("A configured origin must not require a workspace default."); }), ).toBe("https://dashscope.aliyuncs.com/service"); }); 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 | 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((resolve) => { globalThis.fetch = async (_url, init) => { fetchSignal = init?.signal as AbortSignal | undefined; resolve(); return await new Promise((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 | 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); expect(resolveWatermark(undefined, false)).toBe(false); expect(resolveWatermark("true", false)).toBe(true); expect(resolveWatermark("false", true)).toBe(false); }); 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(); });