feat: rename skills/skiff to pouch and move ACK state under .pouch
Use ~/.pouch, the pouch CLI, and .pouch.yaml as the SSOT container. Keep the inner skills/ packages, and store ACK project state in .pouch/ack instead of docs/ack.
This commit is contained in:
@@ -291,7 +291,7 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
||||
errors = validate_delivery.validate_tasks_link(contract, tasks)
|
||||
|
||||
self.assertIn(
|
||||
"tasks.project.deliveryFile 必须固定为 docs/ack/delivery.yaml",
|
||||
"tasks.project.deliveryFile 必须固定为 .pouch/ack/delivery.yaml",
|
||||
errors,
|
||||
)
|
||||
self.assertIn("delivery.project.name 必须与 tasks.project.name 一致", errors)
|
||||
|
||||
+14
-14
@@ -445,7 +445,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
tasks_data = {
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
},
|
||||
"tasks": [task],
|
||||
}
|
||||
@@ -470,7 +470,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
tasks = {
|
||||
"project": {
|
||||
"name": "another-project",
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
},
|
||||
"tasks": [],
|
||||
}
|
||||
@@ -502,7 +502,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
tasks_data = {
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
},
|
||||
"tasks": [task],
|
||||
}
|
||||
@@ -522,7 +522,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
tasks_data = {
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
@@ -567,7 +567,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
project = Path(temp_dir) / "project"
|
||||
ack_dir = project / "docs" / "ack"
|
||||
ack_dir = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
knowledge_path = ack_dir / "knowledge.yaml"
|
||||
tasks_path = ack_dir / "tasks.yaml"
|
||||
@@ -578,7 +578,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"repoPath": str(project),
|
||||
"knowledgeFile": "docs/ack/other.yaml",
|
||||
"knowledgeFile": ".pouch/ack/other.yaml",
|
||||
},
|
||||
"tasks": [task],
|
||||
}
|
||||
@@ -595,9 +595,9 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
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"
|
||||
tasks_data["project"]["knowledgeFile"] = ".pouch/ack/knowledge.yaml"
|
||||
staging_root = Path(temp_dir) / "staging"
|
||||
staged = staging_root / "docs" / "ack" / "knowledge.yaml"
|
||||
staged = staging_root / ".pouch" / "ack" / "knowledge.yaml"
|
||||
staged.parent.mkdir(parents=True)
|
||||
staged.write_text(
|
||||
yaml.safe_dump(data, allow_unicode=True), encoding="utf-8"
|
||||
@@ -636,7 +636,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base = Path(temp_dir)
|
||||
project = base / "project"
|
||||
ack_dir = project / "docs" / "ack"
|
||||
ack_dir = project / ".pouch" / "ack"
|
||||
outside = base / "outside"
|
||||
ack_dir.mkdir(parents=True)
|
||||
outside.mkdir()
|
||||
@@ -653,7 +653,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"repoPath": str(project / "missing"),
|
||||
"knowledgeFile": "docs/ack/not-the-current-file.yaml",
|
||||
"knowledgeFile": ".pouch/ack/not-the-current-file.yaml",
|
||||
},
|
||||
"tasks": [task],
|
||||
}
|
||||
@@ -746,7 +746,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
tasks_data = {
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
},
|
||||
"tasks": [task],
|
||||
}
|
||||
@@ -765,7 +765,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
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 = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
knowledge_path = ack_dir / "knowledge.yaml"
|
||||
knowledge_path.write_text(
|
||||
@@ -779,7 +779,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
"project": {
|
||||
"name": "notes-web",
|
||||
"repoPath": str(project),
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
@@ -878,7 +878,7 @@ class AckKnowledgeTests(unittest.TestCase):
|
||||
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 = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
knowledge_path = ack_dir / "knowledge.yaml"
|
||||
tasks_path = ack_dir / "tasks.yaml"
|
||||
|
||||
@@ -435,7 +435,7 @@ class PlanTests(unittest.TestCase):
|
||||
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
project = Path(temporary).resolve()
|
||||
ack_dir = project / "docs" / "ack"
|
||||
ack_dir = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
task_board = board(project)
|
||||
del task_board["project"]["repoPath"]
|
||||
@@ -454,7 +454,7 @@ class PlanTests(unittest.TestCase):
|
||||
def test_authoritative_board_ignores_legacy_repo_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
project = Path(temporary).resolve()
|
||||
ack_dir = project / "docs" / "ack"
|
||||
ack_dir = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
task_board = board(project)
|
||||
task_board["project"]["repoPath"] = "/legacy/other-worktree"
|
||||
|
||||
@@ -13,11 +13,11 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
content = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(encoding="utf-8")
|
||||
|
||||
for expected in (
|
||||
"skiff init ack --project <project-root>",
|
||||
"docs/ack/project.md",
|
||||
"docs/ack/tasks.yaml",
|
||||
"docs/ack/knowledge.yaml",
|
||||
"docs/ack/delivery.yaml",
|
||||
"pouch init ack --project <project-root>",
|
||||
".pouch/ack/project.md",
|
||||
".pouch/ack/tasks.yaml",
|
||||
".pouch/ack/knowledge.yaml",
|
||||
".pouch/ack/delivery.yaml",
|
||||
"tasks: []",
|
||||
"validate_tasks.py",
|
||||
"validate_knowledge.py",
|
||||
@@ -131,7 +131,7 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
):
|
||||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||||
self.assertEqual(version, "0.18.0")
|
||||
self.assertEqual(version, "0.19.0")
|
||||
self.assertIn(
|
||||
f'ackVersion: "{version}"',
|
||||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||||
@@ -156,7 +156,7 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
# v0.19 起 allowedWorktrees 白名单废弃:模板只保留废弃说明注释,不再生成该字段
|
||||
self.assertNotIn("allowedWorktrees:", template)
|
||||
self.assertIn("allowedWorktrees", template)
|
||||
self.assertIn('knowledgeFile: "docs/ack/knowledge.yaml"', template)
|
||||
self.assertIn('knowledgeFile: ".pouch/ack/knowledge.yaml"', template)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -364,7 +364,7 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
def test_delivery_run_is_separate_and_requires_verified_tasks(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["project"]["deliveryFile"] = ".pouch/ack/delivery.yaml"
|
||||
board["tasks"][0]["status"] = "verified"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
@@ -405,7 +405,7 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
def test_validation_ready_delivery_run_does_not_require_pull_request(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["project"]["deliveryFile"] = ".pouch/ack/delivery.yaml"
|
||||
board["tasks"][0]["status"] = "verified"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
@@ -440,7 +440,7 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
def test_intent_delivery_run_allows_empty_task_ids(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["project"]["deliveryFile"] = ".pouch/ack/delivery.yaml"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
"id": "DR-test-env-1",
|
||||
@@ -488,7 +488,7 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["project"]["deliveryFile"] = ".pouch/ack/delivery.yaml"
|
||||
self.assert_board_rejected_in_all_modes(
|
||||
board,
|
||||
"引用 deliveryFile 的任务板必须包含 deliveryRuns 列表",
|
||||
@@ -496,7 +496,7 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
def test_delivery_run_binds_revisions_and_final_artifact_digest(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["project"]["deliveryFile"] = ".pouch/ack/delivery.yaml"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
"id": "DR-demo-2",
|
||||
@@ -912,11 +912,11 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
def test_knowledge_file_is_fixed_in_all_modes(self) -> None:
|
||||
board = valid_knowledge_board()
|
||||
board["project"]["knowledgeFile"] = "docs/ack/alternate.yaml"
|
||||
board["project"]["knowledgeFile"] = ".pouch/ack/alternate.yaml"
|
||||
|
||||
self.assert_board_rejected_in_all_modes(
|
||||
board,
|
||||
"project.knowledgeFile 必须固定为 docs/ack/knowledge.yaml",
|
||||
"project.knowledgeFile 必须固定为 .pouch/ack/knowledge.yaml",
|
||||
)
|
||||
|
||||
def test_basic_identifiers_must_be_nonempty_strings_in_all_modes(self) -> None:
|
||||
@@ -1116,8 +1116,8 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
"repoPath": "/repo",
|
||||
"baseUrl": "http://127.0.0.1:3000",
|
||||
"devWorktree": "/repo-dev",
|
||||
"overlayFile": "docs/ack/project.md",
|
||||
"knowledgeFile": "docs/ack/knowledge.yaml",
|
||||
"overlayFile": ".pouch/ack/project.md",
|
||||
"knowledgeFile": ".pouch/ack/knowledge.yaml",
|
||||
}
|
||||
)
|
||||
board["tasks"][0].update(
|
||||
|
||||
@@ -42,7 +42,7 @@ class AckVerificationRunnerTests(unittest.TestCase):
|
||||
project: Path,
|
||||
knowledge: dict,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
|
||||
knowledge_path = project / ".pouch" / "ack" / "knowledge.yaml"
|
||||
knowledge_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
knowledge_path.write_text(
|
||||
yaml.safe_dump(knowledge, allow_unicode=True),
|
||||
@@ -153,7 +153,7 @@ class AckVerificationRunnerTests(unittest.TestCase):
|
||||
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 = project / ".pouch" / "ack" / "knowledge.yaml"
|
||||
knowledge_path.parent.mkdir(parents=True)
|
||||
knowledge_path.write_text(
|
||||
yaml.safe_dump(
|
||||
@@ -194,7 +194,7 @@ class AckVerificationRunnerTests(unittest.TestCase):
|
||||
encoding="utf-8",
|
||||
)
|
||||
target.chmod(target.stat().st_mode | 0o111)
|
||||
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
|
||||
knowledge_path = project / ".pouch" / "ack" / "knowledge.yaml"
|
||||
knowledge_path.parent.mkdir(parents=True)
|
||||
knowledge_path.write_text(
|
||||
"""\
|
||||
@@ -238,7 +238,7 @@ entries: []
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base = Path(temp_dir)
|
||||
project = base / "project"
|
||||
ack_dir = project / "docs" / "ack"
|
||||
ack_dir = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
document = yaml.safe_dump(
|
||||
knowledge_with_target("checks/reviewed", []),
|
||||
@@ -293,7 +293,7 @@ entries: []
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base = Path(temp_dir)
|
||||
project = base / "project"
|
||||
canonical = project / "docs" / "ack" / "knowledge.yaml"
|
||||
canonical = project / ".pouch" / "ack" / "knowledge.yaml"
|
||||
canonical.parent.mkdir(parents=True)
|
||||
safe_document = knowledge_with_target("checks/safe", [])
|
||||
forged_document = knowledge_with_target("checks/danger", [])
|
||||
@@ -335,7 +335,7 @@ entries: []
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
project = Path(temp_dir) / "project"
|
||||
canonical = project / "docs" / "ack" / "knowledge.yaml"
|
||||
canonical = project / ".pouch" / "ack" / "knowledge.yaml"
|
||||
canonical.parent.mkdir(parents=True)
|
||||
canonical.write_text(
|
||||
yaml.safe_dump(
|
||||
@@ -346,7 +346,7 @@ entries: []
|
||||
)
|
||||
source_fd, error = RUNNER_MODULE._open_regular_beneath(
|
||||
project,
|
||||
"docs/ack/knowledge.yaml",
|
||||
".pouch/ack/knowledge.yaml",
|
||||
require_executable=False,
|
||||
)
|
||||
self.assertIsNone(error)
|
||||
@@ -521,7 +521,7 @@ entries: []
|
||||
encoding="utf-8",
|
||||
)
|
||||
target.chmod(target.stat().st_mode | 0o111)
|
||||
knowledge_path = project / "docs" / "ack" / "knowledge.yaml"
|
||||
knowledge_path = project / ".pouch" / "ack" / "knowledge.yaml"
|
||||
knowledge_path.parent.mkdir(parents=True)
|
||||
knowledge_path.write_text(
|
||||
yaml.safe_dump(
|
||||
|
||||
+11
-11
@@ -25,7 +25,7 @@ class AgentsTargetTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.home = Path(self.temp_dir.name)
|
||||
self.skills_home = self.home / ".skills"
|
||||
self.skills_home = self.home / ".pouch"
|
||||
self.skills_home.mkdir(parents=True)
|
||||
(self.skills_home / "catalog.yaml").write_text("", encoding="utf-8")
|
||||
write_skill(self.skills_home, "demo-skill")
|
||||
@@ -33,12 +33,12 @@ class AgentsTargetTests(unittest.TestCase):
|
||||
def tearDown(self) -> None:
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_skiff(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
def run_pouch(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(self.home)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "skiff", *args],
|
||||
[sys.executable, "-m", "pouch", *args],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
@@ -47,7 +47,7 @@ class AgentsTargetTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_global_add_creates_symlink_in_agents_skills(self) -> None:
|
||||
result = self.run_skiff("add", "builtin/demo-skill", "-g", "-a", "agents")
|
||||
result = self.run_pouch("add", "builtin/demo-skill", "-g", "-a", "agents")
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
link = self.home / ".agents" / "skills" / "demo-skill"
|
||||
@@ -56,7 +56,7 @@ class AgentsTargetTests(unittest.TestCase):
|
||||
link.resolve(), (self.skills_home / "skills" / "demo-skill").resolve()
|
||||
)
|
||||
|
||||
removed = self.run_skiff("rm", "builtin/demo-skill", "-g", "-a", "agents")
|
||||
removed = self.run_pouch("rm", "builtin/demo-skill", "-g", "-a", "agents")
|
||||
self.assertEqual(removed.returncode, 0, removed.stderr)
|
||||
self.assertFalse(link.exists())
|
||||
|
||||
@@ -64,7 +64,7 @@ class AgentsTargetTests(unittest.TestCase):
|
||||
project = self.home / "app"
|
||||
project.mkdir()
|
||||
|
||||
added = self.run_skiff(
|
||||
added = self.run_pouch(
|
||||
"add",
|
||||
"builtin/demo-skill",
|
||||
"--project",
|
||||
@@ -80,25 +80,25 @@ class AgentsTargetTests(unittest.TestCase):
|
||||
links = list((project / ".agents" / "skills").glob("demo-skill"))
|
||||
self.assertEqual(len(links), 1)
|
||||
self.assertTrue(links[0].is_symlink())
|
||||
manifest = (project / ".skills.yaml").read_text(encoding="utf-8")
|
||||
manifest = (project / ".pouch.yaml").read_text(encoding="utf-8")
|
||||
self.assertIn("demo-skill", manifest)
|
||||
|
||||
removed = self.run_skiff(
|
||||
removed = self.run_pouch(
|
||||
"rm", "builtin/demo-skill", "--project", str(project), "-y"
|
||||
)
|
||||
self.assertEqual(removed.returncode, 0, removed.stderr)
|
||||
self.assertFalse((project / ".agents" / "skills" / "demo-skill").exists())
|
||||
|
||||
def test_status_lists_installed_skill_for_agents_target(self) -> None:
|
||||
installed = self.run_skiff("add", "builtin/demo-skill", "-g", "-a", "agents")
|
||||
status = self.run_skiff("status", "-a", "agents")
|
||||
installed = self.run_pouch("add", "builtin/demo-skill", "-g", "-a", "agents")
|
||||
status = self.run_pouch("status", "-a", "agents")
|
||||
|
||||
self.assertEqual(installed.returncode, 0, installed.stderr)
|
||||
self.assertEqual(status.returncode, 0, status.stderr)
|
||||
self.assertIn("demo-skill", status.stdout)
|
||||
|
||||
def test_opencode_target_is_no_longer_supported(self) -> None:
|
||||
result = self.run_skiff("add", "builtin/demo-skill", "-g", "-a", "opencode")
|
||||
result = self.run_pouch("add", "builtin/demo-skill", "-g", "-a", "opencode")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("未知 agent", result.stderr)
|
||||
|
||||
@@ -9,8 +9,8 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from skiff import catalog
|
||||
from skiff.catalog import catalog_checkout_path, catalog_repo
|
||||
from pouch import catalog
|
||||
from pouch.catalog import catalog_checkout_path, catalog_repo
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -40,7 +40,7 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.home = Path(self.temp_dir.name)
|
||||
self.skills_home = self.home / ".skills"
|
||||
self.skills_home = self.home / ".pouch"
|
||||
(self.skills_home / "skills").mkdir(parents=True)
|
||||
|
||||
self.upstream = self.home / "upstream"
|
||||
@@ -79,12 +79,12 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
def tearDown(self) -> None:
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_skiff(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
def run_pouch(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(self.home)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "skiff", *args],
|
||||
[sys.executable, "-m", "pouch", *args],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
@@ -95,16 +95,16 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
def test_catalog_repo_expands_home_relative_local_path(self) -> None:
|
||||
with patch.dict(os.environ, {"HOME": str(self.home)}):
|
||||
self.assertEqual(
|
||||
catalog_repo({"repo": "~/.skills"}),
|
||||
catalog_repo({"repo": "~/.pouch"}),
|
||||
str(self.skills_home),
|
||||
)
|
||||
self.assertEqual(
|
||||
catalog_checkout_path("skills", {"repo": "~/.skills"}),
|
||||
catalog_checkout_path("skills", {"repo": "~/.pouch"}),
|
||||
self.skills_home.resolve(),
|
||||
)
|
||||
|
||||
def test_add_collection_installs_every_discovered_skill(self) -> None:
|
||||
result = self.run_skiff("add", "test-pack", "-g", "-a", "codex")
|
||||
result = self.run_pouch("add", "test-pack", "-g", "-a", "codex")
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
skill_dir = self.home / ".codex" / "skills"
|
||||
@@ -115,7 +115,7 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
self.assertFalse((skill_dir / "test-pack").exists())
|
||||
|
||||
def test_add_qualified_collection_skill_installs_only_that_skill(self) -> None:
|
||||
result = self.run_skiff("add", "test-pack/second-skill", "-g", "-a", "codex")
|
||||
result = self.run_pouch("add", "test-pack/second-skill", "-g", "-a", "codex")
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
skill_dir = self.home / ".codex" / "skills"
|
||||
@@ -124,7 +124,7 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
self.assertTrue((skill_dir / "second-skill" / "SKILL.md").is_file())
|
||||
|
||||
def test_add_collection_with_catalog_source_installs_all(self) -> None:
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"add",
|
||||
"test-pack",
|
||||
"--source",
|
||||
@@ -140,7 +140,7 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
self.assertTrue((skill_dir / "second-skill").is_symlink())
|
||||
|
||||
def test_legacy_registry_source_alias_still_installs(self) -> None:
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"add",
|
||||
"test-pack",
|
||||
"--source",
|
||||
@@ -159,7 +159,7 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
project = self.home / "project"
|
||||
project.mkdir()
|
||||
|
||||
added = self.run_skiff(
|
||||
added = self.run_pouch(
|
||||
"add",
|
||||
"test-pack/first-skill",
|
||||
"--project",
|
||||
@@ -169,8 +169,8 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
)
|
||||
link = project / ".agents" / "skills" / "first-skill"
|
||||
link.unlink()
|
||||
shutil.rmtree(self.home / ".local" / "share" / "skills" / "externals")
|
||||
synced = self.run_skiff(
|
||||
shutil.rmtree(self.home / ".local" / "share" / "pouch" / "externals")
|
||||
synced = self.run_pouch(
|
||||
"sync",
|
||||
"--project",
|
||||
str(project),
|
||||
@@ -179,7 +179,7 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(added.returncode, 0, added.stderr)
|
||||
self.assertIn('source: "catalog:test-pack"', project.joinpath(".skills.yaml").read_text())
|
||||
self.assertIn('source: "catalog:test-pack"', project.joinpath(".pouch.yaml").read_text())
|
||||
self.assertEqual(synced.returncode, 0, synced.stderr)
|
||||
self.assertTrue(link.is_symlink())
|
||||
self.assertTrue((link / "SKILL.md").is_file())
|
||||
@@ -188,15 +188,15 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
blocked = self.home / ".codex" / "skills" / "second-skill"
|
||||
blocked.mkdir(parents=True)
|
||||
|
||||
result = self.run_skiff("add", "test-pack", "-g", "-a", "codex")
|
||||
result = self.run_pouch("add", "test-pack", "-g", "-a", "codex")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse((self.home / ".codex" / "skills" / "first-skill").exists())
|
||||
self.assertTrue(blocked.is_dir())
|
||||
|
||||
def test_remove_collection_removes_all_child_links(self) -> None:
|
||||
installed = self.run_skiff("add", "test-pack", "-g", "-a", "codex")
|
||||
removed = self.run_skiff("remove", "test-pack", "-g", "-a", "codex")
|
||||
installed = self.run_pouch("add", "test-pack", "-g", "-a", "codex")
|
||||
removed = self.run_pouch("remove", "test-pack", "-g", "-a", "codex")
|
||||
|
||||
self.assertEqual(installed.returncode, 0, installed.stderr)
|
||||
self.assertEqual(removed.returncode, 0, removed.stderr)
|
||||
@@ -205,8 +205,8 @@ class CatalogCollectionTests(unittest.TestCase):
|
||||
self.assertFalse((skill_dir / "second-skill").exists())
|
||||
|
||||
def test_status_expands_collection_children(self) -> None:
|
||||
installed = self.run_skiff("add", "test-pack", "-g", "-a", "codex")
|
||||
status = self.run_skiff("status", "-a", "codex")
|
||||
installed = self.run_pouch("add", "test-pack", "-g", "-a", "codex")
|
||||
status = self.run_pouch("status", "-a", "codex")
|
||||
|
||||
self.assertEqual(installed.returncode, 0, installed.stderr)
|
||||
self.assertEqual(status.returncode, 0, status.stderr)
|
||||
|
||||
@@ -9,7 +9,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from skiff import cli
|
||||
from pouch import cli
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -19,7 +19,7 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.home = Path(self.temp_dir.name)
|
||||
self.skills_home = self.home / ".skills"
|
||||
self.skills_home = self.home / ".pouch"
|
||||
template = self.skills_home / "skills" / "_template"
|
||||
template.mkdir(parents=True)
|
||||
template.joinpath("SKILL.md").write_text(
|
||||
@@ -35,28 +35,28 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
"给出一条可以直接交给 Agent 的示例请求。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
project_skill = self.skills_home / "skills" / "skiff"
|
||||
project_skill = self.skills_home / "skills" / "pouch"
|
||||
project_skill.mkdir()
|
||||
project_skill.joinpath("SKILL.md").write_text(
|
||||
"---\nname: skiff\ndescription: >-\n"
|
||||
"---\nname: pouch\ndescription: >-\n"
|
||||
" 创建和维护自研 skill。用于创建、安装、反馈或更新 skill 时。\n"
|
||||
"---\n\n# skiff\n\n## 步骤\n\n1. 维护 skill。\n",
|
||||
"---\n\n# pouch\n\n## 步骤\n\n1. 维护 skill。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
project_skill.joinpath("README.md").write_text(
|
||||
"# skiff\n\n供人类使用的 skiff 说明。\n",
|
||||
"# pouch\n\n供人类使用的 pouch 说明。\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_skiff(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
def run_pouch(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(self.home)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "skiff", *args],
|
||||
[sys.executable, "-m", "pouch", *args],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
@@ -65,25 +65,25 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_setup_command_is_not_exposed(self) -> None:
|
||||
help_result = self.run_skiff("--help")
|
||||
setup_result = self.run_skiff("setup", str(self.skills_home))
|
||||
help_result = self.run_pouch("--help")
|
||||
setup_result = self.run_pouch("setup", str(self.skills_home))
|
||||
|
||||
self.assertEqual(help_result.returncode, 0, help_result.stderr)
|
||||
self.assertNotIn("setup", help_result.stdout)
|
||||
self.assertNotEqual(setup_result.returncode, 0)
|
||||
|
||||
def test_bootstrap_installs_project_skill_globally_for_all_agents(self) -> None:
|
||||
first = self.run_skiff("bootstrap")
|
||||
second = self.run_skiff("bootstrap")
|
||||
first = self.run_pouch("bootstrap")
|
||||
second = self.run_pouch("bootstrap")
|
||||
|
||||
self.assertEqual(first.returncode, 0, first.stderr)
|
||||
self.assertEqual(second.returncode, 0, second.stderr)
|
||||
expected = (self.skills_home / "skills" / "skiff").resolve()
|
||||
expected = (self.skills_home / "skills" / "pouch").resolve()
|
||||
for relative in (
|
||||
".cursor/skills/skiff",
|
||||
".claude/skills/skiff",
|
||||
".codex/skills/skiff",
|
||||
".agents/skills/skiff",
|
||||
".cursor/skills/pouch",
|
||||
".claude/skills/pouch",
|
||||
".codex/skills/pouch",
|
||||
".agents/skills/pouch",
|
||||
):
|
||||
link = self.home / relative
|
||||
self.assertTrue(link.is_symlink(), relative)
|
||||
@@ -93,8 +93,8 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
|
||||
def test_update_pulls_skills_home(self) -> None:
|
||||
with (
|
||||
patch.object(cli, "SKILLS_HOME", self.skills_home),
|
||||
patch.object(cli, "ensure_skills_home"),
|
||||
patch.object(cli, "POUCH_HOME", self.skills_home),
|
||||
patch.object(cli, "ensure_pouch_home"),
|
||||
patch.object(cli.subprocess, "run") as run,
|
||||
):
|
||||
cli.cmd_update(argparse.Namespace())
|
||||
@@ -108,7 +108,7 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
project = self.home / "project"
|
||||
project.mkdir()
|
||||
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"create",
|
||||
"migration-review",
|
||||
"--idea",
|
||||
@@ -128,19 +128,19 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
self.assertIn("idea: 检查数据库迁移", brief)
|
||||
self.assertIn(f"source_project: {project.resolve()}", brief)
|
||||
self.assertFalse((self.skills_home / "skills" / "migration-review").exists())
|
||||
self.assertIn("请完善 skiff 草稿 migration-review", result.stdout)
|
||||
self.assertIn("请完善 pouch 草稿 migration-review", result.stdout)
|
||||
|
||||
def test_check_rejects_template_placeholders(self) -> None:
|
||||
created = self.run_skiff("create", "migration-review", "--idea", "检查数据库迁移")
|
||||
created = self.run_pouch("create", "migration-review", "--idea", "检查数据库迁移")
|
||||
self.assertEqual(created.returncode, 0, created.stderr)
|
||||
|
||||
result = self.run_skiff("check", "migration-review")
|
||||
result = self.run_pouch("check", "migration-review")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("模板占位", result.stderr)
|
||||
|
||||
def test_finalize_moves_valid_draft_and_removes_brief(self) -> None:
|
||||
created = self.run_skiff("create", "migration-review", "--idea", "检查数据库迁移")
|
||||
created = self.run_pouch("create", "migration-review", "--idea", "检查数据库迁移")
|
||||
self.assertEqual(created.returncode, 0, created.stderr)
|
||||
draft = self.skills_home / ".drafts" / "migration-review"
|
||||
draft.joinpath("SKILL.md").write_text(
|
||||
@@ -156,7 +156,7 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_skiff("finalize", "migration-review")
|
||||
result = self.run_pouch("finalize", "migration-review")
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
final = self.skills_home / "skills" / "migration-review"
|
||||
@@ -179,7 +179,7 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_skiff("check", "reference-check")
|
||||
result = self.run_pouch("check", "reference-check")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("引用文件不存在", result.stderr)
|
||||
@@ -194,19 +194,19 @@ class CreateWorkflowTests(unittest.TestCase):
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_skiff("check", "missing-readme")
|
||||
result = self.run_pouch("check", "missing-readme")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("缺少 README.md", result.stderr)
|
||||
|
||||
def test_finalize_does_not_overwrite_existing_owned_skill(self) -> None:
|
||||
created = self.run_skiff("create", "collision-check", "--idea", "检查名称冲突")
|
||||
created = self.run_pouch("create", "collision-check", "--idea", "检查名称冲突")
|
||||
self.assertEqual(created.returncode, 0, created.stderr)
|
||||
final = self.skills_home / "skills" / "collision-check"
|
||||
final.mkdir()
|
||||
final.joinpath("marker").write_text("keep", encoding="utf-8")
|
||||
|
||||
result = self.run_skiff("finalize", "collision-check")
|
||||
result = self.run_pouch("finalize", "collision-check")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(final.joinpath("marker").read_text(encoding="utf-8"), "keep")
|
||||
|
||||
@@ -8,8 +8,8 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from skiff.project import normalize_skill_entry
|
||||
from skiff.sources import fetch_source
|
||||
from pouch.project import normalize_skill_entry
|
||||
from pouch.sources import fetch_source
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -41,19 +41,19 @@ class CustomSourceTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.home = Path(self.temp_dir.name)
|
||||
self.skills_home = self.home / ".skills"
|
||||
self.skills_home = self.home / ".pouch"
|
||||
(self.skills_home / "skills").mkdir(parents=True)
|
||||
(self.skills_home / "catalog.yaml").write_text("", encoding="utf-8")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_skiff(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
def run_pouch(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(self.home)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "skiff", *args],
|
||||
[sys.executable, "-m", "pouch", *args],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
@@ -65,7 +65,7 @@ class CustomSourceTests(unittest.TestCase):
|
||||
company = self.home / "company"
|
||||
expected = write_skill(company / "internal" / "skills", "code-review")
|
||||
|
||||
added = self.run_skiff(
|
||||
added = self.run_pouch(
|
||||
"source",
|
||||
"add",
|
||||
"company",
|
||||
@@ -74,8 +74,8 @@ class CustomSourceTests(unittest.TestCase):
|
||||
"--skills-path",
|
||||
"internal/skills",
|
||||
)
|
||||
listed = self.run_skiff("list", "--source", "company")
|
||||
installed = self.run_skiff("add", "company/code-review", "-g", "-a", "codex")
|
||||
listed = self.run_pouch("list", "--source", "company")
|
||||
installed = self.run_pouch("add", "company/code-review", "-g", "-a", "codex")
|
||||
|
||||
self.assertEqual(added.returncode, 0, added.stderr)
|
||||
self.assertEqual(listed.returncode, 0, listed.stderr)
|
||||
@@ -84,7 +84,7 @@ class CustomSourceTests(unittest.TestCase):
|
||||
link = self.home / ".codex" / "skills" / "code-review"
|
||||
self.assertTrue(link.is_symlink())
|
||||
self.assertEqual(link.resolve(), expected.resolve())
|
||||
config = self.home / ".config" / "skiff" / "config.yaml"
|
||||
config = self.home / ".config" / "pouch" / "config.yaml"
|
||||
self.assertIn("company:", config.read_text(encoding="utf-8"))
|
||||
|
||||
def test_local_source_can_expose_a_single_skill(self) -> None:
|
||||
@@ -94,7 +94,7 @@ class CustomSourceTests(unittest.TestCase):
|
||||
"---\nname: solo\ndescription: Test single source.\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
added = self.run_skiff(
|
||||
added = self.run_pouch(
|
||||
"source",
|
||||
"add",
|
||||
"solo",
|
||||
@@ -103,7 +103,7 @@ class CustomSourceTests(unittest.TestCase):
|
||||
"--skills-path",
|
||||
".",
|
||||
)
|
||||
installed = self.run_skiff(
|
||||
installed = self.run_pouch(
|
||||
"add",
|
||||
"solo",
|
||||
"-g",
|
||||
@@ -122,10 +122,10 @@ class CustomSourceTests(unittest.TestCase):
|
||||
expected = write_skill(company / "skills", "code-review")
|
||||
project = self.home / "project"
|
||||
project.mkdir()
|
||||
added = self.run_skiff("source", "add", "company", "--local", str(company))
|
||||
added = self.run_pouch("source", "add", "company", "--local", str(company))
|
||||
self.assertEqual(added.returncode, 0, added.stderr)
|
||||
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"add",
|
||||
"company/code-review",
|
||||
"--project",
|
||||
@@ -135,7 +135,7 @@ class CustomSourceTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
manifest = project.joinpath(".skills.yaml").read_text(encoding="utf-8")
|
||||
manifest = project.joinpath(".pouch.yaml").read_text(encoding="utf-8")
|
||||
self.assertIn('name: "code-review"', manifest)
|
||||
self.assertIn("source: company", manifest)
|
||||
self.assertEqual(
|
||||
@@ -149,14 +149,14 @@ class CustomSourceTests(unittest.TestCase):
|
||||
write_skill(company / "skills", "code-review")
|
||||
project = self.home / "project"
|
||||
project.mkdir()
|
||||
project.joinpath(".skills.yaml").write_text(
|
||||
project.joinpath(".pouch.yaml").write_text(
|
||||
"skills:\n - code-review\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
added = self.run_skiff("source", "add", "company", "--local", str(company))
|
||||
added = self.run_pouch("source", "add", "company", "--local", str(company))
|
||||
self.assertEqual(added.returncode, 0, added.stderr)
|
||||
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"add",
|
||||
"company/code-review",
|
||||
"--project",
|
||||
@@ -166,17 +166,17 @@ class CustomSourceTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
manifest = project.joinpath(".skills.yaml").read_text(encoding="utf-8")
|
||||
manifest = project.joinpath(".pouch.yaml").read_text(encoding="utf-8")
|
||||
self.assertIn("source: company", manifest)
|
||||
|
||||
def test_unqualified_duplicate_requires_explicit_source(self) -> None:
|
||||
write_skill(self.skills_home / "skills", "code-review")
|
||||
company = self.home / "company"
|
||||
write_skill(company / "skills", "code-review")
|
||||
added = self.run_skiff("source", "add", "company", "--local", str(company))
|
||||
added = self.run_pouch("source", "add", "company", "--local", str(company))
|
||||
self.assertEqual(added.returncode, 0, added.stderr)
|
||||
|
||||
result = self.run_skiff("add", "code-review", "-g", "-a", "codex")
|
||||
result = self.run_pouch("add", "code-review", "-g", "-a", "codex")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("builtin/code-review", result.stderr)
|
||||
@@ -194,9 +194,9 @@ class CustomSourceTests(unittest.TestCase):
|
||||
" path: skills\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
added = self.run_skiff("source", "add", "company", "--local", str(company))
|
||||
added = self.run_pouch("source", "add", "company", "--local", str(company))
|
||||
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"add",
|
||||
"company/code-review",
|
||||
"-g",
|
||||
@@ -216,15 +216,15 @@ class CustomSourceTests(unittest.TestCase):
|
||||
expected = write_skill(company / "skills", "code-review")
|
||||
project = self.home / "project"
|
||||
project.mkdir()
|
||||
project.joinpath(".skills.yaml").write_text(
|
||||
project.joinpath(".pouch.yaml").write_text(
|
||||
"skills:\n - name: code-review\n source: company\n"
|
||||
"targets:\n - codex\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
added = self.run_skiff("source", "add", "company", "--local", str(company))
|
||||
added = self.run_pouch("source", "add", "company", "--local", str(company))
|
||||
self.assertEqual(added.returncode, 0, added.stderr)
|
||||
|
||||
result = self.run_skiff("sync", "--project", str(project), "-a", "codex")
|
||||
result = self.run_pouch("sync", "--project", str(project), "-a", "codex")
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
link = project / ".agents" / "skills" / "code-review"
|
||||
@@ -233,16 +233,16 @@ class CustomSourceTests(unittest.TestCase):
|
||||
def test_sync_reports_missing_machine_source(self) -> None:
|
||||
project = self.home / "project"
|
||||
project.mkdir()
|
||||
project.joinpath(".skills.yaml").write_text(
|
||||
project.joinpath(".pouch.yaml").write_text(
|
||||
"skills:\n - name: code-review\n source: company\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_skiff("sync", "--project", str(project), "-a", "codex")
|
||||
result = self.run_pouch("sync", "--project", str(project), "-a", "codex")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("本机尚未配置", result.stderr)
|
||||
self.assertIn("skiff source add company", result.stderr)
|
||||
self.assertIn("pouch source add company", result.stderr)
|
||||
|
||||
def test_git_source_is_cloned_to_default_checkout(self) -> None:
|
||||
upstream = self.home / "upstream"
|
||||
@@ -266,10 +266,10 @@ class CustomSourceTests(unittest.TestCase):
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
result = self.run_skiff("source", "add", "company", str(upstream))
|
||||
result = self.run_pouch("source", "add", "company", str(upstream))
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
checkout = self.home / ".local" / "share" / "skiff" / "sources" / "company"
|
||||
checkout = self.home / ".local" / "share" / "pouch" / "sources" / "company"
|
||||
self.assertTrue((checkout / ".git").is_dir())
|
||||
self.assertTrue((checkout / "skills" / "release-check" / "SKILL.md").is_file())
|
||||
|
||||
@@ -281,7 +281,7 @@ class CustomSourceTests(unittest.TestCase):
|
||||
"checkout": str(checkout),
|
||||
}
|
||||
|
||||
with patch("skiff.sources.subprocess.run") as run:
|
||||
with patch("pouch.sources.subprocess.run") as run:
|
||||
fetch_source("company", entry)
|
||||
|
||||
run.assert_called_once_with(
|
||||
|
||||
@@ -11,10 +11,10 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class InstallScriptTests(unittest.TestCase):
|
||||
def test_install_bootstraps_skiff_skill_for_all_agents(self) -> None:
|
||||
def test_install_bootstraps_pouch_skill_for_all_agents(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
home = Path(temp)
|
||||
(home / ".skills").symlink_to(REPO_ROOT, target_is_directory=True)
|
||||
(home / ".pouch").symlink_to(REPO_ROOT, target_is_directory=True)
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(home)
|
||||
|
||||
@@ -29,15 +29,15 @@ class InstallScriptTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
for relative in (
|
||||
".cursor/skills/skiff",
|
||||
".claude/skills/skiff",
|
||||
".codex/skills/skiff",
|
||||
".agents/skills/skiff",
|
||||
".cursor/skills/pouch",
|
||||
".claude/skills/pouch",
|
||||
".codex/skills/pouch",
|
||||
".agents/skills/pouch",
|
||||
):
|
||||
link = home / relative
|
||||
self.assertTrue(link.is_symlink(), relative)
|
||||
self.assertEqual(link.resolve(), (REPO_ROOT / "skills" / "skiff").resolve())
|
||||
self.assertIn("已全局安装 builtin skiff skill", result.stdout)
|
||||
self.assertEqual(link.resolve(), (REPO_ROOT / "skills" / "pouch").resolve())
|
||||
self.assertIn("已全局安装 builtin pouch skill", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -8,18 +8,18 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ProjectSkillContentTests(unittest.TestCase):
|
||||
def test_skiff_skill_documents_project_usage_and_feedback_loop(self) -> None:
|
||||
content = (REPO_ROOT / "skills" / "skiff" / "SKILL.md").read_text(encoding="utf-8")
|
||||
def test_pouch_skill_documents_project_usage_and_feedback_loop(self) -> None:
|
||||
content = (REPO_ROOT / "skills" / "pouch" / "SKILL.md").read_text(encoding="utf-8")
|
||||
|
||||
for expected in (
|
||||
"skiff add <name>",
|
||||
"pouch add <name>",
|
||||
"问题或优化回流",
|
||||
"实际结果",
|
||||
"期望结果",
|
||||
"skiff check <name>",
|
||||
"~/.skills/skills/<name>/",
|
||||
"pouch check <name>",
|
||||
"~/.pouch/skills/<name>/",
|
||||
"第三方 skill",
|
||||
"skiff init ack",
|
||||
"pouch init ack",
|
||||
"显式调用全局 `/ack` skill",
|
||||
):
|
||||
self.assertIn(expected, content)
|
||||
|
||||
+16
-16
@@ -10,10 +10,10 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from skiff import cli
|
||||
from skiff import yaml_io
|
||||
from skiff.catalog import catalog_repo_path, catalog_skill_path
|
||||
from skiff.selector import SkillChoice, filter_choices, fit_to_width, select_skills
|
||||
from pouch import cli
|
||||
from pouch import yaml_io
|
||||
from pouch.catalog import catalog_repo_path, catalog_skill_path
|
||||
from pouch.selector import SkillChoice, filter_choices, fit_to_width, select_skills
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -210,8 +210,8 @@ class SelectorTests(unittest.TestCase):
|
||||
"path": "skills",
|
||||
}
|
||||
|
||||
with patch("skiff.catalog.catalog_checkout_path", return_value=checkout):
|
||||
from skiff.catalog import discover_catalog_skills
|
||||
with patch("pouch.catalog.catalog_checkout_path", return_value=checkout):
|
||||
from pouch.catalog import discover_catalog_skills
|
||||
|
||||
self.assertEqual(discover_catalog_skills("unsafe", entry), {})
|
||||
|
||||
@@ -257,13 +257,13 @@ class SelectCommandTests(unittest.TestCase):
|
||||
def test_non_tty_exits_with_add_guidance(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
home = Path(temp)
|
||||
(home / ".skills" / "skills").mkdir(parents=True)
|
||||
(home / ".pouch" / "skills").mkdir(parents=True)
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(home)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "skiff", "select"],
|
||||
[sys.executable, "-m", "pouch", "select"],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
@@ -272,7 +272,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("skiff add <name>", result.stderr)
|
||||
self.assertIn("pouch add <name>", result.stderr)
|
||||
|
||||
def test_project_selection_installs_new_and_records_all_selected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
@@ -306,7 +306,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
with (
|
||||
patch.object(cli.sys, "stdin", stdin),
|
||||
patch.object(cli.sys, "stdout", stdout),
|
||||
patch.object(cli, "ensure_skills_home"),
|
||||
patch.object(cli, "ensure_pouch_home"),
|
||||
patch.object(cli, "list_builtin_skills", return_value=["builtin-one"]),
|
||||
patch.object(cli, "skill_description", return_value="builtin"),
|
||||
patch.object(
|
||||
@@ -346,7 +346,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
):
|
||||
cli.cmd_select(args)
|
||||
|
||||
manifest = (project / ".skills.yaml").read_text(encoding="utf-8")
|
||||
manifest = (project / ".pouch.yaml").read_text(encoding="utf-8")
|
||||
|
||||
self.assertEqual(installed, ["catalog-one"])
|
||||
self.assertIn("builtin-one", manifest)
|
||||
@@ -405,7 +405,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
with (
|
||||
patch.object(cli.sys, "stdin", stdin),
|
||||
patch.object(cli.sys, "stdout", stdout),
|
||||
patch.object(cli, "ensure_skills_home"),
|
||||
patch.object(cli, "ensure_pouch_home"),
|
||||
patch.object(cli, "list_builtin_skills", return_value=[]),
|
||||
patch.object(
|
||||
cli,
|
||||
@@ -440,7 +440,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
with (
|
||||
patch.object(cli.sys, "stdin", stdin),
|
||||
patch.object(cli.sys, "stdout", stdout),
|
||||
patch.object(cli, "ensure_skills_home"),
|
||||
patch.object(cli, "ensure_pouch_home"),
|
||||
patch.object(cli, "list_builtin_skills", return_value=[]),
|
||||
patch.object(
|
||||
cli,
|
||||
@@ -506,7 +506,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
with (
|
||||
patch.object(cli.sys, "stdin", stdin),
|
||||
patch.object(cli.sys, "stdout", stdout),
|
||||
patch.object(cli, "ensure_skills_home"),
|
||||
patch.object(cli, "ensure_pouch_home"),
|
||||
patch.object(cli, "list_builtin_skills", return_value=[]),
|
||||
patch.object(cli, "load_catalog", return_value={}),
|
||||
patch.object(
|
||||
@@ -567,7 +567,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
patch.object(cli.sys, "stdin", stdin),
|
||||
patch.object(cli.sys, "stdout", stdout),
|
||||
patch.object(cli, "SKILLS_DIR", skills_root),
|
||||
patch.object(cli, "ensure_skills_home"),
|
||||
patch.object(cli, "ensure_pouch_home"),
|
||||
patch.object(cli, "list_builtin_skills", return_value=["ack"]),
|
||||
patch.object(cli, "skill_description", return_value="builtin ack"),
|
||||
patch.object(
|
||||
@@ -575,7 +575,7 @@ class SelectCommandTests(unittest.TestCase):
|
||||
"load_catalog",
|
||||
return_value={
|
||||
"skills": {
|
||||
"repo": "~/.skills",
|
||||
"repo": "~/.pouch",
|
||||
"ref": "main",
|
||||
"path": "skills",
|
||||
}
|
||||
|
||||
+127
-127
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import skiff.cli
|
||||
import pouch.cli
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -18,7 +18,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.home = Path(self.temp_dir.name)
|
||||
self.skills_home = self.home / ".skills"
|
||||
self.skills_home = self.home / ".pouch"
|
||||
skill = self.skills_home / "skills" / "ack"
|
||||
(skill / "templates").mkdir(parents=True)
|
||||
(skill / "scripts").mkdir()
|
||||
@@ -40,7 +40,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
tasks_template = skill / "templates" / "tasks.template.yaml"
|
||||
tasks_template.write_text(
|
||||
tasks_template.read_text(encoding="utf-8")
|
||||
+ ' deliveryFile: "docs/ack/delivery.yaml"\n'
|
||||
+ ' deliveryFile: ".pouch/ack/delivery.yaml"\n'
|
||||
+ 'deliveryRuns: []\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -78,12 +78,12 @@ class SkillInitTests(unittest.TestCase):
|
||||
def tearDown(self) -> None:
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def run_skiff(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
def run_pouch(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["HOME"] = str(self.home)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "skiff", *args],
|
||||
[sys.executable, "-m", "pouch", *args],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
@@ -95,10 +95,10 @@ class SkillInitTests(unittest.TestCase):
|
||||
project = self.home / "sample-app"
|
||||
project.mkdir()
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "ack"
|
||||
self.assertFalse((target / "kit").exists())
|
||||
self.assertFalse((target / "framework").exists())
|
||||
project_content = (target / "project.md").read_text(encoding="utf-8")
|
||||
@@ -117,12 +117,12 @@ class SkillInitTests(unittest.TestCase):
|
||||
|
||||
def test_init_refuses_to_overwrite_existing_files(self) -> None:
|
||||
project = self.home / "existing-app"
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "ack"
|
||||
target.mkdir(parents=True)
|
||||
existing = target / "project.md"
|
||||
existing.write_text("keep me", encoding="utf-8")
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("拒绝覆盖已有路径", result.stderr)
|
||||
@@ -133,12 +133,12 @@ class SkillInitTests(unittest.TestCase):
|
||||
|
||||
def test_init_refuses_to_overwrite_existing_knowledge_file(self) -> None:
|
||||
project = self.home / "existing-knowledge-app"
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "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))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("拒绝覆盖已有路径", result.stderr)
|
||||
@@ -148,12 +148,12 @@ class SkillInitTests(unittest.TestCase):
|
||||
|
||||
def test_init_refuses_to_overwrite_existing_delivery_file(self) -> None:
|
||||
project = self.home / "existing-delivery-app"
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "ack"
|
||||
target.mkdir(parents=True)
|
||||
existing = target / "delivery.yaml"
|
||||
existing.write_text("keep me", encoding="utf-8")
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("拒绝覆盖已有路径", result.stderr)
|
||||
@@ -163,25 +163,25 @@ class SkillInitTests(unittest.TestCase):
|
||||
self.assertFalse((target / "knowledge.yaml").exists())
|
||||
|
||||
def test_init_rejects_symlinked_destination_directories(self) -> None:
|
||||
for symlink_level in ("docs", "ack"):
|
||||
for symlink_level in (".pouch", "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(
|
||||
if symlink_level == ".pouch":
|
||||
(project / ".pouch").symlink_to(
|
||||
outside,
|
||||
target_is_directory=True,
|
||||
)
|
||||
else:
|
||||
(project / "docs").mkdir()
|
||||
(project / "docs" / "ack").symlink_to(
|
||||
(project / ".pouch").mkdir()
|
||||
(project / ".pouch" / "ack").symlink_to(
|
||||
outside,
|
||||
target_is_directory=True,
|
||||
)
|
||||
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"init",
|
||||
"ack",
|
||||
"--project",
|
||||
@@ -202,7 +202,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
outside.mkdir()
|
||||
(project / "skills").symlink_to(outside, target_is_directory=True)
|
||||
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"init",
|
||||
"../skills/ack",
|
||||
"--project",
|
||||
@@ -218,34 +218,34 @@ class SkillInitTests(unittest.TestCase):
|
||||
project.mkdir()
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_rename_directory_noreplace",
|
||||
side_effect=RuntimeError("publish interrupted"),
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, "publish interrupted"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.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()), [])
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
pouch_dir = project / ".pouch"
|
||||
if pouch_dir.exists():
|
||||
self.assertEqual(list(pouch_dir.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
|
||||
real_publish = pouch.cli._rename_directory_noreplace
|
||||
|
||||
def create_destination_then_publish(
|
||||
source_parent_fd: int,
|
||||
@@ -268,25 +268,25 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_rename_directory_noreplace",
|
||||
side_effect=create_destination_then_publish,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "拒绝覆盖已有路径"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "ack"
|
||||
self.assertTrue(target.is_dir())
|
||||
self.assertEqual(target.stat().st_ino, raced_inode)
|
||||
self.assertEqual(list(target.iterdir()), [])
|
||||
@@ -294,9 +294,9 @@ class SkillInitTests(unittest.TestCase):
|
||||
|
||||
def test_published_destination_replacement_never_reports_success(self) -> None:
|
||||
project = self.home / "published-destination-app"
|
||||
moved_target = project / "docs" / "ack-moved"
|
||||
moved_target = project / ".pouch" / "ack-moved"
|
||||
project.mkdir()
|
||||
real_publish = skiff.cli._rename_directory_noreplace
|
||||
real_publish = pouch.cli._rename_directory_noreplace
|
||||
|
||||
def replace_destination_after_publish(
|
||||
source_parent_fd: int,
|
||||
@@ -310,30 +310,30 @@ class SkillInitTests(unittest.TestCase):
|
||||
destination_parent_fd,
|
||||
destination_name,
|
||||
)
|
||||
target = project / "docs" / destination_name
|
||||
target = project / ".pouch" / destination_name
|
||||
target.rename(moved_target)
|
||||
target.mkdir()
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_rename_directory_noreplace",
|
||||
side_effect=replace_destination_after_publish,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "ACK 目录已被替换"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
self.assertEqual(list((project / "docs" / "ack").iterdir()), [])
|
||||
self.assertEqual(list((project / ".pouch" / "ack").iterdir()), [])
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in moved_target.iterdir()),
|
||||
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
|
||||
@@ -345,7 +345,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
project.mkdir()
|
||||
attacker.mkdir()
|
||||
(attacker / "marker").write_text("forged", encoding="utf-8")
|
||||
real_publish = skiff.cli._rename_directory_noreplace
|
||||
real_publish = pouch.cli._rename_directory_noreplace
|
||||
|
||||
def replace_outer_transaction_then_publish(
|
||||
source_parent_fd: int,
|
||||
@@ -353,15 +353,15 @@ class SkillInitTests(unittest.TestCase):
|
||||
destination_parent_fd: int,
|
||||
destination_name: str,
|
||||
) -> None:
|
||||
docs = project / "docs"
|
||||
pouch_dir = project / ".pouch"
|
||||
transactions = [
|
||||
path
|
||||
for path in docs.iterdir()
|
||||
for path in pouch_dir.iterdir()
|
||||
if path.name.startswith(".ack-init-")
|
||||
]
|
||||
self.assertEqual(len(transactions), 1)
|
||||
transaction = transactions[0]
|
||||
saved = docs / f"{transaction.name}.saved"
|
||||
saved = pouch_dir / f"{transaction.name}.saved"
|
||||
transaction.rename(saved)
|
||||
transaction.symlink_to(attacker, target_is_directory=True)
|
||||
real_publish(
|
||||
@@ -372,24 +372,24 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_rename_directory_noreplace",
|
||||
side_effect=replace_outer_transaction_then_publish,
|
||||
),
|
||||
):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "ack"
|
||||
self.assertTrue(target.is_dir())
|
||||
self.assertFalse(target.is_symlink())
|
||||
self.assertFalse((target / "marker").exists())
|
||||
@@ -412,25 +412,25 @@ class SkillInitTests(unittest.TestCase):
|
||||
real_fsync(file_descriptor)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli.os,
|
||||
pouch.cli.os,
|
||||
"fsync",
|
||||
side_effect=fail_directory_fsync_after_publish,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "已完整发布"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
target = project / "docs" / "ack"
|
||||
target = project / ".pouch" / "ack"
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in target.iterdir()),
|
||||
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
|
||||
@@ -440,7 +440,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
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
|
||||
real_open_docs = pouch.cli._open_or_create_directory_at
|
||||
replaced = False
|
||||
|
||||
def replace_root_then_open_docs(
|
||||
@@ -455,32 +455,32 @@ class SkillInitTests(unittest.TestCase):
|
||||
return real_open_docs(parent_fd, name)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_open_or_create_directory_at",
|
||||
side_effect=replace_root_then_open_docs,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "项目目录已被替换"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
self.assertFalse((moved_project / "docs" / "ack").exists())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
self.assertFalse((moved_project / ".pouch" / "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
|
||||
real_publish = pouch.cli._rename_directory_noreplace
|
||||
|
||||
def replace_root_then_publish(
|
||||
source_parent_fd: int,
|
||||
@@ -498,36 +498,36 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_rename_directory_noreplace",
|
||||
side_effect=replace_root_then_publish,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "ACK 目录已移动或不可访问"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
target = moved_project / "docs" / "ack"
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
target = moved_project / ".pouch" / "ack"
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in target.iterdir()),
|
||||
["delivery.yaml", "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"
|
||||
def test_pouch_dir_replacement_aborts_before_publish(self) -> None:
|
||||
project = self.home / "pouch-dir-replacement-app"
|
||||
moved_pouch = project / "pouch-dir-moved"
|
||||
project.mkdir()
|
||||
real_assert_binding = skiff.cli._assert_open_directory_path
|
||||
real_assert_binding = pouch.cli._assert_open_directory_path
|
||||
replaced = False
|
||||
|
||||
def replace_docs_at_publish_check(
|
||||
@@ -538,9 +538,9 @@ class SkillInitTests(unittest.TestCase):
|
||||
label: str = "项目目录",
|
||||
) -> None:
|
||||
nonlocal replaced
|
||||
if label == "docs 目录" and phase == "发布" and not replaced:
|
||||
(project / "docs").rename(moved_docs)
|
||||
(project / "docs").mkdir()
|
||||
if label == ".pouch 目录" and phase == "发布" and not replaced:
|
||||
(project / ".pouch").rename(moved_pouch)
|
||||
(project / ".pouch").mkdir()
|
||||
replaced = True
|
||||
real_assert_binding(
|
||||
directory_fd,
|
||||
@@ -550,32 +550,32 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_assert_open_directory_path",
|
||||
side_effect=replace_docs_at_publish_check,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "docs 目录已被替换"):
|
||||
skiff.cli.cmd_init(
|
||||
with self.assertRaisesRegex(SystemExit, "\\.pouch 目录已被替换"):
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
self.assertFalse((moved_docs / "ack").exists())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
self.assertFalse((moved_pouch / "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"
|
||||
def test_pouch_dir_replacement_at_publish_never_reports_success(self) -> None:
|
||||
project = self.home / "publish-pouch-dir-replacement-app"
|
||||
moved_pouch = project / "pouch-dir-moved"
|
||||
project.mkdir()
|
||||
real_publish = skiff.cli._rename_directory_noreplace
|
||||
real_publish = pouch.cli._rename_directory_noreplace
|
||||
|
||||
def replace_docs_then_publish(
|
||||
source_parent_fd: int,
|
||||
@@ -583,8 +583,8 @@ class SkillInitTests(unittest.TestCase):
|
||||
destination_parent_fd: int,
|
||||
destination_name: str,
|
||||
) -> None:
|
||||
(project / "docs").rename(moved_docs)
|
||||
(project / "docs").mkdir()
|
||||
(project / ".pouch").rename(moved_pouch)
|
||||
(project / ".pouch").mkdir()
|
||||
real_publish(
|
||||
source_parent_fd,
|
||||
source_name,
|
||||
@@ -593,26 +593,26 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(skiff.cli, "SKILLS_HOME", self.skills_home),
|
||||
mock.patch.object(pouch.cli, "POUCH_HOME", self.skills_home),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"SKILLS_DIR",
|
||||
self.skills_home / "skills",
|
||||
),
|
||||
mock.patch.object(skiff.cli, "ensure_skills_home"),
|
||||
mock.patch.object(pouch.cli, "ensure_pouch_home"),
|
||||
mock.patch.object(
|
||||
skiff.cli,
|
||||
pouch.cli,
|
||||
"_rename_directory_noreplace",
|
||||
side_effect=replace_docs_then_publish,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "ACK 目录已移动或不可访问"):
|
||||
skiff.cli.cmd_init(
|
||||
pouch.cli.cmd_init(
|
||||
SimpleNamespace(name="ack", project=str(project))
|
||||
)
|
||||
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
target = moved_docs / "ack"
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
target = moved_pouch / "ack"
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in target.iterdir()),
|
||||
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
|
||||
@@ -629,11 +629,11 @@ class SkillInitTests(unittest.TestCase):
|
||||
/ "knowledge.template.yaml"
|
||||
).unlink()
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("knowledge.template.yaml", result.stderr)
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
|
||||
def test_ack_init_requires_delivery_template(self) -> None:
|
||||
project = self.home / "missing-delivery-template-app"
|
||||
@@ -646,11 +646,11 @@ class SkillInitTests(unittest.TestCase):
|
||||
/ "delivery.template.yaml"
|
||||
).unlink()
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("delivery.template.yaml", result.stderr)
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
|
||||
def test_ack_init_requires_all_validators(self) -> None:
|
||||
for validator_name in (
|
||||
@@ -671,7 +671,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
original = validator.read_text(encoding="utf-8")
|
||||
validator.unlink()
|
||||
try:
|
||||
result = self.run_skiff(
|
||||
result = self.run_pouch(
|
||||
"init",
|
||||
"ack",
|
||||
"--project",
|
||||
@@ -683,7 +683,7 @@ class SkillInitTests(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("缺少初始化校验器", result.stderr)
|
||||
self.assertIn(validator_name, result.stderr)
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
|
||||
def test_validator_failure_leaves_no_partial_initialization(self) -> None:
|
||||
project = self.home / "invalid-knowledge-app"
|
||||
@@ -693,12 +693,12 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
validator.write_text("raise SystemExit(1)\n", encoding="utf-8")
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("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())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
|
||||
def test_delivery_validator_failure_leaves_no_partial_initialization(self) -> None:
|
||||
project = self.home / "invalid-delivery-app"
|
||||
@@ -708,12 +708,12 @@ class SkillInitTests(unittest.TestCase):
|
||||
)
|
||||
validator.write_text("raise SystemExit(1)\n", encoding="utf-8")
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("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())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
|
||||
def test_validator_cannot_replace_staged_bytes_before_install(self) -> None:
|
||||
project = self.home / "mutated-staging-app"
|
||||
@@ -728,11 +728,11 @@ class SkillInitTests(unittest.TestCase):
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("临时文件在校验期间发生变化", result.stderr)
|
||||
self.assertFalse((project / "docs" / "ack").exists())
|
||||
self.assertFalse((project / ".pouch" / "ack").exists())
|
||||
|
||||
def test_ack_init_validates_mirrored_staging_root(self) -> None:
|
||||
project = self.home / "staged-knowledge-app"
|
||||
@@ -749,16 +749,16 @@ class SkillInitTests(unittest.TestCase):
|
||||
"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"
|
||||
"expected = root / '.pouch' / '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))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue((project / "docs" / "ack" / "knowledge.yaml").is_file())
|
||||
self.assertTrue((project / ".pouch" / "ack" / "knowledge.yaml").is_file())
|
||||
|
||||
def test_ack_delivery_init_validates_mirrored_staging_root(self) -> None:
|
||||
project = self.home / "staged-delivery-app"
|
||||
@@ -775,16 +775,16 @@ class SkillInitTests(unittest.TestCase):
|
||||
"root = Path(sys.argv[sys.argv.index('--project-root') + 1])\n"
|
||||
"delivery = Path(sys.argv[1])\n"
|
||||
"tasks = Path(sys.argv[sys.argv.index('--tasks') + 1])\n"
|
||||
"expected = root / 'docs' / 'ack'\n"
|
||||
"expected = root / '.pouch' / 'ack'\n"
|
||||
"raise SystemExit(0 if delivery.parent == expected and "
|
||||
"tasks.parent == expected else 4)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue((project / "docs" / "ack" / "delivery.yaml").is_file())
|
||||
self.assertTrue((project / ".pouch" / "ack" / "delivery.yaml").is_file())
|
||||
|
||||
def test_non_ack_init_still_requires_only_project_and_tasks_templates(self) -> None:
|
||||
skill = self.skills_home / "skills" / "plain"
|
||||
@@ -799,10 +799,10 @@ class SkillInitTests(unittest.TestCase):
|
||||
project = self.home / "plain-app"
|
||||
project.mkdir()
|
||||
|
||||
result = self.run_skiff("init", "plain", "--project", str(project))
|
||||
result = self.run_pouch("init", "plain", "--project", str(project))
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
target = project / "docs" / "plain"
|
||||
target = project / ".pouch" / "plain"
|
||||
self.assertTrue((target / "project.md").is_file())
|
||||
self.assertTrue((target / "tasks.yaml").is_file())
|
||||
self.assertFalse((target / "knowledge.yaml").exists())
|
||||
@@ -811,14 +811,14 @@ class SkillInitTests(unittest.TestCase):
|
||||
def test_init_rejects_missing_project_directory(self) -> None:
|
||||
project = self.home / "missing-app"
|
||||
|
||||
result = self.run_skiff("init", "ack", "--project", str(project))
|
||||
result = self.run_pouch("init", "ack", "--project", str(project))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("项目目录不存在", result.stderr)
|
||||
self.assertFalse(project.exists())
|
||||
|
||||
def test_legacy_kit_command_is_not_exposed(self) -> None:
|
||||
result = self.run_skiff("kit", "init", "ack")
|
||||
result = self.run_pouch("kit", "init", "ack")
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("invalid choice", result.stderr)
|
||||
|
||||
Reference in New Issue
Block a user