mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
2c53b0692b
- 统一代码格式,增加多处代码块的换行和缩进保持一致 - 调整技能安装目标列表的格式,提升可读性 - 修复解压缩逻辑中异常抛出格式,增强异常信息规范 - 优化归一化文件名过滤条件表达式格式 - 修改配置文件检测逻辑,兼容环境变量和旧版配置方案 - 增强对 Bailian 相关模型提供者的检测逻辑支持 - 规范代理详情字段生成方法的代码风格 - 调整 MCP 写回相关函数的格式,提升可维护性 - 改进技能和代理详情函数参数格式,统一参数拆分显示 - 修复单元测试中路径和 JSON 写入格式,增加不同配置场景测试覆盖 - 确保软链接技能目录被正确识别为安装来源 - 增加多代理配置文件和技能安装的检测测试用例,提升测试精准度
216 lines
6.6 KiB
TypeScript
216 lines
6.6 KiB
TypeScript
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vite-plus/test";
|
|
import {
|
|
listSkills,
|
|
listMcpServers,
|
|
listAgents,
|
|
getSkillDetail,
|
|
} from "../src/commands/config/inventory.ts";
|
|
|
|
/** Build an isolated fake $HOME and clean it up afterwards. */
|
|
function withHome(fn: (home: string) => void): void {
|
|
const home = mkdtempSync(join(tmpdir(), "bl-inv-"));
|
|
try {
|
|
fn(home);
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function write(home: string, rel: string, content: string): void {
|
|
const path = join(home, rel);
|
|
mkdirSync(join(path, ".."), { recursive: true });
|
|
writeFileSync(path, content);
|
|
}
|
|
|
|
test("getSkillDetail 返回 SKILL.md 原文,未知 id 返回 null", () => {
|
|
withHome((home) => {
|
|
write(
|
|
home,
|
|
".agents/skills/demo/SKILL.md",
|
|
"---\nname: demo-skill\ndescription: A demo skill.\n---\n# Body\nhello world\n",
|
|
);
|
|
|
|
const detail = getSkillDetail("demo", home);
|
|
expect(detail).not.toBeNull();
|
|
expect(detail?.name).toBe("demo-skill");
|
|
expect(detail?.content).toContain("hello world");
|
|
expect(getSkillDetail("nope", home)).toBeNull();
|
|
});
|
|
});
|
|
|
|
test("listSkills 解析 SKILL.md frontmatter 与文件数", () => {
|
|
withHome((home) => {
|
|
write(
|
|
home,
|
|
".agents/skills/demo/SKILL.md",
|
|
'---\nname: demo-skill\nmetadata:\n version: "2.1.0"\ndescription: A demo skill.\n---\n# Body\n',
|
|
);
|
|
write(home, ".agents/skills/demo/assets/a.md", "x");
|
|
// Directory without SKILL.md is ignored.
|
|
mkdirSync(join(home, ".agents/skills/not-a-skill"), { recursive: true });
|
|
|
|
const skills = listSkills(home);
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0]).toMatchObject({
|
|
id: "demo",
|
|
name: "demo-skill",
|
|
version: "2.1.0",
|
|
description: "A demo skill.",
|
|
sources: ["global"],
|
|
origin: "local",
|
|
});
|
|
expect(skills[0].fileCount).toBe(2);
|
|
});
|
|
});
|
|
|
|
test("listSkills 将 agent 目录下的软链接视为安装来源", () => {
|
|
withHome((home) => {
|
|
write(home, ".agents/skills/bailian-cli/SKILL.md", "---\nname: bailian-cli\n---\n");
|
|
// Mimic `skills add`: the agent copy is a symlink back to the global dir.
|
|
mkdirSync(join(home, ".claude/skills"), { recursive: true });
|
|
symlinkSync(join(home, ".agents/skills/bailian-cli"), join(home, ".claude/skills/bailian-cli"));
|
|
|
|
const skills = listSkills(home);
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].sources).toEqual(["global", "claude-code"]);
|
|
});
|
|
});
|
|
|
|
test("listSkills 跨 agent 模块聚合并记录来源", () => {
|
|
withHome((home) => {
|
|
// Same skill installed in the global dir and two agent modules.
|
|
const skillMd = "---\nname: bailian-cli\n---\n# B\n";
|
|
write(home, ".agents/skills/bailian-cli/SKILL.md", skillMd);
|
|
write(home, ".claude/skills/bailian-cli/SKILL.md", skillMd);
|
|
write(home, ".qwen/skills/bailian-cli/SKILL.md", skillMd);
|
|
// A skill only present in qwen.
|
|
write(home, ".qwen/skills/spark-video/SKILL.md", "---\nname: spark-video\n---\n");
|
|
|
|
const skills = listSkills(home);
|
|
const byId = Object.fromEntries(skills.map((s) => [s.id, s]));
|
|
expect(byId["bailian-cli"].sources).toEqual(["global", "claude-code", "qwen-code"]);
|
|
expect(byId["spark-video"].sources).toEqual(["qwen-code"]);
|
|
});
|
|
});
|
|
|
|
test("listSkills 目录缺失时返回空数组", () => {
|
|
withHome((home) => {
|
|
expect(listSkills(home)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
test("listMcpServers 汇总 codex(toml) 与 claude(json) 的 MCP 定义", () => {
|
|
withHome((home) => {
|
|
write(home, ".codex/config.toml", '[mcp_servers.repl]\ncommand = "node"\nargs = ["repl.js"]\n');
|
|
write(
|
|
home,
|
|
".claude.json",
|
|
JSON.stringify({
|
|
mcpServers: { web: { url: "https://example.com/mcp", type: "sse" } },
|
|
projects: {
|
|
"/proj": {
|
|
mcpServers: { local: { command: "python", args: ["s.py"] } },
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const servers = listMcpServers(home);
|
|
const byName = Object.fromEntries(servers.map((s) => [s.name, s]));
|
|
expect(byName.repl).toMatchObject({
|
|
source: "codex",
|
|
transport: "stdio",
|
|
origin: "local",
|
|
});
|
|
expect(byName.repl.detail).toContain("node repl.js");
|
|
expect(byName.web).toMatchObject({
|
|
source: "claude-code",
|
|
transport: "sse",
|
|
scope: "global",
|
|
});
|
|
expect(byName.local).toMatchObject({
|
|
source: "claude-code",
|
|
transport: "stdio",
|
|
scope: "/proj",
|
|
});
|
|
});
|
|
});
|
|
|
|
test("listMcpServers 无配置时返回空数组", () => {
|
|
withHome((home) => {
|
|
expect(listMcpServers(home)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
test("listAgents 报告安装与已连接 bailian-cli 的状态", () => {
|
|
withHome((home) => {
|
|
// Claude Code: installed + configured (base url present).
|
|
write(
|
|
home,
|
|
".claude/settings.json",
|
|
JSON.stringify({
|
|
env: { ANTHROPIC_BASE_URL: "https://x", ANTHROPIC_MODEL: "qwen3-max" },
|
|
}),
|
|
);
|
|
// Codex: installed but NOT configured (no bailian-cli provider).
|
|
write(home, ".codex/config.toml", 'model = "gpt-5"\n');
|
|
// Qwen Code: configured via the new "[Bailian] <model>" display name.
|
|
write(
|
|
home,
|
|
".qwen/settings.json",
|
|
JSON.stringify({
|
|
modelProviders: {
|
|
openai: [{ id: "qwen3-coder-plus", name: "[Bailian] qwen3-coder-plus" }],
|
|
},
|
|
model: { name: "qwen3-coder-plus" },
|
|
}),
|
|
);
|
|
// Hermes: configured via the official flat model block (no custom_providers).
|
|
write(
|
|
home,
|
|
".hermes/config.yaml",
|
|
[
|
|
"model:",
|
|
" default: qwen3-max",
|
|
" provider: custom",
|
|
" base_url: https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
" api_key: sk-test",
|
|
].join("\n"),
|
|
);
|
|
|
|
const agents = listAgents(home);
|
|
const byId = Object.fromEntries(agents.map((a) => [a.id, a]));
|
|
|
|
expect(byId["claude-code"]).toMatchObject({
|
|
installed: true,
|
|
configured: true,
|
|
model: "qwen3-max",
|
|
origin: "local",
|
|
});
|
|
expect(byId.codex).toMatchObject({
|
|
installed: true,
|
|
configured: false,
|
|
model: "gpt-5",
|
|
});
|
|
expect(byId["qwen-code"]).toMatchObject({
|
|
installed: true,
|
|
configured: true,
|
|
model: "qwen3-coder-plus",
|
|
});
|
|
expect(byId.hermes).toMatchObject({
|
|
installed: true,
|
|
configured: true,
|
|
model: "qwen3-max",
|
|
});
|
|
expect(byId.opencode).toMatchObject({
|
|
installed: false,
|
|
configured: false,
|
|
});
|
|
// Always reports all six known frameworks.
|
|
expect(agents).toHaveLength(6);
|
|
});
|
|
});
|