fix: check-links - handle uninitialized Gitlink targets (#243)

This commit is contained in:
tradecatlabs
2026-10-25 04:53:12 +08:00
committed by GitHub
parent 484c2051bb
commit 38efdb7f4d
5 changed files with 187 additions and 5 deletions
+1 -1
View File
@@ -36,7 +36,7 @@
| 数据与来源 | `check-gongfa.py`;本机研究副本另用`check-research-raw.py`、`check-source-facts.py` |
| Wiki | `check-wiki.py`;仍只校验明确给定的本地checkout |
| 资料维护 | `sync-doc-toc.py`、`sync-faqi-catalog.py`、`sync-gongfa-catalog.py`;`fetch-research-raw.py`会联网并写入本机研究资料,不属于默认测试 |
| 测试 | `test-gongfa.py`、`test-gongfa-catalog.py`、`test-faqi-catalog.py`、`test-markdown-details.py`、`test-source-archive.py`、`test-research-distribution.py` |
| 测试 | `test-gongfa.py`、`test-gongfa-catalog.py`、`test-faqi-catalog.py`、`test-local-links.py`、`test-markdown-details.py`、`test-source-archive.py`、`test-research-distribution.py` |
| 共享实现 | `source_boundaries.py`、`taxonomy.py`、`source_archive.py`、`book_views.py`、`tool_explanations.py`直接平铺;供同级入口与原资料生成器共用,不保留lib包 |
| 依赖 | `requirements-gongfa.txt`、`requirements-faqi.txt` |
+49
View File
@@ -3,7 +3,9 @@
from __future__ import annotations
import os
import re
import subprocess
import sys
import urllib.parse
from pathlib import Path
@@ -104,6 +106,45 @@ def should_skip(path: Path) -> bool:
return any(rel == prefix or prefix in rel.parents for prefix in SKIP_PREFIXES)
def gitlink_roots() -> tuple[Path, ...]:
"""读取已跟踪Gitlink目录,不初始化其工作树。"""
try:
completed = subprocess.run(
["git", "-C", str(ROOT), "ls-files", "--stage", "-z"],
check=False,
capture_output=True,
timeout=10,
)
except (OSError, subprocess.TimeoutExpired):
return ()
if completed.returncode != 0:
return ()
roots: set[Path] = set()
for entry in completed.stdout.split(b"\0"):
if not entry:
continue
try:
metadata, raw_path = entry.split(b"\t", 1)
except ValueError:
continue
if metadata.split(b" ", 1)[0] == b"160000":
roots.add((ROOT / os.fsdecode(raw_path)).resolve())
return tuple(sorted(roots))
def is_uninitialized_gitlink_target(destination: Path, roots: tuple[Path, ...]) -> bool:
for root in roots:
if os.path.lexists(root / ".git"):
continue
try:
destination.relative_to(root)
except ValueError:
continue
return True
return False
def normalize_target(raw: str) -> tuple[str, str]:
raw = raw.strip()
if not raw or raw.startswith(EXTERNAL_PREFIXES):
@@ -122,6 +163,8 @@ def main() -> int:
missing_anchors: list[tuple[Path, str, Path, str]] = []
anchor_cache: dict[Path, set[str]] = {}
checked_files = 0
uninitialized_gitlinks = gitlink_roots()
skipped_gitlink_targets = 0
for markdown_file in sorted(ROOT.rglob("*.md")):
if should_skip(markdown_file):
@@ -145,6 +188,9 @@ def main() -> int:
continue
if not destination.exists():
if is_uninitialized_gitlink_target(destination, uninitialized_gitlinks):
skipped_gitlink_targets += 1
continue
missing_links.append((markdown_file.relative_to(ROOT), raw, markdown_file.parent / target))
continue
if anchor and destination.suffix.lower() == ".md":
@@ -153,6 +199,9 @@ def main() -> int:
if anchor not in anchor_cache[destination]:
missing_anchors.append((markdown_file.relative_to(ROOT), raw, destination.relative_to(ROOT), anchor))
if skipped_gitlink_targets:
print(f"SKIPPED_UNINITIALIZED_GITLINK_TARGETS={skipped_gitlink_targets}")
if missing_links or missing_anchors:
print("MISSING_LINKS")
for source, raw, resolved in missing_links:
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""验证本地链接检查器对Gitlink和普通缺失目标的区分。"""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
GITLINK_PATH = ".github/extensions/tmux"
GITLINK_SHA = "0123456789abcdef0123456789abcdef01234567"
def run_git(repo: Path, *args: str) -> None:
subprocess.run(
["git", "-C", str(repo), *args],
check=True,
capture_output=True,
text=True,
timeout=10,
)
def build_fixture(
artifacts: Path,
checker: bytes,
boundaries: bytes,
name: str,
target: str,
*,
gitlink: bool = False,
initialized: bool = False,
) -> tuple[Path, Path]:
repo = artifacts / name
script = repo / ".github" / "check-local-links.py"
script.parent.mkdir(parents=True)
script.write_bytes(checker)
script.with_name("source_boundaries.py").write_bytes(boundaries)
(repo / "README.md").write_text("# Fixture\n", encoding="utf-8")
extensions = repo / ".github" / "extensions"
extensions.mkdir(parents=True)
(extensions / "README.md").write_text(f"[target]({target})\n", encoding="utf-8")
run_git(repo, "init", "--quiet")
if gitlink:
run_git(repo, "update-index", "--add", "--cacheinfo", f"160000,{GITLINK_SHA},{GITLINK_PATH}")
if initialized:
submodule = repo / GITLINK_PATH
submodule.mkdir(parents=True)
(submodule / ".git").write_text("gitdir: ../.git/modules/tmux\n", encoding="utf-8")
return repo, script
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--artifacts", type=Path)
args = parser.parse_args()
checker_path = Path(__file__).with_name("check-local-links.py")
checker = checker_path.read_bytes()
boundaries = checker_path.with_name("source_boundaries.py").read_bytes()
if args.artifacts:
artifacts = args.artifacts.expanduser().resolve()
artifacts.mkdir(parents=True, exist_ok=False)
else:
artifacts = Path(tempfile.mkdtemp(prefix="local-links-test-"))
cases = (
("uninitialized_gitlink", "tmux/README", True, False, 0),
("ordinary_missing_target", "missing.md", False, False, 1),
("initialized_gitlink_missing_target", "tmux/README", True, True, 1),
)
records = []
class LocalLinksTest(unittest.TestCase):
def test_gitlink_and_missing_target_boundaries(self):
for name, target, gitlink, initialized, expected_exit in cases:
with self.subTest(case=name):
repo, script = build_fixture(
artifacts,
checker,
boundaries,
name,
target,
gitlink=gitlink,
initialized=initialized,
)
command = [sys.executable, str(script)]
completed = subprocess.run(
command,
cwd=repo,
capture_output=True,
text=True,
timeout=10,
)
(artifacts / f"{name}.stdout.log").write_text(completed.stdout, encoding="utf-8")
(artifacts / f"{name}.stderr.log").write_text(completed.stderr, encoding="utf-8")
records.append({
"case": name,
"command": command,
"expected_exit_code": expected_exit,
"actual_exit_code": completed.returncode,
})
self.assertEqual(completed.returncode, expected_exit, completed.stdout + completed.stderr)
if name == "uninitialized_gitlink":
self.assertIn("SKIPPED_UNINITIALIZED_GITLINK_TARGETS=1", completed.stdout)
self.assertNotIn("MISSING_LINKS", completed.stdout)
else:
self.assertIn("MISSING_LINKS", completed.stdout)
self.assertIn("TOTAL_LINKS=1", completed.stdout)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(LocalLinksTest)
result = unittest.TextTestRunner(verbosity=2).run(suite)
report = {
"success": result.wasSuccessful(),
"cases": records,
"checker_sha256": hashlib.sha256(checker).hexdigest(),
"source_boundaries_sha256": hashlib.sha256(boundaries).hexdigest(),
"test_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
}
(artifacts / "result.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"success": report["success"], "cases": len(records), "artifacts": str(artifacts)}, ensure_ascii=False))
return 0 if result.wasSuccessful() else 1
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -61,7 +61,7 @@ Lambda演算已按用户要求加入功法,品级天级上品(现行`tian-ad
| 范围 | 命令 |
|---|---|
| Markdown与导航 | `make lint`、`make check-links`、`make check-details`、`make test-details` |
| Markdown与导航 | `make lint`、`make check-links`、`make test-local-links`、`make check-details`、`make test-details` |
| 结构与索引 | `make check-doc-structure`、`make check-directory-docs`、`make check-metadata` |
| 功法协议与视图 | `make check-gongfa`、`make test-gongfa`、`make sync-gongfa-catalog`、`make check-gongfa-catalog`、`make test-gongfa-catalog` |
| 法器与资料档案 | `make sync-faqi-catalog`、`make check-faqi-catalog`、`make test-faqi-catalog`、`make test-source-archive` |
+6 -3
View File
@@ -1,5 +1,5 @@
# Vibe Coding工具箱:资源分三域,测试与校验维护入口统一在.github。
.PHONY: help lint check-links check-details test-details check-doc-structure check-directory-docs check-metadata check-gongfa test-gongfa sync-gongfa-catalog check-gongfa-catalog test-gongfa-catalog sync-faqi-catalog check-faqi-catalog test-faqi-catalog test-source-archive test-research-distribution check-ai-citation check-research-raw check-source-facts test-local-research check-wiki fetch-research-raw sync-doc-toc build test clean clean-deps
.PHONY: help lint check-links test-local-links check-details test-details check-doc-structure check-directory-docs check-metadata check-gongfa test-gongfa sync-gongfa-catalog check-gongfa-catalog test-gongfa-catalog sync-faqi-catalog check-faqi-catalog test-faqi-catalog test-source-archive test-research-distribution check-ai-citation check-research-raw check-source-facts test-local-research check-wiki fetch-research-raw sync-doc-toc build test clean clean-deps
MARKDOWNLINT = npx --yes markdownlint-cli@0.48.0
MAINT = .github
@@ -9,7 +9,7 @@ SOURCE_IGNORES = --ignore .history --ignore '$(EXT)/tmux/**' --ignore '$(EXT)/oh
help:
@echo "Vibe Coding工具箱"
@echo "lint / check-links / check-details / test-details:Markdown格式、内链、折叠与回归"
@echo "lint / check-links / test-local-links / check-details / test-details:Markdown格式、内链、折叠与回归"
@echo "check-doc-structure / check-directory-docs / check-metadata:阅读结构、目录职责与机器索引"
@echo "check-gongfa / test-gongfa:功法来源与既有评级协议"
@echo "sync-gongfa-catalog / check-gongfa-catalog / test-gongfa-catalog:功法四阶Markdown视图"
@@ -30,6 +30,9 @@ lint:
check-links:
@python3 $(MAINT)/check-local-links.py
test-local-links:
@python3 $(MAINT)/test-local-links.py
check-details:
@python3 $(MAINT)/check-markdown-details.py
@@ -99,7 +102,7 @@ sync-doc-toc:
build:
@echo "无需构建;本仓是文档、既有Skills和小工具的资源集合。"
test: lint check-links check-details test-details check-doc-structure check-directory-docs check-metadata check-gongfa test-gongfa check-gongfa-catalog test-gongfa-catalog check-faqi-catalog test-faqi-catalog test-source-archive test-research-distribution check-ai-citation
test: lint check-links test-local-links check-details test-details check-doc-structure check-directory-docs check-metadata check-gongfa test-gongfa check-gongfa-catalog test-gongfa-catalog check-faqi-catalog test-faqi-catalog test-source-archive test-research-distribution check-ai-citation
@echo "本地默认质量检查完成;不含本机研究档案专用检查,也不代表工具效果、运行可用性或许可批准。"
clean: clean-deps