Skip to content

fix(manifests): reject non-string requires.speckit_version - #3980

Merged
mnriem merged 1 commit into
github:mainfrom
Noor-ul-ain001:fix/nonstring-speckit-version
Aug 4, 2026
Merged

fix(manifests): reject non-string requires.speckit_version#3980
mnriem merged 1 commit into
github:mainfrom
Noor-ul-ain001:fix/nonstring-speckit-version

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Problem

requires.speckit_version is checked for presence but never for type in both the extension and preset manifest validators. An unquoted YAML speckit_version: 1.0 — an easy authoring slip, since YAML parses it as a float — passes validation and reaches SpecifierSet(required) in check_compatibility().

That call is guarded by except InvalidSpecifier alone, which a non-string escapes two different ways:

value what happens
1.0, 5, True, None TypeError: 'float' object is not iterable — raised by the SpecifierSet constructor, not InvalidSpecifier
[">=0.1.0"], {"min": "0.1"} iterable, so SpecifierSet accepts it; fails much later as AttributeError: 'str' object has no attribute 'filter' from inside .contains()

Neither is a CompatibilityError/PresetCompatibilityError, so both bypass the Compatibility Error handlers in extensions/_commands.py:1101 and presets/_commands.py:275, and exit 1 with a raw traceback that names no field — leaving the author no hint which manifest key is wrong. The list/dict case is the nastier one: the error surfaces deep inside packaging with a message that mentions neither the manifest nor the key.

Reproduced on main with an otherwise fully valid manifest:

validate() PASSED. requires_speckit_version = 1.0
...
  File "src/specify_cli/extensions/__init__.py", line 1855, in check_compatibility
    SpecifierSet(required)  # Just to validate
TypeError: 'float' object is not iterable

Fix

  • Both validators now require a non-empty string, so the failure is a clean ValidationError/PresetValidationError naming the field.
  • Both check_compatibility() methods additionally reject a non-string up front. These are public and reachable with a hand-built or mutated manifest (test_check_compatibility_invalid already mutates manifest.data this way), so the guard belongs at both layers.

This mirrors the sibling IntegrationDescriptor, which already requires a non-empty string for the very same key, and completes the type-checking pass started in #3943 for the neighbouring extension/preset fields — id/name/version/description are now type-checked while requires.speckit_version was still presence-only.

Blank strings are rejected too, so speckit_version: " " can't silently mean "any version".

Testing

33 regression tests added across both modules, covering every escape path (scalars, iterables, and blank strings) at both the validator and manager layer. 26 fail without the source change (the rest are existing-behaviour assertions that pass either way).

33 passed

Full tests/test_extensions.py + tests/test_presets.py: 7 failed, 952 passed, 78 errors both with and without this change — identical counts. Those pre-existing failures are Windows-local PermissionError (WinError 5) from symlink/tmpdir restrictions in my environment, unrelated to this change.

🤖 Generated with Claude Code

`requires.speckit_version` was presence-checked but never type-checked in
both the extension and preset manifest validators, so an unquoted YAML
`speckit_version: 1.0` (a float) passed validation and reached
`SpecifierSet(required)` in `check_compatibility()`.

That call is guarded by `except InvalidSpecifier` alone, which a
non-string escapes two different ways:

- a float/int/bool/None raises `TypeError: 'float' object is not
  iterable` from the `SpecifierSet` constructor;
- a list or dict is an *iterable*, so `SpecifierSet` accepts it and the
  failure surfaces much later as `AttributeError: 'str' object has no
  attribute 'filter'` from inside `.contains()`.

Neither is a `CompatibilityError`/`PresetCompatibilityError`, so both
bypass the CLI's "Compatibility Error" handler in `_commands.py` and exit
1 with a raw traceback that names no field, leaving the author with no
hint which manifest key is wrong.

Type-check the field in both validators, requiring a non-empty string,
and additionally guard `check_compatibility()` in both managers since
each is public and reachable with a hand-built or mutated manifest. This
mirrors the sibling `IntegrationDescriptor`, which already requires a
non-empty string for the same key, and completes the type-checking pass
started in github#3943 for the neighbouring `extension`/`preset` fields.

Adds 33 regression tests across both modules covering every escape path;
26 of them fail without this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Assisted-by: Claude Code (model: Claude Opus 5, supervised)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds robust type validation for requires.speckit_version across extension and preset manifests.

Changes:

  • Rejects non-string and blank version requirements during validation.
  • Adds defensive compatibility checks for mutated or hand-built manifests.
  • Adds regression coverage for scalar, iterable, null, and blank values.
Show a summary per file
File Description
src/specify_cli/extensions/__init__.py Hardens extension validation and compatibility checks.
src/specify_cli/presets/__init__.py Hardens preset validation and compatibility checks.
tests/test_extensions.py Covers invalid extension version requirements.
tests/test_presets.py Covers invalid preset version requirements.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Balanced

@mnriem
mnriem merged commit cd996f7 into github:main Aug 4, 2026
14 checks passed
@mnriem

mnriem commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants