168 lines
9.1 KiB
Python
168 lines
9.1 KiB
Python
import json
|
|
import hashlib
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from tools.packages import PackageError, check_package, package_archive
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class PackageTests(unittest.TestCase):
|
|
def test_tdd_is_available_to_owner_started_implementers(self):
|
|
package = ROOT / "packages/tdd-auto"
|
|
skill = (package / "SKILL.md").read_text()
|
|
frontmatter = skill.split("---", 2)[1]
|
|
self.assertIn("name: tdd-auto", frontmatter)
|
|
self.assertNotIn("disable-model-invocation: true", frontmatter)
|
|
self.assertIn("relayview-version: 0.3.1", frontmatter)
|
|
manifest = json.loads((package / "relayview-skill.json").read_text())
|
|
self.assertEqual(manifest["source"], {"kind": "first-party", "reference": "v0.3.1"})
|
|
check_package(package)
|
|
self.assertIn("disable-model-invocation: true", (ROOT / "packages/tdd/SKILL.md").read_text())
|
|
|
|
def test_migrated_role_guidance_has_one_declared_role_step(self):
|
|
expected = {"codebase-design": ("planner", "planning"), "grill-me": ("planner", "planning"),
|
|
"grilling": ("planner", "planning"), "to-questionnaire": ("planner", "planning"),
|
|
"diagnosing-bugs": ("implementer", "implementation"),
|
|
"domain-modeling": ("implementer", "implementation"),
|
|
"migrate-to-shoehorn": ("implementer", "implementation"),
|
|
"pr": ("implementer", "implementation"), "prototype": ("implementer", "implementation"),
|
|
"scaffold-exercises": ("implementer", "implementation"),
|
|
"shared-abstraction-refactor": ("implementer", "implementation"),
|
|
"split-large-files": ("implementer", "implementation"),
|
|
"tdd": ("implementer", "implementation"),
|
|
"writing-for-agents": ("implementer", "implementation"),
|
|
"code-review": ("reviewer", "review"),
|
|
"c4-diff": ("reviewer", "review"),
|
|
"research": ("planner", "planning"),
|
|
"improve-codebase-architecture": ("planner", "planning")}
|
|
for name, role_step in expected.items():
|
|
with self.subTest(name=name):
|
|
package = ROOT / "packages" / name
|
|
manifest = json.loads((package / "relayview-skill.json").read_text())
|
|
self.assertEqual((manifest["role"], manifest["step"]), role_step)
|
|
check_package(package)
|
|
|
|
def test_release_package_and_starter_are_valid(self):
|
|
for path in sorted((ROOT / "packages").iterdir()) + [ROOT / "examples/starter-planner"]:
|
|
check_package(path)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
first = package_archive(path, Path(directory) / "one.zip")
|
|
second = package_archive(path, Path(directory) / "two.zip")
|
|
self.assertEqual(first, second)
|
|
|
|
def test_maintained_guidance_covers_distinct_role_steps(self):
|
|
expected = {
|
|
"planner-scope": ("planner", "planning", "scope.md", "Plan"),
|
|
"implementer-handoff": ("implementer", "implementation", "handoff.md", "documentation"),
|
|
}
|
|
for name, (role, step, resource, marker) in expected.items():
|
|
with self.subTest(name=name):
|
|
package = ROOT / "packages" / name
|
|
manifest = json.loads((package / "relayview-skill.json").read_text())
|
|
self.assertEqual((manifest["role"], manifest["step"]), (role, step))
|
|
self.assertEqual(manifest["source"], {"kind": "first-party", "reference": "v0.5.0" if name == "planner-scope" else "v0.2.0"})
|
|
if name == "planner-scope":
|
|
self.assertEqual(manifest["issueClaim"], {"statusLabel": "status/in-progress"})
|
|
self.assertIn(resource, [item["path"] for item in manifest["files"]])
|
|
self.assertIn(marker, (package / "SKILL.md").read_text() + (package / resource).read_text())
|
|
|
|
def test_user_fork_is_distinct_under_same_role_limits(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "implementer-handoff"
|
|
shutil.copytree(ROOT / "packages/implementer-handoff", package)
|
|
original = check_package(package)
|
|
resource = package / "handoff.md"
|
|
resource.write_text(resource.read_text() + "\n- Prefer the team's concise PR summary format.\n")
|
|
manifest_path = package / "relayview-skill.json"
|
|
manifest = json.loads(manifest_path.read_text())
|
|
manifest["source"] = {"kind": "user", "reference": "team-fork-1"}
|
|
next(item for item in manifest["files"] if item["path"] == "handoff.md")["sha256"] = hashlib.sha256(resource.read_bytes()).hexdigest()
|
|
manifest_path.write_text(json.dumps(manifest))
|
|
self.assertNotEqual(check_package(package), original)
|
|
self.assertEqual((manifest["role"], manifest["step"]), ("implementer", "implementation"))
|
|
|
|
def test_rejects_scripts_and_undeclared_files(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "planner-scope"
|
|
shutil.copytree(ROOT / "packages/planner-scope", package)
|
|
(package / "helper.py").write_text("print('unsafe')\n")
|
|
with self.assertRaises(PackageError):
|
|
check_package(package)
|
|
|
|
def test_rejects_permission_expansion(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "planner-scope"
|
|
shutil.copytree(ROOT / "packages/planner-scope", package)
|
|
manifest = package / "relayview-skill.json"
|
|
data = json.loads(manifest.read_text())
|
|
data["tools"] = ["shell"]
|
|
manifest.write_text(json.dumps(data))
|
|
with self.assertRaises(PackageError):
|
|
check_package(package)
|
|
|
|
def test_rejects_role_mismatch(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "planner-scope"
|
|
shutil.copytree(ROOT / "packages/planner-scope", package)
|
|
manifest = package / "relayview-skill.json"
|
|
data = json.loads(manifest.read_text())
|
|
data["role"] = "reviewer"
|
|
manifest.write_text(json.dumps(data))
|
|
with self.assertRaises(PackageError):
|
|
check_package(package)
|
|
|
|
def test_rejects_changed_resource(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "planner-scope"
|
|
shutil.copytree(ROOT / "packages/planner-scope", package)
|
|
(package / "scope.md").write_text("changed\n")
|
|
with self.assertRaises(PackageError):
|
|
check_package(package)
|
|
|
|
def test_rejects_frontmatter_permission_request(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "planner-scope"
|
|
shutil.copytree(ROOT / "packages/planner-scope", package)
|
|
skill = package / "SKILL.md"
|
|
skill.write_text(skill.read_text().replace("metadata:\n", "tools: [shell]\nmetadata:\n"))
|
|
manifest = package / "relayview-skill.json"
|
|
data = json.loads(manifest.read_text())
|
|
next(item for item in data["files"] if item["path"] == "SKILL.md")["sha256"] = hashlib.sha256(skill.read_bytes()).hexdigest()
|
|
manifest.write_text(json.dumps(data))
|
|
with self.assertRaises(PackageError):
|
|
check_package(package)
|
|
|
|
def test_rejects_owner_incompatible_frontmatter_values(self):
|
|
for old, new in (
|
|
("metadata:\n", "disable-model-invocation: yes\nmetadata:\n"),
|
|
("description: Check assigned issue scope, neighboring work, and verification before drafting a Plan.", "description: "),
|
|
):
|
|
with self.subTest(new=new), tempfile.TemporaryDirectory() as directory:
|
|
package = Path(directory) / "planner-scope"
|
|
shutil.copytree(ROOT / "packages/planner-scope", package)
|
|
skill = package / "SKILL.md"
|
|
skill.write_text(skill.read_text().replace(old, new))
|
|
manifest = package / "relayview-skill.json"
|
|
data = json.loads(manifest.read_text())
|
|
next(item for item in data["files"] if item["path"] == "SKILL.md")["sha256"] = hashlib.sha256(skill.read_bytes()).hexdigest()
|
|
manifest.write_text(json.dumps(data))
|
|
with self.assertRaises(PackageError):
|
|
check_package(package)
|
|
|
|
def test_archive_carries_license_and_notice(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
archive_path = Path(directory) / "planner-scope.zip"
|
|
package_archive(ROOT / "packages/planner-scope", archive_path)
|
|
with zipfile.ZipFile(archive_path) as archive:
|
|
self.assertEqual(archive.read("LICENSE"), (ROOT / "LICENSE").read_bytes())
|
|
self.assertEqual(archive.read("NOTICE.md"), (ROOT / "NOTICE.md").read_bytes())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|