mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { createServer, type IncomingHttpHeaders, type Server } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, test } from "vite-plus/test";
|
|
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "./helpers.ts";
|
|
import { SANDBOX_ROUTES } from "./topic-routes.ts";
|
|
|
|
const directories: string[] = [];
|
|
const servers: Server[] = [];
|
|
const FILE_PATH = "/api/v1/agentstudio/files";
|
|
const UPLOAD_COMMAND = ["sandbox", "file", "upload"];
|
|
const RESPONSE = {
|
|
id: "file_sandbox_test",
|
|
filename: "config.json",
|
|
type: "file",
|
|
status: "available",
|
|
mime_type: "application/json",
|
|
size_bytes: 12,
|
|
requestId: "request-test",
|
|
};
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
servers.splice(0).map(
|
|
(server) =>
|
|
new Promise<void>((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
}),
|
|
),
|
|
);
|
|
for (const directory of directories.splice(0)) {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function setup(config: Record<string, unknown> = {}) {
|
|
const directory = mkdtempSync(join(tmpdir(), "bl-sandbox-file-"));
|
|
directories.push(directory);
|
|
writeFileSync(join(directory, "config.json"), JSON.stringify(config));
|
|
return {
|
|
directory,
|
|
env: {
|
|
BAILIAN_CONFIG_DIR: directory,
|
|
DASHSCOPE_API_KEY: "",
|
|
DASHSCOPE_BASE_URL: "",
|
|
BAILIAN_WORKSPACE_ID: "",
|
|
},
|
|
};
|
|
}
|
|
|
|
async function gateway(responseBody: unknown = RESPONSE, status = 200) {
|
|
const received: {
|
|
method?: string;
|
|
path?: string;
|
|
headers: IncomingHttpHeaders;
|
|
body: Buffer;
|
|
}[] = [];
|
|
const server = createServer((request, response) => {
|
|
const chunks: Buffer[] = [];
|
|
request.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
request.on("end", () => {
|
|
received.push({
|
|
method: request.method,
|
|
path: request.url,
|
|
headers: request.headers,
|
|
body: Buffer.concat(chunks),
|
|
});
|
|
response.writeHead(status, { "content-type": "application/json" });
|
|
response.end(JSON.stringify(responseBody));
|
|
});
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
servers.push(server);
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("Expected a local TCP server.");
|
|
return { origin: `http://127.0.0.1:${address.port}`, received };
|
|
}
|
|
|
|
describe("e2e: Sandbox template file upload", () => {
|
|
test("help documents upload auth, source, mount mapping, and the runtime boundary", async () => {
|
|
const result = await runCommandHelp(SANDBOX_ROUTES, [...UPLOAD_COMMAND, "--help"]);
|
|
expect(result.exitCode).toBe(0);
|
|
for (const text of [
|
|
"bl sandbox file upload",
|
|
"--path",
|
|
"--filename",
|
|
"--mime-type",
|
|
"--base-url",
|
|
"--workspace-id",
|
|
"source=sandbox_template",
|
|
FILE_PATH,
|
|
"mntConfig[].originFileId",
|
|
"status=checking",
|
|
"running instance",
|
|
]) {
|
|
expect(result.stderr).toContain(text);
|
|
}
|
|
expect(result.stderr).not.toContain("--console-site");
|
|
expect(result.stderr).not.toContain("--yes");
|
|
});
|
|
|
|
test("requires --path and does not expose a source override", async () => {
|
|
const { env } = setup();
|
|
for (const args of [[], ["--path", "unused", "--source", "other"]]) {
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[...UPLOAD_COMMAND, ...args, "--output", "json"],
|
|
env,
|
|
);
|
|
expect(result.exitCode).toBe(2);
|
|
}
|
|
});
|
|
|
|
test.each(["flag", "env", "profile", "workspace"])(
|
|
"%s determines the upload origin; dry-run does not read the local file or need credentials",
|
|
async (source) => {
|
|
const selectedOrigin = `https://${source}.example.test:8443`;
|
|
const { directory, env } = setup(
|
|
source === "profile"
|
|
? { active_config: "sandbox", sandbox: { base_url: selectedOrigin } }
|
|
: {},
|
|
);
|
|
const path = join(directory, "does-not-exist.json");
|
|
const args =
|
|
source === "flag"
|
|
? ["--base-url", `${selectedOrigin}/api/v1/agentstudio/sandbox?ignored=1#fragment`]
|
|
: source === "workspace"
|
|
? ["--workspace-id", "ws-files"]
|
|
: [];
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[...UPLOAD_COMMAND, "--path", path, ...args, "--dry-run", "--output", "json"],
|
|
{ ...env, DASHSCOPE_BASE_URL: source === "env" ? selectedOrigin : "" },
|
|
);
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(parseStdoutJson(result.stdout)).toEqual({
|
|
method: "POST",
|
|
endpoint:
|
|
(source === "workspace"
|
|
? "https://ws-files.cn-beijing.maas.aliyuncs.com"
|
|
: selectedOrigin) + FILE_PATH,
|
|
request: {
|
|
source: "sandbox_template",
|
|
file: { path, filename: "does-not-exist.json", mimeType: "application/octet-stream" },
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
test("uploads exact multipart bytes and source with saved Bearer credentials, then reuses the ID in a template mount", async () => {
|
|
const { origin, received } = await gateway();
|
|
const { directory, env } = setup({
|
|
active_config: "sandbox",
|
|
sandbox: { api_key: "sk-upload-test-only", base_url: origin },
|
|
});
|
|
const path = join(directory, "local data.bin");
|
|
const content = Buffer.from([0, 1, 255, 13, 10, 65]);
|
|
writeFileSync(path, content);
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[
|
|
...UPLOAD_COMMAND,
|
|
"--path",
|
|
path,
|
|
"--filename",
|
|
"config.json",
|
|
"--mime-type",
|
|
"application/json",
|
|
"--output",
|
|
"json",
|
|
"--verbose",
|
|
],
|
|
env,
|
|
);
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(parseStdoutJson(result.stdout)).toEqual(RESPONSE);
|
|
expect(result.stdout + result.stderr).not.toContain("sk-upload-test-only");
|
|
expect(received).toHaveLength(1);
|
|
const request = received[0];
|
|
expect(request.method).toBe("POST");
|
|
expect(request.path).toBe(FILE_PATH);
|
|
expect(request.headers.authorization).toBe("Bearer sk-upload-test-only");
|
|
expect(request.headers["x-api-key"]).toBeUndefined();
|
|
expect(request.headers["content-type"]).toMatch(/^multipart\/form-data; boundary=/);
|
|
const form = await new Request(`${origin}${FILE_PATH}`, {
|
|
method: "POST",
|
|
headers: { "content-type": request.headers["content-type"]! },
|
|
body: new Uint8Array(request.body),
|
|
}).formData();
|
|
expect([...form.keys()].sort()).toEqual(["file", "source"]);
|
|
expect(form.get("source")).toBe("sandbox_template");
|
|
const file = form.get("file");
|
|
if (!file || typeof file === "string") throw new Error("Expected a multipart file.");
|
|
expect(file.name).toBe("config.json");
|
|
expect(file.type).toBe("application/json");
|
|
expect(Buffer.from(await file.arrayBuffer())).toEqual(content);
|
|
|
|
const mount = {
|
|
originFileId: RESPONSE.id,
|
|
originFileName: RESPONSE.filename,
|
|
mountPath: "/home/user/config.json",
|
|
};
|
|
const template = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[
|
|
"sandbox",
|
|
"template",
|
|
"create",
|
|
"--name",
|
|
"files",
|
|
"--cpu-count",
|
|
"1",
|
|
"--memory-mb",
|
|
"2048",
|
|
"--body",
|
|
JSON.stringify({ mntConfig: [mount] }),
|
|
"--dry-run",
|
|
"--output",
|
|
"json",
|
|
],
|
|
env,
|
|
);
|
|
expect(template.exitCode, template.stderr).toBe(0);
|
|
expect(parseStdoutJson(template.stdout)).toMatchObject({ request: { mntConfig: [mount] } });
|
|
expect(received).toHaveLength(1);
|
|
});
|
|
|
|
test.each(["json", "text", "quiet"])(
|
|
"%s output returns checking status or the bare ID without polling or creating a template",
|
|
async (output) => {
|
|
const pending = { ...RESPONSE, status: "checking" };
|
|
const { origin, received } = await gateway(pending);
|
|
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
|
const path = join(directory, "notes.txt");
|
|
writeFileSync(path, "Only synthetic test data.");
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[
|
|
...UPLOAD_COMMAND,
|
|
"--path",
|
|
path,
|
|
...(output === "quiet" ? ["--quiet"] : ["--output", output]),
|
|
],
|
|
env,
|
|
);
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
if (output === "quiet") expect(result.stdout.trim()).toBe(RESPONSE.id);
|
|
else {
|
|
expect(result.stdout).toContain("checking");
|
|
expect(result.stdout).toContain(RESPONSE.id);
|
|
}
|
|
expect(received).toHaveLength(1);
|
|
},
|
|
);
|
|
|
|
test("missing files fail locally without an upload request", async () => {
|
|
const { origin, received } = await gateway();
|
|
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[...UPLOAD_COMMAND, "--path", join(directory, "missing.txt"), "--output", "json"],
|
|
env,
|
|
);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stderr).toContain("ENOENT");
|
|
expect(received).toHaveLength(0);
|
|
});
|
|
|
|
test("server errors are passed through without translating them", async () => {
|
|
const { origin, received } = await gateway(
|
|
{ code: "InvalidSource", message: "source rejected by server", request_id: "req-error" },
|
|
400,
|
|
);
|
|
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
|
const path = join(directory, "notes.txt");
|
|
writeFileSync(path, "test");
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[...UPLOAD_COMMAND, "--path", path, "--output", "json"],
|
|
env,
|
|
);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(JSON.parse(result.stderr)).toMatchObject({
|
|
error: {
|
|
message: "source rejected by server",
|
|
http_status: 400,
|
|
api_code: "InvalidSource",
|
|
request_id: "req-error",
|
|
},
|
|
});
|
|
expect(received).toHaveLength(1);
|
|
});
|
|
|
|
test("a response without an ID is not reported as a successful upload", async () => {
|
|
const { origin } = await gateway({ status: "checking" });
|
|
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
|
const path = join(directory, "notes.txt");
|
|
writeFileSync(path, "test");
|
|
const result = await runCommandE2e(
|
|
SANDBOX_ROUTES,
|
|
[...UPLOAD_COMMAND, "--path", path, "--quiet", "--output", "json"],
|
|
env,
|
|
);
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toContain("File ID");
|
|
});
|
|
});
|