* fix(treemap): verify every cell's area, including cells too small to label Closes #89. The area check derived its basis from in-cell labels, so a cell with no label was excluded from it. That is precisely the sliver -- the cell a 4px grid distorts most, and the one that shipped 24% undersized during review of #87. On current main, widening Oceania from 16px to 24px, a 50% area distortion, still reports OK. Every cell now declares `data-share`, its percentage of the whole, and the verifier checks all of them against it. Cells without it are reported rather than skipped: a treemap whose cells cannot be parsed is not a passing treemap, it is an unchecked one, and answering OK to it is the defect. The label and the metadata are also cross-checked, because two statements of one fact is a new way for the picture to lie while the gate stays green. Adversarial tests gain the oversized and undersized unlabelled sliver (both pass on current main and fail here), a fail-closed case for missing metadata, and a label/metadata divergence case. SKILL.md `metadata.version` said 2.4 against 2.5.0 manifests. The manifests are bumped by a script and that field is hand-maintained, so nothing read both numbers and nothing could notice. verify-plugin-package now asserts it tracks the manifest MAJOR.MINOR. The package test's fixture gains the field with it -- a SKILL.md without one is not a valid package, and a fixture missing it would only prove the gate can be tripped by its own test data. The examples regained the `PARTS ROUNDED, MAY NOT SUM` disclosure the type reference requires: displayed parts round up to 8.10B against a stated 8.09B. It was dropped when the examples were rebuilt around the legend block. The anti-pattern list, which still forbade outright what the honest-data rule permits once disclosed, now agrees with it. Also nudges the info mark onto the label row of its neighbours rather than the vertical centre of its own cell, so the bottom row reads as one row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(treemap): reject ambiguous share metadata --------- Co-authored-by: 0xDarkMatter <0xDarkMatter@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
349 lines
12 KiB
Python
Executable File
349 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Regression tests for plugin versioning and marketplace package verification."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
from typing import Iterator, Optional
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
VERIFY_SCRIPT = ROOT / "scripts/verify-plugin-package.py"
|
|
BUMP_SCRIPT = ROOT / "scripts/bump-plugin-version.py"
|
|
PLUGIN_NAME = "diagram-design"
|
|
|
|
|
|
def load_module(name: str, path: Path) -> ModuleType:
|
|
sys.dont_write_bytecode = True
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError(f"could not load {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
VERIFY = load_module("verify_plugin_package", VERIFY_SCRIPT)
|
|
BUMP = load_module("bump_plugin_version", BUMP_SCRIPT)
|
|
|
|
|
|
def write_json(path: Path, payload: dict) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
def manifest(version: str, codex: bool = False) -> dict:
|
|
payload = {
|
|
"name": PLUGIN_NAME,
|
|
"description": "Create editorial diagrams.",
|
|
"version": version,
|
|
"author": {"name": "Cathryn Lavery"},
|
|
}
|
|
if codex:
|
|
payload["skills"] = "./skills/"
|
|
return payload
|
|
|
|
|
|
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",
|
|
{
|
|
"name": PLUGIN_NAME,
|
|
"plugins": [{"name": PLUGIN_NAME, "source": "./"}],
|
|
},
|
|
)
|
|
write_json(
|
|
root / ".agents/plugins/marketplace.json",
|
|
{
|
|
"name": PLUGIN_NAME,
|
|
"plugins": [
|
|
{
|
|
"name": PLUGIN_NAME,
|
|
"source": {"source": "local", "path": "./"},
|
|
"policy": {
|
|
"installation": "AVAILABLE",
|
|
"authentication": "ON_INSTALL",
|
|
},
|
|
"category": "Productivity",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
write_skill(root, version)
|
|
command = root / "commands" / "export-diagram.md"
|
|
command.parent.mkdir(parents=True, exist_ok=True)
|
|
command.write_text("Export the diagram.\n", encoding="utf-8")
|
|
|
|
|
|
def write_skill(root: Path, version: str) -> None:
|
|
"""Fixture SKILL.md, whose metadata.version tracks the manifest MAJOR.MINOR.
|
|
|
|
The verifier gates that relationship, so a fixture that omits it is not a
|
|
valid package and would only prove the gate can be tripped by its own
|
|
test data.
|
|
"""
|
|
skill = root / "skills" / PLUGIN_NAME / "SKILL.md"
|
|
skill.parent.mkdir(parents=True, exist_ok=True)
|
|
minor = ".".join(version.split(".")[:2])
|
|
skill.write_text(
|
|
f'---\nname: {PLUGIN_NAME}\nmetadata:\n version: "{minor}"\n---\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def package_repo(include_factory: bool = True) -> Iterator[Path]:
|
|
with tempfile.TemporaryDirectory() as scratch:
|
|
root = Path(scratch)
|
|
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(
|
|
["git", "config", "user.email", "package-test@example.invalid"],
|
|
cwd=root,
|
|
check=True,
|
|
)
|
|
subprocess.run(["git", "add", "."], cwd=root, check=True)
|
|
subprocess.run(["git", "commit", "-qm", "base package"], cwd=root, check=True)
|
|
yield root
|
|
|
|
|
|
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)
|
|
# Keep the fixture's SKILL.md in step with the Claude manifest, so these
|
|
# cases exercise the version rules they are about rather than tripping the
|
|
# metadata-drift gate every time.
|
|
write_skill(root, claude)
|
|
|
|
|
|
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}")
|
|
print(f"OK: {label} rejected")
|
|
|
|
|
|
def test_verifier() -> None:
|
|
with package_repo() as root:
|
|
set_versions(root, "1.2.4", "1.2.4")
|
|
errors = VERIFY.verify_package(root, "HEAD")
|
|
if errors:
|
|
raise AssertionError(f"valid bump failed: {errors}")
|
|
print("OK: valid synchronized bump accepted")
|
|
|
|
with package_repo() as root:
|
|
expect_failure(
|
|
"missing bump",
|
|
VERIFY.verify_package(root, "HEAD"),
|
|
"must increase",
|
|
)
|
|
|
|
with package_repo() as root:
|
|
set_versions(root, "1.2.4", "1.2.5")
|
|
expect_failure(
|
|
"mismatched manifests",
|
|
VERIFY.verify_package(root, "HEAD"),
|
|
"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(
|
|
"malformed versions",
|
|
VERIFY.verify_package(root, "HEAD"),
|
|
"strict MAJOR.MINOR.PATCH",
|
|
)
|
|
|
|
with package_repo() as root:
|
|
set_versions(root, "1.2.4", "1.2.4")
|
|
marketplace_path = root / ".agents/plugins/marketplace.json"
|
|
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
|
|
marketplace["plugins"][0]["source"]["path"] = "./missing"
|
|
write_json(marketplace_path, marketplace)
|
|
expect_failure(
|
|
"missing marketplace target",
|
|
VERIFY.verify_package(root, "HEAD"),
|
|
"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",
|
|
)
|
|
|
|
with package_repo() as root:
|
|
set_versions(root, "1.2.4", "1.2.4")
|
|
skill = root / "skills" / PLUGIN_NAME / "SKILL.md"
|
|
skill.write_text(
|
|
f'---\nname: {PLUGIN_NAME}\n---\n\nExample only:\nversion: "1.2"\n',
|
|
encoding="utf-8",
|
|
)
|
|
expect_failure(
|
|
"version line outside frontmatter",
|
|
VERIFY.verify_package(root, "HEAD"),
|
|
"frontmatter has no metadata.version",
|
|
)
|
|
|
|
with package_repo() as root:
|
|
set_versions(root, "1.2.4", "1.2.4")
|
|
skill = root / "skills" / PLUGIN_NAME / "SKILL.md"
|
|
skill.write_text(
|
|
f'---\nname: {PLUGIN_NAME}\nversion: "1.2"\nmetadata:\n owner: test\n---\n',
|
|
encoding="utf-8",
|
|
)
|
|
expect_failure(
|
|
"version outside metadata mapping",
|
|
VERIFY.verify_package(root, "HEAD"),
|
|
"frontmatter has no metadata.version",
|
|
)
|
|
|
|
|
|
def test_bumper() -> None:
|
|
cases = (("patch", "1.2.4"), ("minor", "1.3.0"), ("major", "2.0.0"))
|
|
for part, expected in cases:
|
|
with tempfile.TemporaryDirectory() as scratch:
|
|
root = Path(scratch)
|
|
seed_package(root)
|
|
actual = BUMP.bump(root, part)
|
|
versions = {
|
|
json.loads((root / relative).read_text(encoding="utf-8"))["version"]
|
|
for relative in BUMP.MANIFEST_PATHS
|
|
}
|
|
if actual != expected or versions != {expected}:
|
|
raise AssertionError(
|
|
f"{part} bump: expected {expected}, got {actual} and {versions}"
|
|
)
|
|
print(f"OK: {part} bump produced {expected}")
|
|
|
|
with tempfile.TemporaryDirectory() as scratch:
|
|
root = Path(scratch)
|
|
seed_package(root)
|
|
set_versions(root, "1.2.3", "1.2.4")
|
|
try:
|
|
BUMP.bump(root)
|
|
except BUMP.PackageVersionError as exc:
|
|
if "not synchronized" not in str(exc):
|
|
raise AssertionError(f"unexpected mismatch error: {exc}") from exc
|
|
else:
|
|
raise AssertionError("version bumper accepted mismatched manifests")
|
|
print("OK: version bumper rejects mismatched manifests")
|
|
|
|
|
|
def main() -> int:
|
|
test_verifier()
|
|
test_bumper()
|
|
print("All plugin package tests passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|