From 096ab5270b9c05b9bc92cb69e30a02feb3960ace Mon Sep 17 00:00:00 2001 From: ainetx Date: Thu, 18 Jun 2026 14:54:42 +0300 Subject: [PATCH] fix: refactor manifest path resolution to use centralized resolve_kit_manifest_path helper Signed-off-by: ainetx --- skills/studio/scripts/studio/commands/kit.py | 16 +++---- .../studio/scripts/studio/utils/manifest.py | 31 +++++++++--- tests/test_kit_manifest_validate.py | 47 +++++++++++++++++++ tests/test_manifest.py | 37 +++++++++++++++ 4 files changed, 117 insertions(+), 14 deletions(-) diff --git a/skills/studio/scripts/studio/commands/kit.py b/skills/studio/scripts/studio/commands/kit.py index 5ab9b33b..1834b48b 100644 --- a/skills/studio/scripts/studio/commands/kit.py +++ b/skills/studio/scripts/studio/commands/kit.py @@ -1404,11 +1404,11 @@ def _local_path_provenance(kit_source: Path, install_mode: str, studio_dir: Path def _load_manifest_install_adapter(kit_source: Path, kit_slug: str = "") -> Optional[Manifest]: """Load a manifest installer adapter only through a manifest-backed KitModel.""" - if not ((kit_source / ".cf-studio-kit.toml").is_file() or (kit_source / "manifest.toml").is_file()): - return None - from ..utils.kit_model import load_kit_model - from ..utils.manifest import load_manifest + from ..utils.manifest import load_manifest, resolve_kit_manifest_path + + if resolve_kit_manifest_path(kit_source) is None: + return None model = load_kit_model(kit_source, source_hint="manifest", kit_slug=kit_slug) if getattr(model, "manifest_source", "") not in {"canonical", "legacy_manifest"}: @@ -1432,10 +1432,10 @@ def _validate_register_manifest_containment( source_root = kit_source.resolve() if not _path_is_within(source_root, root): errors.append(f"Kit source '{kit_source}' must be inside project root '{root}' for register mode") - manifest_file = kit_source / "manifest.toml" - if not manifest_file.is_file(): - manifest_file = kit_source / ".cf-studio-kit.toml" - if not manifest_file.is_file() or not _path_is_within(manifest_file, root): + from ..utils.manifest import resolve_kit_manifest_path + + manifest_file = resolve_kit_manifest_path(kit_source) + if manifest_file is None or not manifest_file.is_file() or not _path_is_within(manifest_file, root): errors.append("Kit manifest must be inside the project root for register mode") manifest_root_value = manifest.root.replace( "{cf-studio-path}", ".", diff --git a/skills/studio/scripts/studio/utils/manifest.py b/skills/studio/scripts/studio/utils/manifest.py index a355099a..7fd1ab3d 100644 --- a/skills/studio/scripts/studio/utils/manifest.py +++ b/skills/studio/scripts/studio/utils/manifest.py @@ -1026,16 +1026,35 @@ def _validate_against_schema(data: Dict[str, Any]) -> List[str]: # Public API # --------------------------------------------------------------------------- +def resolve_kit_manifest_path(kit_source: Path) -> Optional[Path]: + """Return the effective manifest path for a kit source. + + Canonical ``.cf-studio-kit.toml`` always wins when both formats exist. + Legacy ``manifest.toml`` is only used as a fallback. + """ + canonical_path = kit_source / ".cf-studio-kit.toml" + if canonical_path.is_file(): + return canonical_path + legacy_path = kit_source / "manifest.toml" + if legacy_path.is_file(): + return legacy_path + return None + + # @cpt-begin:cpt-studio-algo-kit-manifest-install:p1:inst-manifest-read def load_manifest(kit_source: Path, kit_slug: str = "") -> Optional[Union[Manifest, ManifestV2]]: - """Read and parse ``manifest.toml`` from *kit_source*. + """Read and parse the effective kit manifest from *kit_source*. - Returns ``None`` if the file does not exist. V2 manifests are delegated - to ``parse_manifest_v2`` and return a ``ManifestV2`` instance. - Raises ``ValueError`` if the file exists but is invalid. + Returns ``None`` if neither canonical nor legacy manifest exists. + Canonical ``.cf-studio-kit.toml`` takes precedence over legacy + ``manifest.toml`` when both are present. V2 manifests are delegated to + ``parse_manifest_v2`` and return a ``ManifestV2`` instance. Raises + ``ValueError`` if the selected manifest exists but is invalid. """ - manifest_path = kit_source / "manifest.toml" - if not manifest_path.is_file(): + manifest_path = resolve_kit_manifest_path(kit_source) + if manifest_path is None: + return None + if manifest_path.name == ".cf-studio-kit.toml": return _load_canonical_kit_manifest(kit_source, kit_slug=kit_slug) try: diff --git a/tests/test_kit_manifest_validate.py b/tests/test_kit_manifest_validate.py index 401060d1..c20185c1 100644 --- a/tests/test_kit_manifest_validate.py +++ b/tests/test_kit_manifest_validate.py @@ -1077,6 +1077,53 @@ def test_no_manifest_no_resource_check(self): ] self.assertEqual(len(resource_errors), 0) + def test_canonical_manifest_ignores_malformed_legacy_manifest(self): + """Standalone validation ignores manifest.toml when canonical manifest exists.""" + from studio.commands.validate_kits import _validate_kit_by_path + + with TemporaryDirectory() as td: + td_path = Path(td) + kit_dir = td_path / "mykit" + kit_dir.mkdir() + (kit_dir / "constraints.toml").write_text( + "[FEATURE.identifiers.flow]\nrequired = true\n", + encoding="utf-8", + ) + (kit_dir / "feature-template.md").write_text("# Feature\n", encoding="utf-8") + (kit_dir / ".cf-studio-kit.toml").write_text( + "\n".join([ + 'manifest_version = "1.0"', + "", + "[[kits]]", + 'slug = "mykit"', + 'version = "1.0"', + "", + "[[kits.resources]]", + 'id = "ruleset"', + 'kind = "constraints"', + 'source = "constraints.toml"', + 'type = "file"', + "", + "[[kits.resources]]", + 'id = "feature-template"', + 'kind = "template"', + 'source = "feature-template.md"', + 'type = "file"', + ]) + "\n", + encoding="utf-8", + ) + (kit_dir / "manifest.toml").write_text("[broken\ninvalid", encoding="utf-8") + + rc, result = _validate_kit_by_path(kit_dir, verbose=True) + + self.assertEqual(rc, 0) + self.assertEqual(result["kits"][0]["manifest_source"], "canonical") + resource_errors = [ + e for e in result.get("errors", []) + if e.get("type") == "resources" + ] + self.assertEqual(resource_errors, []) + def test_canonical_constraints_kind_resources_with_arbitrary_names(self): """Standalone canonical kit loads constraints by kind, not filename or id.""" from studio.commands.validate_kits import _validate_kit_by_path diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 9ca6e3a0..c5ef948a 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -194,6 +194,43 @@ def test_defaults_applied(self, tmp_path: Path) -> None: assert m.resources[0].user_modifiable is True assert m.resources[0].description == "" + def test_canonical_manifest_wins_over_legacy_manifest(self, tmp_path: Path) -> None: + """When both manifests exist, load_manifest ignores legacy manifest.toml.""" + kit = tmp_path / "dual-manifest-kit" + kit.mkdir() + (kit / "constraints.toml").write_text("[artifacts]\n", encoding="utf-8") + (kit / "feature-template.md").write_text("# Feature\n", encoding="utf-8") + (kit / ".cf-studio-kit.toml").write_text( + textwrap.dedent( + """\ + manifest_version = "1.0" + + [[kits]] + slug = "dual-manifest-kit" + version = "1.0" + + [[kits.resources]] + id = "ruleset" + kind = "constraints" + source = "constraints.toml" + type = "file" + + [[kits.resources]] + id = "feature-template" + kind = "template" + source = "feature-template.md" + type = "file" + """ + ), + encoding="utf-8", + ) + (kit / "manifest.toml").write_text("[broken\ninvalid", encoding="utf-8") + + m = load_manifest(kit) + + assert m is not None + assert [res.id for res in m.resources] == ["ruleset", "feature-template"] + # --------------------------------------------------------------------------- # validate_manifest