openyida 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cache/.gitkeep +0 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/ci.yml +123 -0
- package/.github/workflows/publish.yml +105 -0
- package/.github/workflows/update-contributors.yml +151 -0
- package/.openclaw/skills/yida-issue/SKILL.md +27 -0
- package/.openclaw/skills/yida-issue/scripts/create-issue.js +317 -0
- package/CLAUDE.md +168 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/bin/yida.js +811 -0
- package/config.json +4 -0
- package/install-skills.ps1 +162 -0
- package/install-skills.sh +175 -0
- package/package.json +34 -0
- package/pages/dist/.gitkeep +0 -0
- package/pages/src/.gitkeep +0 -0
- package/prd/salary-calculator.md +15 -0
- package/tests/cli.test.js +930 -0
- package/tests/install.test.js +277 -0
- package/tests/yida-issue.test.js +314 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenYida CLI 测试套件
|
|
5
|
+
*
|
|
6
|
+
* 测试策略:
|
|
7
|
+
* 1. 纯函数单元测试(内联副本):findProjectRoot、parseShellArgs
|
|
8
|
+
* 2. CLI 行为集成测试(子进程):通过 spawnSync 调用 node bin/yida.js 验证输出
|
|
9
|
+
* 3. config 命令测试:通过临时目录模拟不同的文件系统状态
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawnSync } = require("child_process");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const os = require("os");
|
|
16
|
+
|
|
17
|
+
const CLI_PATH = path.resolve(__dirname, "../bin/yida.js");
|
|
18
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 执行 CLI 命令,返回 { stdout, stderr, status }
|
|
22
|
+
*/
|
|
23
|
+
function runCli(args = [], options = {}) {
|
|
24
|
+
const result = spawnSync("node", [CLI_PATH, ...args], {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
cwd: options.cwd || PROJECT_ROOT,
|
|
27
|
+
env: { ...process.env, ...options.env },
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
stdout: result.stdout || "",
|
|
31
|
+
stderr: result.stderr || "",
|
|
32
|
+
status: result.status ?? 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 纯函数单元测试(内联副本,与 bin/yida.js 保持一致)────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* parseShellArgs 的内联副本,用于单元测试
|
|
40
|
+
*/
|
|
41
|
+
function parseShellArgs(input) {
|
|
42
|
+
const args = [];
|
|
43
|
+
let current = "";
|
|
44
|
+
let inQuote = false;
|
|
45
|
+
let quoteChar = "";
|
|
46
|
+
|
|
47
|
+
for (const char of input) {
|
|
48
|
+
if (inQuote) {
|
|
49
|
+
if (char === quoteChar) {
|
|
50
|
+
inQuote = false;
|
|
51
|
+
} else {
|
|
52
|
+
current += char;
|
|
53
|
+
}
|
|
54
|
+
} else if (char === '"' || char === "'") {
|
|
55
|
+
inQuote = true;
|
|
56
|
+
quoteChar = char;
|
|
57
|
+
} else if (char === " ") {
|
|
58
|
+
if (current) {
|
|
59
|
+
args.push(current);
|
|
60
|
+
current = "";
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
current += char;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (current) {
|
|
68
|
+
args.push(current);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* findProjectRoot 的内联副本,用于单元测试
|
|
76
|
+
*/
|
|
77
|
+
function findProjectRoot(startDir) {
|
|
78
|
+
let currentDir = startDir || process.cwd();
|
|
79
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
80
|
+
if (
|
|
81
|
+
fs.existsSync(path.join(currentDir, "config.json")) ||
|
|
82
|
+
fs.existsSync(path.join(currentDir, ".git"))
|
|
83
|
+
) {
|
|
84
|
+
return currentDir;
|
|
85
|
+
}
|
|
86
|
+
currentDir = path.dirname(currentDir);
|
|
87
|
+
}
|
|
88
|
+
return startDir || process.cwd();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── parseShellArgs 单元测试 ───────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
describe("parseShellArgs", () => {
|
|
94
|
+
test("解析普通空格分隔的参数", () => {
|
|
95
|
+
expect(parseShellArgs("create-app 考勤管理")).toEqual(["create-app", "考勤管理"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("解析双引号包裹的含空格参数", () => {
|
|
99
|
+
expect(parseShellArgs('create-app "我的 测试 应用"')).toEqual([
|
|
100
|
+
"create-app",
|
|
101
|
+
"我的 测试 应用",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("解析单引号包裹的含空格参数", () => {
|
|
106
|
+
expect(parseShellArgs("create-app '我的 测试 应用'")).toEqual([
|
|
107
|
+
"create-app",
|
|
108
|
+
"我的 测试 应用",
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("解析多个引号参数混合", () => {
|
|
113
|
+
expect(parseShellArgs('create-page APP_XXX "首页 Dashboard"')).toEqual([
|
|
114
|
+
"create-page",
|
|
115
|
+
"APP_XXX",
|
|
116
|
+
"首页 Dashboard",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("空字符串返回空数组", () => {
|
|
121
|
+
expect(parseShellArgs("")).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("多余空格被正确忽略", () => {
|
|
125
|
+
expect(parseShellArgs(" login ")).toEqual(["login"]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("引号内的引号字符被保留", () => {
|
|
129
|
+
expect(parseShellArgs('"hello world"')).toEqual(["hello world"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("解析带选项的命令", () => {
|
|
133
|
+
expect(parseShellArgs("create-app 考勤 -d 考勤系统 -i xian-daka")).toEqual([
|
|
134
|
+
"create-app",
|
|
135
|
+
"考勤",
|
|
136
|
+
"-d",
|
|
137
|
+
"考勤系统",
|
|
138
|
+
"-i",
|
|
139
|
+
"xian-daka",
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ── findProjectRoot 单元测试 ──────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
describe("findProjectRoot", () => {
|
|
147
|
+
test("从项目根目录本身出发,返回根目录", () => {
|
|
148
|
+
// openyida 项目根目录有 config.json
|
|
149
|
+
const result = findProjectRoot(PROJECT_ROOT);
|
|
150
|
+
expect(result).toBe(PROJECT_ROOT);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("从子目录出发,向上找到含 config.json 的根目录", () => {
|
|
154
|
+
const subDir = path.join(PROJECT_ROOT, "tests");
|
|
155
|
+
const result = findProjectRoot(subDir);
|
|
156
|
+
expect(result).toBe(PROJECT_ROOT);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("从不存在 config.json 或 .git 的目录出发,返回起始目录", () => {
|
|
160
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-test-"));
|
|
161
|
+
const result = findProjectRoot(tmpDir);
|
|
162
|
+
expect(result).toBe(tmpDir);
|
|
163
|
+
fs.rmdirSync(tmpDir);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── CLI 帮助和版本测试 ────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
describe("CLI 基本信息", () => {
|
|
170
|
+
test("--help 输出包含所有核心命令", () => {
|
|
171
|
+
const { stdout, status } = runCli(["--help"]);
|
|
172
|
+
expect(status).toBe(0);
|
|
173
|
+
expect(stdout).toContain("login");
|
|
174
|
+
expect(stdout).toContain("logout");
|
|
175
|
+
expect(stdout).toContain("create-app");
|
|
176
|
+
expect(stdout).toContain("create-page");
|
|
177
|
+
expect(stdout).toContain("create-form");
|
|
178
|
+
expect(stdout).toContain("publish");
|
|
179
|
+
expect(stdout).toContain("get-schema");
|
|
180
|
+
expect(stdout).toContain("config");
|
|
181
|
+
expect(stdout).toContain("shell");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("--version 输出版本号 0.1.0", () => {
|
|
185
|
+
const { stdout, status } = runCli(["--version"]);
|
|
186
|
+
expect(status).toBe(0);
|
|
187
|
+
expect(stdout.trim()).toBe("0.1.0");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("无参数时输出帮助信息", () => {
|
|
191
|
+
const { stdout, stderr } = runCli([]);
|
|
192
|
+
// Commander 无子命令时将 usage 输出到 stdout 或 stderr
|
|
193
|
+
const output = stdout + stderr;
|
|
194
|
+
expect(output).toContain("Usage");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("未知命令输出错误提示", () => {
|
|
198
|
+
const { stderr, status } = runCli(["unknown-command"]);
|
|
199
|
+
expect(status).not.toBe(0);
|
|
200
|
+
expect(stderr).toContain("unknown command");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── 各命令帮助文本测试 ────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
describe("子命令帮助文本", () => {
|
|
207
|
+
test("create-app --help 包含选项说明", () => {
|
|
208
|
+
const { stdout, status } = runCli(["create-app", "--help"]);
|
|
209
|
+
expect(status).toBe(0);
|
|
210
|
+
expect(stdout).toContain("--description");
|
|
211
|
+
expect(stdout).toContain("--icon");
|
|
212
|
+
expect(stdout).toContain("--color");
|
|
213
|
+
expect(stdout).toContain("xian-yingyong"); // 默认图标
|
|
214
|
+
expect(stdout).toContain("#0089FF"); // 默认颜色
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("create-app --help 包含示例", () => {
|
|
218
|
+
const { stdout } = runCli(["create-app", "--help"]);
|
|
219
|
+
expect(stdout).toContain("示例");
|
|
220
|
+
expect(stdout).toContain("考勤管理");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("create-page --help 包含参数说明", () => {
|
|
224
|
+
const { stdout, status } = runCli(["create-page", "--help"]);
|
|
225
|
+
expect(status).toBe(0);
|
|
226
|
+
expect(stdout).toContain("<app>");
|
|
227
|
+
expect(stdout).toContain("<name>");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("publish --help 包含参数说明", () => {
|
|
231
|
+
const { stdout, status } = runCli(["publish", "--help"]);
|
|
232
|
+
expect(status).toBe(0);
|
|
233
|
+
expect(stdout).toContain("<file>");
|
|
234
|
+
expect(stdout).toContain("<app>");
|
|
235
|
+
expect(stdout).toContain("<form>");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("get-schema --help 包含示例", () => {
|
|
239
|
+
const { stdout, status } = runCli(["get-schema", "--help"]);
|
|
240
|
+
expect(status).toBe(0);
|
|
241
|
+
expect(stdout).toContain("示例");
|
|
242
|
+
expect(stdout).toContain("APP_XXXXXXXXXXXXX");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ── 缺少必填参数时的错误处理 ─────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
describe("缺少必填参数", () => {
|
|
249
|
+
test("create-app 缺少 name 参数时报错退出", () => {
|
|
250
|
+
const { stderr, status } = runCli(["create-app"]);
|
|
251
|
+
expect(status).not.toBe(0);
|
|
252
|
+
expect(stderr).toMatch(/missing required argument/i);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("create-page 缺少参数时报错退出", () => {
|
|
256
|
+
const { stderr, status } = runCli(["create-page"]);
|
|
257
|
+
expect(status).not.toBe(0);
|
|
258
|
+
expect(stderr).toMatch(/missing required argument/i);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("publish 缺少参数时报错退出", () => {
|
|
262
|
+
const { stderr, status } = runCli(["publish"]);
|
|
263
|
+
expect(status).not.toBe(0);
|
|
264
|
+
expect(stderr).toMatch(/missing required argument/i);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("get-schema 缺少参数时报错退出", () => {
|
|
268
|
+
const { stderr, status } = runCli(["get-schema"]);
|
|
269
|
+
expect(status).not.toBe(0);
|
|
270
|
+
expect(stderr).toMatch(/missing required argument/i);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("create-form 缺少参数时报错退出", () => {
|
|
274
|
+
const { stderr, status } = runCli(["create-form"]);
|
|
275
|
+
expect(status).not.toBe(0);
|
|
276
|
+
expect(stderr).toMatch(/missing required argument/i);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ── yida config 命令测试 ──────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
describe("yida config 命令", () => {
|
|
283
|
+
test("在项目根目录运行时读取 config.json", () => {
|
|
284
|
+
const { stdout, status } = runCli(["config"], { cwd: PROJECT_ROOT });
|
|
285
|
+
expect(status).toBe(0);
|
|
286
|
+
expect(stdout).toContain("aliwork.com");
|
|
287
|
+
expect(stdout).toContain("loginUrl");
|
|
288
|
+
expect(stdout).toContain("defaultBaseUrl");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("显示项目根目录路径", () => {
|
|
292
|
+
const { stdout } = runCli(["config"], { cwd: PROJECT_ROOT });
|
|
293
|
+
expect(stdout).toContain("项目根目录");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("显示登录态信息", () => {
|
|
297
|
+
const { stdout } = runCli(["config"], { cwd: PROJECT_ROOT });
|
|
298
|
+
// 无论已登录还是未登录,都应该显示登录态行
|
|
299
|
+
expect(stdout).toContain("登录态");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("显示 Skills 安装信息", () => {
|
|
303
|
+
const { stdout } = runCli(["config"], { cwd: PROJECT_ROOT });
|
|
304
|
+
// 无论已安装还是未安装,都应该显示 Skills 相关信息
|
|
305
|
+
expect(stdout).toContain("Skills");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("在无 config.json 的临时目录运行时显示默认配置提示", () => {
|
|
309
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-noconfig-"));
|
|
310
|
+
// 创建 .git 让 findProjectRoot 停在这里
|
|
311
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
312
|
+
|
|
313
|
+
const { stdout, status } = runCli(["config"], { cwd: tmpDir });
|
|
314
|
+
expect(status).toBe(0);
|
|
315
|
+
expect(stdout).toContain("未找到 config.json");
|
|
316
|
+
expect(stdout).toContain("aliwork.com");
|
|
317
|
+
|
|
318
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("登录态:cookie 文件不存在时显示未登录", () => {
|
|
322
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-nologin-"));
|
|
323
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
324
|
+
fs.writeFileSync(
|
|
325
|
+
path.join(tmpDir, "config.json"),
|
|
326
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
330
|
+
expect(stdout).toContain("未登录");
|
|
331
|
+
|
|
332
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("登录态:cookie 文件存在且含 tianshu_csrf_token 时显示已登录", () => {
|
|
336
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-loggedin-"));
|
|
337
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
338
|
+
fs.writeFileSync(
|
|
339
|
+
path.join(tmpDir, "config.json"),
|
|
340
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
341
|
+
);
|
|
342
|
+
fs.mkdirSync(path.join(tmpDir, ".cache"));
|
|
343
|
+
fs.writeFileSync(
|
|
344
|
+
path.join(tmpDir, ".cache", "cookies.json"),
|
|
345
|
+
JSON.stringify([
|
|
346
|
+
{ name: "tianshu_csrf_token", value: "abc123" },
|
|
347
|
+
{ name: "other_cookie", value: "xyz" },
|
|
348
|
+
])
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
352
|
+
expect(stdout).toContain("已登录");
|
|
353
|
+
|
|
354
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("登录态:cookie 文件存在但不含 tianshu_csrf_token 时显示可能已过期", () => {
|
|
358
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-expired-"));
|
|
359
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
360
|
+
fs.writeFileSync(
|
|
361
|
+
path.join(tmpDir, "config.json"),
|
|
362
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
363
|
+
);
|
|
364
|
+
fs.mkdirSync(path.join(tmpDir, ".cache"));
|
|
365
|
+
fs.writeFileSync(
|
|
366
|
+
path.join(tmpDir, ".cache", "cookies.json"),
|
|
367
|
+
JSON.stringify([{ name: "other_cookie", value: "xyz" }])
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
371
|
+
expect(stdout).toContain("过期");
|
|
372
|
+
|
|
373
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("Skills 已安装时显示 skill 列表", () => {
|
|
377
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-skills-"));
|
|
378
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
379
|
+
fs.writeFileSync(
|
|
380
|
+
path.join(tmpDir, "config.json"),
|
|
381
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
382
|
+
);
|
|
383
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills", "skills", "yida-login"), { recursive: true });
|
|
384
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills", "skills", "yida-logout"), { recursive: true });
|
|
385
|
+
|
|
386
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
387
|
+
expect(stdout).toContain("yida-login");
|
|
388
|
+
expect(stdout).toContain("yida-logout");
|
|
389
|
+
expect(stdout).toContain("2 个");
|
|
390
|
+
|
|
391
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("Skills 未安装时显示未安装提示", () => {
|
|
395
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-noskills-"));
|
|
396
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
397
|
+
fs.writeFileSync(
|
|
398
|
+
path.join(tmpDir, "config.json"),
|
|
399
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
403
|
+
expect(stdout).toContain("未安装");
|
|
404
|
+
|
|
405
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ── Skill 脚本未安装时的错误提示 ─────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
describe("Skill 未安装时的错误提示", () => {
|
|
412
|
+
let tmpDir;
|
|
413
|
+
|
|
414
|
+
beforeEach(() => {
|
|
415
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-noskill-"));
|
|
416
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
417
|
+
fs.writeFileSync(
|
|
418
|
+
path.join(tmpDir, "config.json"),
|
|
419
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
afterEach(() => {
|
|
424
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("create-app 在 skills 未安装时输出安装指引", () => {
|
|
428
|
+
const { stderr, status } = runCli(["create-app", "测试应用"], { cwd: tmpDir });
|
|
429
|
+
expect(status).toBe(1);
|
|
430
|
+
expect(stderr).toContain("未找到 skill 脚本");
|
|
431
|
+
expect(stderr).toContain("install-skills.sh");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("create-page 在 skills 未安装时输出安装指引", () => {
|
|
435
|
+
const { stderr, status } = runCli(["create-page", "APP_XXX", "首页"], { cwd: tmpDir });
|
|
436
|
+
expect(status).toBe(1);
|
|
437
|
+
expect(stderr).toContain("未找到 skill 脚本");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("publish 在 skills 未安装时输出安装指引", () => {
|
|
441
|
+
const { stderr, status } = runCli(
|
|
442
|
+
["publish", "pages/src/app.js", "APP_XXX", "FORM-XXX"],
|
|
443
|
+
{ cwd: tmpDir }
|
|
444
|
+
);
|
|
445
|
+
expect(status).toBe(1);
|
|
446
|
+
expect(stderr).toContain("未找到 skill 脚本");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("get-schema 在 skills 未安装时输出安装指引", () => {
|
|
450
|
+
const { stderr, status } = runCli(["get-schema", "APP_XXX", "FORM-XXX"], { cwd: tmpDir });
|
|
451
|
+
expect(status).toBe(1);
|
|
452
|
+
expect(stderr).toContain("未找到 skill 脚本");
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ── 头像 URL 版本参数修复测试(Issue #31)────────────────────────────
|
|
457
|
+
//
|
|
458
|
+
// workflow 中使用 re.sub(r'\?v=\d+', '?v=4', avatar_url) 修复版本参数。
|
|
459
|
+
// 以下测试用 JS 等价逻辑验证该正则替换的正确性。
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 模拟 workflow 中的头像 URL 版本参数修复逻辑(Python re.sub 的 JS 等价实现)
|
|
463
|
+
*/
|
|
464
|
+
function fixAvatarUrlVersion(avatarUrl) {
|
|
465
|
+
return avatarUrl.replace(/\?v=\d+/, "?v=4");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 模拟 workflow 中完整的头像 URL 构建逻辑
|
|
470
|
+
*/
|
|
471
|
+
function buildAvatarUrl(avatarUrl) {
|
|
472
|
+
const avatarUrlV4 = fixAvatarUrlVersion(avatarUrl);
|
|
473
|
+
return `${avatarUrlV4}&s=48`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
describe("头像 URL 版本参数修复(Issue #31)", () => {
|
|
477
|
+
test("v=3 的旧版头像 URL 被替换为 v=4", () => {
|
|
478
|
+
const input = "https://avatars.githubusercontent.com/u/1578814?v=3";
|
|
479
|
+
expect(fixAvatarUrlVersion(input)).toBe(
|
|
480
|
+
"https://avatars.githubusercontent.com/u/1578814?v=4"
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("v=4 的头像 URL 保持不变", () => {
|
|
485
|
+
const input = "https://avatars.githubusercontent.com/u/1011681?v=4";
|
|
486
|
+
expect(fixAvatarUrlVersion(input)).toBe(
|
|
487
|
+
"https://avatars.githubusercontent.com/u/1011681?v=4"
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("v=1 的旧版头像 URL 被替换为 v=4", () => {
|
|
492
|
+
const input = "https://avatars.githubusercontent.com/u/12345?v=1";
|
|
493
|
+
expect(fixAvatarUrlVersion(input)).toBe(
|
|
494
|
+
"https://avatars.githubusercontent.com/u/12345?v=4"
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("不含版本参数的 URL 保持不变", () => {
|
|
499
|
+
const input = "https://avatars.githubusercontent.com/u/12345";
|
|
500
|
+
expect(fixAvatarUrlVersion(input)).toBe(
|
|
501
|
+
"https://avatars.githubusercontent.com/u/12345"
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("v=3 的 URL 构建后包含正确的 v=4&s=48 参数", () => {
|
|
506
|
+
const input = "https://avatars.githubusercontent.com/u/1578814?v=3";
|
|
507
|
+
const result = buildAvatarUrl(input);
|
|
508
|
+
expect(result).toBe("https://avatars.githubusercontent.com/u/1578814?v=4&s=48");
|
|
509
|
+
expect(result).not.toContain("v=3");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("v=4 的 URL 构建后包含正确的 v=4&s=48 参数", () => {
|
|
513
|
+
const input = "https://avatars.githubusercontent.com/u/1011681?v=4";
|
|
514
|
+
const result = buildAvatarUrl(input);
|
|
515
|
+
expect(result).toBe("https://avatars.githubusercontent.com/u/1011681?v=4&s=48");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("构建的头像 URL 不包含 v=3", () => {
|
|
519
|
+
const testCases = [
|
|
520
|
+
"https://avatars.githubusercontent.com/u/111?v=3",
|
|
521
|
+
"https://avatars.githubusercontent.com/u/222?v=3",
|
|
522
|
+
"https://avatars.githubusercontent.com/u/333?v=1",
|
|
523
|
+
];
|
|
524
|
+
testCases.forEach((url) => {
|
|
525
|
+
const result = buildAvatarUrl(url);
|
|
526
|
+
expect(result).not.toContain("v=3");
|
|
527
|
+
expect(result).not.toContain("v=1");
|
|
528
|
+
expect(result).toContain("v=4");
|
|
529
|
+
expect(result).toContain("&s=48");
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("README 中已有的 v=4 头像 URL 格式正确", () => {
|
|
534
|
+
// 验证 README 中初始贡献者头像 URL 格式符合规范
|
|
535
|
+
const readmePath = require("path").resolve(__dirname, "../README.md");
|
|
536
|
+
const readmeContent = require("fs").readFileSync(readmePath, "utf-8");
|
|
537
|
+
const avatarUrls = readmeContent.match(/src="https:\/\/avatars\.githubusercontent\.com[^"]+"/g) || [];
|
|
538
|
+
avatarUrls.forEach((srcAttr) => {
|
|
539
|
+
// 所有头像 URL 应该包含 v=4 而不是 v=3
|
|
540
|
+
expect(srcAttr).not.toContain("v=3");
|
|
541
|
+
expect(srcAttr).toContain("v=4");
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ── yida doctor 命令测试 ──────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
describe("yida doctor 命令", () => {
|
|
549
|
+
test("--help 包含 --repair 选项说明", () => {
|
|
550
|
+
const { stdout, status } = runCli(["doctor", "--help"]);
|
|
551
|
+
expect(status).toBe(0);
|
|
552
|
+
expect(stdout).toContain("--repair");
|
|
553
|
+
expect(stdout).toContain("自动修复");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("--help 包含示例", () => {
|
|
557
|
+
const { stdout } = runCli(["doctor", "--help"]);
|
|
558
|
+
expect(stdout).toContain("示例");
|
|
559
|
+
expect(stdout).toContain("yida doctor");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("在完整环境下输出所有检查通过", () => {
|
|
563
|
+
// 当前开发环境满足所有依赖,doctor 应全部通过
|
|
564
|
+
const { stdout, status } = runCli(["doctor"], { cwd: PROJECT_ROOT });
|
|
565
|
+
expect(status).toBe(0);
|
|
566
|
+
expect(stdout).toContain("Node.js");
|
|
567
|
+
expect(stdout).toContain("Python");
|
|
568
|
+
expect(stdout).toContain("Playwright");
|
|
569
|
+
expect(stdout).toContain("gh");
|
|
570
|
+
expect(stdout).toContain("config.json");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("doctor 命令正常运行并输出诊断结果", () => {
|
|
574
|
+
// CI 环境可能缺少 Playwright、gh 登录等,不断言"所有检查通过"
|
|
575
|
+
// 只验证 doctor 命令能正常运行(退出码为 0)并输出诊断信息
|
|
576
|
+
const { stdout, status } = runCli(["doctor"], { cwd: PROJECT_ROOT });
|
|
577
|
+
expect(status).toBe(0);
|
|
578
|
+
// doctor 无论发现问题与否,都应输出检查结果摘要
|
|
579
|
+
const hasAllPassed = stdout.includes("所有检查通过");
|
|
580
|
+
const hasIssuesSummary = stdout.includes("个问题");
|
|
581
|
+
expect(hasAllPassed || hasIssuesSummary).toBe(true);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("config.json 缺失时显示警告并提示 --repair", () => {
|
|
585
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-doctor-noconfig-"));
|
|
586
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
587
|
+
|
|
588
|
+
const { stdout, status } = runCli(["doctor"], { cwd: tmpDir });
|
|
589
|
+
// 有问题时退出码仍为 0(doctor 是诊断命令,不应因环境问题而报错退出)
|
|
590
|
+
expect(status).toBe(0);
|
|
591
|
+
expect(stdout).toContain("config.json");
|
|
592
|
+
expect(stdout).toContain("--repair");
|
|
593
|
+
|
|
594
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("--repair 在 config.json 缺失时自动创建模板", () => {
|
|
598
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-doctor-repair-"));
|
|
599
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
600
|
+
|
|
601
|
+
const { stdout, status } = runCli(["doctor", "--repair"], { cwd: tmpDir });
|
|
602
|
+
expect(status).toBe(0);
|
|
603
|
+
expect(stdout).toContain("已创建 config.json 模板");
|
|
604
|
+
|
|
605
|
+
// 验证文件确实被创建
|
|
606
|
+
const configPath = path.join(tmpDir, "config.json");
|
|
607
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
608
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
609
|
+
expect(config).toHaveProperty("loginUrl");
|
|
610
|
+
expect(config).toHaveProperty("defaultBaseUrl");
|
|
611
|
+
|
|
612
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ── yida completion 命令测试 ──────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
describe("yida completion 命令", () => {
|
|
619
|
+
test("--help 包含 bash/zsh/fish 说明", () => {
|
|
620
|
+
const { stdout, status } = runCli(["completion", "--help"]);
|
|
621
|
+
expect(status).toBe(0);
|
|
622
|
+
expect(stdout).toContain("bash");
|
|
623
|
+
expect(stdout).toContain("zsh");
|
|
624
|
+
expect(stdout).toContain("fish");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("completion bash 输出 bash 补全脚本", () => {
|
|
628
|
+
const { stdout, status } = runCli(["completion", "bash"]);
|
|
629
|
+
expect(status).toBe(0);
|
|
630
|
+
expect(stdout).toContain("_yida_completion");
|
|
631
|
+
expect(stdout).toContain("complete -F _yida_completion yida");
|
|
632
|
+
expect(stdout).toContain("login");
|
|
633
|
+
expect(stdout).toContain("doctor");
|
|
634
|
+
expect(stdout).toContain("completion");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("completion zsh 输出 zsh 补全脚本", () => {
|
|
638
|
+
const { stdout, status } = runCli(["completion", "zsh"]);
|
|
639
|
+
expect(status).toBe(0);
|
|
640
|
+
expect(stdout).toContain("_yida()");
|
|
641
|
+
expect(stdout).toContain("compdef _yida yida");
|
|
642
|
+
expect(stdout).toContain("login:扫码登录宜搭");
|
|
643
|
+
expect(stdout).toContain("doctor");
|
|
644
|
+
expect(stdout).toContain("completion");
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("completion fish 输出 fish 补全脚本", () => {
|
|
648
|
+
const { stdout, status } = runCli(["completion", "fish"]);
|
|
649
|
+
expect(status).toBe(0);
|
|
650
|
+
expect(stdout).toContain("complete -c yida");
|
|
651
|
+
expect(stdout).toContain("login");
|
|
652
|
+
expect(stdout).toContain("doctor");
|
|
653
|
+
expect(stdout).toContain("completion");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("completion zsh 包含 config 子选项补全", () => {
|
|
657
|
+
const { stdout } = runCli(["completion", "zsh"]);
|
|
658
|
+
expect(stdout).toContain("--validate");
|
|
659
|
+
expect(stdout).toContain("--rollback");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("completion bash 包含 config 子选项补全", () => {
|
|
663
|
+
const { stdout } = runCli(["completion", "bash"]);
|
|
664
|
+
expect(stdout).toContain("--validate");
|
|
665
|
+
expect(stdout).toContain("--rollback");
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("不支持的 shell 类型输出错误并以非零退出", () => {
|
|
669
|
+
const { stderr, status } = runCli(["completion", "powershell"]);
|
|
670
|
+
expect(status).not.toBe(0);
|
|
671
|
+
expect(stderr).toContain("不支持的 shell 类型");
|
|
672
|
+
expect(stderr).toContain("bash | zsh | fish");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("缺少 shell 参数时报错退出", () => {
|
|
676
|
+
const { stderr, status } = runCli(["completion"]);
|
|
677
|
+
expect(status).not.toBe(0);
|
|
678
|
+
expect(stderr).toMatch(/missing required argument/i);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// ── yida config --validate / --rollback 测试 ─────────────────────────
|
|
683
|
+
|
|
684
|
+
describe("yida config --validate", () => {
|
|
685
|
+
test("config.json 格式正确时输出校验通过", () => {
|
|
686
|
+
const { stdout, status } = runCli(["config", "--validate"], { cwd: PROJECT_ROOT });
|
|
687
|
+
expect(status).toBe(0);
|
|
688
|
+
expect(stdout).toContain("校验通过");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("config.json 缺少 loginUrl 时校验失败", () => {
|
|
692
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-validate-"));
|
|
693
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
694
|
+
fs.writeFileSync(
|
|
695
|
+
path.join(tmpDir, "config.json"),
|
|
696
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const { stderr, status } = runCli(["config", "--validate"], { cwd: tmpDir });
|
|
700
|
+
expect(status).not.toBe(0);
|
|
701
|
+
expect(stderr).toContain("loginUrl");
|
|
702
|
+
|
|
703
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("config.json 缺少 defaultBaseUrl 时校验失败", () => {
|
|
707
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-validate2-"));
|
|
708
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
709
|
+
fs.writeFileSync(
|
|
710
|
+
path.join(tmpDir, "config.json"),
|
|
711
|
+
JSON.stringify({ loginUrl: "https://www.aliwork.com/workPlatform" })
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
const { stderr, status } = runCli(["config", "--validate"], { cwd: tmpDir });
|
|
715
|
+
expect(status).not.toBe(0);
|
|
716
|
+
expect(stderr).toContain("defaultBaseUrl");
|
|
717
|
+
|
|
718
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("config.json 为非法 JSON 时校验失败", () => {
|
|
722
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-validate3-"));
|
|
723
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
724
|
+
fs.writeFileSync(path.join(tmpDir, "config.json"), "{ invalid json }");
|
|
725
|
+
|
|
726
|
+
const { stderr, status } = runCli(["config", "--validate"], { cwd: tmpDir });
|
|
727
|
+
expect(status).not.toBe(0);
|
|
728
|
+
expect(stderr).toContain("JSON");
|
|
729
|
+
|
|
730
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("config.json 不存在时校验失败", () => {
|
|
734
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-validate4-"));
|
|
735
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
736
|
+
|
|
737
|
+
const { stderr, status } = runCli(["config", "--validate"], { cwd: tmpDir });
|
|
738
|
+
expect(status).not.toBe(0);
|
|
739
|
+
expect(stderr).toContain("config.json");
|
|
740
|
+
|
|
741
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe("yida config --rollback", () => {
|
|
746
|
+
test("备份文件存在时回滚成功", () => {
|
|
747
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-rollback-"));
|
|
748
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
749
|
+
fs.mkdirSync(path.join(tmpDir, ".cache"));
|
|
750
|
+
|
|
751
|
+
const backupConfig = {
|
|
752
|
+
loginUrl: "https://www.aliwork.com/workPlatform",
|
|
753
|
+
defaultBaseUrl: "https://www.aliwork.com",
|
|
754
|
+
};
|
|
755
|
+
fs.writeFileSync(
|
|
756
|
+
path.join(tmpDir, ".cache", "config.backup.json"),
|
|
757
|
+
JSON.stringify(backupConfig, null, 2)
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const { stdout, status } = runCli(["config", "--rollback"], { cwd: tmpDir });
|
|
761
|
+
expect(status).toBe(0);
|
|
762
|
+
expect(stdout).toContain("已回滚");
|
|
763
|
+
expect(stdout).toContain("aliwork.com");
|
|
764
|
+
|
|
765
|
+
// 验证 config.json 内容与备份一致
|
|
766
|
+
const restoredConfig = JSON.parse(
|
|
767
|
+
fs.readFileSync(path.join(tmpDir, "config.json"), "utf-8")
|
|
768
|
+
);
|
|
769
|
+
expect(restoredConfig.loginUrl).toBe(backupConfig.loginUrl);
|
|
770
|
+
expect(restoredConfig.defaultBaseUrl).toBe(backupConfig.defaultBaseUrl);
|
|
771
|
+
|
|
772
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("备份文件不存在时报错退出", () => {
|
|
776
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-rollback2-"));
|
|
777
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
778
|
+
|
|
779
|
+
const { stderr, status } = runCli(["config", "--rollback"], { cwd: tmpDir });
|
|
780
|
+
expect(status).not.toBe(0);
|
|
781
|
+
expect(stderr).toContain("未找到备份配置文件");
|
|
782
|
+
|
|
783
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("备份文件为非法 JSON 时回滚失败", () => {
|
|
787
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-rollback3-"));
|
|
788
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
789
|
+
fs.mkdirSync(path.join(tmpDir, ".cache"));
|
|
790
|
+
fs.writeFileSync(path.join(tmpDir, ".cache", "config.backup.json"), "{ bad json }");
|
|
791
|
+
|
|
792
|
+
const { stderr, status } = runCli(["config", "--rollback"], { cwd: tmpDir });
|
|
793
|
+
expect(status).not.toBe(0);
|
|
794
|
+
expect(stderr).toContain("回滚失败");
|
|
795
|
+
|
|
796
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("运行 config 后自动生成备份文件", () => {
|
|
800
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-autobackup-"));
|
|
801
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
802
|
+
fs.writeFileSync(
|
|
803
|
+
path.join(tmpDir, "config.json"),
|
|
804
|
+
JSON.stringify({
|
|
805
|
+
loginUrl: "https://www.aliwork.com/workPlatform",
|
|
806
|
+
defaultBaseUrl: "https://www.aliwork.com",
|
|
807
|
+
}, null, 2)
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
runCli(["config"], { cwd: tmpDir });
|
|
811
|
+
|
|
812
|
+
// 验证备份文件被自动创建
|
|
813
|
+
const backupPath = path.join(tmpDir, ".cache", "config.backup.json");
|
|
814
|
+
expect(fs.existsSync(backupPath)).toBe(true);
|
|
815
|
+
const backup = JSON.parse(fs.readFileSync(backupPath, "utf-8"));
|
|
816
|
+
expect(backup.defaultBaseUrl).toBe("https://www.aliwork.com");
|
|
817
|
+
|
|
818
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// ── --help 中包含新命令 ───────────────────────────────────────────────
|
|
823
|
+
|
|
824
|
+
describe("--help 包含新增命令", () => {
|
|
825
|
+
test("全局 --help 包含 doctor 命令", () => {
|
|
826
|
+
const { stdout } = runCli(["--help"]);
|
|
827
|
+
expect(stdout).toContain("doctor");
|
|
828
|
+
expect(stdout).toContain("检查 OpenYida 环境依赖");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("全局 --help 包含 completion 命令", () => {
|
|
832
|
+
const { stdout } = runCli(["--help"]);
|
|
833
|
+
expect(stdout).toContain("completion");
|
|
834
|
+
expect(stdout).toContain("shell 自动补全");
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("全局 --help 中 config 描述包含校验和回滚", () => {
|
|
838
|
+
const { stdout } = runCli(["config", "--help"]);
|
|
839
|
+
expect(stdout).toContain("--validate");
|
|
840
|
+
expect(stdout).toContain("--rollback");
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// ── skills 路径修正回归测试 ───────────────────────────────────────────
|
|
845
|
+
//
|
|
846
|
+
// 验证 skills 实际路径为 .claude/skills/skills/<name>(而非 .claude/skills/<name>)
|
|
847
|
+
// 对应 PR #49 修复:bin/yida.js 中 getSkillScript / config / doctor 路径修正
|
|
848
|
+
|
|
849
|
+
describe("skills 路径修正回归测试", () => {
|
|
850
|
+
test("config 命令:skills 安装在 .claude/skills/skills/ 下时正确识别", () => {
|
|
851
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-skills-path-"));
|
|
852
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
853
|
+
fs.writeFileSync(
|
|
854
|
+
path.join(tmpDir, "config.json"),
|
|
855
|
+
JSON.stringify({ loginUrl: "https://www.aliwork.com/workPlatform", defaultBaseUrl: "https://www.aliwork.com" })
|
|
856
|
+
);
|
|
857
|
+
// 正确路径:.claude/skills/skills/<name>
|
|
858
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills", "skills", "yida-login"), { recursive: true });
|
|
859
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills", "skills", "yida-create-app"), { recursive: true });
|
|
860
|
+
|
|
861
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
862
|
+
expect(stdout).toContain("yida-login");
|
|
863
|
+
expect(stdout).toContain("yida-create-app");
|
|
864
|
+
expect(stdout).toContain("2 个");
|
|
865
|
+
|
|
866
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("config 命令:skills 仅在旧路径 .claude/skills/<name> 下时显示未安装", () => {
|
|
870
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-skills-oldpath-"));
|
|
871
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
872
|
+
fs.writeFileSync(
|
|
873
|
+
path.join(tmpDir, "config.json"),
|
|
874
|
+
JSON.stringify({ loginUrl: "https://www.aliwork.com/workPlatform", defaultBaseUrl: "https://www.aliwork.com" })
|
|
875
|
+
);
|
|
876
|
+
// 旧路径(错误路径):.claude/skills/<name>,不应被识别
|
|
877
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills", "yida-login"), { recursive: true });
|
|
878
|
+
|
|
879
|
+
const { stdout } = runCli(["config"], { cwd: tmpDir });
|
|
880
|
+
// 旧路径下的 skill 不应被识别为已安装
|
|
881
|
+
expect(stdout).toContain("未安装");
|
|
882
|
+
|
|
883
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("doctor 命令:skills 安装在 .claude/skills/skills/ 下时显示已安装", () => {
|
|
887
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-doctor-skills-"));
|
|
888
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
889
|
+
fs.writeFileSync(
|
|
890
|
+
path.join(tmpDir, "config.json"),
|
|
891
|
+
JSON.stringify({ loginUrl: "https://www.aliwork.com/workPlatform", defaultBaseUrl: "https://www.aliwork.com" })
|
|
892
|
+
);
|
|
893
|
+
// 正确路径:.claude/skills/skills/<name>
|
|
894
|
+
fs.mkdirSync(path.join(tmpDir, ".claude", "skills", "skills", "yida-login"), { recursive: true });
|
|
895
|
+
|
|
896
|
+
const { stdout } = runCli(["doctor"], { cwd: tmpDir });
|
|
897
|
+
expect(stdout).toContain("Skills 已安装");
|
|
898
|
+
|
|
899
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("doctor 命令:skills 未安装时显示未安装提示", () => {
|
|
903
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-doctor-noskills-"));
|
|
904
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
905
|
+
fs.writeFileSync(
|
|
906
|
+
path.join(tmpDir, "config.json"),
|
|
907
|
+
JSON.stringify({ loginUrl: "https://www.aliwork.com/workPlatform", defaultBaseUrl: "https://www.aliwork.com" })
|
|
908
|
+
);
|
|
909
|
+
// 不创建 skills 目录
|
|
910
|
+
|
|
911
|
+
const { stdout } = runCli(["doctor"], { cwd: tmpDir });
|
|
912
|
+
expect(stdout).toContain("Skills 未安装");
|
|
913
|
+
|
|
914
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("skill 未安装时错误提示包含 git clone --branch main(不含 git submodule)", () => {
|
|
918
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "yida-errmsg-"));
|
|
919
|
+
fs.mkdirSync(path.join(tmpDir, ".git"));
|
|
920
|
+
fs.writeFileSync(
|
|
921
|
+
path.join(tmpDir, "config.json"),
|
|
922
|
+
JSON.stringify({ defaultBaseUrl: "https://www.aliwork.com" })
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const { stderr } = runCli(["create-app", "测试应用"], { cwd: tmpDir });
|
|
926
|
+
expect(stderr).toContain("git clone");
|
|
927
|
+
expect(stderr).toContain("--branch main");
|
|
928
|
+
expect(stderr).not.toContain("git submodule");
|
|
929
|
+
});
|
|
930
|
+
});
|