mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, test } from "vite-plus/test";
|
|
import type { Identity } from "bailian-cli-core";
|
|
import { installCommandPack, parseCommandPackSpec } from "../src/command-packs/manager.ts";
|
|
import { getCommandPacksDir } from "../src/command-packs/paths.ts";
|
|
import { loadAndValidateCommandPack } from "../src/command-packs/validate.ts";
|
|
import type { CommandPackPackageJson, CommandPackPolicy } from "../src/command-packs/types.ts";
|
|
|
|
const packageRoot = resolve(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
"../../cli/tests/fixtures/command-pack",
|
|
);
|
|
const identity: Identity = {
|
|
binName: "bl",
|
|
version: "1.7.0",
|
|
clientName: "bailian-cli",
|
|
npmPackage: "bailian-cli",
|
|
};
|
|
const policy: CommandPackPolicy = {
|
|
supported: {
|
|
"@ali/bailian-plugin-agent": {
|
|
commandPrefixes: ["agent"],
|
|
credentialAccess: ["apiKey"],
|
|
},
|
|
},
|
|
};
|
|
const packageJson: CommandPackPackageJson = {
|
|
name: "@ali/bailian-plugin-agent",
|
|
version: "0.0.0-test",
|
|
bailianCli: {
|
|
type: "command-pack",
|
|
apiVersion: 1,
|
|
entry: "./commands.mjs",
|
|
minCliVersion: "1.7.0",
|
|
},
|
|
};
|
|
|
|
const fakeNpmSource = `#!/usr/bin/env node
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const args = process.argv.slice(2);
|
|
const cwd = process.cwd();
|
|
const logPath = path.join(process.env.TMPDIR ?? cwd, "npm.log");
|
|
fs.appendFileSync(
|
|
logPath,
|
|
JSON.stringify({
|
|
args,
|
|
env: {
|
|
registry: process.env.NPM_CONFIG_REGISTRY ?? null,
|
|
catalog: process.env.npm_config_catalog ?? null,
|
|
recursive: process.env.npm_config_recursive ?? null,
|
|
overrides: process.env.npm_config_overrides ?? null,
|
|
},
|
|
}) + "\\n",
|
|
);
|
|
|
|
if (args[0] === "--version") {
|
|
process.stdout.write("10.0.0\\n");
|
|
process.exit(0);
|
|
}
|
|
|
|
const manifestPath = path.join(cwd, "package.json");
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
manifest.dependencies ??= {};
|
|
const name = "@ali/bailian-plugin-agent";
|
|
const packageRoot = path.join(cwd, "node_modules", "@ali", "bailian-plugin-agent");
|
|
|
|
if (args[0] === "uninstall") {
|
|
delete manifest.dependencies[name];
|
|
fs.rmSync(packageRoot, { recursive: true, force: true });
|
|
} else if (args[0] === "install") {
|
|
const requested = args[1] ?? "";
|
|
const broken = requested.endsWith("@broken");
|
|
const version = requested.endsWith("@1.0.0") ? "1.0.0" : "2.0.0";
|
|
manifest.dependencies[name] = version;
|
|
fs.mkdirSync(packageRoot, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(packageRoot, "package.json"),
|
|
JSON.stringify({
|
|
name,
|
|
version,
|
|
bailianCli: {
|
|
type: "command-pack",
|
|
apiVersion: broken ? 2 : 1,
|
|
entry: "./commands.mjs",
|
|
},
|
|
}),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(packageRoot, "commands.mjs"),
|
|
'export default { "agent ping": { description: "Ping", auth: "none", async run() {} } };\\n',
|
|
);
|
|
}
|
|
|
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\\n");
|
|
`;
|
|
|
|
function restoreEnv(name: string, value: string | undefined): void {
|
|
if (value === undefined) delete process.env[name];
|
|
else process.env[name] = value;
|
|
}
|
|
|
|
test("only accepts a package supported by the current product policy", () => {
|
|
expect(parseCommandPackSpec("@ali/bailian-plugin-agent@beta", identity, policy)).toEqual({
|
|
name: "@ali/bailian-plugin-agent",
|
|
requested: "@ali/bailian-plugin-agent@beta",
|
|
});
|
|
expect(() =>
|
|
parseCommandPackSpec("@ali/bailian-plugin-agent@file:../pack", identity, policy),
|
|
).toThrow(/Unsupported Command Pack package spec/);
|
|
expect(() => parseCommandPackSpec("@ali/not-allowlisted", identity, policy)).toThrow(
|
|
/not allowlisted for bl/,
|
|
);
|
|
});
|
|
|
|
test("isolates each product in its own Command Pack sandbox", () => {
|
|
expect(getCommandPacksDir(identity).endsWith(join("plugins", "bailian-cli"))).toBe(true);
|
|
expect(
|
|
getCommandPacksDir({
|
|
...identity,
|
|
binName: "kscli",
|
|
clientName: "knowledge-studio-cli",
|
|
npmPackage: "knowledge-studio-cli",
|
|
}).endsWith(join("plugins", "knowledge-studio-cli")),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("loads an API 1 Command Pack and preserves its command contract", async () => {
|
|
const commands = await loadAndValidateCommandPack(
|
|
"@ali/bailian-plugin-agent",
|
|
packageJson,
|
|
policy.supported["@ali/bailian-plugin-agent"]!,
|
|
identity,
|
|
packageRoot,
|
|
);
|
|
|
|
expect(Object.keys(commands)).toEqual([
|
|
"agent credential",
|
|
"agent credential-denied",
|
|
"agent dangerous",
|
|
"agent fail",
|
|
"agent output",
|
|
"agent ping",
|
|
]);
|
|
expect(commands["agent credential"]?.auth).toBe("apiKey");
|
|
expect(commands["agent ping"]?.auth).toBe("none");
|
|
expect(commands["agent ping"]?.description).toEqual({
|
|
"en-US": "Ping the Command Pack fixture",
|
|
"zh-CN": "调用 Command Pack 测试命令",
|
|
});
|
|
expect(commands["agent ping"]?.risk).toBeUndefined();
|
|
expect(commands["agent dangerous"]?.risk).toEqual({
|
|
level: "high",
|
|
message: {
|
|
"en-US": "This fixture represents a high-risk operation.",
|
|
"zh-CN": "该测试命令代表高风险操作。",
|
|
},
|
|
});
|
|
expect(commands["agent ping"]?.flags?.message).toMatchObject({ required: true, type: "string" });
|
|
});
|
|
|
|
test.each([
|
|
["null", null],
|
|
["a non-object value", "high"],
|
|
["a missing level", { message: "Dangerous operation." }],
|
|
["an unsupported level", { level: "low", message: "Dangerous operation." }],
|
|
["an invalid message", { level: "high", message: "" }],
|
|
])("rejects Command Pack risk metadata with %s", async (_caseName, risk) => {
|
|
const root = await mkdtemp(join(tmpdir(), "command-pack-risk-test-"));
|
|
|
|
try {
|
|
await writeFile(
|
|
join(root, "commands.mjs"),
|
|
`export default {
|
|
"agent dangerous": {
|
|
description: "Dangerous command",
|
|
auth: "none",
|
|
risk: ${JSON.stringify(risk)},
|
|
async run() {},
|
|
},
|
|
};\n`,
|
|
);
|
|
|
|
await expect(
|
|
loadAndValidateCommandPack(
|
|
"@ali/bailian-plugin-agent",
|
|
packageJson,
|
|
policy.supported["@ali/bailian-plugin-agent"]!,
|
|
identity,
|
|
root,
|
|
),
|
|
).rejects.toThrow(/invalid risk/);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("rejects incompatible protocol versions and invalid command prefixes", async () => {
|
|
await expect(
|
|
loadAndValidateCommandPack(
|
|
"@ali/bailian-plugin-agent",
|
|
{ ...packageJson, bailianCli: { ...packageJson.bailianCli!, apiVersion: 2 } },
|
|
policy.supported["@ali/bailian-plugin-agent"]!,
|
|
identity,
|
|
packageRoot,
|
|
),
|
|
).rejects.toThrow(/Command Pack API 2 is not supported/);
|
|
|
|
await expect(
|
|
loadAndValidateCommandPack(
|
|
"@ali/bailian-plugin-agent",
|
|
packageJson,
|
|
{ commandPrefixes: ["other"] },
|
|
identity,
|
|
packageRoot,
|
|
),
|
|
).rejects.toThrow(/outside the allowed prefixes/);
|
|
});
|
|
|
|
test("installs once on success and restores the previous version after validation failure", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "command-pack-install-test-"));
|
|
const binDir = join(root, "bin");
|
|
const logPath = join(root, "npm.log");
|
|
const npmPath = join(binDir, "npm");
|
|
const previousConfigDir = process.env.BAILIAN_CONFIG_DIR;
|
|
const previousPath = process.env.PATH;
|
|
const previousTmpdir = process.env.TMPDIR;
|
|
const previousRegistry = process.env.NPM_CONFIG_REGISTRY;
|
|
const previousCatalog = process.env.npm_config_catalog;
|
|
const previousRecursive = process.env.npm_config_recursive;
|
|
const previousOverrides = process.env.npm_config_overrides;
|
|
|
|
try {
|
|
await mkdir(binDir, { recursive: true });
|
|
await writeFile(npmPath, fakeNpmSource);
|
|
await chmod(npmPath, 0o755);
|
|
await writeFile(logPath, "");
|
|
process.env.BAILIAN_CONFIG_DIR = join(root, "config");
|
|
process.env.PATH = `${binDir}${delimiter}${previousPath ?? ""}`;
|
|
process.env.TMPDIR = root;
|
|
process.env.NPM_CONFIG_REGISTRY = "https://registry.example.test";
|
|
process.env.npm_config_catalog = "prefer";
|
|
process.env.npm_config_recursive = "true";
|
|
process.env.npm_config_overrides = "true";
|
|
|
|
const installed = await installCommandPack("@ali/bailian-plugin-agent@1.0.0", identity, policy);
|
|
expect(installed.version).toBe("1.0.0");
|
|
const successfulCalls = (await readFile(logPath, "utf8"))
|
|
.trim()
|
|
.split("\n")
|
|
.map(
|
|
(line) =>
|
|
JSON.parse(line) as {
|
|
args: string[];
|
|
env: Record<string, string | null>;
|
|
},
|
|
);
|
|
// runNpm probes `npm --version` before each install/uninstall.
|
|
expect(successfulCalls.map((call) => call.args[0])).toEqual(["--version", "install"]);
|
|
expect(successfulCalls[1]?.env).toEqual({
|
|
registry: "https://registry.example.test",
|
|
catalog: null,
|
|
recursive: null,
|
|
overrides: null,
|
|
});
|
|
|
|
await writeFile(logPath, "");
|
|
await expect(
|
|
installCommandPack("@ali/bailian-plugin-agent@broken", identity, policy),
|
|
).rejects.toThrow(/Command Pack API 2 is not supported/);
|
|
|
|
const rollbackCalls = (await readFile(logPath, "utf8"))
|
|
.trim()
|
|
.split("\n")
|
|
.map(
|
|
(line) =>
|
|
JSON.parse(line) as {
|
|
args: string[];
|
|
env: Record<string, string | null>;
|
|
},
|
|
);
|
|
expect(rollbackCalls.map((call) => call.args.slice(0, 2))).toEqual([
|
|
["--version"],
|
|
["install", "@ali/bailian-plugin-agent@broken"],
|
|
["--version"],
|
|
["install", "@ali/bailian-plugin-agent@1.0.0"],
|
|
]);
|
|
|
|
const manifest = JSON.parse(
|
|
await readFile(join(root, "config", "plugins", "bailian-cli", "package.json"), "utf8"),
|
|
) as { dependencies?: Record<string, string> };
|
|
expect(manifest.dependencies?.["@ali/bailian-plugin-agent"]).toBe("1.0.0");
|
|
} finally {
|
|
restoreEnv("BAILIAN_CONFIG_DIR", previousConfigDir);
|
|
restoreEnv("PATH", previousPath);
|
|
restoreEnv("TMPDIR", previousTmpdir);
|
|
restoreEnv("NPM_CONFIG_REGISTRY", previousRegistry);
|
|
restoreEnv("npm_config_catalog", previousCatalog);
|
|
restoreEnv("npm_config_recursive", previousRecursive);
|
|
restoreEnv("npm_config_overrides", previousOverrides);
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|