Merge branch 'main' into rename

Keep pouch naming and .pouch/ack project state, and bring in ACK
regression mode, deployer test-environment binding, and manage-release
updates from main.
This commit is contained in:
2026-08-25 15:23:48 +08:00
40 changed files with 1968 additions and 224 deletions
+98 -6
View File
@@ -65,10 +65,18 @@ class SkillInitTests(unittest.TestCase):
'profiles: {}\n',
encoding="utf-8",
)
(skill / "templates" / "regression.template.yaml").write_text(
'updatedAt: "<YYYY-MM-DDTHH:mm:ss+TZ>"\n'
'project:\n'
' name: "<project_name>"\n'
'cases: []\n',
encoding="utf-8",
)
for validator_name in (
"validate_tasks.py",
"validate_knowledge.py",
"validate_delivery.py",
"validate_regression.py",
):
(skill / "scripts" / validator_name).write_text(
"raise SystemExit(0)\n",
@@ -105,15 +113,19 @@ class SkillInitTests(unittest.TestCase):
tasks_content = (target / "tasks.yaml").read_text(encoding="utf-8")
knowledge_content = (target / "knowledge.yaml").read_text(encoding="utf-8")
delivery_content = (target / "delivery.yaml").read_text(encoding="utf-8")
regression_content = (target / "regression.yaml").read_text(encoding="utf-8")
self.assertIn("# sample-app", project_content)
self.assertIn("version=1.2.3", project_content)
self.assertIn(f'repoPath: "{project}"', tasks_content)
self.assertIn(f'repoPath: "{project}"', knowledge_content)
self.assertIn('name: "sample-app"', delivery_content)
self.assertIn("enabled: false", delivery_content)
self.assertIn('name: "sample-app"', regression_content)
self.assertIn("cases: []", regression_content)
self.assertNotIn("<project_name>", tasks_content)
self.assertNotIn("<project_name>", knowledge_content)
self.assertNotIn("<project_name>", delivery_content)
self.assertNotIn("<project_name>", regression_content)
def test_init_refuses_to_overwrite_existing_files(self) -> None:
project = self.home / "existing-app"
@@ -130,6 +142,7 @@ class SkillInitTests(unittest.TestCase):
self.assertFalse((target / "tasks.yaml").exists())
self.assertFalse((target / "knowledge.yaml").exists())
self.assertFalse((target / "delivery.yaml").exists())
self.assertFalse((target / "regression.yaml").exists())
def test_init_refuses_to_overwrite_existing_knowledge_file(self) -> None:
project = self.home / "existing-knowledge-app"
@@ -161,6 +174,22 @@ class SkillInitTests(unittest.TestCase):
self.assertFalse((target / "project.md").exists())
self.assertFalse((target / "tasks.yaml").exists())
self.assertFalse((target / "knowledge.yaml").exists())
self.assertFalse((target / "regression.yaml").exists())
def test_init_refuses_to_overwrite_existing_regression_file(self) -> None:
project = self.home / "existing-regression-app"
target = project / ".pouch" / "ack"
target.mkdir(parents=True)
existing = target / "regression.yaml"
existing.write_text("keep me", encoding="utf-8")
result = self.run_pouch("init", "ack", "--project", str(project))
self.assertNotEqual(result.returncode, 0)
self.assertIn("拒绝覆盖已有路径", result.stderr)
self.assertEqual(existing.read_text(encoding="utf-8"), "keep me")
self.assertFalse((target / "project.md").exists())
self.assertFalse((target / "tasks.yaml").exists())
def test_init_rejects_symlinked_destination_directories(self) -> None:
for symlink_level in (".pouch", "ack"):
@@ -336,7 +365,13 @@ class SkillInitTests(unittest.TestCase):
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"],
[
"delivery.yaml",
"knowledge.yaml",
"project.md",
"regression.yaml",
"tasks.yaml",
],
)
def test_transaction_container_replacement_cannot_forge_payload(self) -> None:
@@ -395,7 +430,13 @@ class SkillInitTests(unittest.TestCase):
self.assertFalse((target / "marker").exists())
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
[
"delivery.yaml",
"knowledge.yaml",
"project.md",
"regression.yaml",
"tasks.yaml",
],
)
def test_post_publish_fsync_failure_preserves_complete_state(self) -> None:
@@ -407,7 +448,7 @@ class SkillInitTests(unittest.TestCase):
def fail_directory_fsync_after_publish(file_descriptor: int) -> None:
nonlocal calls
calls += 1
if calls == 8:
if calls == 9:
raise OSError("simulated directory fsync failure")
real_fsync(file_descriptor)
@@ -433,7 +474,13 @@ class SkillInitTests(unittest.TestCase):
target = project / ".pouch" / "ack"
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
[
"delivery.yaml",
"knowledge.yaml",
"project.md",
"regression.yaml",
"tasks.yaml",
],
)
def test_project_root_replacement_aborts_before_publish(self) -> None:
@@ -520,7 +567,13 @@ class SkillInitTests(unittest.TestCase):
target = moved_project / ".pouch" / "ack"
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
[
"delivery.yaml",
"knowledge.yaml",
"project.md",
"regression.yaml",
"tasks.yaml",
],
)
def test_pouch_dir_replacement_aborts_before_publish(self) -> None:
@@ -615,7 +668,13 @@ class SkillInitTests(unittest.TestCase):
target = moved_pouch / "ack"
self.assertEqual(
sorted(path.name for path in target.iterdir()),
["delivery.yaml", "knowledge.yaml", "project.md", "tasks.yaml"],
[
"delivery.yaml",
"knowledge.yaml",
"project.md",
"regression.yaml",
"tasks.yaml",
],
)
def test_ack_init_requires_knowledge_template(self) -> None:
@@ -652,11 +711,29 @@ class SkillInitTests(unittest.TestCase):
self.assertIn("delivery.template.yaml", result.stderr)
self.assertFalse((project / ".pouch" / "ack").exists())
def test_ack_init_requires_regression_template(self) -> None:
project = self.home / "missing-regression-template-app"
project.mkdir()
(
self.skills_home
/ "skills"
/ "ack"
/ "templates"
/ "regression.template.yaml"
).unlink()
result = self.run_pouch("init", "ack", "--project", str(project))
self.assertNotEqual(result.returncode, 0)
self.assertIn("regression.template.yaml", result.stderr)
self.assertFalse((project / ".pouch" / "ack").exists())
def test_ack_init_requires_all_validators(self) -> None:
for validator_name in (
"validate_tasks.py",
"validate_knowledge.py",
"validate_delivery.py",
"validate_regression.py",
):
with self.subTest(validator_name=validator_name):
project = self.home / f"missing-{validator_name}-app"
@@ -715,6 +792,21 @@ class SkillInitTests(unittest.TestCase):
self.assertIn("初始化交付契约校验失败", result.stderr)
self.assertFalse((project / ".pouch" / "ack").exists())
def test_regression_validator_failure_leaves_no_partial_initialization(self) -> None:
project = self.home / "invalid-regression-app"
project.mkdir()
validator = (
self.skills_home / "skills" / "ack" / "scripts" / "validate_regression.py"
)
validator.write_text("raise SystemExit(1)\n", encoding="utf-8")
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 / ".pouch" / "ack").exists())
def test_validator_cannot_replace_staged_bytes_before_install(self) -> None:
project = self.home / "mutated-staging-app"
project.mkdir()