mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
335 lines
10 KiB
TypeScript
335 lines
10 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test";
|
|
import { parseStdoutJson, runCommandE2e as runBaseCommandE2e, runCommandHelp } from "./helpers.ts";
|
|
import { SANDBOX_ROUTES } from "./topic-routes.ts";
|
|
|
|
const AUTH_ARGS = ["--api-key", "sk-sandbox-e2e", "--workspace-id", "ws-e2e"];
|
|
let configDirectory: string;
|
|
|
|
beforeEach(() => {
|
|
configDirectory = mkdtempSync(join(tmpdir(), "bl-sandbox-e2e-"));
|
|
writeFileSync(join(configDirectory, "config.json"), "{}");
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(configDirectory, { recursive: true, force: true });
|
|
});
|
|
|
|
function runCommandE2e(routes: typeof SANDBOX_ROUTES, args: string[]) {
|
|
return runBaseCommandE2e(routes, args, {
|
|
BAILIAN_CONFIG_DIR: configDirectory,
|
|
DASHSCOPE_BASE_URL: "",
|
|
DASHSCOPE_API_KEY: "",
|
|
BAILIAN_WORKSPACE_ID: "",
|
|
});
|
|
}
|
|
|
|
describe("e2e: Sandbox command discovery", () => {
|
|
test("built-in images are discoverable without API Key authentication", async () => {
|
|
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"official-images",
|
|
"--help",
|
|
]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
expect(stderr).toContain("Usage: bl sandbox official-images");
|
|
expect(stderr).not.toContain("Authentication: API Key");
|
|
expect(stderr).toContain("code-interpreter");
|
|
expect(stderr).toContain("all-in-one");
|
|
});
|
|
test.each([
|
|
["sandbox", "create"],
|
|
["sandbox", "list"],
|
|
["sandbox", "get"],
|
|
["sandbox", "connect"],
|
|
["sandbox", "pause"],
|
|
["sandbox", "resume"],
|
|
["sandbox", "delete"],
|
|
["sandbox", "template", "create"],
|
|
["sandbox", "template", "list"],
|
|
["sandbox", "template", "get"],
|
|
["sandbox", "template", "update"],
|
|
["sandbox", "template", "build-status"],
|
|
["sandbox", "template", "delete"],
|
|
])("%s help resolves", async (...commandPath) => {
|
|
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [...commandPath, "--help"]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
expect(stderr).toContain(`Usage: bl ${commandPath.join(" ")}`);
|
|
expect(stderr).toContain("Authentication: API Key");
|
|
});
|
|
|
|
test("instance timeout is separate from the global request/poll timeout", async () => {
|
|
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"create",
|
|
"--help",
|
|
]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
expect(stderr).toMatch(/--instance-timeout <seconds>/);
|
|
expect(stderr).toMatch(/--timeout <seconds>/);
|
|
expect(stderr).toMatch(/no E2B key is sent/i);
|
|
});
|
|
|
|
test("template create exposes max-running-time and default polling controls", async () => {
|
|
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"template",
|
|
"create",
|
|
"--help",
|
|
]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
expect(stderr).toMatch(/--max-running-time <seconds>/);
|
|
expect(stderr).toMatch(/--image <[^>]*code-interpreter[^>]*browser[^>]*all-in-one[^>]*>/);
|
|
expect(stderr).toContain("browser:v0.0.44");
|
|
expect(stderr).toMatch(/--async/);
|
|
expect(stderr).toMatch(/--poll-interval <seconds>/);
|
|
expect(stderr).toMatch(/waits for build status ready/i);
|
|
});
|
|
});
|
|
|
|
describe("e2e: Sandbox offline validation and dry-run", () => {
|
|
test("image catalog is available offline in JSON, text, and quiet output", async () => {
|
|
const jsonResult = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"official-images",
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(jsonResult.exitCode, jsonResult.stderr).toBe(0);
|
|
const images = JSON.parse(jsonResult.stdout) as {
|
|
id: string;
|
|
imageName: string;
|
|
imageUrl: string;
|
|
}[];
|
|
expect(images.map((image) => image.id)).toEqual(["code-interpreter", "browser", "all-in-one"]);
|
|
expect(images[1]).toMatchObject({
|
|
imageName: "浏览器",
|
|
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
|
|
});
|
|
const textResult = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"official-images",
|
|
"--output",
|
|
"text",
|
|
]);
|
|
expect(textResult.exitCode, textResult.stderr).toBe(0);
|
|
expect(textResult.stdout).toContain("代码解释器");
|
|
expect(textResult.stdout).toContain("browser:v0.0.44");
|
|
const quietResult = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"official-images",
|
|
"--quiet",
|
|
]);
|
|
expect(quietResult.exitCode, quietResult.stderr).toBe(0);
|
|
expect(quietResult.stdout.trim().split("\n")).toEqual([
|
|
"code-interpreter",
|
|
"browser",
|
|
"all-in-one",
|
|
]);
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
operation: "create",
|
|
args: ["--name", "test", "--cpu-count", "1", "--memory-mb", "2048"],
|
|
selector: "browser",
|
|
method: "POST",
|
|
},
|
|
{
|
|
operation: "update",
|
|
args: ["--template-id", "template-test"],
|
|
selector: "浏览器",
|
|
method: "PUT",
|
|
},
|
|
])(
|
|
"template $operation resolves --image before dry-run output",
|
|
async ({ operation, args, selector, method }) => {
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"template",
|
|
operation,
|
|
...args,
|
|
"--image",
|
|
selector,
|
|
...AUTH_ARGS,
|
|
"--dry-run",
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
expect(parseStdoutJson(stdout)).toMatchObject({
|
|
method,
|
|
request: {
|
|
fromImage: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
|
|
imageName: "浏览器",
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
test("unknown image selector fails before a request", async () => {
|
|
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"template",
|
|
"create",
|
|
"--name",
|
|
"test",
|
|
"--cpu-count",
|
|
"1",
|
|
"--memory-mb",
|
|
"2048",
|
|
"--image",
|
|
"unknown",
|
|
...AUTH_ARGS,
|
|
"--dry-run",
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(exitCode).toBe(2);
|
|
expect(JSON.parse(stderr)).toMatchObject({
|
|
error: { code: 2, message: expect.stringContaining("--image") },
|
|
});
|
|
});
|
|
|
|
test("create requires a template in flags or body", async () => {
|
|
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"create",
|
|
...AUTH_ARGS,
|
|
"--dry-run",
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(exitCode).toBe(2);
|
|
expect(JSON.parse(stderr)).toMatchObject({
|
|
error: { code: 2, message: expect.stringMatching(/template-id|templateID/) },
|
|
});
|
|
});
|
|
|
|
test("create body flags win and request environment values are redacted", async () => {
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
|
|
"sandbox",
|
|
"create",
|
|
"--body",
|
|
'{"templateID":"template-body","timeout":600,"envVars":{"BODY":"secret"}}',
|
|
"--template-id",
|
|
"template-flag",
|
|
"--instance-timeout",
|
|
"900",
|
|
"--env",
|
|
"FLAG=secret",
|
|
...AUTH_ARGS,
|
|
"--dry-run",
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
expect(parseStdoutJson(stdout)).toMatchObject({
|
|
method: "POST",
|
|
endpoint: "https://ws-e2e.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes",
|
|
request: {
|
|
templateID: "template-flag",
|
|
timeout: 900,
|
|
envVars: { BODY: "[REDACTED]", FLAG: "[REDACTED]" },
|
|
},
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
name: "connect instance",
|
|
args: ["sandbox", "connect", "--sandbox-id", "sandbox-test", "--instance-timeout", "900"],
|
|
method: "POST",
|
|
suffix: "/sandboxes/sandbox-test/connect",
|
|
},
|
|
{
|
|
name: "pause instance",
|
|
args: ["sandbox", "pause", "--sandbox-id", "sandbox-test"],
|
|
method: "POST",
|
|
suffix: "/sandboxes/sandbox-test/pause",
|
|
},
|
|
{
|
|
name: "resume instance",
|
|
args: ["sandbox", "resume", "--sandbox-id", "sandbox-test"],
|
|
method: "POST",
|
|
suffix: "/sandboxes/sandbox-test/resume",
|
|
},
|
|
{
|
|
name: "delete instance",
|
|
args: ["sandbox", "delete", "--sandbox-id", "sandbox-test"],
|
|
method: "DELETE",
|
|
suffix: "/sandboxes/sandbox-test",
|
|
},
|
|
{
|
|
name: "create template",
|
|
args: [
|
|
"sandbox",
|
|
"template",
|
|
"create",
|
|
"--name",
|
|
"python",
|
|
"--cpu-count",
|
|
"1",
|
|
"--memory-mb",
|
|
"2048",
|
|
],
|
|
method: "POST",
|
|
suffix: "/v3/templates",
|
|
},
|
|
{
|
|
name: "update template",
|
|
args: [
|
|
"sandbox",
|
|
"template",
|
|
"update",
|
|
"--template-id",
|
|
"template-test",
|
|
"--description",
|
|
"updated",
|
|
],
|
|
method: "PUT",
|
|
suffix: "/templates/template-test",
|
|
},
|
|
{
|
|
name: "delete template",
|
|
args: ["sandbox", "template", "delete", "--template-id", "template-test"],
|
|
method: "DELETE",
|
|
suffix: "/templates/template-test",
|
|
},
|
|
])("$name dry-run stays offline", async ({ args, method, suffix }) => {
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
|
|
...args,
|
|
...AUTH_ARGS,
|
|
"--dry-run",
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(exitCode, stderr).toBe(0);
|
|
const result = parseStdoutJson<{ method: string; endpoint: string }>(stdout);
|
|
expect(result.method).toBe(method);
|
|
expect(result.endpoint).toBe(
|
|
`https://ws-e2e.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox${suffix}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("e2e: Sandbox high-risk confirmation", () => {
|
|
test.each([
|
|
["sandbox", "delete", "--sandbox-id", "sandbox-test"],
|
|
["sandbox", "template", "delete", "--template-id", "template-test"],
|
|
])("%s requires --yes before a remote delete", async (...commandArgs) => {
|
|
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
|
|
...commandArgs,
|
|
...AUTH_ARGS,
|
|
"--output",
|
|
"json",
|
|
]);
|
|
expect(exitCode).toBe(7);
|
|
expect(JSON.parse(stderr)).toMatchObject({
|
|
error: { code: 7, type: "requires_confirmation" },
|
|
});
|
|
});
|
|
});
|