补齐行政区划三级联动接口文档与模板门禁(#6422)
changelog-filename-gate / validate (pull_request) Successful in 2s

这个提交包含在:
lc
2026-08-26 16:23:19 +08:00
父节点 ff16cfd94c
当前提交 3d6f6245a0
共修改 7 个文件,包含 1038 行新增和 160 行删除
+90 -2
查看文件
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
@@ -21,6 +21,7 @@ function metadata(overrides = {}) {
ticket: '5218',
title: '工作流治理',
consumer: 'admin',
author: 'lc(GIT)',
change_type: '修改接口',
backend_status: 'deployed',
gateway_status: 'verified',
@@ -41,7 +42,15 @@ function document(overrides = {}) {
const frontmatter = Object.entries(fields)
.map(([key, value]) => `${key}: "${value}"`)
.join('\n');
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- 无业务接口变化。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 二、变更接口清单\n\n| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |\n|---|---|---|---|---|---|\n| 1 | 保存配置 | POST | \`/admin/workflow/config\` | 修改请求 | 保存工作流配置 |\n\n## 三、接口详情\n\n### 1. 保存配置 \`POST /admin/workflow/config\`\n\n**VO**: \`WorkflowConfigReqVO / String\`\n\n#### 使用场景\n\n- 管理员保存工作流配置。\n\n#### 入参\n\n| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |\n|---|---|---|---|---|---|\n| name | Body | String | ✅ | 非空 | 配置名称 |\n\n#### 出参 \`Result<String>\`\n\n| 字段 | 类型 | 说明 |\n|---|---|---|\n| data | String | 配置 ID |\n\n#### 请求示例\n\n\`\`\`json\n{ "name": "审批流" }\n\`\`\`\n\n#### 响应示例\n\n\`\`\`json\n{ "code": 200, "message": "成功", "data": "1", "success": true }\n\`\`\`\n\n#### 空数据 / 降级响应\n\n成功时一定返回字符串 ID。\n\n#### 错误响应\n\n\`\`\`json\n{ "code": 400, "message": "name 不能为空", "data": null, "success": false }\n\`\`\`\n\n#### 业务边界\n\n- 业务失败也必须检查响应体 code。\n\n## 四、契约约束与正确调用方式\n\n- 请求体必须使用 JSON。\n\n## 五、数据库行为\n\n- 成功请求保存一份配置,失败请求不产生业务写入。\n\n## 六、边界行为\n\n- 未登录返回 401。\n\n## 六.6、修改前后对比\n\n| 字段 | 改前 | 改后 |\n|---|---|---|\n| name | 可为空 | 必填 |\n\n## 六.7、影响评估\n\n- **是否破坏向后兼容**: 否\n- **前端是否必须同步上线**: 是\n- **前端 workaround 清理点**: 无\n\n## 七、不影响范围\n\n- 查询接口不变。\n\n## 八、测试环境已验证\n\n- POST /admin/workflow/config → 200 ✓\n\n## 十、相关文档\n\n- Issue: #5218\n\n## 关联 / 联系人\n\n- **后端负责人**: @lc\n`;
}
function incompleteApiDocument() {
const fields = metadata();
const frontmatter = Object.entries(fields)
.map(([key, value]) => `${key}: "${value}"`)
.join('\n');
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- POST /admin/workflow/config。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
}
test('parses quoted flat YAML frontmatter', () => {
@@ -50,10 +59,89 @@ test('parses quoted flat YAML frontmatter', () => {
assert.equal(parsed.metadata.frontend_status, 'pending');
});
test('CHANGELOG_TEMPLATE.md contains every section enforced for API handoffs', () => {
const template = readFileSync(
new URL('../CHANGELOG_TEMPLATE.md', import.meta.url),
'utf8',
);
for (const value of [
'author: "{推送者登录名}(GIT)"',
'## 二、变更接口清单',
'## 三、接口详情',
'#### 使用场景',
'#### 入参',
'#### 出参',
'#### 请求示例',
'#### 响应示例',
'#### 空数据 / 降级响应',
'#### 错误响应',
'#### 业务边界',
'## 四、契约约束与正确调用方式',
'## 五、数据库行为',
'## 六、边界行为',
'## 六.6、修改前后对比',
'## 六.7、影响评估',
'## 七、不影响范围',
'## 八、测试环境已验证',
'## 十、相关文档',
'## 关联 / 联系人',
]) {
assert.ok(template.includes(value), `template missing ${value}`);
}
});
test('accepts a complete v2 handoff with pending frontend consumption', () => {
assert.deepEqual(validateV2Document(FILE, document(), { requireV2: true }), []);
});
test('rejects an API handoff that does not follow CHANGELOG_TEMPLATE.md', () => {
const errors = validateV2Document(FILE, incompleteApiDocument(), { requireV2: true });
assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE'));
});
test('rejects an endpoint detail without a required self-contained contract block', () => {
const value = document().replace('#### 错误响应', '#### 异常说明');
const errors = validateV2Document(FILE, value, { requireV2: true });
assert.ok(errors.some(({ code }) => code === 'E_API_DETAIL'));
});
test('requires a use case and explicit empty/degraded behavior for every endpoint', () => {
const value = document()
.replace('#### 使用场景', '#### 调用时机')
.replace('#### 空数据 / 降级响应', '#### 无数据');
const errors = validateV2Document(FILE, value, { requireV2: true });
assert.ok(errors.some(({ code, message }) => (
code === 'E_API_DETAIL'
&& message.includes('使用场景')
&& message.includes('空数据 / 降级响应')
)));
});
test('rejects a mismatch between the API list and endpoint detail headings', () => {
const value = document().replace(
'### 1. 保存配置 `POST /admin/workflow/config`',
'### 1. 保存配置 `POST /admin/workflow/other`',
);
const errors = validateV2Document(FILE, value, { requireV2: true });
assert.ok(errors.some(({ code }) => code === 'E_API_ENDPOINTS'));
});
test('requires the template author format and write-operation database section', () => {
const invalidAuthor = validateV2Document(
FILE,
document({ author: 'lc' }),
{ requireV2: true },
);
assert.ok(invalidAuthor.some(({ code }) => code === 'E_AUTHOR'));
const withoutDatabaseBehavior = document().replace(
'## 五、数据库行为',
'## 五、持久化说明',
);
const errors = validateV2Document(FILE, withoutDatabaseBehavior, { requireV2: true });
assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE'));
});
test('rejects backend and gateway pending at publication', () => {
const errors = validateV2Document(
FILE,