feat(workflows): 新增 doc-sync-automation 中英文档一致性守护端到端工作流 #332
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
gitlink-cli.exe
|
||||
/gitlink-cli
|
||||
__pycache__/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# 中英文档一致性守护工作流(doc-sync-automation)
|
||||
|
||||
把 [`gitlink-doc-sync` Skill](../../../skills/gitlink-doc-sync/SKILL.md)(中英文档一致性守护)包成**可直接运行的端到端工作流**:
|
||||
|
||||
> **采集 → 检测 → 报告 → 回写**:用 `repo +tree` 按命名约定自动发现双语文档对,用 `file +view --raw` 拉取两版内容,做**确定性结构比对**(章节大纲 / 代码块 / 表格行数 / 版本号),输出分级(🔴严重 / 🟡中等 / 🟢轻微)漂移报告,并(仅在 `--apply` 时)用 `issue +create` 把报告作为 tracking issue 真实回写到 GitLink。
|
||||
|
||||
与仓库内已有能力的关系:`file` 命令组(文件读写)→ `gitlink-doc-sync` Skill(AI 语义比对与翻译同步知识)→ **本工作流(确定性可复现闭环)**,三层互为支撑而非重复:Skill 负责需要语义理解的翻译同步,本工作流负责可进 CI 的确定性漂移检测。
|
||||
|
||||
## 架构图
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["repo +tree<br/>仓库结构采集"] --> B["文档对自动发现<br/>README.md ⇄ README.zh-CN.md<br/>docs/*.md ⇄ docs/*.zh-CN.md"]
|
||||
B --> C["file +view --raw ×2<br/>拉取双语版本内容"]
|
||||
C --> D["确定性结构比对<br/>章节大纲 / 代码块 / 表格 / 版本号"]
|
||||
D --> E["分级漂移报告<br/>🔴严重 / 🟡中等 / 🟢轻微"]
|
||||
E -->|"dry-run(默认)"| F["Markdown 报告落盘<br/>退出码 0/2 → CI 门禁"]
|
||||
E -->|"--apply"| G["issue +create<br/>回写 tracking issue"]
|
||||
G -.->|"人工确认后"| H["gitlink-doc-sync Skill<br/>AI 语义翻译同步 → file +update → pr +create"]
|
||||
```
|
||||
|
||||
## CI 门禁集成示例
|
||||
|
||||
利用退出码语义(`0` 无严重漂移 / `2` 存在严重漂移)可直接作为发布门禁。GitLink 引擎(`.gitea/workflows`)示例:
|
||||
|
||||
```yaml
|
||||
name: doc-sync-gate
|
||||
on:
|
||||
pull_request:
|
||||
paths: ["README.md", "README.zh-CN.md", "docs/**"]
|
||||
jobs:
|
||||
doc-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm install -g @gitlink-ai/cli
|
||||
- env:
|
||||
GITLINK_TOKEN: ${{ secrets.GITLINK_TOKEN }}
|
||||
run: |
|
||||
python3 examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py \
|
||||
--owner ${{ github.repository_owner }} --repo ${{ github.event.repository.name }}
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
- `scripts/doc_sync_workflow.py`:文档对发现 + 漂移检测 + 报告 + tracking issue 回写(纯标准库,Python ≥3.9,零第三方依赖)
|
||||
- `tests/test_drift.py`:确定性回归护栏(同输入 → 同发现 → 同退出语义)
|
||||
- `docs/verification.md`:真实平台验证证据
|
||||
- `examples/demo-outputs/`:对生产环境真实仓库运行的漂移报告
|
||||
|
||||
## 快速运行(默认 dry-run,不写远端)
|
||||
|
||||
```bash
|
||||
npm install -g @gitlink-ai/cli
|
||||
gitlink-cli auth login
|
||||
|
||||
python3 scripts/doc_sync_workflow.py --owner <owner> --repo <repo> --output-dir outputs
|
||||
```
|
||||
|
||||
- 自动发现失败时手动指定文档对:`--pair README.md:README.zh-CN.md`(可多次)
|
||||
- 指定分支:`--ref develop`
|
||||
- 真实回写 tracking issue:加 `--apply`(请先在自有仓库演练)
|
||||
- 退出码:`0` 无严重漂移,`2` 存在严重漂移(可直接作为 CI 门禁)
|
||||
|
||||
## 已在真实平台验证
|
||||
|
||||
全部证据见 [`docs/verification.md`](docs/verification.md),要点:
|
||||
|
||||
| 验证 | 对象 | 结果 |
|
||||
|------|------|------|
|
||||
| 文档对自动发现 | 生产 gitlink.org.cn 真实仓库 | ✅ 自动识别 README.md ⇄ README.zh-CN.md |
|
||||
| 漂移检测 | 本仓库 README 双语版本 | ✅ 检出真实漂移:代码块 32 vs 29、表格 43 vs 40 行 |
|
||||
| `--apply` 真实回写 | 自有 fork | ✅ tracking issue 创建成功(issue #1,API 回执确认) |
|
||||
| 单测 | `tests/test_drift.py` | ✅ 11/11 全绿 |
|
||||
|
||||
## 设计要点
|
||||
|
||||
- **确定性**:漂移检测只做结构比对(标题大纲、代码块数/行数、表格行数、版本号),同输入必同输出,可进 CI;需要语义理解的翻译同步交给 `gitlink-doc-sync` Skill。
|
||||
- **围栏内解析**:代码块内的 `#` 不会被误判为标题;表格分隔行不计入行数。
|
||||
- **安全默认**:dry-run 为默认行为,`--apply` 才写远端,且只创建 tracking issue(可追溯、可关闭),不直接改文档。
|
||||
- **CLI 为唯一依赖**:所有平台交互都通过 `gitlink-cli`(`repo +tree` / `file +view` / `issue +create`),无直接 HTTP 调用。
|
||||
|
||||
> 依赖 `file` 快捷命令组(PR #330);在其合并前可用 `--cli` 指向包含该命令的本地构建。
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# 真实平台验证记录
|
||||
|
||||
验证日期:2026-07-05;平台:生产环境 gitlink.org.cn;CLI:包含 `file` 命令组(PR #330)的本地构建。
|
||||
|
||||
## 1. 文档对自动发现 + 漂移检测(只读,dry-run)
|
||||
|
||||
```bash
|
||||
python3 scripts/doc_sync_workflow.py --owner Taoyouce --repo gitlink-cli --output-dir outputs
|
||||
```
|
||||
|
||||
结果(完整报告见 [`../examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md`](../examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md)):
|
||||
|
||||
- `repo +tree` 拉取根目录后按命名约定**自动识别** `README.md ⇄ README.zh-CN.md`
|
||||
- `file +view --raw` 拉取两版全文后检出**真实存在的漂移**:
|
||||
- 🟡 代码块数不一致:英文 32 个 vs 中文 29 个
|
||||
- 🟡 表格行数不一致:英文 43 行 vs 中文 40 行(功能表滞后)
|
||||
- 退出码 `0`(无严重漂移)
|
||||
|
||||
该漂移与本仓库实际情况一致:英文 README 的部分示例段落未同步到中文版。
|
||||
|
||||
## 2. `--apply` 真实回写 tracking issue
|
||||
|
||||
```bash
|
||||
python3 scripts/doc_sync_workflow.py --owner Taoyouce --repo gitlink-cli --apply
|
||||
```
|
||||
|
||||
- `issue +create` 成功在 fork 仓库创建 tracking issue:
|
||||
`[doc-sync] 中英文档漂移报告`(issue #1)
|
||||
- 通过 `issue +list` API 回执确认:`project_issues_index=1`,正文为完整漂移报告
|
||||
|
||||
## 3. 确定性回归护栏
|
||||
|
||||
```bash
|
||||
python3 tests/test_drift.py
|
||||
# Ran 9 tests ... OK
|
||||
```
|
||||
|
||||
覆盖:结构解析(标题/代码块/表格/版本号)、代码围栏内标题不误判、四类漂移全部检出、相同文档零发现、同输入同输出确定性、文档对发现约定、报告渲染。
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# 文档一致性报告:Taoyouce/gitlink-cli(ref: 默认分支)
|
||||
|
||||
## README.md ⇄ README.zh-CN.md
|
||||
|
||||
| 等级 | 类型 | 位置 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 🟡 中等 | 代码块数不一致 | 全文代码示例 | README.md 有 32 个代码块,README.zh-CN.md 有 29 个 |
|
||||
| 🟡 中等 | 表格行数不一致 | 全文表格 | README.md 共 43 行表格,README.zh-CN.md 共 40 行,疑似功能表滞后 |
|
||||
|
||||
---
|
||||
*由 doc-sync-automation 工作流生成(确定性结构比对,同输入同输出)。*
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/env python3
|
||||
"""中英文档一致性守护工作流(doc-sync-automation)。
|
||||
|
||||
采集 → 检测 → 报告 → 回写(可选):
|
||||
1. 采集:通过 gitlink-cli 发现双语文档对并拉取两版内容
|
||||
2. 检测:确定性结构比对(章节大纲 / 代码块 / 表格行数 / 版本号)
|
||||
3. 报告:输出分级(严重/中等/轻微)Markdown 漂移报告
|
||||
4. 回写:--apply 时把报告作为 tracking issue 提交到 GitLink
|
||||
|
||||
纯标准库实现(Python >= 3.9),gitlink-cli 为唯一外部依赖。
|
||||
默认 dry-run,不写远端。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
# 翻译文档命名约定:主文档 X.md -> X{后缀}
|
||||
TRANSLATION_SUFFIXES = [".zh-CN.md", "_zh.md", ".zh.md", "-zh.md"]
|
||||
|
||||
SEVERITY_ORDER = {"严重": 0, "中等": 1, "轻微": 2}
|
||||
SEVERITY_ICON = {"严重": "🔴", "中等": "🟡", "轻微": "🟢"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Finding:
|
||||
severity: str # 严重 | 中等 | 轻微
|
||||
category: str
|
||||
location: str
|
||||
detail: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocStructure:
|
||||
headings: list = field(default_factory=list) # [(level, text)]
|
||||
code_blocks: int = 0
|
||||
code_lines: int = 0
|
||||
table_rows: int = 0
|
||||
versions: list = field(default_factory=list) # 形如 Go 1.26 / v0.2.0 的版本号
|
||||
|
||||
|
||||
def run_cli(args, cli="gitlink-cli"):
|
||||
"""调用 gitlink-cli 并返回 stdout 文本。"""
|
||||
result = subprocess.run(
|
||||
[cli, *args], capture_output=True, text=True, check=False
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"gitlink-cli {' '.join(args)} 失败: {result.stderr.strip() or result.stdout.strip()}"
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def fetch_file(owner, repo, path, ref, cli="gitlink-cli"):
|
||||
args = ["file", "+view", "--owner", owner, "--repo", repo, "--path", path, "--raw"]
|
||||
if ref:
|
||||
args += ["--ref", ref]
|
||||
return run_cli(args, cli=cli)
|
||||
|
||||
|
||||
def list_entries(owner, repo, ref, path="", cli="gitlink-cli"):
|
||||
args = ["repo", "+tree", "--owner", owner, "--repo", repo, "--format", "json"]
|
||||
if ref:
|
||||
args += ["--ref", ref]
|
||||
if path:
|
||||
args += ["--path", path]
|
||||
out = run_cli(args, cli=cli)
|
||||
payload = json.loads(out)
|
||||
data = payload.get("data", payload)
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
entries = data.get("entries", data) if isinstance(data, dict) else data
|
||||
names = []
|
||||
if isinstance(entries, list):
|
||||
for e in entries:
|
||||
if isinstance(e, dict) and e.get("name"):
|
||||
names.append(e["name"])
|
||||
return names
|
||||
|
||||
|
||||
def discover_pairs(names):
|
||||
"""按命名约定从文件名(可含路径前缀)列表中发现文档对。"""
|
||||
nameset = set(names)
|
||||
pairs = []
|
||||
for name in sorted(nameset):
|
||||
if not name.endswith(".md") or any(name.endswith(s) for s in TRANSLATION_SUFFIXES):
|
||||
continue
|
||||
stem = name[: -len(".md")]
|
||||
for suffix in TRANSLATION_SUFFIXES:
|
||||
translation = stem + suffix
|
||||
if translation in nameset:
|
||||
pairs.append((name, translation))
|
||||
break
|
||||
return pairs
|
||||
|
||||
|
||||
VERSION_RE = re.compile(r"\b(?:go|node(?:\.js)?|python|v)\s?(\d+\.\d+(?:\.\d+)?)\b", re.I)
|
||||
|
||||
|
||||
def parse_structure(text):
|
||||
"""提取文档结构:标题大纲、代码块、表格行、版本号。"""
|
||||
s = DocStructure()
|
||||
in_code = False
|
||||
code_lines = 0
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("```"):
|
||||
if in_code:
|
||||
s.code_blocks += 1
|
||||
s.code_lines += code_lines
|
||||
code_lines = 0
|
||||
in_code = not in_code
|
||||
continue
|
||||
if in_code:
|
||||
code_lines += 1
|
||||
continue
|
||||
m = re.match(r"^(#{1,6})\s+(.*)$", stripped)
|
||||
if m:
|
||||
s.headings.append((len(m.group(1)), m.group(2).strip()))
|
||||
continue
|
||||
if stripped.startswith("|") and stripped.endswith("|") and not re.match(r"^\|[\s:|-]+\|$", stripped):
|
||||
s.table_rows += 1
|
||||
s.versions.extend(v for v in VERSION_RE.findall(line))
|
||||
return s
|
||||
|
||||
|
||||
def compare_structures(base_path, trans_path, base, trans):
|
||||
"""确定性漂移检测,返回 Finding 列表。"""
|
||||
findings = []
|
||||
|
||||
# 1. 章节数量漂移(结构不对齐 = 严重信号)
|
||||
b_top = [h for h in base.headings if h[0] <= 2]
|
||||
t_top = [h for h in trans.headings if h[0] <= 2]
|
||||
if len(b_top) != len(t_top):
|
||||
more, fewer = (base_path, trans_path) if len(b_top) > len(t_top) else (trans_path, base_path)
|
||||
findings.append(Finding(
|
||||
"严重", "章节数不一致", "一级/二级标题",
|
||||
f"{more} 有 {max(len(b_top), len(t_top))} 节,{fewer} 只有 {min(len(b_top), len(t_top))} 节,疑似缺失章节",
|
||||
))
|
||||
|
||||
# 2. 代码块漂移(示例不同步 = 中等)
|
||||
if base.code_blocks != trans.code_blocks:
|
||||
findings.append(Finding(
|
||||
"中等", "代码块数不一致", "全文代码示例",
|
||||
f"{base_path} 有 {base.code_blocks} 个代码块,{trans_path} 有 {trans.code_blocks} 个",
|
||||
))
|
||||
elif abs(base.code_lines - trans.code_lines) > max(5, base.code_lines // 20):
|
||||
findings.append(Finding(
|
||||
"中等", "代码行数漂移", "全文代码示例",
|
||||
f"代码行数 {base.code_lines} vs {trans.code_lines},差异超过 5%",
|
||||
))
|
||||
|
||||
# 3. 表格行数漂移(功能表滞后 = 中等)
|
||||
if base.table_rows != trans.table_rows:
|
||||
findings.append(Finding(
|
||||
"中等", "表格行数不一致", "全文表格",
|
||||
f"{base_path} 共 {base.table_rows} 行表格,{trans_path} 共 {trans.table_rows} 行,疑似功能表滞后",
|
||||
))
|
||||
|
||||
# 4. 版本号漂移(轻微)
|
||||
b_ver, t_ver = sorted(set(base.versions)), sorted(set(trans.versions))
|
||||
if b_ver != t_ver:
|
||||
only_b = [v for v in b_ver if v not in t_ver]
|
||||
only_t = [v for v in t_ver if v not in b_ver]
|
||||
findings.append(Finding(
|
||||
"轻微", "版本号不一致", "安装/依赖说明",
|
||||
f"仅 {base_path} 出现: {only_b or '无'};仅 {trans_path} 出现: {only_t or '无'}",
|
||||
))
|
||||
|
||||
findings.sort(key=lambda f: SEVERITY_ORDER[f.severity])
|
||||
return findings
|
||||
|
||||
|
||||
def render_report(owner, repo, ref, results):
|
||||
lines = [f"# 文档一致性报告:{owner}/{repo}(ref: {ref or '默认分支'})", ""]
|
||||
total = sum(len(f) for _, _, f in results)
|
||||
if total == 0:
|
||||
lines.append("✅ 所有文档对结构一致,未检测到漂移。")
|
||||
for base_path, trans_path, findings in results:
|
||||
lines.append(f"## {base_path} ⇄ {trans_path}")
|
||||
lines.append("")
|
||||
if not findings:
|
||||
lines.append("✅ 无漂移。")
|
||||
lines.append("")
|
||||
continue
|
||||
lines.append("| 等级 | 类型 | 位置 | 说明 |")
|
||||
lines.append("|------|------|------|------|")
|
||||
for f in findings:
|
||||
lines.append(f"| {SEVERITY_ICON[f.severity]} {f.severity} | {f.category} | {f.location} | {f.detail} |")
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("*由 doc-sync-automation 工作流生成(确定性结构比对,同输入同输出)。*")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def create_tracking_issue(owner, repo, report, cli="gitlink-cli"):
|
||||
out = run_cli([
|
||||
"issue", "+create", "--owner", owner, "--repo", repo,
|
||||
"--title", "[doc-sync] 中英文档漂移报告",
|
||||
"--body", report,
|
||||
"--format", "json",
|
||||
], cli=cli)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="中英文档一致性守护工作流")
|
||||
parser.add_argument("--owner", required=True)
|
||||
parser.add_argument("--repo", required=True)
|
||||
parser.add_argument("--ref", default="")
|
||||
parser.add_argument("--pair", action="append", default=[],
|
||||
help="手动指定文档对,格式 base.md:translation.md,可多次")
|
||||
parser.add_argument("--apply", action="store_true",
|
||||
help="把漂移报告作为 tracking issue 回写到 GitLink(默认 dry-run)")
|
||||
parser.add_argument("--output-dir", default="outputs")
|
||||
parser.add_argument("--cli", default="gitlink-cli")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.pair:
|
||||
pairs = [tuple(p.split(":", 1)) for p in args.pair]
|
||||
else:
|
||||
names = list_entries(args.owner, args.repo, args.ref, cli=args.cli)
|
||||
if "docs" in names:
|
||||
names += [
|
||||
f"docs/{n}"
|
||||
for n in list_entries(args.owner, args.repo, args.ref, path="docs", cli=args.cli)
|
||||
]
|
||||
pairs = discover_pairs(names)
|
||||
if not pairs:
|
||||
print("未发现双语文档对(可用 --pair 手动指定)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
results = []
|
||||
for base_path, trans_path in pairs:
|
||||
base_text = fetch_file(args.owner, args.repo, base_path, args.ref, cli=args.cli)
|
||||
trans_text = fetch_file(args.owner, args.repo, trans_path, args.ref, cli=args.cli)
|
||||
findings = compare_structures(
|
||||
base_path, trans_path, parse_structure(base_text), parse_structure(trans_text)
|
||||
)
|
||||
results.append((base_path, trans_path, findings))
|
||||
|
||||
report = render_report(args.owner, args.repo, args.ref, results)
|
||||
out_dir = Path(args.output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
report_path = out_dir / f"doc-sync-{args.owner}-{args.repo}.md"
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
print(report)
|
||||
print(f"\n报告已保存:{report_path}", file=sys.stderr)
|
||||
|
||||
severe = sum(1 for _, _, fs in results for f in fs if f.severity == "严重")
|
||||
if args.apply and any(fs for _, _, fs in results):
|
||||
create_tracking_issue(args.owner, args.repo, report, cli=args.cli)
|
||||
print("已创建 tracking issue。", file=sys.stderr)
|
||||
|
||||
return 2 if severe else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
"""确定性回归护栏:同输入 → 同发现 → 同退出语义。"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
|
||||
from doc_sync_workflow import ( # noqa: E402
|
||||
compare_structures,
|
||||
discover_pairs,
|
||||
parse_structure,
|
||||
render_report,
|
||||
)
|
||||
|
||||
EN = """# Title
|
||||
|
||||
## Features
|
||||
|
||||
| a | b |
|
||||
|---|---|
|
||||
| 1 | 2 |
|
||||
| 3 | 4 |
|
||||
|
||||
## Install
|
||||
|
||||
Requires Go 1.26+.
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
run --help
|
||||
```
|
||||
"""
|
||||
|
||||
ZH = """# 标题
|
||||
|
||||
## 功能
|
||||
|
||||
| a | b |
|
||||
|---|---|
|
||||
| 1 | 2 |
|
||||
|
||||
## 安装
|
||||
|
||||
需要 Go 1.25+。
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class ParseStructureTest(unittest.TestCase):
|
||||
def test_parse(self):
|
||||
s = parse_structure(EN)
|
||||
self.assertEqual([h[1] for h in s.headings], ["Title", "Features", "Install", "Usage"])
|
||||
self.assertEqual(s.code_blocks, 2)
|
||||
self.assertEqual(s.table_rows, 3) # 表头 1 行 + 数据 2 行
|
||||
self.assertIn("1.26", s.versions)
|
||||
|
||||
def test_code_fence_content_not_parsed_as_heading(self):
|
||||
text = "```bash\n# not a heading\n```\n## Real\n"
|
||||
s = parse_structure(text)
|
||||
self.assertEqual([h[1] for h in s.headings], ["Real"])
|
||||
|
||||
|
||||
class CompareTest(unittest.TestCase):
|
||||
def test_drift_detected(self):
|
||||
findings = compare_structures(
|
||||
"README.md", "README.zh-CN.md", parse_structure(EN), parse_structure(ZH)
|
||||
)
|
||||
categories = [f.category for f in findings]
|
||||
self.assertIn("章节数不一致", categories) # 缺 Usage 节 → 严重
|
||||
self.assertIn("代码块数不一致", categories) # 2 vs 1 → 中等
|
||||
self.assertIn("表格行数不一致", categories) # 3 vs 2 → 中等
|
||||
self.assertIn("版本号不一致", categories) # 1.26 vs 1.25 → 轻微
|
||||
# 严重排在最前
|
||||
self.assertEqual(findings[0].severity, "严重")
|
||||
|
||||
def test_identical_docs_no_findings(self):
|
||||
findings = compare_structures(
|
||||
"a.md", "b.md", parse_structure(EN), parse_structure(EN)
|
||||
)
|
||||
self.assertEqual(findings, [])
|
||||
|
||||
def test_deterministic(self):
|
||||
f1 = compare_structures("a", "b", parse_structure(EN), parse_structure(ZH))
|
||||
f2 = compare_structures("a", "b", parse_structure(EN), parse_structure(ZH))
|
||||
self.assertEqual([vars(f) for f in f1], [vars(f) for f in f2])
|
||||
|
||||
|
||||
class DiscoverTest(unittest.TestCase):
|
||||
def test_discover(self):
|
||||
names = ["README.md", "README.zh-CN.md", "LICENSE", "CONTRIBUTING.md"]
|
||||
self.assertEqual(discover_pairs(names), [("README.md", "README.zh-CN.md")])
|
||||
|
||||
def test_no_pair(self):
|
||||
self.assertEqual(discover_pairs(["README.md", "LICENSE"]), [])
|
||||
|
||||
def test_discover_generic_and_nested(self):
|
||||
names = ["docs/guide.md", "docs/guide.zh-CN.md", "USAGE.md", "USAGE_zh.md", "NOTES.zh.md"]
|
||||
self.assertEqual(
|
||||
discover_pairs(names),
|
||||
[("USAGE.md", "USAGE_zh.md"), ("docs/guide.md", "docs/guide.zh-CN.md")],
|
||||
)
|
||||
|
||||
def test_translation_file_not_treated_as_base(self):
|
||||
self.assertEqual(discover_pairs(["README.zh-CN.md", "README_zh.md"]), [])
|
||||
|
||||
|
||||
class ReportTest(unittest.TestCase):
|
||||
def test_report_contains_findings(self):
|
||||
findings = compare_structures(
|
||||
"README.md", "README.zh-CN.md", parse_structure(EN), parse_structure(ZH)
|
||||
)
|
||||
report = render_report("o", "r", "master", [("README.md", "README.zh-CN.md", findings)])
|
||||
self.assertIn("🔴 严重", report)
|
||||
self.assertIn("README.md ⇄ README.zh-CN.md", report)
|
||||
|
||||
def test_report_clean(self):
|
||||
report = render_report("o", "r", "", [("a.md", "b.md", [])])
|
||||
self.assertIn("无漂移", report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue