1
0

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:
2026-05-17 20:12:39 +08:00
parent f7193e98ff
commit 31fd3a2a43
29 changed files with 382 additions and 119 deletions

View File

@@ -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 () => {