mirror of
https://github.com/tradecatlabs/vibe-coding-cn.git
synced 2026-11-02 09:56:12 +00:00
fix: check-links - handle uninitialized Gitlink targets (#243)
This commit is contained in:
+1
-1
@@ -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` |
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user