feat(factory): add native Droid plugin packaging (#120)
* feat(factory): add native Droid plugin packaging * test(factory): guard documented install contract * fix(factory): enforce install command order
This commit is contained in:
@@ -11,7 +11,7 @@ import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Iterator
|
||||
from typing import Iterator, Optional
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
VERIFY_SCRIPT = ROOT / "scripts/verify-plugin-package.py"
|
||||
@@ -50,9 +50,24 @@ def manifest(version: str, codex: bool = False) -> dict:
|
||||
return payload
|
||||
|
||||
|
||||
def seed_package(root: Path, version: str = "1.2.3") -> None:
|
||||
def seed_factory(root: Path, version: str) -> None:
|
||||
write_json(root / ".factory-plugin/plugin.json", manifest(version))
|
||||
write_json(
|
||||
root / ".factory-plugin/marketplace.json",
|
||||
{
|
||||
"name": PLUGIN_NAME,
|
||||
"plugins": [{"name": PLUGIN_NAME, "source": "./"}],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def seed_package(
|
||||
root: Path, version: str = "1.2.3", include_factory: bool = True
|
||||
) -> None:
|
||||
write_json(root / ".claude-plugin/plugin.json", manifest(version))
|
||||
write_json(root / ".codex-plugin/plugin.json", manifest(version, codex=True))
|
||||
if include_factory:
|
||||
seed_factory(root, version)
|
||||
write_json(
|
||||
root / ".claude-plugin/marketplace.json",
|
||||
{
|
||||
@@ -80,13 +95,16 @@ def seed_package(root: Path, version: str = "1.2.3") -> None:
|
||||
skill = root / "skills" / PLUGIN_NAME / "SKILL.md"
|
||||
skill.parent.mkdir(parents=True, exist_ok=True)
|
||||
skill.write_text(f"---\nname: {PLUGIN_NAME}\n---\n", encoding="utf-8")
|
||||
command = root / "commands" / "export-diagram.md"
|
||||
command.parent.mkdir(parents=True, exist_ok=True)
|
||||
command.write_text("Export the diagram.\n", encoding="utf-8")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def package_repo() -> Iterator[Path]:
|
||||
def package_repo(include_factory: bool = True) -> Iterator[Path]:
|
||||
with tempfile.TemporaryDirectory() as scratch:
|
||||
root = Path(scratch)
|
||||
seed_package(root)
|
||||
seed_package(root, include_factory=include_factory)
|
||||
subprocess.run(["git", "init", "-q"], cwd=root, check=True)
|
||||
subprocess.run(["git", "config", "user.name", "Package Test"], cwd=root, check=True)
|
||||
subprocess.run(
|
||||
@@ -99,16 +117,31 @@ def package_repo() -> Iterator[Path]:
|
||||
yield root
|
||||
|
||||
|
||||
def set_versions(root: Path, claude: str, codex: str) -> None:
|
||||
def set_versions(
|
||||
root: Path, claude: str, codex: str, factory: Optional[str] = None
|
||||
) -> None:
|
||||
if factory is None:
|
||||
factory = codex
|
||||
for relative, version in (
|
||||
(Path(".claude-plugin/plugin.json"), claude),
|
||||
(Path(".codex-plugin/plugin.json"), codex),
|
||||
(Path(".factory-plugin/plugin.json"), factory),
|
||||
):
|
||||
payload = json.loads((root / relative).read_text(encoding="utf-8"))
|
||||
payload["version"] = version
|
||||
write_json(root / relative, payload)
|
||||
|
||||
|
||||
def set_existing_versions(root: Path, version: str) -> None:
|
||||
for relative in VERIFY.MANIFEST_PATHS.values():
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
continue
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
payload["version"] = version
|
||||
write_json(path, payload)
|
||||
|
||||
|
||||
def expect_failure(label: str, errors: list[str], needle: str) -> None:
|
||||
if not any(needle in error for error in errors):
|
||||
raise AssertionError(f"{label}: expected {needle!r}, got {errors}")
|
||||
@@ -138,6 +171,14 @@ def test_verifier() -> None:
|
||||
"versions must match",
|
||||
)
|
||||
|
||||
with package_repo() as root:
|
||||
set_versions(root, "1.2.4", "1.2.4", "1.2.5")
|
||||
expect_failure(
|
||||
"Factory version drift",
|
||||
VERIFY.verify_package(root, "HEAD"),
|
||||
"versions must match",
|
||||
)
|
||||
|
||||
with package_repo() as root:
|
||||
set_versions(root, "1.2", "1.2")
|
||||
expect_failure(
|
||||
@@ -158,6 +199,68 @@ def test_verifier() -> None:
|
||||
"target does not exist",
|
||||
)
|
||||
|
||||
with package_repo() as root:
|
||||
set_versions(root, "1.2.4", "1.2.4")
|
||||
marketplace_path = root / ".factory-plugin/marketplace.json"
|
||||
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
|
||||
marketplace["plugins"][0]["source"] = {
|
||||
"source": "local",
|
||||
"path": "./",
|
||||
}
|
||||
write_json(marketplace_path, marketplace)
|
||||
expect_failure(
|
||||
"non-native Factory marketplace source",
|
||||
VERIFY.verify_package(root, "HEAD"),
|
||||
"must be a non-empty local path string",
|
||||
)
|
||||
|
||||
with package_repo() as root:
|
||||
set_versions(root, "1.2.4", "1.2.4")
|
||||
(root / "commands/export-diagram.md").unlink()
|
||||
expect_failure(
|
||||
"missing shared command surface",
|
||||
VERIFY.verify_package(root, "HEAD"),
|
||||
"packaged commands are missing",
|
||||
)
|
||||
|
||||
with package_repo(include_factory=False) as root:
|
||||
set_existing_versions(root, "1.2.4")
|
||||
seed_factory(root, "1.2.4")
|
||||
errors = VERIFY.verify_package(root, "HEAD")
|
||||
if errors:
|
||||
raise AssertionError(f"valid Factory bootstrap failed: {errors}")
|
||||
print("OK: synchronized Factory bootstrap accepted")
|
||||
|
||||
with package_repo(include_factory=False) as root:
|
||||
seed_factory(root, "1.2.3")
|
||||
expect_failure(
|
||||
"Factory bootstrap without package bump",
|
||||
VERIFY.verify_package(root, "HEAD"),
|
||||
"must increase",
|
||||
)
|
||||
|
||||
with package_repo(include_factory=False) as root:
|
||||
set_existing_versions(root, "1.2.4")
|
||||
seed_factory(root, "1.2.4")
|
||||
factory_path = root / ".factory-plugin/plugin.json"
|
||||
factory = json.loads(factory_path.read_text(encoding="utf-8"))
|
||||
factory["description"] = "Drifted Factory description."
|
||||
write_json(factory_path, factory)
|
||||
expect_failure(
|
||||
"Factory bootstrap with manifest drift",
|
||||
VERIFY.verify_package(root, "HEAD"),
|
||||
"must match Claude",
|
||||
)
|
||||
|
||||
with package_repo() as root:
|
||||
set_versions(root, "1.2.4", "1.2.4")
|
||||
(root / ".factory-plugin/plugin.json").unlink()
|
||||
expect_failure(
|
||||
"Factory manifest deletion",
|
||||
VERIFY.verify_package(root, "HEAD"),
|
||||
"could not read",
|
||||
)
|
||||
|
||||
|
||||
def test_bumper() -> None:
|
||||
cases = (("patch", "1.2.4"), ("minor", "1.3.0"), ("major", "2.0.0"))
|
||||
|
||||
Reference in New Issue
Block a user