mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(bootstrap): 新增创建控制台用户步骤,完善服务激活流程
- 在 bootstrap 命令流程中添加第 3 步,调用 createUser 接口创建控制台账号用户 - 修改 callApi 函数支持传入请求体参数 - 增强错误日志输出,显示更完整的错误响应内容 - 在服务启动时校验登录信息包含 aliyun.uid,否则抛出错误 - 优化商品检查与激活逻辑,更清晰的状态输出及错误处理 - 在 CLI-core 控制台网关请求中添加日志,打印结构化请求负载,支持 verbose 模式 - BailianError 新增 rawResponse 字段,保存原始错误响应数据 - 增加 bootstrap 命令单元测试,覆盖用户创建及不同初始化场景 - 调整测试框架用例,增加调用控制台网关及错误处理的测试覆盖
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
## 登录
|
||||||
|
|
||||||
|
https://signin.aliyun.com/1062516667359476.onaliyun.com/login.htm
|
||||||
|
lisheng@1062516667359476.onaliyun.com
|
||||||
|
app$$5%%%Ehiliao
|
||||||
|
|
||||||
|
## 获得 AK SK STS 三元组
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm bl auth generate-access-token --access-key-id STS.NXsfUgEqhDQBJTz1V5JJSMRDw --access-key-secret CmJU9so7yMTjZ3mpVF9eMqFSZXkEn2LhFMogi1hfn8rk --security-token CAIS1gJ1q6Ft5B2yfSjIr5vGLe/TqK5J85OpSHLL1VZgRsV/opfvlTz2IHhMe3BtAuwXtvQ1mG9R7/0ZlqBpR4RIXlfFas0oFyyqTp/6MeT7oMWQweEuqv/MQBq+aXPS2MvVfJ+KLrf0ceusbFbpjzJ6xaCAGxypQ12iN+/i6/clFKN1ODO1dj1bHtxbCxJ/ocsBTxvrOO2qLwThjxi7biMqmHIl2T8ns/vlnpbHs0KP0gWq8IJP+dSteKrDRtJ3IZJyX+2y2OFLbafb2EdSkUMSrPgv0fcYqG+X5I3CWgAKuA/MKefP9cB1JwJ1Z7I3ELJDtun1nvZ/p+rPno/8xg1WJ+ZRXjRD7XJMD2hdcQnAF6HaFd6TUxylurgExgnkPL5jz1gvlRKYWhvQG45hiCZWPhXwAIHJtv6kTMnd5abLPm9I37QLATeM356+Q3LrJRHx74QEOMJUBysagAFIsU+wgReSHvEEXx5y3qyr3JB3t7tY9nI0FrIx0GudXEjl2vE4sD7aTxleD1Weqlq8XAzIu8DrdU8tkNdQ0rGricEZd9DY2WWDer1eD7IdoLuavwGHXyLaJkw1c1UPq2O3fKp2EQ8b5zei/Ep+MGfMCYDHyVqDPAPLD5yTtN6eOCAA
|
||||||
|
```
|
||||||
@@ -4,6 +4,7 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
|
|||||||
const API = {
|
const API = {
|
||||||
loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo",
|
loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo",
|
||||||
initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace",
|
initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace",
|
||||||
|
createUser: "zeldaEasy.bailian-dash-workspace.account.createUser",
|
||||||
queryBuyResult: "zeldaEasy.bailian-commerce.bill.queryBuyPostpaidResult",
|
queryBuyResult: "zeldaEasy.bailian-commerce.bill.queryBuyPostpaidResult",
|
||||||
commodityOrderInfo: "zeldaEasy.bailian-commerce.bill.postpaidCommodityOrderInfo",
|
commodityOrderInfo: "zeldaEasy.bailian-commerce.bill.postpaidCommodityOrderInfo",
|
||||||
buyCommodity: "zeldaEasy.bailian-commerce.bill.buyPostpaidCommodity",
|
buyCommodity: "zeldaEasy.bailian-commerce.bill.buyPostpaidCommodity",
|
||||||
@@ -51,16 +52,21 @@ export default defineCommand({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 3,
|
step: 3,
|
||||||
|
api: API.createUser,
|
||||||
|
description: "Create console account user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 4,
|
||||||
api: API.queryBuyResult,
|
api: API.queryBuyResult,
|
||||||
description: "Query postpaid order status",
|
description: "Query postpaid order status",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 4,
|
step: 5,
|
||||||
api: API.commodityOrderInfo,
|
api: API.commodityOrderInfo,
|
||||||
description: "Query commodity activation status",
|
description: "Query commodity activation status",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 5,
|
step: 6,
|
||||||
api: API.buyCommodity,
|
api: API.buyCommodity,
|
||||||
description: "Activate postpaid commodities (if needed)",
|
description: "Activate postpaid commodities (if needed)",
|
||||||
},
|
},
|
||||||
@@ -72,14 +78,18 @@ export default defineCommand({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verbose = settings.verbose;
|
const verbose = settings.verbose;
|
||||||
const callApi = async (api: string) => {
|
const callApi = async (api: string, data: Record<string, unknown> = {}) => {
|
||||||
if (verbose) process.stderr.write(`> ${api}\n`);
|
if (verbose) process.stderr.write(`> ${api}\n`);
|
||||||
try {
|
try {
|
||||||
const resp = await ctx.client.console<any>(api, {});
|
const resp = await ctx.client.console<any>(api, data);
|
||||||
if (verbose) process.stderr.write(`< ${JSON.stringify(resp)}\n`);
|
if (verbose) process.stderr.write(`< ${JSON.stringify(resp)}\n`);
|
||||||
return resp;
|
return resp;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (verbose) process.stderr.write(`< ERROR: ${err instanceof Error ? err.message : err}\n`);
|
if (verbose) {
|
||||||
|
const message =
|
||||||
|
err instanceof BailianError ? (err.rawResponse ?? err.message) : String(err);
|
||||||
|
process.stderr.write(`< ERROR: ${message}\n`);
|
||||||
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -99,12 +109,25 @@ export default defineCommand({
|
|||||||
emitBare("Workspace already initialized.");
|
emitBare("Workspace already initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3-5: Order & commodity flow
|
// Step 3: Create console user
|
||||||
|
const uid = loginData?.aliyun?.uid;
|
||||||
|
if (typeof uid !== "string" || uid.length === 0) {
|
||||||
|
throw new BailianError("Console login info did not include aliyun.uid.", ExitCode.GENERAL);
|
||||||
|
}
|
||||||
|
await callApi(API.createUser, {
|
||||||
|
reqDTO: {
|
||||||
|
outerKey: uid,
|
||||||
|
nickName: uid,
|
||||||
|
userName: uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 4-6: Order & commodity flow
|
||||||
await ensureCommoditiesActive(callApi, format);
|
await ensureCommoditiesActive(callApi, format);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type ApiCall = (api: string) => Promise<any>;
|
type ApiCall = (api: string, data?: Record<string, unknown>) => Promise<any>;
|
||||||
|
|
||||||
async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"): Promise<void> {
|
async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"): Promise<void> {
|
||||||
emitBare("Checking service activation status...");
|
emitBare("Checking service activation status...");
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { expect, test } from "vite-plus/test";
|
||||||
|
import bootstrapCommand from "../src/commands/bootstrap/index.ts";
|
||||||
|
|
||||||
|
const API = {
|
||||||
|
loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo",
|
||||||
|
initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace",
|
||||||
|
createUser: "zeldaEasy.bailian-dash-workspace.account.createUser",
|
||||||
|
queryBuyResult: "zeldaEasy.bailian-commerce.bill.queryBuyPostpaidResult",
|
||||||
|
commodityOrderInfo: "zeldaEasy.bailian-commerce.bill.postpaidCommodityOrderInfo",
|
||||||
|
buyCommodity: "zeldaEasy.bailian-commerce.bill.buyPostpaidCommodity",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface ConsoleCall {
|
||||||
|
api: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureStdout(): { read: () => string; restore: () => void } {
|
||||||
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
let stdout = "";
|
||||||
|
process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||||
|
stdout += String(chunk);
|
||||||
|
return true;
|
||||||
|
}) as typeof process.stdout.write;
|
||||||
|
return {
|
||||||
|
read: () => stdout,
|
||||||
|
restore: () => {
|
||||||
|
process.stdout.write = originalWrite;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function gatewayResponse(data: unknown): unknown {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
DataV2: {
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createContext(
|
||||||
|
consoleImpl: (api: string, data: Record<string, unknown>) => Promise<unknown>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
settings: {
|
||||||
|
dryRun: false,
|
||||||
|
output: "json",
|
||||||
|
verbose: false,
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
console: consoleImpl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("bootstrap --dry-run lists createUser after initSpace", async () => {
|
||||||
|
const stdout = captureStdout();
|
||||||
|
try {
|
||||||
|
await bootstrapCommand.run({
|
||||||
|
...createContext(async () => ({})),
|
||||||
|
settings: { dryRun: true, output: "json", verbose: false },
|
||||||
|
} as any);
|
||||||
|
} finally {
|
||||||
|
stdout.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(stdout.read()) as {
|
||||||
|
apis: Array<{ step: number; api: string }>;
|
||||||
|
};
|
||||||
|
expect(data.apis.map((item) => item.api)).toEqual([
|
||||||
|
API.loginInfo,
|
||||||
|
API.initSpace,
|
||||||
|
API.createUser,
|
||||||
|
API.queryBuyResult,
|
||||||
|
API.commodityOrderInfo,
|
||||||
|
API.buyCommodity,
|
||||||
|
]);
|
||||||
|
expect(data.apis.map((item) => item.step)).toEqual([1, 2, 3, 4, 5, 6]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bootstrap creates user from loginInfo uid when workspace is not initialized", async () => {
|
||||||
|
const uid = "AssumedRoleUser300715349082471133";
|
||||||
|
const calls: ConsoleCall[] = [];
|
||||||
|
const stdout = captureStdout();
|
||||||
|
const ctx = createContext(async (api, data) => {
|
||||||
|
calls.push({ api, data });
|
||||||
|
if (api === API.loginInfo) {
|
||||||
|
return gatewayResponse({ spaceInited: false, aliyun: { uid } });
|
||||||
|
}
|
||||||
|
if (api === API.queryBuyResult) {
|
||||||
|
return gatewayResponse("success");
|
||||||
|
}
|
||||||
|
if (api === API.commodityOrderInfo) {
|
||||||
|
return gatewayResponse([{ commodityCode: "postpaid", status: 10 }]);
|
||||||
|
}
|
||||||
|
return gatewayResponse({});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bootstrapCommand.run(ctx as any);
|
||||||
|
} finally {
|
||||||
|
stdout.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(calls.map((call) => call.api)).toEqual([
|
||||||
|
API.loginInfo,
|
||||||
|
API.initSpace,
|
||||||
|
API.createUser,
|
||||||
|
API.queryBuyResult,
|
||||||
|
API.commodityOrderInfo,
|
||||||
|
]);
|
||||||
|
expect(calls.find((call) => call.api === API.createUser)?.data).toEqual({
|
||||||
|
reqDTO: {
|
||||||
|
outerKey: uid,
|
||||||
|
nickName: uid,
|
||||||
|
userName: uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bootstrap skips initSpace but still creates user when workspace is already initialized", async () => {
|
||||||
|
const uid = "AssumedRoleUser300715349082471133";
|
||||||
|
const calls: ConsoleCall[] = [];
|
||||||
|
const stdout = captureStdout();
|
||||||
|
const ctx = createContext(async (api, data) => {
|
||||||
|
calls.push({ api, data });
|
||||||
|
if (api === API.loginInfo) {
|
||||||
|
return gatewayResponse({
|
||||||
|
spaceInited: true,
|
||||||
|
aliyun: { uid },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (api === API.queryBuyResult) {
|
||||||
|
return gatewayResponse("success");
|
||||||
|
}
|
||||||
|
if (api === API.commodityOrderInfo) {
|
||||||
|
return gatewayResponse([{ commodityCode: "postpaid", status: 10 }]);
|
||||||
|
}
|
||||||
|
return gatewayResponse({});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bootstrapCommand.run(ctx as any);
|
||||||
|
} finally {
|
||||||
|
stdout.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(calls.map((call) => call.api)).toEqual([
|
||||||
|
API.loginInfo,
|
||||||
|
API.createUser,
|
||||||
|
API.queryBuyResult,
|
||||||
|
API.commodityOrderInfo,
|
||||||
|
]);
|
||||||
|
expect(calls.some((call) => call.api === API.initSpace)).toBe(false);
|
||||||
|
expect(calls.find((call) => call.api === API.createUser)?.data).toEqual({
|
||||||
|
reqDTO: {
|
||||||
|
outerKey: uid,
|
||||||
|
nickName: uid,
|
||||||
|
userName: uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,10 @@ interface ConsoleGatewayInfo {
|
|||||||
|
|
||||||
const REGION_GATEWAYS: Record<string, Record<ConsoleSite, ConsoleGatewayInfo>> = {
|
const REGION_GATEWAYS: Record<string, Record<ConsoleSite, ConsoleGatewayInfo>> = {
|
||||||
"cn-beijing": {
|
"cn-beijing": {
|
||||||
domestic: { csGateway: "bailian-cs.console.aliyun.com", action: "BroadScopeAspnGateway" },
|
domestic: {
|
||||||
|
csGateway: "bailian-cs.console.aliyun.com",
|
||||||
|
action: "BroadScopeAspnGateway",
|
||||||
|
},
|
||||||
international: {
|
international: {
|
||||||
csGateway: "bailian-cs.console.alibabacloud.com",
|
csGateway: "bailian-cs.console.alibabacloud.com",
|
||||||
action: "BroadScopeAspnGateway",
|
action: "BroadScopeAspnGateway",
|
||||||
@@ -74,6 +77,7 @@ function buildGatewayParams(
|
|||||||
protocol: "V2",
|
protocol: "V2",
|
||||||
console: "ONE_CONSOLE",
|
console: "ONE_CONSOLE",
|
||||||
productCode: "p_efm",
|
productCode: "p_efm",
|
||||||
|
switchUserType: 3,
|
||||||
consoleSite: "BAILIAN_ALIYUN",
|
consoleSite: "BAILIAN_ALIYUN",
|
||||||
...(switchAgent != null ? { switchAgent } : {}),
|
...(switchAgent != null ? { switchAgent } : {}),
|
||||||
...(typeof data.cornerstoneParam === "object" && data.cornerstoneParam !== null
|
...(typeof data.cornerstoneParam === "object" && data.cornerstoneParam !== null
|
||||||
@@ -119,6 +123,9 @@ export async function callConsoleGateway(
|
|||||||
const endpoint = `${gatewayBase}/cli/api.json?action=${action}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`;
|
const endpoint = `${gatewayBase}/cli/api.json?action=${action}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`;
|
||||||
if (settings?.verbose) {
|
if (settings?.verbose) {
|
||||||
process.stderr.write(`> POST ${endpoint}\n`);
|
process.stderr.write(`> POST ${endpoint}\n`);
|
||||||
|
process.stderr.write(
|
||||||
|
`> payload ${JSON.stringify({ params: JSON.parse(params), region: target.region }, null, 2)}\n`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
@@ -142,17 +149,14 @@ export async function callConsoleGateway(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const json = (await res.json()) as Record<string, unknown>;
|
const json = (await res.json()) as Record<string, unknown>;
|
||||||
if (settings?.verbose) {
|
|
||||||
process.stderr.write(`< ${JSON.stringify(json)}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const innerData = json.data as Record<string, unknown> | undefined;
|
const innerData = json.data as Record<string, unknown> | undefined;
|
||||||
if (innerData?.success === false && innerData.errorCode) {
|
if (innerData?.success === false && innerData.errorCode) {
|
||||||
|
const rawResponse = JSON.stringify(json);
|
||||||
const rawErrorCode = innerData.errorCode;
|
const rawErrorCode = innerData.errorCode;
|
||||||
const errorCode =
|
const errorCode =
|
||||||
typeof rawErrorCode === "string" ? rawErrorCode : JSON.stringify(rawErrorCode);
|
typeof rawErrorCode === "string" ? rawErrorCode : JSON.stringify(rawErrorCode);
|
||||||
const notLogined = errorCode.includes("NotLogined");
|
const notLogined = errorCode.includes("NotLogined");
|
||||||
const errorMsg = typeof innerData.errorMsg === "string" ? innerData.errorMsg : undefined;
|
|
||||||
throw new BailianError(
|
throw new BailianError(
|
||||||
notLogined
|
notLogined
|
||||||
? "Console session is not logged in or has expired."
|
? "Console session is not logged in or has expired."
|
||||||
@@ -160,9 +164,8 @@ export async function callConsoleGateway(
|
|||||||
notLogined ? ExitCode.AUTH : ExitCode.GENERAL,
|
notLogined ? ExitCode.AUTH : ExitCode.GENERAL,
|
||||||
notLogined
|
notLogined
|
||||||
? "Run `bl auth login --console` to sign in or refresh your console session."
|
? "Run `bl auth login --console` to sign in or refresh your console session."
|
||||||
: errorMsg && errorMsg !== errorCode
|
: undefined,
|
||||||
? errorMsg
|
{ rawResponse },
|
||||||
: undefined,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ export interface ApiErrorContext {
|
|||||||
export interface BailianErrorOptions {
|
export interface BailianErrorOptions {
|
||||||
cause?: unknown;
|
cause?: unknown;
|
||||||
api?: ApiErrorContext;
|
api?: ApiErrorContext;
|
||||||
|
rawResponse?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BailianError extends Error {
|
export class BailianError extends Error {
|
||||||
readonly exitCode: ExitCode;
|
readonly exitCode: ExitCode;
|
||||||
readonly hint?: string;
|
readonly hint?: string;
|
||||||
readonly api?: ApiErrorContext;
|
readonly api?: ApiErrorContext;
|
||||||
|
readonly rawResponse?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
@@ -27,6 +29,7 @@ export class BailianError extends Error {
|
|||||||
this.exitCode = exitCode;
|
this.exitCode = exitCode;
|
||||||
this.hint = hint;
|
this.hint = hint;
|
||||||
this.api = options?.api;
|
this.api = options?.api;
|
||||||
|
this.rawResponse = options?.rawResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { expect, test } from "vite-plus/test";
|
import { expect, test } from "vite-plus/test";
|
||||||
import type { Identity, Settings } from "../src/index.ts";
|
import type { Identity, Settings } from "../src/index.ts";
|
||||||
import { BailianError, ExitCode, McpClient, mapApiError, request } from "../src/index.ts";
|
import {
|
||||||
|
BailianError,
|
||||||
|
ExitCode,
|
||||||
|
McpClient,
|
||||||
|
callConsoleGateway,
|
||||||
|
mapApiError,
|
||||||
|
request,
|
||||||
|
} from "../src/index.ts";
|
||||||
import { parseConfigFile } from "../src/config/schema.ts";
|
import { parseConfigFile } from "../src/config/schema.ts";
|
||||||
import {
|
import {
|
||||||
parseBooleanValue,
|
parseBooleanValue,
|
||||||
@@ -8,7 +15,10 @@ import {
|
|||||||
resolveWatermark,
|
resolveWatermark,
|
||||||
} from "../src/utils/boolean-flag.ts";
|
} from "../src/utils/boolean-flag.ts";
|
||||||
|
|
||||||
function testDeps(identity: Partial<Identity> = {}): { identity: Identity; settings: Settings } {
|
function testDeps(identity: Partial<Identity> = {}): {
|
||||||
|
identity: Identity;
|
||||||
|
settings: Settings;
|
||||||
|
} {
|
||||||
return {
|
return {
|
||||||
identity: {
|
identity: {
|
||||||
binName: "bl",
|
binName: "bl",
|
||||||
@@ -83,7 +93,10 @@ test("BailianError propagates cause via options-bag and exposes it in toJSON", (
|
|||||||
|
|
||||||
test("toJSON splits service-error metadata into structured fields", () => {
|
test("toJSON splits service-error metadata into structured fields", () => {
|
||||||
const err = mapApiError(404, {
|
const err = mapApiError(404, {
|
||||||
error: { message: "The model `qwen3.7` does not exist", type: "invalid_request_error" },
|
error: {
|
||||||
|
message: "The model `qwen3.7` does not exist",
|
||||||
|
type: "invalid_request_error",
|
||||||
|
},
|
||||||
request_id: "c55e1acc",
|
request_id: "c55e1acc",
|
||||||
});
|
});
|
||||||
expect(err.toJSON()).toEqual({
|
expect(err.toJSON()).toEqual({
|
||||||
@@ -97,6 +110,96 @@ test("toJSON splits service-error metadata into structured fields", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
test("request uses injected client identity for User-Agent", async () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
let userAgent: string | undefined;
|
let userAgent: string | undefined;
|
||||||
@@ -160,7 +263,9 @@ test("McpClient uses injected client identity for initialize and User-Agent", as
|
|||||||
userAgents.push(headers?.["User-Agent"] ?? "");
|
userAgents.push(headers?.["User-Agent"] ?? "");
|
||||||
const body = init?.body;
|
const body = init?.body;
|
||||||
if (typeof body === "string") bodies.push(JSON.parse(body));
|
if (typeof body === "string") bodies.push(JSON.parse(body));
|
||||||
return new Response(JSON.stringify({ jsonrpc: "2.0", id: 1, result: {} }), { status: 200 });
|
return new Response(JSON.stringify({ jsonrpc: "2.0", id: 1, result: {} }), {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user