refactor: 统一 target name/description 可空语义,前端展示 fallback 到 id
- schema: name/description 允许省略或显式 null,TypeBox Union([Null, String]) - 类型: RawTargetConfig/ResolvedTargetBase/子类型/StoredTarget/TargetStatus name 改为 string | null - checker resolve: name: t.name ?? null,不再 fallback 到 id - 语义校验: 拒绝空字符串和纯空白 name - SQLite: targets.name 列改为可空 TEXT - 前端: 新增 getTargetDisplayName(target) 展示 name ?? id - 测试: 覆盖 name/description null 全场景,查找改为按 id - 文档: 更新 README/DEVELOPMENT 和 6 个 openspec specs
This commit is contained in:
@@ -234,7 +234,7 @@ targets:
|
||||
expect(cmd.cmd.maxOutputBytes).toBe(10485760);
|
||||
});
|
||||
|
||||
test("name 缺省时 fallback 到 id", async () => {
|
||||
test("name 缺省时保留为 null", async () => {
|
||||
const configPath = join(tempDir, "name-fallback.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
@@ -249,7 +249,105 @@ targets:
|
||||
const config = await loadConfig(configPath);
|
||||
const target = config.targets[0]!;
|
||||
expect(target.id).toBe("api-health");
|
||||
expect(target.name).toBe("api-health");
|
||||
expect(target.name).toBeNull();
|
||||
});
|
||||
|
||||
test("name 显式 null 保留为 null", async () => {
|
||||
const configPath = join(tempDir, "name-explicit-null.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
name: null
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.name).toBeNull();
|
||||
});
|
||||
|
||||
test("name YAML 空值保留为 null", async () => {
|
||||
const configPath = join(tempDir, "name-yaml-null.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
name:
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.name).toBeNull();
|
||||
});
|
||||
|
||||
test("name 为空字符串抛出错误", async () => {
|
||||
const configPath = join(tempDir, "empty-name.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
name: ""
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("name 不能为空白");
|
||||
});
|
||||
|
||||
test("name 仅包含空白字符抛出错误", async () => {
|
||||
const configPath = join(tempDir, "whitespace-name.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
name: " "
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("name 不能为空白");
|
||||
});
|
||||
|
||||
test("description 显式 null 保留为 null", async () => {
|
||||
const configPath = join(tempDir, "description-null.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
description: null
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.description).toBeNull();
|
||||
});
|
||||
|
||||
test("description YAML 空值保留为 null", async () => {
|
||||
const configPath = join(tempDir, "description-yaml-null.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
description:
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.description).toBeNull();
|
||||
});
|
||||
|
||||
test("name 支持变量替换且不要求唯一", async () => {
|
||||
|
||||
Reference in New Issue
Block a user