tests.test_plugin module¶
Conformance of the Claude Code plugin manifest, marketplace and archive.
Claude Code ignores manifest fields it does not recognize and resolves component
paths lazily, so a typo (agent for agents) or a path that stopped existing
loads a plugin that is simply missing half its content. Nothing surfaces that at
runtime, which is what these tests are for.
Caution
The sharpest edge is that claude plugin validate --strict passes a manifest
whose agents path loads zero agents (see the
plugin module docstring), so validation alone proves nothing
about the archive being usable. test_archive_uses_the_default_component_dirs()
and test_manifest_declares_no_component_paths() are what keep the layout on
the shape that was verified to actually load.
- tests.test_plugin.MANIFEST_FIELDS = frozenset({'$schema', 'agents', 'author', 'commands', 'dependencies', 'description', 'displayName', 'homepage', 'hooks', 'keywords', 'license', 'lspServers', 'mcpServers', 'monitors', 'name', 'outputStyles', 'repository', 'settings', 'skills', 'themes', 'userConfig', 'version'})¶
Top-level fields the plugin manifest schema recognizes.
Mirrors the published schema. Claude Code silently ignores anything else, so an unknown key here is almost always a misspelling of a real one rather than a deliberate addition.
- tests.test_plugin.ARCHIVE_VERSION = '1.2.3'¶
Stand-in version for the packing tests, distinct from the package’s own
__version__so a stamped manifest cannot pass by coincidence.
- tests.test_plugin.VERSION_RE = re.compile('^\\d+\\.\\d+\\.\\d+$')¶
A released version, bare because it names a package rather than a tag.
Both the manifest and the catalog entry carry one, and both are written by the release freeze, so neither may hold a
.devNsuffix.
- tests.test_plugin.marketplace_raw() str[source]¶
The checked-in marketplace catalog, as text the freeze can rewrite.
- Return type:
- tests.test_plugin.test_manifest_identity(manifest)[source]¶
The manifest names the plugin the constants and the docs promise.
- Return type:
- tests.test_plugin.test_manifest_pins_the_last_released_version(manifest)[source]¶
The manifest names a release, written by the freeze and never by hand.
A
git-subdirmarketplace source publishes this file as it stands, andclaude plugin validate --strictfails a manifest with no version at all. Claude Code treats the string as the update signal, so a hand-maintained one left stale would strand every user on the plugin they already have.- Return type:
- tests.test_plugin.test_manifest_fields_are_recognized(manifest)[source]¶
No manifest field is one Claude Code would silently ignore.
- Return type:
- tests.test_plugin.test_manifest_declares_no_component_paths(manifest, field)[source]¶
The manifest carries metadata only, never a component path.
Every asset already sits at the spec’s default location, precisely so these fields are unnecessary. Re-introducing one is a regression rather than a refactor: an
agentspath passesclaude plugin validate --strictand then loads zero agents.- Return type:
- tests.test_plugin.test_marketplace_identity(marketplace)[source]¶
The catalog names the marketplace and plugin the install command uses.
- Return type:
- tests.test_plugin.MARKETPLACE_REFS = ('main', re.compile('^v\\d+\\.\\d+\\.\\d+$'))¶
The two shapes the entry’s
refis ever allowed to take.The default branch is the unfrozen state, which is what lets the app’s
Sync automaticallyobserve a change at all. Av-prefixed release tag is whatPrepareRelease.freeze_marketplace_pinwrites for the release commit, and it is the state this test sees when the suite runs from a released tree. Never avX.Y.Z.devNtag, which was never created and would resolve against nothing.
- tests.test_plugin.test_marketplace_installs_from_the_plugin_root(marketplace)[source]¶
The single entry publishes
PLUGIN_ROOTfrom a pinned tag.An
archivesource was the previous shape here, and it syncs only in the CLI: the claude.ai ingester rejects the type outright, which is what made every Desktop and Cowork marketplace sync fail.git-subdiris one of the three types it does accept, and the narrowest of them.- Return type:
- tests.test_plugin.test_marketplace_entry_carries_a_version(marketplace)[source]¶
The entry pins a version, which is what update detection reads.
Bumping
refalone leaves the app’s Update button greyed out: detection compares this string, not the manifest the source resolves to. Hand-writing it would strand everyone on the plugin they already had, so the freeze writes it beside therefit is already moving.- Return type:
- tests.test_plugin.test_marketplace_pin_moves_on_the_next_freeze(tmp_path, marketplace_raw)[source]¶
The next release freeze lands both halves of the pin on that release.
Asserted by running the freeze rather than by comparing against the checked-in values, which legitimately name the last published release rather than the version under development.
- Return type:
- tests.test_plugin.test_plugin_root_is_installable_as_it_stands(marketplace)[source]¶
The directory the entry names really is a plugin root.
A
git-subdirsource publishes the tree verbatim, with no packing step to rearrange it, so the manifest and both component directories have to be exactly where the spec scans for them. Moving any of the three without moving the marketplacepathwould ship a catalog entry resolving to a directory Claude Code loads nothing from.- Return type:
- tests.test_plugin.test_pack_plugin_layout(tmp_path)[source]¶
The archive holds one top-level folder, with the manifest inside it.
- Return type:
- tests.test_plugin.test_pack_plugin_ships_every_registered_asset(tmp_path)[source]¶
Exactly the manifest plus every registered agent and skill, nothing else.
- Return type:
- tests.test_plugin.test_archive_uses_the_default_component_dirs(tmp_path)[source]¶
Assets land at the spec’s default locations, not their
.claude/paths.This is the invariant that makes the plugin actually load: a manifest pointing at a custom agents path validates cleanly and contributes no agents. Verified against Claude Code 2.1.220 with
claude plugin details, which reportedAgents (0)for the custom-path layout andAgents (3)for this one.- Return type:
- tests.test_plugin.test_archive_skill_folders_keep_their_subdirectories(tmp_path)[source]¶
A skill’s own
references/,scripts/andassets/travel with it.No bundled skill has one yet, so this covers the relocation arithmetic with a synthetic tree rather than waiting for the first skill to grow one and finding out in a release.
- Return type:
- tests.test_plugin.test_pack_plugin_stamps_the_version(tmp_path)[source]¶
The packaged manifest carries the version it was packed with.
- Return type:
- tests.test_plugin.test_pack_plugin_defaults_to_the_running_version(tmp_path)[source]¶
Omitting the version stamps the one the package reports.
- Return type:
- tests.test_plugin.test_pack_plugin_is_byte_deterministic(tmp_path)[source]¶
Re-packing an unchanged tree produces an identical archive.
With no
sha256pin in the marketplace entry, the archive digest is what Claude Code falls back to, so a nondeterministic pack would advertise an update on every release whether or not anything changed.- Return type:
- tests.test_plugin.test_pack_plugin_creates_missing_parents(tmp_path)[source]¶
A destination in a directory that does not exist yet still packs.
- Return type:
- tests.test_plugin.test_pack_plugin_without_a_manifest(tmp_path)[source]¶
A tree with no manifest fails loudly instead of packing a broken plugin.
- Return type:
- tests.test_plugin.test_settings_wiring_registers_and_enables()[source]¶
A fresh document gets both the marketplace and the enablement entry.
- Return type:
- tests.test_plugin.test_settings_wiring_is_format_json_shaped()[source]¶
Serialized with the tab indent and sorted keys
format-jsonimposes.- Return type:
- tests.test_plugin.test_biome_json_indent_follows_the_declared_formatter(tmp_path, files, expected)[source]¶
The indent is read from wherever
repomatic run biomewould read it.- Return type:
- tests.test_plugin.test_merge_plugin_settings_converges_under_the_declared_style(tmp_path, declared, indent)[source]¶
The write agrees with
format-json, so the two cannot undo each other.A repository declaring spaces used to get a tab-indented document back from every run, which
format-jsonreindented right back, leavingsync-repomaticand the autofix lane opening pull requests against each other forever.- Return type:
- tests.test_plugin.test_merge_plugin_settings_leaves_a_biome_formatted_document_alone(tmp_path)[source]¶
A collapsed short array is not drift, so the wiring rewrites nothing.
json.dumpsexpands every array one item per line, while Biome collapses one that fits its line width. Comparing the rendered text reported a difference on a documentformat-jsonhad already settled, so the two rewrote each other on every run. Matching the indent alone never closed that gap.- Return type:
- tests.test_plugin.test_settings_wiring_preserves_existing_content(prior, survivor)[source]¶
Merging never drops what a repository already declared.
- Return type:
- tests.test_plugin.test_settings_wiring_replaces_a_non_object_section()[source]¶
A malformed section is replaced rather than raising, so init converges.
- Return type: