feat(ack): add project knowledge guardrails

This commit is contained in:
2026-07-31 21:09:36 +08:00
parent 7d1994cf93
commit ee66dbe9ce
32 changed files with 8353 additions and 139 deletions
+937
View File
@@ -0,0 +1,937 @@
from __future__ import annotations
import copy
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
ACK_DIR = REPO_ROOT / "skills" / "ack"
SCRIPTS_DIR = ACK_DIR / "scripts"
SCHEMA_PATH = ACK_DIR / "templates" / "knowledge.schema.json"
EXAMPLE_PATH = ACK_DIR / "examples" / "knowledge.example.yaml"
TASKS_EXAMPLE_PATH = ACK_DIR / "examples" / "tasks.example.yaml"
sys.path.insert(0, str(SCRIPTS_DIR))
import select_knowledge # noqa: E402
import validate_knowledge # noqa: E402
def example_data() -> dict:
return yaml.safe_load(EXAMPLE_PATH.read_text(encoding="utf-8"))
class AckKnowledgeTests(unittest.TestCase):
def test_schema_template_and_example_are_aligned(self) -> None:
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
template = yaml.safe_load(
(ACK_DIR / "templates" / "knowledge.template.yaml").read_text(
encoding="utf-8"
)
)
self.assertEqual(
schema["definitions"]["entry"]["properties"]["kind"]["enum"],
["guardrail", "pitfall", "verification"],
)
self.assertEqual(
schema["definitions"]["entry"]["properties"]["status"]["enum"],
["active", "stale", "superseded", "archived"],
)
self.assertEqual(template["entries"], [])
self.assertEqual(template["verificationRegistry"], {})
errors, _ = validate_knowledge.validate_all(
example_data(), SCHEMA_PATH, use_schema=False
)
self.assertEqual(errors, [])
self.assertEqual(
validate_knowledge.stable_ref(example_data()["entries"][0]), "K-001@1"
)
def test_semantics_run_even_when_schema_reports_no_errors(self) -> None:
data = example_data()
data["entries"].append(copy.deepcopy(data["entries"][0]))
with mock.patch.object(
validate_knowledge, "validate_with_schema", return_value=[]
):
errors, mode = validate_knowledge.validate_all(
data, SCHEMA_PATH, use_schema=True
)
self.assertIn("内置语义", mode)
self.assertTrue(any("稳定引用 K-001@1 重复" in error for error in errors))
def test_blank_required_text_fails_in_builtin_and_schema_modes(self) -> None:
data = example_data()
data["entries"][0]["title"] = " \t "
builtin_errors, _ = validate_knowledge.validate_all(
data, SCHEMA_PATH, use_schema=False
)
with mock.patch.object(
validate_knowledge, "validate_with_schema", return_value=[]
):
schema_errors, _ = validate_knowledge.validate_all(
data, SCHEMA_PATH, use_schema=True
)
self.assertTrue(any("title: 必须是非空字符串" in e for e in builtin_errors))
self.assertTrue(any("title: 必须是非空字符串" in e for e in schema_errors))
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
self.assertEqual(
schema["definitions"]["entry"]["properties"]["title"]["pattern"],
"\\S",
)
def test_temporary_entry_requires_removal_condition_and_review_date(self) -> None:
data = example_data()
entry = data["entries"][0]
entry["temporary"] = True
entry["removalCondition"] = None
entry["reviewAfter"] = None
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("removalCondition" in error for error in errors))
self.assertTrue(any("reviewAfter" in error for error in errors))
def test_overdue_active_entry_requires_revalidation(self) -> None:
data = example_data()
data["entries"][0]["reviewAfter"] = "2020-01-01T00:00:00+00:00"
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("必须重新验证或标记 stale" in error for error in errors))
def test_global_active_entry_requires_decision_owner_approval(self) -> None:
data = example_data()
data["entries"][0]["scope"] = {
"all": True,
"components": [],
"paths": [],
"dependencies": [],
"versions": [],
"tags": [],
"symbols": [],
"errorSignatures": [],
}
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("Decision Owner approval" in error for error in errors))
def test_active_conflicts_and_duplicate_active_revision_are_rejected(self) -> None:
data = example_data()
conflicting = copy.deepcopy(data["entries"][0])
conflicting.update(
{
"id": "K-002",
"subject": "service-process-alignment",
"conflictsWith": ["K-001@1"],
}
)
data["entries"].append(conflicting)
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("active 条目不能与 active K-001@1 冲突" in e for e in errors))
conflicting["id"] = "K-001"
conflicting["revision"] = 2
conflicting["conflictsWith"] = []
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("多个 active revision" in error for error in errors))
def test_verification_ref_must_resolve_to_contained_registry_target(self) -> None:
data = example_data()
data["entries"][0]["verification"]["ref"] = "missing-check"
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("未在 verificationRegistry 注册" in e for e in errors))
data = example_data()
data["verificationRegistry"]["service-worktree-alignment"]["path"] = "../run.sh"
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("必须是仓库内相对路径" in error for error in errors))
def test_verification_registry_rejects_any_symlink_component(self) -> None:
data = example_data()
data["verificationRegistry"]["service-worktree-alignment"]["path"] = (
"checks/verify.py"
)
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
outside = base / "outside"
project.mkdir()
outside.mkdir()
(project / "checks").symlink_to(outside, target_is_directory=True)
errors = validate_knowledge.validate_semantics(
data, project_root=project
)
self.assertTrue(any("symlink" in error for error in errors))
data = example_data()
data["verificationRegistry"]["service-worktree-alignment"]["path"] = (
"checks/verify.py"
)
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
real_checks = project / "real-checks"
real_checks.mkdir(parents=True)
target = real_checks / "verify.py"
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(target.stat().st_mode | 0o111)
(project / "checks").symlink_to(
real_checks,
target_is_directory=True,
)
errors = validate_knowledge.validate_semantics(
data,
project_root=project,
)
self.assertTrue(any("symlink" in error for error in errors))
data = example_data()
data["verificationRegistry"]["service-worktree-alignment"]["path"] = (
"tests/check.sh;touch"
)
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("必须是仓库内相对路径" in error for error in errors))
def test_verification_registry_requires_regular_executable_target(self) -> None:
data = example_data()
data["verificationRegistry"]["service-worktree-alignment"]["path"] = (
"checks/verify"
)
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
project.mkdir()
missing = validate_knowledge.validate_semantics(
data,
project_root=project,
)
self.assertTrue(any("不存在" in error for error in missing))
target = project / "checks" / "verify"
target.parent.mkdir()
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
not_executable = validate_knowledge.validate_semantics(
data,
project_root=project,
)
self.assertTrue(
any("不可执行" in error for error in not_executable)
)
target.chmod(target.stat().st_mode | 0o111)
valid = validate_knowledge.validate_semantics(
data,
project_root=project,
)
self.assertFalse(
any("verificationRegistry" in error for error in valid),
valid,
)
def test_free_command_fields_are_rejected_and_never_executed(self) -> None:
data = example_data()
data["entries"][0]["command"] = "echo unsafe"
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("不保存或执行自由命令" in error for error in errors))
with tempfile.TemporaryDirectory() as temp_dir:
marker = Path(temp_dir) / "should-not-exist"
safe = example_data()
safe["entries"][0]["directive"] = f"touch {marker}"
selected = select_knowledge.select_entries(
safe,
{
"components": ["web"],
"paths": ["web/app.py"],
"dependencies": [],
"versions": [],
"tags": ["long-running-service"],
"symbols": [],
"errorSignatures": [],
},
)
self.assertEqual(len(selected), 1)
self.assertFalse(marker.exists())
def test_common_secret_material_is_rejected(self) -> None:
data = example_data()
data["entries"][0]["rationale"] = (
"debug token=sk-abcdefghijklmnopqrstuvwxyz123456"
)
errors = validate_knowledge.validate_semantics(data)
self.assertTrue(any("OpenAI-style token" in error for error in errors))
self.assertTrue(any("脱敏摘要" in error for error in errors))
def test_selection_is_active_deterministic_and_limited(self) -> None:
data = example_data()
stale = copy.deepcopy(data["entries"][0])
stale.update(
{
"id": "K-002",
"status": "stale",
"statusReason": "相关服务已移除",
}
)
global_entry = copy.deepcopy(data["entries"][0])
global_entry.update(
{
"id": "K-003",
"subject": "global-release-check",
"scope": {
"all": True,
"components": [],
"paths": [],
"dependencies": [],
"versions": [],
"tags": [],
"symbols": [],
"errorSignatures": [],
},
}
)
data["entries"].extend([stale, global_entry])
context = {
"components": ["web"],
"paths": ["web/app.py"],
"dependencies": [],
"versions": [],
"tags": ["long-running-service"],
"symbols": [],
"errorSignatures": [],
}
selected = select_knowledge.select_entries(data, context, limit=1)
self.assertEqual(
[validate_knowledge.stable_ref(entry) for entry in selected], ["K-003@1"]
)
self.assertFalse(
select_knowledge.scope_matches(
data["entries"][0]["scope"],
{**context, "tags": []},
)
)
def test_global_entries_are_never_silently_dropped_by_limit(self) -> None:
data = example_data()
for number in range(2, 13):
scoped = copy.deepcopy(data["entries"][0])
scoped.update(
{
"id": f"K-{number:03d}",
"subject": f"scoped-check-{number}",
}
)
data["entries"].append(scoped)
global_entry = copy.deepcopy(data["entries"][0])
global_entry.update(
{
"id": "K-999",
"subject": "global-release-check",
"scope": {
"all": True,
"components": [],
"paths": [],
"dependencies": [],
"versions": [],
"tags": [],
"symbols": [],
"errorSignatures": [],
},
}
)
data["entries"].append(global_entry)
context = {
"components": ["web"],
"paths": ["web/app.py"],
"dependencies": [],
"versions": [],
"tags": ["long-running-service"],
"symbols": [],
"errorSignatures": [],
}
selected = select_knowledge.select_entries(data, context, limit=10)
refs = [validate_knowledge.stable_ref(entry) for entry in selected]
self.assertEqual(len(refs), 10)
self.assertIn("K-999@1", refs)
second_global = copy.deepcopy(global_entry)
second_global.update({"id": "K-998", "subject": "global-security-check"})
data["entries"].append(second_global)
with self.assertRaisesRegex(ValueError, "不能静默丢弃"):
select_knowledge.select_entries(data, context, limit=1)
def test_path_glob_does_not_let_single_star_cross_directory(self) -> None:
self.assertTrue(select_knowledge._path_glob_matches("web/app.py", "web/*"))
self.assertFalse(
select_knowledge._path_glob_matches("web/pages/app.py", "web/*")
)
self.assertTrue(
select_knowledge._path_glob_matches("web/pages/app.py", "web/**")
)
def test_limit_prefers_narrow_scope_without_rewarding_or_patterns(self) -> None:
data = example_data()
narrow = copy.deepcopy(data["entries"][0])
narrow.update({"id": "K-002", "subject": "narrow-service-check"})
narrow["scope"]["paths"] = ["web/special/**"]
broad_or = copy.deepcopy(data["entries"][0])
broad_or.update({"id": "K-003", "subject": "broad-or-service-check"})
broad_or["scope"]["paths"] = ["web/special/**", "api/**"]
data["entries"].extend([narrow, broad_or])
context = {
"components": ["web"],
"paths": ["web/special/pages/app.py"],
"dependencies": [],
"versions": [],
"tags": ["long-running-service"],
"symbols": [],
"errorSignatures": [],
}
selected = select_knowledge.select_entries(data, context, limit=1)
self.assertEqual(validate_knowledge.stable_ref(selected[0]), "K-002@1")
def test_task_cross_validation_requires_active_exact_ref_and_passed_check(self) -> None:
data = example_data()
task = {
"id": "BUG-002",
"status": "verified",
"knowledgeRefs": ["K-001@1"],
"knowledgeApplied": [
{
"ref": "K-001@1",
"result": "applied",
"evidence": "developer report",
}
],
"knowledgeChecks": [
{
"ref": "K-001@1",
"result": "passed",
"evidence": "test report",
}
],
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-002-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "BUG-002-A2",
"result": "passed",
}
]
},
}
tasks_data = {
"project": {
"name": "notes-web",
"knowledgeFile": "docs/ack/knowledge.yaml",
},
"tasks": [task],
}
self.assertEqual(
validate_knowledge.validate_task_references(data, tasks_data), []
)
task["knowledgeChecks"][0]["result"] = "not_applicable"
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertTrue(any("缺少 passed knowledgeCheck" in error for error in errors))
data["entries"][0]["kind"] = "pitfall"
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertTrue(any("缺少 passed knowledgeCheck" in error for error in errors))
task["knowledgeRefs"] = ["K-001@2"]
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertTrue(any("找不到精确版本 K-001@2" in error for error in errors))
def test_task_cross_validation_rejects_another_project(self) -> None:
data = example_data()
tasks = {
"project": {
"name": "another-project",
"knowledgeFile": "docs/ack/knowledge.yaml",
},
"tasks": [],
}
errors = validate_knowledge.validate_task_references(data, tasks)
self.assertTrue(any("与知识库项目" in error for error in errors))
def test_cross_validation_requires_traceable_explicit_attempt_id(self) -> None:
data = example_data()
task = {
"id": "BUG-002",
"status": "open",
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-002-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "orca-dispatch-91",
"result": "passed",
}
]
},
}
tasks_data = {
"project": {
"name": "notes-web",
"knowledgeFile": "docs/ack/knowledge.yaml",
},
"tasks": [task],
}
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertTrue(any("必须精确等于 'BUG-002-A2'" in e for e in errors))
self.assertTrue(any("未命中任务 'BUG-002'" in e for e in errors))
task["dispatch"]["rounds"][1]["attemptId"] = "BUG-002-A2"
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertFalse(any("provenance" in error for error in errors))
def test_cross_validation_rejects_non_contiguous_rounds(self) -> None:
data = example_data()
data["entries"][0]["provenance"]["attemptId"] = "BUG-002-A3"
tasks_data = {
"project": {
"name": "notes-web",
"knowledgeFile": "docs/ack/knowledge.yaml",
},
"tasks": [
{
"id": "BUG-002",
"status": "open",
"dispatch": {
"rounds": [
{
"round": 3,
"attemptId": "BUG-002-A3",
"result": "passed",
}
]
},
}
],
}
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertTrue(any("round 必须按 1..N 连续" in error for error in errors))
def test_cross_validation_binds_configured_knowledge_file(self) -> None:
data = example_data()
task = {
"id": "BUG-002",
"status": "open",
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-002-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "BUG-002-A2",
"result": "passed",
}
]
},
}
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
ack_dir = project / "docs" / "ack"
ack_dir.mkdir(parents=True)
knowledge_path = ack_dir / "knowledge.yaml"
tasks_path = ack_dir / "tasks.yaml"
knowledge_path.write_text(
yaml.safe_dump(data, allow_unicode=True), encoding="utf-8"
)
tasks_data = {
"project": {
"name": "notes-web",
"repoPath": str(project),
"knowledgeFile": "docs/ack/other.yaml",
},
"tasks": [task],
}
errors = validate_knowledge.validate_task_references(
data,
tasks_data,
knowledge_path=knowledge_path,
tasks_path=tasks_path,
)
self.assertTrue(any("与当前知识文件" in error for error in errors))
del tasks_data["project"]["knowledgeFile"]
errors = validate_knowledge.validate_task_references(data, tasks_data)
self.assertTrue(any("project.knowledgeFile 必填" in e for e in errors))
tasks_data["project"]["knowledgeFile"] = "docs/ack/knowledge.yaml"
staging_root = Path(temp_dir) / "staging"
staged = staging_root / "docs" / "ack" / "knowledge.yaml"
staged.parent.mkdir(parents=True)
staged.write_text(
yaml.safe_dump(data, allow_unicode=True), encoding="utf-8"
)
errors = validate_knowledge.validate_task_references(
data,
tasks_data,
knowledge_path=staged,
tasks_path=tasks_path,
project_root=staging_root,
project_root_is_explicit=True,
)
self.assertFalse(any("knowledgeFile" in error for error in errors))
def test_cli_fails_closed_for_missing_repo_root_and_wrong_binding(self) -> None:
data = example_data()
task = {
"id": "BUG-002",
"title": "source task",
"status": "open",
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-002-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "BUG-002-A2",
"result": "passed",
}
]
},
}
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
ack_dir = project / "docs" / "ack"
outside = base / "outside"
ack_dir.mkdir(parents=True)
outside.mkdir()
(project / "checks").symlink_to(outside, target_is_directory=True)
data["verificationRegistry"]["service-worktree-alignment"]["path"] = (
"checks/verify.py"
)
knowledge_path = ack_dir / "knowledge.yaml"
knowledge_path.write_text(
yaml.safe_dump(data, allow_unicode=True), encoding="utf-8"
)
tasks_data = {
"version": 1,
"project": {
"name": "notes-web",
"repoPath": str(project / "missing"),
"knowledgeFile": "docs/ack/not-the-current-file.yaml",
},
"tasks": [task],
}
tasks_path = ack_dir / "tasks.yaml"
tasks_path.write_text(
yaml.safe_dump(tasks_data, allow_unicode=True), encoding="utf-8"
)
command = [
sys.executable,
str(SCRIPTS_DIR / "validate_knowledge.py"),
str(knowledge_path),
"--tasks",
str(tasks_path),
]
inferred = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
)
explicit = subprocess.run(
[*command, "--project-root", str(project)],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(inferred.returncode, 1)
self.assertIn("project.repoPath", inferred.stderr)
self.assertIn("与当前知识文件", inferred.stderr)
self.assertIn("symlink", inferred.stderr)
self.assertEqual(explicit.returncode, 1)
self.assertIn("与当前知识文件", explicit.stderr)
self.assertIn("symlink", explicit.stderr)
def test_cli_requires_root_for_nonempty_registry_outside_project_layout(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
knowledge_path = Path(temp_dir) / "knowledge.yaml"
knowledge_path.write_text(
EXAMPLE_PATH.read_text(encoding="utf-8"), encoding="utf-8"
)
result = subprocess.run(
[
sys.executable,
str(SCRIPTS_DIR / "validate_knowledge.py"),
str(knowledge_path),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 1)
self.assertIn("无法确定项目根目录", result.stderr)
self.assertIn("--project-root", result.stderr)
def test_terminal_task_keeps_historical_ref_after_entry_becomes_stale(self) -> None:
data = example_data()
data["entries"][0]["status"] = "stale"
data["entries"][0]["statusReason"] = "service architecture changed"
task = {
"id": "BUG-002",
"status": "verified",
"knowledgeRefs": ["K-001@1"],
"knowledgeApplied": [],
"knowledgeChecks": [
{
"ref": "K-001@1",
"result": "passed",
"evidence": "historical test evidence",
}
],
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-002-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "BUG-002-A2",
"result": "passed",
}
]
},
}
tasks_data = {
"project": {
"name": "notes-web",
"knowledgeFile": "docs/ack/knowledge.yaml",
},
"tasks": [task],
}
terminal_errors = validate_knowledge.validate_task_references(
data, tasks_data
)
self.assertFalse(any("必须 active" in error for error in terminal_errors))
task["status"] = "open"
active_errors = validate_knowledge.validate_task_references(
data, tasks_data
)
self.assertTrue(any("必须 active" in error for error in active_errors))
def test_cli_validates_tasks_and_selector_outputs_stable_refs(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
ack_dir = project / "docs" / "ack"
ack_dir.mkdir(parents=True)
knowledge_path = ack_dir / "knowledge.yaml"
knowledge_path.write_text(
EXAMPLE_PATH.read_text(encoding="utf-8"), encoding="utf-8"
)
target = project / "tests" / "ack" / "check_service_worktree.py"
target.parent.mkdir(parents=True)
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(target.stat().st_mode | 0o111)
tasks = {
"project": {
"name": "notes-web",
"repoPath": str(project),
"knowledgeFile": "docs/ack/knowledge.yaml",
},
"tasks": [
{
"id": "BUG-002",
"status": "verified",
"knowledgeRefs": ["K-001@1"],
"knowledgeApplied": [],
"knowledgeChecks": [
{
"ref": "K-001@1",
"result": "passed",
"evidence": "independent retest",
}
],
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-002-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "BUG-002-A2",
"result": "passed",
}
]
},
}
],
}
tasks_path = ack_dir / "tasks.yaml"
tasks_path.write_text(
yaml.safe_dump(tasks, allow_unicode=True), encoding="utf-8"
)
validated = subprocess.run(
[
sys.executable,
str(SCRIPTS_DIR / "validate_knowledge.py"),
str(knowledge_path),
"--tasks",
str(tasks_path),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(validated.returncode, 0, validated.stderr)
selected = subprocess.run(
[
sys.executable,
str(SCRIPTS_DIR / "select_knowledge.py"),
str(knowledge_path),
"--component",
"web",
"--path",
"web/app.py",
"--tag",
"long-running-service",
"--limit",
"1",
"--format",
"refs",
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(selected.returncode, 0, selected.stderr)
self.assertEqual(selected.stdout.strip(), "K-001@1")
selected_json = subprocess.run(
[
sys.executable,
str(SCRIPTS_DIR / "select_knowledge.py"),
str(knowledge_path),
"--component",
"web",
"--path",
"web/app.py",
"--tag",
"long-running-service",
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(selected_json.returncode, 0, selected_json.stderr)
payload = json.loads(selected_json.stdout)
self.assertEqual(
payload["entries"][0]["verificationTarget"]["path"],
"tests/ack/check_service_worktree.py",
)
def test_paired_examples_validate_when_installed_in_project_layout(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "notes-web"
ack_dir = project / "docs" / "ack"
ack_dir.mkdir(parents=True)
knowledge_path = ack_dir / "knowledge.yaml"
tasks_path = ack_dir / "tasks.yaml"
knowledge_path.write_text(
EXAMPLE_PATH.read_text(encoding="utf-8"),
encoding="utf-8",
)
tasks = yaml.safe_load(
TASKS_EXAMPLE_PATH.read_text(encoding="utf-8")
)
tasks["project"]["repoPath"] = str(project)
tasks["project"]["devWorktree"] = str(project)
tasks_path.write_text(
yaml.safe_dump(tasks, allow_unicode=True, sort_keys=False),
encoding="utf-8",
)
target = project / "tests" / "ack" / "check_service_worktree.py"
target.parent.mkdir(parents=True)
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(target.stat().st_mode | 0o111)
tasks_result = subprocess.run(
[
sys.executable,
str(SCRIPTS_DIR / "validate_tasks.py"),
str(tasks_path),
],
capture_output=True,
text=True,
check=False,
)
knowledge_result = subprocess.run(
[
sys.executable,
str(SCRIPTS_DIR / "validate_knowledge.py"),
str(knowledge_path),
"--tasks",
str(tasks_path),
"--project-root",
str(project),
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(tasks_result.returncode, 0, tasks_result.stderr)
self.assertEqual(
knowledge_result.returncode,
0,
knowledge_result.stderr,
)
if __name__ == "__main__":
unittest.main()
+17
View File
@@ -15,8 +15,11 @@ class AckSkillContentTests(unittest.TestCase):
"skiff init ack --project <project-root>",
"docs/ack/project.md",
"docs/ack/tasks.yaml",
"docs/ack/knowledge.yaml",
"tasks: []",
"validate_tasks.py",
"validate_knowledge.py",
"select_knowledge.py",
"references/kickoff.md",
"不要修改项目的 `AGENTS.md`",
"当前会话担任 Coordinator",
@@ -31,6 +34,20 @@ class AckSkillContentTests(unittest.TestCase):
self.assertIn('display_name: "ACK"', metadata)
self.assertIn("allow_implicit_invocation: false", metadata)
def test_ack_knowledge_resources_and_version_are_present(self) -> None:
ack_dir = REPO_ROOT / "skills" / "ack"
for relative_path in (
"templates/knowledge.template.yaml",
"templates/knowledge.schema.json",
"examples/knowledge.example.yaml",
"scripts/validate_knowledge.py",
"scripts/select_knowledge.py",
"scripts/run_verification.py",
):
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
self.assertEqual((ack_dir / "VERSION").read_text(encoding="utf-8").strip(), "0.9.0")
if __name__ == "__main__":
unittest.main()
+705
View File
@@ -0,0 +1,705 @@
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
VALIDATOR = REPO_ROOT / "skills" / "ack" / "scripts" / "validate_tasks.py"
EXAMPLE = REPO_ROOT / "skills" / "ack" / "examples" / "tasks.example.yaml"
def valid_knowledge_board() -> dict:
return {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "T-1",
"title": "validate knowledge fields",
"status": "open",
"knowledgeRefs": ["K-001@1"],
"knowledgeApplied": [
{
"ref": "K-001@1",
"result": "applied",
"evidence": "followed the guardrail",
}
],
"knowledgeCandidates": [
{
"kind": "pitfall",
"title": "candidate",
"claim": "the failure is reproducible",
"scope": {"components": ["web"]},
"appliesWhen": "the web component changes",
"directive": "run the reviewed check",
"rationale": "avoid the repeated failure",
"evidenceRefs": ["tasks.yaml#T-1"],
"proposedBy": "developer",
"proposedAt": "2026-07-31T10:00:00+08:00",
}
],
"knowledgeChecks": [
{
"ref": "K-001@1",
"result": "passed",
"evidence": "independently verified",
"checkedBy": "test",
"checkedAt": "2026-07-31T10:05:00+08:00",
}
],
}
],
}
class AckTaskValidationTests(unittest.TestCase):
def run_validator(
self,
content: str | None = None,
*extra_args: str,
no_site_packages: bool = False,
suffix: str = ".yaml",
) -> subprocess.CompletedProcess[str]:
command = [sys.executable]
if no_site_packages:
command.append("-S")
command.append(str(VALIDATOR))
if content is None:
return subprocess.run(
[*command, *extra_args, str(EXAMPLE)],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
with tempfile.TemporaryDirectory() as temp_dir:
task_file = Path(temp_dir) / f"tasks{suffix}"
task_file.write_text(textwrap.dedent(content), encoding="utf-8")
return subprocess.run(
[*command, *extra_args, str(task_file)],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
def assert_board_rejected_in_all_modes(
self,
board: dict,
*expected_messages: str,
) -> None:
for no_site_packages in (False, True):
with self.subTest(no_site_packages=no_site_packages):
result = self.run_validator(
json.dumps(board),
no_site_packages=no_site_packages,
suffix=".json",
)
self.assertEqual(result.returncode, 1, result.stdout)
for message in expected_messages:
self.assertIn(message, result.stderr)
if no_site_packages:
self.assertIn("内置语义规则", result.stderr)
self.assertNotIn("[schema]", result.stderr)
def assert_board_accepted_in_all_modes(self, board: dict) -> None:
for no_site_packages in (False, True):
with self.subTest(no_site_packages=no_site_packages):
result = self.run_validator(
json.dumps(board),
no_site_packages=no_site_packages,
suffix=".json",
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_example_with_knowledge_fields_is_valid(self) -> None:
result = self.run_validator()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("任务板校验通过", result.stdout)
def test_knowledge_applied_and_checks_must_reference_selected_knowledge(self) -> None:
result = self.run_validator(
"""
version: 1
project:
name: demo
tasks:
- id: T-1
title: invalid refs
status: verified
knowledgeRefs: ["K-001@1"]
knowledgeApplied:
- ref: "K-002@1"
result: applied
evidence: "used the rule"
knowledgeChecks:
- ref: "K-003@1"
result: failed
evidence: "still broken"
"""
)
self.assertEqual(result.returncode, 1)
self.assertIn("K-002@1 不在 knowledgeRefs 中", result.stderr)
self.assertIn("K-003@1 不在 knowledgeRefs 中", result.stderr)
self.assertIn("verified 任务不能保留失败", result.stderr)
def test_knowledge_refs_require_revision(self) -> None:
result = self.run_validator(
"""
version: 1
project:
name: demo
tasks:
- id: T-1
title: invalid ref
status: open
knowledgeRefs: ["K-001"]
"""
)
self.assertEqual(result.returncode, 1)
self.assertIn("K-<id>@<revision>", result.stderr)
def test_candidate_requires_actionable_scope_and_evidence(self) -> None:
result = self.run_validator(
"""
version: 1
project:
name: demo
tasks:
- id: T-1
title: invalid candidate
status: open
knowledgeCandidates:
- kind: guess
title: maybe
claim: uncertain
scope: {}
appliesWhen: sometimes
directive: retry
rationale: unknown
evidenceRefs: []
"""
)
self.assertEqual(result.returncode, 1)
self.assertIn(".kind: 必须是", result.stderr)
self.assertIn(".scope: 至少包含一个非空作用域", result.stderr)
self.assertIn(".evidenceRefs: 必须是非空字符串列表", result.stderr)
def test_forbidden_knowledge_properties_fail_with_and_without_jsonschema(self) -> None:
board = valid_knowledge_board()
task = board["tasks"][0]
task["knowledgeApplied"][0]["unexpectedApplication"] = True
task["knowledgeCandidates"][0]["unexpectedCandidate"] = True
task["knowledgeChecks"][0]["unexpectedCheck"] = True
self.assert_board_rejected_in_all_modes(
board,
"knowledgeApplied[0]: 未知字段 'unexpectedApplication'",
"knowledgeCandidates[0]: 未知字段 'unexpectedCandidate'",
"knowledgeChecks[0]: 未知字段 'unexpectedCheck'",
)
def test_valid_optional_knowledge_fields_pass_in_all_modes(self) -> None:
self.assert_board_accepted_in_all_modes(valid_knowledge_board())
def test_explicit_null_knowledge_collections_fail_in_all_modes(self) -> None:
board = {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "T-REFS",
"title": "null refs",
"status": "open",
"knowledgeRefs": None,
},
{
"id": "T-APPLIED",
"title": "null applications",
"status": "open",
"knowledgeApplied": None,
},
{
"id": "T-CANDIDATES",
"title": "null candidates",
"status": "open",
"knowledgeCandidates": None,
},
{
"id": "T-CHECKS",
"title": "null checks",
"status": "open",
"knowledgeChecks": None,
},
],
}
self.assert_board_rejected_in_all_modes(
board,
"knowledgeRefs: 必须是列表",
"knowledgeApplied: 必须是列表",
"knowledgeCandidates: 必须是列表",
"knowledgeChecks: 必须是列表",
)
def test_knowledge_item_types_and_blank_evidence_fail_in_all_modes(self) -> None:
board = valid_knowledge_board()
task = board["tasks"][0]
task["knowledgeApplied"][0].update(
{"ref": 1, "result": "unknown", "evidence": " "}
)
task["knowledgeChecks"][0].update(
{"ref": False, "result": "unknown", "evidence": "\t"}
)
self.assert_board_rejected_in_all_modes(
board,
"knowledgeApplied[0].ref: 必须使用 K-<id>@<revision> 格式",
"knowledgeApplied[0].result: 必须是 applied/not_applicable",
"knowledgeApplied[0].evidence: 必须提供非空证据",
"knowledgeChecks[0].ref: 必须使用 K-<id>@<revision> 格式",
"knowledgeChecks[0].result: 必须是",
"knowledgeChecks[0].evidence: 必须提供非空证据",
)
def test_candidate_text_scope_and_evidence_refs_fail_in_all_modes(self) -> None:
board = valid_knowledge_board()
candidate = board["tasks"][0]["knowledgeCandidates"][0]
candidate.update(
{
"title": 1,
"claim": " ",
"appliesWhen": [],
"directive": "",
"rationale": None,
"scope": {
"components": ["web", "web"],
"paths": [" "],
},
"evidenceRefs": ["tasks.yaml#T-1", "tasks.yaml#T-1"],
}
)
self.assert_board_rejected_in_all_modes(
board,
"knowledgeCandidates[0].title: 必须是非空字符串",
"knowledgeCandidates[0].claim: 必须是非空字符串",
"knowledgeCandidates[0].appliesWhen: 必须是非空字符串",
"knowledgeCandidates[0].directive: 必须是非空字符串",
"knowledgeCandidates[0].rationale: 必须是非空字符串",
"knowledgeCandidates[0].scope.components: 不能包含重复值",
"knowledgeCandidates[0].scope.paths: 必须是非空字符串列表",
"knowledgeCandidates[0].evidenceRefs: 不能包含重复值",
)
def test_candidate_evidence_refs_reject_blank_strings_in_all_modes(self) -> None:
board = valid_knowledge_board()
board["tasks"][0]["knowledgeCandidates"][0]["evidenceRefs"] = [" "]
self.assert_board_rejected_in_all_modes(
board,
"knowledgeCandidates[0].evidenceRefs: 必须是非空字符串列表",
)
def test_optional_knowledge_field_types_fail_in_all_modes(self) -> None:
board = valid_knowledge_board()
candidate = board["tasks"][0]["knowledgeCandidates"][0]
candidate["proposedBy"] = 1
candidate["proposedAt"] = []
check = board["tasks"][0]["knowledgeChecks"][0]
check["checkedBy"] = False
check["checkedAt"] = {}
self.assert_board_rejected_in_all_modes(
board,
"knowledgeCandidates[0].proposedBy: 必须是字符串",
"knowledgeCandidates[0].proposedAt: 必须是字符串",
"knowledgeChecks[0].checkedBy: 必须是字符串",
"knowledgeChecks[0].checkedAt: 必须是字符串",
)
def test_attempt_id_must_match_task_and_round_and_be_unique(self) -> None:
board = {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "BUG-017",
"title": "invalid attempt ids",
"status": "failed_retest",
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "OTHER-A1",
"result": "failed",
},
{
"round": 1,
"attemptId": "OTHER-A1",
"result": "failed",
},
{
"round": 3,
"attemptId": "invalid/attempt",
"result": "failed",
},
]
},
}
],
}
self.assert_board_rejected_in_all_modes(
board,
"dispatch.rounds[0].attemptId: 应为 BUG-017-A1",
"dispatch.rounds[1].attemptId: 轮次内不能重复: OTHER-A1",
"dispatch.rounds[2].attemptId: 必须使用 <task-id>-A<round> 格式",
)
def test_valid_optional_attempt_ids_pass_in_all_modes(self) -> None:
board = {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "BUG-017",
"title": "valid attempt ids",
"status": "failed_retest",
"dispatch": {
"rounds": [
{
"round": 1,
"attemptId": "BUG-017-A1",
"result": "failed",
},
{
"round": 2,
"attemptId": "BUG-017-A2",
"result": "failed",
},
]
},
}
],
}
self.assert_board_accepted_in_all_modes(board)
def test_boolean_version_fails_in_all_modes(self) -> None:
board = valid_knowledge_board()
board["version"] = True
self.assert_board_rejected_in_all_modes(
board,
"version 必须是 >=1 的整数",
)
def test_knowledge_file_is_fixed_in_all_modes(self) -> None:
board = valid_knowledge_board()
board["project"]["knowledgeFile"] = "docs/ack/alternate.yaml"
self.assert_board_rejected_in_all_modes(
board,
"project.knowledgeFile 必须固定为 docs/ack/knowledge.yaml",
)
def test_basic_identifiers_must_be_nonempty_strings_in_all_modes(self) -> None:
board = valid_knowledge_board()
board["project"]["name"] = 7
board["tasks"][0]["id"] = 9
board["tasks"][0]["title"] = " "
self.assert_board_rejected_in_all_modes(
board,
"project.name 必须是非空字符串",
"id 必须是非空字符串",
"title 必须是非空字符串",
)
def test_root_project_and_summary_types_match_schema_in_all_modes(self) -> None:
board = valid_knowledge_board()
board.update(
{
"updatedAt": [],
"source": {},
"ackVersion": 1,
"kitVersion": False,
"summary": {
"verified": [1],
"open": {},
"failedRetest": [False],
"leftovers": None,
},
"statusReference": [],
}
)
board["project"].update(
{
"repoPath": [],
"baseUrl": {},
"devWorktree": 1,
"overlayFile": False,
}
)
self.assert_board_rejected_in_all_modes(
board,
"<root>.updatedAt: 必须是字符串",
"<root>.source: 必须是字符串",
"<root>.ackVersion: 必须是字符串",
"<root>.kitVersion: 必须是字符串",
"project.repoPath: 必须是字符串",
"project.baseUrl: 必须是字符串",
"project.devWorktree: 必须是字符串",
"project.overlayFile: 必须是字符串",
"summary.verified: 列表项必须是字符串",
"summary.open: 必须是列表",
"summary.failedRetest: 列表项必须是字符串",
"summary.leftovers: 必须是列表",
"statusReference 必须是对象",
)
def test_task_optional_types_match_schema_in_all_modes(self) -> None:
board = valid_knowledge_board()
board["tasks"][0].update(
{
"type": [],
"priority": {},
"assignee": False,
"component": 1,
"specRefs": {},
"testRefs": [1],
"description": [],
"stepsToReproduce": [{}],
"expected": False,
"actual": None,
"evidence": [],
"verification": [],
}
)
self.assert_board_rejected_in_all_modes(
board,
".type: 必须是字符串",
".priority: 必须是字符串",
".assignee: 必须是字符串",
".component: 必须是字符串",
".specRefs: 必须是列表",
".testRefs: 列表项必须是字符串",
".description: 必须是字符串",
".stepsToReproduce: 列表项必须是字符串",
".expected: 必须是字符串",
".actual: 必须是字符串",
".evidence: 必须是对象",
".verification: 必须是对象",
)
def test_dispatch_and_resolution_types_match_schema_in_all_modes(self) -> None:
board = valid_knowledge_board()
board["tasks"][0].update(
{
"dispatch": {
"taskId": [],
"dispatchId": {},
"worker": False,
"rounds": [
{
"round": 1,
"result": "failed",
"evidence": [],
}
],
},
"resolution": {
"fixedBy": [],
"verifiedBy": {},
"verifiedAt": False,
"leftoverReason": 1,
"evidence": [],
},
}
)
self.assert_board_rejected_in_all_modes(
board,
".dispatch.taskId: 必须是字符串或 null",
".dispatch.dispatchId: 必须是字符串或 null",
".dispatch.worker: 必须是字符串或 null",
".dispatch.rounds[0].evidence: 必须是字符串",
".resolution.fixedBy: 必须是字符串或 null",
".resolution.verifiedBy: 必须是字符串或 null",
".resolution.verifiedAt: 必须是字符串或 null",
".resolution.leftoverReason: 必须是字符串或 null",
".resolution.evidence: 必须是对象",
)
def test_explicit_null_structures_fail_in_all_modes(self) -> None:
board = {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "T-DISPATCH",
"title": "invalid dispatch",
"status": "open",
"dispatch": None,
},
{
"id": "T-ROUNDS",
"title": "invalid rounds",
"status": "open",
"dispatch": {"rounds": None},
},
{
"id": "T-RESOLUTION",
"title": "invalid resolution",
"status": "open",
"resolution": None,
},
],
}
self.assert_board_rejected_in_all_modes(
board,
"T-DISPATCH.dispatch: 必须是对象",
"T-ROUNDS.dispatch.rounds: 必须是列表",
"T-RESOLUTION.resolution: 必须是对象",
)
def test_valid_optional_schema_fields_pass_in_all_modes(self) -> None:
board = valid_knowledge_board()
board.update(
{
"updatedAt": "2026-07-31T10:00:00+08:00",
"source": "manual",
"ackVersion": "0.9.0",
"kitVersion": "0.8.0",
"summary": {
"verified": ["T-1"],
"open": [],
"failedRetest": [],
"leftovers": [],
},
"statusReference": {},
}
)
board["project"].update(
{
"repoPath": "/repo",
"baseUrl": "http://127.0.0.1:3000",
"devWorktree": "/repo-dev",
"overlayFile": "docs/ack/project.md",
"knowledgeFile": "docs/ack/knowledge.yaml",
}
)
board["tasks"][0].update(
{
"type": "bug",
"priority": "P1",
"assignee": "developer",
"component": "web",
"specRefs": ["spec.md"],
"testRefs": ["tests/test_web.py"],
"description": "description",
"stepsToReproduce": ["open page"],
"expected": "works",
"actual": "fails",
"evidence": {},
"verification": {},
"dispatch": {
"taskId": "orca-task",
"dispatchId": None,
"worker": "worker-1",
"rounds": [
{
"round": 1,
"attemptId": "T-1-A1",
"result": "failed",
"evidence": "test output",
}
],
},
"resolution": {
"fixedBy": "developer",
"verifiedBy": None,
"verifiedAt": None,
"leftoverReason": None,
"evidence": {},
},
}
)
self.assert_board_accepted_in_all_modes(board)
def test_round_numbers_must_be_contiguous_and_within_budget(self) -> None:
board = {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "BUG-017",
"title": "invalid round number",
"status": "failed_retest",
"dispatch": {
"rounds": [
{
"round": 999,
"attemptId": "BUG-017-A999",
"result": "failed",
}
]
},
}
],
}
self.assert_board_rejected_in_all_modes(
board,
"dispatch.rounds[0].round: 必须是 1..3 的整数",
"dispatch.rounds: round 必须从 1 连续递增且不重复",
)
def test_leftover_reason_must_be_nonempty_string_in_all_modes(self) -> None:
board = {
"version": 1,
"project": {"name": "demo"},
"tasks": [
{
"id": "T-1",
"title": "invalid leftover reason",
"status": "leftover",
"resolution": {"leftoverReason": True},
}
],
}
self.assert_board_rejected_in_all_modes(
board,
"leftover 必须填 resolution.leftoverReason",
)
def test_explicit_missing_schema_is_an_environment_error(self) -> None:
result = self.run_validator(None, "--schema", "/definitely/missing/schema.json")
self.assertEqual(result.returncode, 2)
self.assertIn("找不到指定的 schema 文件", result.stderr)
if __name__ == "__main__":
unittest.main()
+591
View File
@@ -0,0 +1,591 @@
from __future__ import annotations
import importlib.util
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
RUNNER = REPO_ROOT / "skills" / "ack" / "scripts" / "run_verification.py"
sys.path.insert(0, str(RUNNER.parent))
RUNNER_SPEC = importlib.util.spec_from_file_location("ack_run_verification", RUNNER)
assert RUNNER_SPEC is not None and RUNNER_SPEC.loader is not None
RUNNER_MODULE = importlib.util.module_from_spec(RUNNER_SPEC)
RUNNER_SPEC.loader.exec_module(RUNNER_MODULE)
def knowledge_with_target(path: str, args: list[str]) -> dict:
return {
"version": 1,
"updatedAt": "2026-07-31T12:00:00+08:00",
"project": {"name": "demo"},
"verificationRegistry": {
"reviewed-check": {
"path": path,
"args": args,
}
},
"entries": [],
}
class AckVerificationRunnerTests(unittest.TestCase):
def run_check(
self,
project: Path,
knowledge: dict,
) -> subprocess.CompletedProcess[str]:
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
knowledge_path.parent.mkdir(parents=True, exist_ok=True)
knowledge_path.write_text(
yaml.safe_dump(knowledge, allow_unicode=True),
encoding="utf-8",
)
return subprocess.run(
[
sys.executable,
str(RUNNER),
str(knowledge_path),
"reviewed-check",
"--project-root",
str(project),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
def test_executes_reviewed_target_with_structured_args(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
target = project / "checks" / "write-marker"
target.parent.mkdir(parents=True)
target.write_text(
"#!/bin/sh\nprintf 'passed' > \"$1\"\n",
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
result = self.run_check(
project,
knowledge_with_target(
"checks/write-marker",
["verification-marker.txt"],
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
(project / "verification-marker.txt").read_text(encoding="utf-8"),
"passed",
)
def test_exposes_project_rooted_execution_contract(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
target = project / "checks" / "show-context"
target.parent.mkdir(parents=True)
target.write_text(
"#!/bin/sh\n"
"printf '%s\\n%s\\n%s\\n%s\\n' "
'"$PWD" "$ACK_PROJECT_ROOT" "$ACK_VERIFICATION_REF" '
'"$ACK_VERIFICATION_PATH" > "$1"\n',
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
result = self.run_check(
project,
knowledge_with_target(
"checks/show-context",
["verification-context.txt"],
),
)
self.assertEqual(result.returncode, 0, result.stderr)
context = (
(project / "verification-context.txt")
.read_text(encoding="utf-8")
.splitlines()
)
self.assertEqual(context[0], str(project))
self.assertRegex(context[1], r"^/(?:proc/self|dev)/fd/[0-9]+$")
self.assertEqual(context[2:], ["reviewed-check", "checks/show-context"])
def test_rejects_missing_or_non_executable_target(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
project.mkdir()
missing = self.run_check(
project,
knowledge_with_target("checks/missing", []),
)
self.assertEqual(missing.returncode, 1)
self.assertIn("不存在", missing.stderr)
target = project / "checks" / "not-executable"
target.parent.mkdir(parents=True)
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(target.stat().st_mode & ~0o111)
not_executable = self.run_check(
project,
knowledge_with_target("checks/not-executable", []),
)
self.assertEqual(not_executable.returncode, 1)
self.assertIn("不可执行", not_executable.stderr)
def test_rejects_project_root_that_is_wider_than_knowledge_project(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
outside = base / "outside"
outside.mkdir()
target = outside / "unsafe"
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(target.stat().st_mode | 0o111)
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
knowledge_path.parent.mkdir(parents=True)
knowledge_path.write_text(
yaml.safe_dump(
knowledge_with_target("outside/unsafe", []),
allow_unicode=True,
),
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(RUNNER),
str(knowledge_path),
"reviewed-check",
"--project-root",
str(base),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 2)
self.assertIn("推断的项目根目录", result.stderr)
def test_rejects_duplicate_registry_ids_before_execution(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
checks = project / "checks"
checks.mkdir(parents=True)
marker = project / "unsafe-marker"
for name in ("safe", "unsafe"):
target = checks / name
target.write_text(
f"#!/bin/sh\nprintf '{name}' > {marker}\n",
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
knowledge_path.parent.mkdir(parents=True)
knowledge_path.write_text(
"""\
version: 1
updatedAt: "2026-07-31T12:00:00+08:00"
project:
name: demo
verificationRegistry:
reviewed-check:
path: checks/safe
args: []
reviewed-check:
path: checks/unsafe
args: []
entries: []
""",
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(RUNNER),
str(knowledge_path),
"reviewed-check",
"--project-root",
str(project),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("duplicate key", result.stderr)
self.assertFalse(marker.exists())
@unittest.skipUnless(hasattr(os, "symlink"), "requires symlink support")
def test_rejects_non_authoritative_knowledge_files(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
ack_dir = project / "docs" / "ack"
ack_dir.mkdir(parents=True)
document = yaml.safe_dump(
knowledge_with_target("checks/reviewed", []),
allow_unicode=True,
)
for label, knowledge_path in (
("external", base / "attacker-knowledge.yaml"),
("alternate", ack_dir / "alternate.yaml"),
):
with self.subTest(label=label):
knowledge_path.write_text(document, encoding="utf-8")
result = subprocess.run(
[
sys.executable,
str(RUNNER),
str(knowledge_path),
"reviewed-check",
"--project-root",
str(project),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 2)
self.assertIn("权威知识库", result.stderr)
outside = base / "outside-knowledge.yaml"
outside.write_text(document, encoding="utf-8")
canonical = ack_dir / "knowledge.yaml"
canonical.symlink_to(outside)
symlinked = subprocess.run(
[
sys.executable,
str(RUNNER),
str(canonical),
"reviewed-check",
"--project-root",
str(project),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(symlinked.returncode, 2)
self.assertIn("路径包含软链接", symlinked.stderr)
def test_authoritative_knowledge_is_read_from_an_opened_snapshot(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
canonical = project / "docs" / "ack" / "knowledge.yaml"
canonical.parent.mkdir(parents=True)
safe_document = knowledge_with_target("checks/safe", [])
forged_document = knowledge_with_target("checks/danger", [])
canonical.write_text(
yaml.safe_dump(safe_document, allow_unicode=True),
encoding="utf-8",
)
forged_path = base / "forged.yaml"
forged_path.write_text(
yaml.safe_dump(forged_document, allow_unicode=True),
encoding="utf-8",
)
original_reader = RUNNER_MODULE._read_stable_bytes
def swap_after_open(
source_fd: int,
*,
maximum: int,
) -> tuple[bytes | None, str | None]:
canonical.unlink()
canonical.symlink_to(forged_path)
return original_reader(source_fd, maximum=maximum)
with mock.patch.object(
RUNNER_MODULE,
"_read_stable_bytes",
side_effect=swap_after_open,
):
data, error = RUNNER_MODULE.load_authoritative_knowledge(project)
self.assertIsNone(error)
self.assertEqual(
data["verificationRegistry"]["reviewed-check"]["path"],
"checks/safe",
)
def test_authoritative_knowledge_rejects_same_inode_change_during_read(
self,
) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
canonical = project / "docs" / "ack" / "knowledge.yaml"
canonical.parent.mkdir(parents=True)
canonical.write_text(
yaml.safe_dump(
knowledge_with_target("checks/safe", []),
allow_unicode=True,
),
encoding="utf-8",
)
source_fd, error = RUNNER_MODULE._open_regular_beneath(
project,
"docs/ack/knowledge.yaml",
require_executable=False,
)
self.assertIsNone(error)
self.assertIsNotNone(source_fd)
assert source_fd is not None
original_read = os.read
changed = False
def mutate_during_read(
file_descriptor: int,
size: int,
) -> bytes:
nonlocal changed
chunk = original_read(file_descriptor, size)
if not changed:
changed = True
canonical.write_text(
yaml.safe_dump(
knowledge_with_target("checks/danger", []),
allow_unicode=True,
),
encoding="utf-8",
)
return chunk
try:
with mock.patch.object(
RUNNER_MODULE.os,
"read",
side_effect=mutate_during_read,
):
content, read_error = RUNNER_MODULE._read_stable_bytes(
source_fd,
maximum=RUNNER_MODULE.MAX_KNOWLEDGE_BYTES,
)
finally:
os.close(source_fd)
self.assertIsNone(content)
self.assertIn("读取期间发生变化", read_error)
@unittest.skipUnless(hasattr(os, "symlink"), "requires symlink support")
def test_rejects_target_that_resolves_outside_project(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
outside = base / "outside"
project.mkdir()
outside.mkdir()
target = outside / "unsafe"
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(target.stat().st_mode | 0o111)
(project / "checks").symlink_to(outside, target_is_directory=True)
result = self.run_check(
project,
knowledge_with_target("checks/unsafe", []),
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("不能包含 symlink", result.stderr)
@unittest.skipUnless(
hasattr(os, "O_NOFOLLOW") and Path("/proc/self/fd").is_dir(),
"requires fd-based POSIX execution",
)
def test_opened_target_cannot_be_swapped_before_execution(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
target = project / "checks" / "reviewed"
outside = base / "outside"
marker = project / "marker.txt"
target.parent.mkdir(parents=True)
target.write_text(
"#!/bin/sh\nprintf 'reviewed' > \"$1\"\n",
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
outside.write_text(
"#!/bin/sh\nprintf 'swapped' > \"$1\"\n",
encoding="utf-8",
)
outside.chmod(outside.stat().st_mode | 0o111)
data = knowledge_with_target("checks/reviewed", [str(marker)])
target_fd, target_args, error = RUNNER_MODULE.open_target(
data,
"reviewed-check",
project,
)
self.assertIsNone(error)
self.assertIsNotNone(target_fd)
self.assertIsNotNone(target_args)
assert target_fd is not None and target_args is not None
target.unlink()
target.symlink_to(outside)
executable = RUNNER_MODULE._fd_executable_path(target_fd)
self.assertIsNotNone(executable)
try:
completed = subprocess.run(
[str(executable), *target_args],
cwd=project,
pass_fds=(target_fd,),
check=False,
)
finally:
os.close(target_fd)
self.assertEqual(completed.returncode, 0)
self.assertEqual(marker.read_text(encoding="utf-8"), "reviewed")
@unittest.skipUnless(
hasattr(os, "O_NOFOLLOW") and Path("/proc/self/fd").is_dir(),
"requires fd-based POSIX execution",
)
def test_opened_target_snapshot_ignores_same_inode_rewrite(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
target = project / "checks" / "reviewed"
marker = project / "marker.txt"
target.parent.mkdir(parents=True)
target.write_text(
"#!/bin/sh\nprintf 'reviewed' > \"$1\"\n",
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
data = knowledge_with_target("checks/reviewed", [str(marker)])
target_fd, target_args, error = RUNNER_MODULE.open_target(
data,
"reviewed-check",
project,
)
self.assertIsNone(error)
self.assertIsNotNone(target_fd)
self.assertIsNotNone(target_args)
assert target_fd is not None and target_args is not None
target.write_text(
"#!/bin/sh\nprintf 'mutated' > \"$1\"\n",
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
executable = RUNNER_MODULE._fd_executable_path(target_fd)
self.assertIsNotNone(executable)
try:
completed = subprocess.run(
[str(executable), *target_args],
cwd=project,
pass_fds=(target_fd,),
check=False,
)
finally:
os.close(target_fd)
self.assertEqual(completed.returncode, 0)
self.assertEqual(marker.read_text(encoding="utf-8"), "reviewed")
@unittest.skipUnless(
hasattr(os, "O_NOFOLLOW") and Path("/proc/self/fd").is_dir(),
"requires fd-based POSIX execution",
)
def test_project_root_replacement_cannot_swap_target_or_cwd(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base = Path(temp_dir)
project = base / "project"
original = base / "original-project"
target = project / "checks" / "reviewed"
target.parent.mkdir(parents=True)
target.write_text(
"#!/bin/sh\nprintf 'reviewed' > marker.txt\n",
encoding="utf-8",
)
target.chmod(target.stat().st_mode | 0o111)
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
knowledge_path.parent.mkdir(parents=True)
knowledge_path.write_text(
yaml.safe_dump(
knowledge_with_target("checks/reviewed", []),
allow_unicode=True,
),
encoding="utf-8",
)
original_loader = RUNNER_MODULE.load_authoritative_knowledge
def replace_root_after_knowledge(
project_root: Path | int,
) -> tuple[dict | None, str | None]:
data, error = original_loader(project_root)
project.rename(original)
replacement = project / "checks" / "reviewed"
replacement.parent.mkdir(parents=True)
replacement.write_text(
"#!/bin/sh\nprintf 'swapped' > marker.txt\n",
encoding="utf-8",
)
replacement.chmod(replacement.stat().st_mode | 0o111)
return data, error
with mock.patch.object(
RUNNER_MODULE,
"load_authoritative_knowledge",
side_effect=replace_root_after_knowledge,
):
result = RUNNER_MODULE.main(
[
str(knowledge_path),
"reviewed-check",
"--project-root",
str(project),
]
)
self.assertEqual(result, 0)
self.assertEqual(
(original / "marker.txt").read_text(encoding="utf-8"),
"reviewed",
)
self.assertFalse((project / "marker.txt").exists())
def test_rejects_oversized_verification_target_before_snapshot(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
project = Path(temp_dir) / "project"
target = project / "checks" / "oversized"
target.parent.mkdir(parents=True)
with target.open("wb") as target_file:
target_file.truncate(RUNNER_MODULE.MAX_TARGET_BYTES + 1)
target.chmod(target.stat().st_mode | 0o111)
target_fd, target_args, error = RUNNER_MODULE.open_target(
knowledge_with_target("checks/oversized", []),
"reviewed-check",
project,
)
self.assertIsNone(target_fd)
self.assertIsNone(target_args)
self.assertIn("超过大小上限", error)
if __name__ == "__main__":
unittest.main()
+182
View File
@@ -0,0 +1,182 @@
from __future__ import annotations
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "skills" / "ack" / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
import validate_knowledge # noqa: E402
import validate_tasks # noqa: E402
from yaml_subset import YamlSubsetError, load_yaml_subset # noqa: E402
class AckYamlSubsetTests(unittest.TestCase):
def test_real_templates_and_examples_parse_under_clean_python(self) -> None:
script = """
import sys
from pathlib import Path
sys.path.insert(0, str(Path('skills/ack/scripts').resolve()))
from yaml_subset import load_yaml_subset
paths = (
Path('skills/ack/templates/tasks.template.yaml'),
Path('skills/ack/examples/tasks.example.yaml'),
Path('skills/ack/templates/knowledge.template.yaml'),
Path('skills/ack/examples/knowledge.example.yaml'),
)
for path in paths:
value = load_yaml_subset(path.read_text(encoding='utf-8'))
if not isinstance(value, dict):
raise SystemExit(f'{path}: top-level value is not a mapping')
print('parsed=4')
"""
result = subprocess.run(
[sys.executable, "-S", "-c", script],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "parsed=4")
def test_tasks_validator_runs_without_site_packages(self) -> None:
result = subprocess.run(
[
sys.executable,
"-S",
str(SCRIPTS_DIR / "validate_tasks.py"),
str(REPO_ROOT / "skills" / "ack" / "examples" / "tasks.example.yaml"),
],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("任务板校验通过", result.stdout)
def test_supported_subset_types_and_block_scalars(self) -> None:
document = load_yaml_subset(
"""
# comment
root:
list:
- null
- true
- -2
- name: 'single quoted'
flags: [false, "double quoted", {count: 3}]
emptyList: []
emptyMap: {}
folded: >
first line
second line
next paragraph
literal: |
first line
second line
"""
)
self.assertEqual(document["root"]["list"][:3], [None, True, -2])
self.assertEqual(
document["root"]["list"][3],
{
"name": "single quoted",
"flags": [False, "double quoted", {"count": 3}],
},
)
self.assertEqual(document["root"]["emptyList"], [])
self.assertEqual(document["root"]["emptyMap"], {})
self.assertEqual(
document["root"]["folded"],
"first line second line\nnext paragraph\n",
)
self.assertEqual(
document["root"]["literal"],
"first line\nsecond line\n",
)
def test_quoted_mapping_key_supports_yaml_single_quote_escape(self) -> None:
self.assertEqual(load_yaml_subset("'owner''s-key': value\n"), {"owner's-key": "value"})
def test_subset_rejects_duplicate_keys_at_any_depth(self) -> None:
invalid_documents = (
"name: first\nname: second\n",
"outer:\n name: first\n name: second\n",
"outer: {name: first, name: second}\n",
)
for document in invalid_documents:
with self.subTest(document=document), self.assertRaises(YamlSubsetError):
load_yaml_subset(document)
def test_subset_rejects_unsupported_yaml_instead_of_guessing(self) -> None:
invalid_documents = (
"root: &node\n value: 1\ncopy: *node\n",
"root: !custom value\n",
"root: {<<: {value: 1}}\n",
"---\nroot: value\n",
"root: >-\n value\n",
"root: 1.25\n",
"root:\n\tchild: value\n",
)
for document in invalid_documents:
with self.subTest(document=document), self.assertRaises(YamlSubsetError):
load_yaml_subset(document)
class AckDocumentLoaderTests(unittest.TestCase):
def test_tasks_yaml_loader_rejects_duplicate_keys_with_pyyaml(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "tasks.yaml"
path.write_text("version: 1\nversion: 2\n", encoding="utf-8")
with self.assertRaises(SystemExit) as raised:
validate_tasks.load_document(path)
self.assertEqual(raised.exception.code, 1)
def test_knowledge_yaml_loader_rejects_alias_graphs(self) -> None:
with self.assertRaises(SystemExit) as raised:
validate_knowledge.load_yaml_text(
"root: &root\n child: *root\n",
"知识库",
)
self.assertEqual(raised.exception.code, 1)
def test_tasks_json_loader_rejects_duplicate_keys(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "tasks.json"
path.write_text('{"version": 1, "version": 2}', encoding="utf-8")
with self.assertRaises(SystemExit) as raised:
validate_tasks.load_document(path)
self.assertEqual(raised.exception.code, 1)
def test_knowledge_json_loader_rejects_nested_duplicate_keys(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "knowledge.json"
path.write_text(
'{"project": {"name": "first", "name": "second"}}',
encoding="utf-8",
)
with self.assertRaises(SystemExit) as raised:
validate_knowledge.load_yaml(path, "知识库")
self.assertEqual(raised.exception.code, 1)
if __name__ == "__main__":
unittest.main()
+616
View File
@@ -6,7 +6,10 @@ import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
import skiff.cli
REPO_ROOT = Path(__file__).resolve().parents[1]
@@ -34,6 +37,18 @@ class SkillInitTests(unittest.TestCase):
' devWorktree: "<dev_worktree>"\n',
encoding="utf-8",
)
(skill / "templates" / "knowledge.template.yaml").write_text(
'updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"\n'
'project:\n'
' name: "<project_name>"\n'
' repoPath: "<repo_path>"\n',
encoding="utf-8",
)
for validator_name in ("validate_tasks.py", "validate_knowledge.py"):
(skill / "scripts" / validator_name).write_text(
"raise SystemExit(0)\n",
encoding="utf-8",
)
def tearDown(self) -> None:
self.temp_dir.cleanup()
@@ -63,10 +78,13 @@ class SkillInitTests(unittest.TestCase):
self.assertFalse((target / "framework").exists())
project_content = (target / "project.md").read_text(encoding="utf-8")
tasks_content = (target / "tasks.yaml").read_text(encoding="utf-8")
knowledge_content = (target / "knowledge.yaml").read_text(encoding="utf-8")
self.assertIn("# sample-app", project_content)
self.assertIn("version=1.2.3", project_content)
self.assertIn(f'repoPath: "{project}"', tasks_content)
self.assertIn(f'repoPath: "{project}"', knowledge_content)
self.assertNotIn("<project_name>", tasks_content)
self.assertNotIn("<project_name>", knowledge_content)
def test_init_refuses_to_overwrite_existing_files(self) -> None:
project = self.home / "existing-app"
@@ -81,6 +99,604 @@ class SkillInitTests(unittest.TestCase):
self.assertIn("拒绝覆盖已有路径", result.stderr)
self.assertEqual(existing.read_text(encoding="utf-8"), "keep me")
self.assertFalse((target / "tasks.yaml").exists())
self.assertFalse((target / "knowledge.yaml").exists())
def test_init_refuses_to_overwrite_existing_knowledge_file(self) -> None:
project = self.home / "existing-knowledge-app"
target = project / "docs" / "ack"
target.mkdir(parents=True)
existing = target / "knowledge.yaml"
existing.write_text("keep me", encoding="utf-8")
result = self.run_skiff("init", "ack", "--project", str(project))
self.assertNotEqual(result.returncode, 0)
self.assertIn("拒绝覆盖已有路径", result.stderr)
self.assertEqual(existing.read_text(encoding="utf-8"), "keep me")
self.assertFalse((target / "project.md").exists())
self.assertFalse((target / "tasks.yaml").exists())
def test_init_rejects_symlinked_destination_directories(self) -> None:
for symlink_level in ("docs", "ack"):
with self.subTest(symlink_level=symlink_level):
project = self.home / f"symlink-{symlink_level}-app"
outside = self.home / f"symlink-{symlink_level}-outside"
project.mkdir()
outside.mkdir()
if symlink_level == "docs":
(project / "docs").symlink_to(
outside,
target_is_directory=True,
)
else:
(project / "docs").mkdir()
(project / "docs" / "ack").symlink_to(
outside,
target_is_directory=True,
)
result = self.run_skiff(
"init",
"ack",
"--project",
str(project),
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("不能是软链接", result.stderr)
self.assertFalse((outside / "project.md").exists())
self.assertFalse((outside / "tasks.yaml").exists())
self.assertFalse((outside / "knowledge.yaml").exists())
def test_init_rejects_path_like_skill_name_before_resolving_targets(self) -> None:
project = self.home / "path-traversal-app"
outside = self.home / "path-traversal-outside"
project.mkdir()
outside.mkdir()
(project / "skills").symlink_to(outside, target_is_directory=True)
result = self.run_skiff(
"init",
"../skills/ack",
"--project",
str(project),
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("skill 名称无效", result.stderr)
self.assertFalse((outside / "ack").exists())
def test_atomic_publish_failure_never_exposes_partial_ack_directory(self) -> None:
project = self.home / "atomic-publish-app"
project.mkdir()
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_rename_directory_noreplace",
side_effect=RuntimeError("publish interrupted"),
),
):
with self.assertRaisesRegex(RuntimeError, "publish interrupted"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
self.assertFalse((project / "docs" / "ack").exists())
docs = project / "docs"
if docs.exists():
self.assertEqual(list(docs.iterdir()), [])
def test_atomic_publish_never_replaces_a_raced_destination(self) -> None:
project = self.home / "atomic-no-replace-app"
project.mkdir()
raced_inode: int | None = None
real_publish = skiff.cli._rename_directory_noreplace
def create_destination_then_publish(
source_parent_fd: int,
source_name: str,
destination_parent_fd: int,
destination_name: str,
) -> None:
nonlocal raced_inode
os.mkdir(destination_name, mode=0o711, dir_fd=destination_parent_fd)
raced_inode = os.stat(
destination_name,
dir_fd=destination_parent_fd,
follow_symlinks=False,
).st_ino
real_publish(
source_parent_fd,
source_name,
destination_parent_fd,
destination_name,
)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_rename_directory_noreplace",
side_effect=create_destination_then_publish,
),
):
with self.assertRaisesRegex(SystemExit, "拒绝覆盖已有路径"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
target = project / "docs" / "ack"
self.assertTrue(target.is_dir())
self.assertEqual(target.stat().st_ino, raced_inode)
self.assertEqual(list(target.iterdir()), [])
self.assertEqual(target.stat().st_mode & 0o777, 0o711)
def test_published_destination_replacement_never_reports_success(self) -> None:
project = self.home / "published-destination-app"
moved_target = project / "docs" / "ack-moved"
project.mkdir()
real_publish = skiff.cli._rename_directory_noreplace
def replace_destination_after_publish(
source_parent_fd: int,
source_name: str,
destination_parent_fd: int,
destination_name: str,
) -> None:
real_publish(
source_parent_fd,
source_name,
destination_parent_fd,
destination_name,
)
target = project / "docs" / destination_name
target.rename(moved_target)
target.mkdir()
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_rename_directory_noreplace",
side_effect=replace_destination_after_publish,
),
):
with self.assertRaisesRegex(SystemExit, "ACK 目录已被替换"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
self.assertEqual(list((project / "docs" / "ack").iterdir()), [])
self.assertEqual(
sorted(path.name for path in moved_target.iterdir()),
["knowledge.yaml", "project.md", "tasks.yaml"],
)
def test_transaction_container_replacement_cannot_forge_payload(self) -> None:
project = self.home / "transaction-source-app"
attacker = self.home / "transaction-attacker"
project.mkdir()
attacker.mkdir()
(attacker / "marker").write_text("forged", encoding="utf-8")
real_publish = skiff.cli._rename_directory_noreplace
def replace_outer_transaction_then_publish(
source_parent_fd: int,
source_name: str,
destination_parent_fd: int,
destination_name: str,
) -> None:
docs = project / "docs"
transactions = [
path
for path in docs.iterdir()
if path.name.startswith(".ack-init-")
]
self.assertEqual(len(transactions), 1)
transaction = transactions[0]
saved = docs / f"{transaction.name}.saved"
transaction.rename(saved)
transaction.symlink_to(attacker, target_is_directory=True)
real_publish(
source_parent_fd,
source_name,
destination_parent_fd,
destination_name,
)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_rename_directory_noreplace",
side_effect=replace_outer_transaction_then_publish,
),
):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
target = project / "docs" / "ack"
self.assertTrue(target.is_dir())
self.assertFalse(target.is_symlink())
self.assertFalse((target / "marker").exists())
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["knowledge.yaml", "project.md", "tasks.yaml"],
)
def test_post_publish_fsync_failure_preserves_complete_state(self) -> None:
project = self.home / "post-publish-fsync-app"
project.mkdir()
real_fsync = os.fsync
calls = 0
def fail_directory_fsync_after_publish(file_descriptor: int) -> None:
nonlocal calls
calls += 1
if calls == 7:
raise OSError("simulated directory fsync failure")
real_fsync(file_descriptor)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli.os,
"fsync",
side_effect=fail_directory_fsync_after_publish,
),
):
with self.assertRaisesRegex(SystemExit, "已完整发布"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
target = project / "docs" / "ack"
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["knowledge.yaml", "project.md", "tasks.yaml"],
)
def test_project_root_replacement_aborts_before_publish(self) -> None:
project = self.home / "root-replacement-app"
moved_project = self.home / "root-replacement-moved"
project.mkdir()
real_open_docs = skiff.cli._open_or_create_directory_at
replaced = False
def replace_root_then_open_docs(
parent_fd: int,
name: str,
) -> tuple[int, bool]:
nonlocal replaced
if not replaced:
project.rename(moved_project)
project.mkdir()
replaced = True
return real_open_docs(parent_fd, name)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_open_or_create_directory_at",
side_effect=replace_root_then_open_docs,
),
):
with self.assertRaisesRegex(SystemExit, "项目目录已被替换"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
self.assertFalse((project / "docs" / "ack").exists())
self.assertFalse((moved_project / "docs" / "ack").exists())
def test_project_root_replacement_at_publish_never_reports_success(self) -> None:
project = self.home / "publish-root-replacement-app"
moved_project = self.home / "publish-root-replacement-moved"
project.mkdir()
real_publish = skiff.cli._rename_directory_noreplace
def replace_root_then_publish(
source_parent_fd: int,
source_name: str,
destination_parent_fd: int,
destination_name: str,
) -> None:
project.rename(moved_project)
project.mkdir()
real_publish(
source_parent_fd,
source_name,
destination_parent_fd,
destination_name,
)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_rename_directory_noreplace",
side_effect=replace_root_then_publish,
),
):
with self.assertRaisesRegex(SystemExit, "ACK 目录已移动或不可访问"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
self.assertFalse((project / "docs" / "ack").exists())
target = moved_project / "docs" / "ack"
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["knowledge.yaml", "project.md", "tasks.yaml"],
)
def test_docs_replacement_aborts_before_publish(self) -> None:
project = self.home / "docs-replacement-app"
moved_docs = project / "docs-moved"
project.mkdir()
real_assert_binding = skiff.cli._assert_open_directory_path
replaced = False
def replace_docs_at_publish_check(
directory_fd: int,
path: Path,
*,
phase: str,
label: str = "项目目录",
) -> None:
nonlocal replaced
if label == "docs 目录" and phase == "发布" and not replaced:
(project / "docs").rename(moved_docs)
(project / "docs").mkdir()
replaced = True
real_assert_binding(
directory_fd,
path,
phase=phase,
label=label,
)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_assert_open_directory_path",
side_effect=replace_docs_at_publish_check,
),
):
with self.assertRaisesRegex(SystemExit, "docs 目录已被替换"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
self.assertFalse((project / "docs" / "ack").exists())
self.assertFalse((moved_docs / "ack").exists())
def test_docs_replacement_at_publish_never_reports_success(self) -> None:
project = self.home / "publish-docs-replacement-app"
moved_docs = project / "docs-moved"
project.mkdir()
real_publish = skiff.cli._rename_directory_noreplace
def replace_docs_then_publish(
source_parent_fd: int,
source_name: str,
destination_parent_fd: int,
destination_name: str,
) -> None:
(project / "docs").rename(moved_docs)
(project / "docs").mkdir()
real_publish(
source_parent_fd,
source_name,
destination_parent_fd,
destination_name,
)
with (
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
mock.patch.object(
skiff.cli,
"SKILLS_DIR",
self.skills_home / "skills",
),
mock.patch.object(skiff.cli, "ensure_skills_home"),
mock.patch.object(
skiff.cli,
"_rename_directory_noreplace",
side_effect=replace_docs_then_publish,
),
):
with self.assertRaisesRegex(SystemExit, "ACK 目录已移动或不可访问"):
skiff.cli.cmd_init(
SimpleNamespace(name="ack", project=str(project))
)
self.assertFalse((project / "docs" / "ack").exists())
target = moved_docs / "ack"
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["knowledge.yaml", "project.md", "tasks.yaml"],
)
def test_ack_init_requires_knowledge_template(self) -> None:
project = self.home / "missing-knowledge-template-app"
project.mkdir()
(
self.skills_home
/ "skills"
/ "ack"
/ "templates"
/ "knowledge.template.yaml"
).unlink()
result = self.run_skiff("init", "ack", "--project", str(project))
self.assertNotEqual(result.returncode, 0)
self.assertIn("knowledge.template.yaml", result.stderr)
self.assertFalse((project / "docs" / "ack").exists())
def test_ack_init_requires_both_validators(self) -> None:
for validator_name in ("validate_tasks.py", "validate_knowledge.py"):
with self.subTest(validator_name=validator_name):
project = self.home / f"missing-{validator_name}-app"
project.mkdir()
validator = (
self.skills_home
/ "skills"
/ "ack"
/ "scripts"
/ validator_name
)
original = validator.read_text(encoding="utf-8")
validator.unlink()
try:
result = self.run_skiff(
"init",
"ack",
"--project",
str(project),
)
finally:
validator.write_text(original, encoding="utf-8")
self.assertNotEqual(result.returncode, 0)
self.assertIn("缺少初始化校验器", result.stderr)
self.assertIn(validator_name, result.stderr)
self.assertFalse((project / "docs" / "ack").exists())
def test_validator_failure_leaves_no_partial_initialization(self) -> None:
project = self.home / "invalid-knowledge-app"
project.mkdir()
validator = (
self.skills_home / "skills" / "ack" / "scripts" / "validate_knowledge.py"
)
validator.write_text("raise SystemExit(1)\n", encoding="utf-8")
result = self.run_skiff("init", "ack", "--project", str(project))
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("Traceback", result.stderr)
self.assertIn("初始化知识库校验失败", result.stderr)
self.assertFalse((project / "docs" / "ack").exists())
def test_validator_cannot_replace_staged_bytes_before_install(self) -> None:
project = self.home / "mutated-staging-app"
project.mkdir()
validator = (
self.skills_home / "skills" / "ack" / "scripts" / "validate_knowledge.py"
)
validator.write_text(
"import sys\n"
"from pathlib import Path\n"
"Path(sys.argv[1]).write_text('forged: true\\n', encoding='utf-8')\n",
encoding="utf-8",
)
result = self.run_skiff("init", "ack", "--project", str(project))
self.assertNotEqual(result.returncode, 0)
self.assertIn("临时文件在校验期间发生变化", result.stderr)
self.assertFalse((project / "docs" / "ack").exists())
def test_ack_init_validates_mirrored_staging_root(self) -> None:
project = self.home / "staged-knowledge-app"
project.mkdir()
validator = (
self.skills_home / "skills" / "ack" / "scripts" / "validate_knowledge.py"
)
validator.write_text(
"import sys\n"
"from pathlib import Path\n"
"required = ['--tasks', '--project-root']\n"
"if any(item not in sys.argv for item in required):\n"
" raise SystemExit(3)\n"
"root = Path(sys.argv[sys.argv.index('--project-root') + 1])\n"
"knowledge = Path(sys.argv[1])\n"
"tasks = Path(sys.argv[sys.argv.index('--tasks') + 1])\n"
"expected = root / 'docs' / 'ack'\n"
"raise SystemExit(0 if knowledge.parent == expected and "
"tasks.parent == expected else 4)\n",
encoding="utf-8",
)
result = self.run_skiff("init", "ack", "--project", str(project))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((project / "docs" / "ack" / "knowledge.yaml").is_file())
def test_non_ack_init_still_requires_only_project_and_tasks_templates(self) -> None:
skill = self.skills_home / "skills" / "plain"
(skill / "templates").mkdir(parents=True)
(skill / "SKILL.md").write_text("---\nname: plain\n---\n", encoding="utf-8")
(skill / "templates" / "project.template.md").write_text(
"# <project_name>\n", encoding="utf-8"
)
(skill / "templates" / "tasks.template.yaml").write_text(
'project: "<project_name>"\n', encoding="utf-8"
)
project = self.home / "plain-app"
project.mkdir()
result = self.run_skiff("init", "plain", "--project", str(project))
self.assertEqual(result.returncode, 0, result.stderr)
target = project / "docs" / "plain"
self.assertTrue((target / "project.md").is_file())
self.assertTrue((target / "tasks.yaml").is_file())
self.assertFalse((target / "knowledge.yaml").exists())
def test_init_rejects_missing_project_directory(self) -> None:
project = self.home / "missing-app"