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:
+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