🔧 chore(integration): merge CI maintenance updates

This commit is contained in:
csh
2026-08-20 16:13:17 +08:00
14 changed files with 892 additions and 366 deletions
+151
View File
@@ -0,0 +1,151 @@
import importlib.util
import io
import sys
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from types import SimpleNamespace
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = REPO_ROOT / "skills/gitea-fix-ci/scripts/fetch_ci_logs.py"
SPEC = importlib.util.spec_from_file_location("gitea_fetch_ci_logs", SCRIPT_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class FakeClient:
def __init__(self, payload):
self.payload = payload
self.urls = []
def get_json(self, url):
self.urls.append(url)
return self.payload
class FetchCILogsRunsTests(unittest.TestCase):
def setUp(self):
self.target = MODULE.RepoTarget(
base_url="https://gitea.example.test",
owner="owner",
repo="repo",
)
def test_run_row_preserves_workflow_provenance(self):
sha = "a" * 40
row = MODULE._run_row(
{
"id": 42,
"path": "checks.yml@refs/heads/main",
"display_title": "CI title",
"event": "workflow_run",
"status": "completed",
"conclusion": "failure",
"head_branch": "main",
"head_sha": sha,
"started_at": "2026-08-20T10:00:00+08:00",
"completed_at": "2026-08-20T10:01:00+08:00",
"run_number": 9,
"run_attempt": 2,
}
)
self.assertEqual(row["name"], "checks.yml")
self.assertEqual(row["workflow"], "checks.yml")
self.assertEqual(row["path"], "checks.yml@refs/heads/main")
self.assertEqual(row["title"], "CI title")
self.assertEqual(row["sha"], sha)
self.assertEqual(row["run_number"], 9)
self.assertEqual(row["run_attempt"], 2)
def test_workflow_filter_uses_workflow_endpoint_and_all_omits_status(self):
sha = "b" * 40
args = SimpleNamespace(
workflow="checks.yml",
branch=None,
event="workflow_run",
status="all",
sha=None,
limit=30,
json=True,
)
client = FakeClient(
{
"workflow_runs": [
{
"id": 7,
"path": "checks.yml@refs/heads/main",
"event": "workflow_run",
"conclusion": "success",
"head_sha": sha,
}
]
}
)
stdout = io.StringIO()
with redirect_stdout(stdout):
result = MODULE.cmd_runs(args, self.target, client)
self.assertEqual(result, 0)
self.assertIn("/actions/workflows/checks.yml/runs?", client.urls[0])
self.assertIn("event=workflow_run", client.urls[0])
self.assertIn("limit=30", client.urls[0])
self.assertNotIn("status=", client.urls[0])
self.assertIn(sha, stdout.getvalue())
def test_sha_filter_sends_and_preserves_full_sha(self):
sha = "c" * 40
args = SimpleNamespace(
workflow="prepare.yml",
branch=None,
event="pull_request",
status="all",
sha=sha,
limit=20,
json=True,
)
client = FakeClient(
{
"workflow_runs": [
{
"id": 8,
"path": "prepare.yml@refs/pull/42/head",
"event": "pull_request",
"conclusion": "success",
"head_sha": sha,
}
]
}
)
stdout = io.StringIO()
with redirect_stdout(stdout):
result = MODULE.cmd_runs(args, self.target, client)
self.assertEqual(result, 0)
self.assertIn(f"head_sha={sha}", client.urls[0])
self.assertIn(sha, stdout.getvalue())
def test_sha_filter_rejects_abbreviated_sha(self):
args = SimpleNamespace(
workflow="prepare.yml",
branch=None,
event="pull_request",
status="all",
sha="abc123",
limit=20,
json=True,
)
client = FakeClient({"workflow_runs": []})
with self.assertRaisesRegex(MODULE.ConfigError, "full 40-character"):
MODULE.cmd_runs(args, self.target, client)
self.assertEqual(client.urls, [])
if __name__ == "__main__":
unittest.main()
+172 -7
View File
@@ -1,4 +1,5 @@
import importlib.util
import re
import subprocess
import sys
import tempfile
@@ -275,6 +276,104 @@ class TslSyntaxReferenceTests(unittest.TestCase):
self.assertTrue(any("assignment" in message for message in messages))
def test_quickstart_owner_drift_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
write_reference(
references,
"01_quickstart.md",
"""
# Quickstart
## 本篇职责
<!-- section-id: syntax-01-001 -->
派生摘要。
## 语言核心事实速查
<!-- section-id: syntax-01-002 -->
<!-- quickstart-rule: assignment -->
- 普通赋值使用 `:=`。
Owner Section`syntax-02-999`
""",
)
write_reference(
references,
"02_topic.md",
"""
# Topic
## 本篇职责
<!-- section-id: syntax-02-001 -->
完整事实源。
## 核心规则
<!-- section-id: syntax-02-002 -->
<!-- quickstart-rule: assignment -->
- 普通赋值使用 `:=`。
""",
)
messages = [item.message for item in lookup.validate_references(references)]
self.assertTrue(any("Owner Section 不存在" in message for message in messages))
def test_quickstart_routes_every_derived_rule_to_its_owner_section(self):
quickstart = run_lookup("--section", "syntax-01-002")
self.assertEqual(0, quickstart.returncode, quickstart.stderr)
owner_ids = set(
re.findall(r"Owner Section`(syntax-\d{2}-\d{3})`", quickstart.stdout)
)
self.assertEqual(
{
"syntax-02-002",
"syntax-03-002",
"syntax-03-004",
"syntax-05-002",
"syntax-06-004",
"syntax-08-002",
"syntax-09-002",
},
owner_ids,
)
for owner_id in owner_ids:
with self.subTest(owner_id=owner_id):
owner = run_lookup("--section", owner_id)
self.assertEqual(0, owner.returncode, owner.stderr)
def test_oversized_leaf_section_fails_structure_check(self):
with tempfile.TemporaryDirectory() as temp_dir:
references = Path(temp_dir)
long_body = "\n".join(f"事实行 {index}" for index in range(181))
write_reference(
references,
"99_fixture.md",
"# Fixture\n\n"
"## 本篇职责\n\n"
"<!-- section-id: syntax-99-001 -->\n\n"
"测试职责。\n\n"
"## 过长事实段\n\n"
"<!-- section-id: syntax-99-002 -->\n\n"
f"{long_body}\n",
)
result = run_lookup(
"--check",
"--references-dir",
str(references),
)
self.assertEqual(1, result.returncode)
self.assertIn("叶子 Section 正文超过 180 行", result.stderr)
def test_missing_references_are_installation_errors_for_every_action(self):
with tempfile.TemporaryDirectory() as temp_dir:
missing = Path(temp_dir) / "missing"
@@ -317,6 +416,25 @@ class TslSyntaxReferenceTests(unittest.TestCase):
self.assertEqual("syntax-05-008", write_result.matches[0].section.id)
self.assertEqual("syntax-02-006", diagnose_result.matches[0].section.id)
def test_object_and_class_queries_return_focused_sections(self):
cases = {
"成员访问可见性": "syntax-08-013",
"类外实现": "syntax-08-014",
"固定索引 property": "syntax-08-015",
"参数化 property": "syntax-08-016",
"方法隐藏 hide": "syntax-08-017",
"调用父类 inherited": "syntax-08-018",
"析构 destroy": "syntax-08-019",
}
for query, expected_id in cases.items():
with self.subTest(query=query):
result = lookup.query_sections(query, "explain", limit=1)
self.assertTrue(result.matches, query)
self.assertEqual(expected_id, result.matches[0].section.id)
section = run_lookup("--section", expected_id)
self.assertEqual(0, section.returncode, section.stderr)
self.assertLessEqual(len(section.stdout.splitlines()), 186)
def test_external_call_queries_retrieve_platform_and_abi_boundaries(self):
cases = {
"动态库常驻": "syntax-17-012",
@@ -438,18 +556,65 @@ class TslSyntaxReferenceTests(unittest.TestCase):
self.assertNotIn("<!-- section-id:", output)
self.assertNotIn("<!-- quickstart-rule:", output)
def test_skill_contract_uses_extracted_queries_and_deliverable_api_checks(self):
def test_skill_contract_uses_extracted_queries_and_scope_checks(self):
skill = SKILL_PATH.read_text(encoding="utf-8")
help_text = lookup._parser().format_help()
help_result = run_lookup("--help")
self.assertEqual(0, help_result.returncode, help_result.stderr)
help_text = help_result.stdout
self.assertIn("保留这些词在用户原话中", skill)
self.assertIn("不传完整句", skill)
self.assertNotIn("## 构造查询词", skill)
self.assertNotIn("用户怎么说就怎么传", skill)
self.assertIn("从用户原话提取", help_text)
self.assertIn("保留原写法但不传完整用户句", help_text)
self.assertIn("不传完整用户句", help_text)
self.assertIn("面向用户交付的 TSL/TSF 代码", skill)
self.assertIn("每个 builtin/API", skill)
self.assertIn("代码交付只要依赖这些未验证事实", skill)
self.assertIn("不声称签名、行为、可用性或输出", skill)
self.assertIn("纯语法说明", skill)
self.assertIn("不得声称该占位调用的 API 行为或输出", skill)
self.assertIn("按「缺口时停止」处理", skill)
def test_help_owns_cli_details_and_skill_defers_to_it(self):
skill = SKILL_PATH.read_text(encoding="utf-8")
result = run_lookup("--help")
self.assertEqual(0, result.returncode, result.stderr)
for text in (
"一次 --query 只覆盖一个语法要素",
"Required: yes",
"Owner Section",
"混合候选",
"180 行粒度上限",
"退出码",
):
with self.subTest(text=text):
self.assertIn(text, result.stdout)
self.assertIn("构造任一命令前先运行", skill)
self.assertIn("scripts/lookup.py --help", skill)
self.assertIn("不用于查询 API 签名", skill)
self.assertIn("不用于判断解释器、平台或运行方式", skill)
def test_skill_is_self_contained_and_does_not_reference_host_rules(self):
skill = SKILL_PATH.read_text(encoding="utf-8")
for forbidden in (
"AGENTS.md",
"AGENT_RULES",
".agents/",
"CONTEXT.md",
"memory-bank",
"docs/",
"tsl-api-reference",
"data/README.md",
"项目脚本",
"项目文档",
"CI",
):
with self.subTest(forbidden=forbidden):
self.assertNotIn(forbidden, skill)
self.assertNotRegex(skill, r"\[[^\]]+\]\([^)]*\.md[^)]*\)")
self.assertEqual(
{"SKILL.md"},
set(re.findall(r"[A-Za-z0-9_./-]+\.md", skill)),
)
def test_ci_runs_syntax_structure_and_format_gates(self):
workflow = CI_PATH.read_text(encoding="utf-8")