Commit 1a44a6a
fix(presets): skip an unreadable restore source in
* fix(presets): skip an unreadable restore source in `preset remove`
`_unregister_skills_in_dir` restores each preset-owned SKILL.md from a core
command template or an extension source. Both of those reads were bare
`read_text(encoding="utf-8")` calls, so a project-owned override in
`.specify/templates/commands/` that exists but cannot be read or decoded
raised a raw `UnicodeDecodeError`/`OSError` straight out of
`PresetManager.remove()`, which has no handler for it — `specify preset
remove` dies with a traceback.
Every other failure in this loop degrades with `continue`: an unsafe
registry name, a missing skill subdirectory, a foreign owner. Sibling reads
of the very same directory are already guarded — `_infer_legacy_skill_
provenance` and `_delete_agent_preset_skills` both wrap their SKILL.md read
in `except (OSError, UnicodeDecodeError): continue`, and the read inside
`_substitute_core_template` was just given the same boundary in #3961. The
two restore reads were the remaining gap.
`continue` is the right recovery here rather than falling through: the
`else` branch below removes the skill outright, so treating an unreadable
source as "no source" would delete a user's skill at exactly the moment its
replacement cannot be generated. Skipping leaves the skill in place and
keeps it out of the returned `mutated_names`, so callers don't record a
restore that never happened.
Two regression tests, one per exception arm: a non-UTF-8 core template, and
a mocked `PermissionError` so the `OSError` half is also covered under
privileged CI where permission bits aren't enforced. Both assert the skill
survives untouched and is not reported as mutated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): warn when a skill keeps preset content after a failed restore
Review follow-up on two points.
Surface the skipped restore. Skipping is still the correct recovery — the
alternative branch deletes the skill — but it was silent, and it is a
partial removal: `remove()` goes on to delete the preset directory and the
registry entry, while this `SKILL.md` keeps the removed preset's content,
and leaving the name out of `mutated_names` also keeps it out of
reconciliation, so nothing retries it. Both arms now emit a warning naming
the skill, the unreadable source, and the exception, and pointing at the
re-run that refreshes it once the file is fixed. `warnings.warn` matches
how the surrounding code reports non-fatal degradation (the reconciliation
failures in `remove()`/`install_from_directory`, the unreadable core
template in `_substitute_core_template` from #3961).
Cover the extension arm. A skill backed by an installed extension never
reaches the core-template read, so the two branches can regress
independently and both prior tests exercised only the core one.
`test_unregister_skills_in_dir_unreadable_extension_source_skips` installs
an extension whose command file is non-UTF-8 and asserts the skill survives
byte-for-byte and is absent from `mutated_names`. Verified it raises the
raw `UnicodeDecodeError` against unpatched source. The two existing tests
now assert the warning via `pytest.warns` so dropping it fails the suite.
pytest tests/test_presets.py -> 583 passed, 2 skipped, 7 failed; the 7 are
the pre-existing Windows symlink tests that need elevation, unchanged from
main. ruff check passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>preset remove (#4020)1 parent f0ee1fc commit 1a44a6a
2 files changed
Lines changed: 197 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3268 | 3268 | | |
3269 | 3269 | | |
3270 | 3270 | | |
| 3271 | + | |
| 3272 | + | |
| 3273 | + | |
| 3274 | + | |
| 3275 | + | |
| 3276 | + | |
| 3277 | + | |
| 3278 | + | |
| 3279 | + | |
| 3280 | + | |
| 3281 | + | |
| 3282 | + | |
| 3283 | + | |
| 3284 | + | |
| 3285 | + | |
| 3286 | + | |
| 3287 | + | |
| 3288 | + | |
| 3289 | + | |
| 3290 | + | |
| 3291 | + | |
| 3292 | + | |
| 3293 | + | |
| 3294 | + | |
3271 | 3295 | | |
3272 | 3296 | | |
3273 | 3297 | | |
| |||
3394 | 3418 | | |
3395 | 3419 | | |
3396 | 3420 | | |
3397 | | - | |
3398 | | - | |
| 3421 | + | |
| 3422 | + | |
| 3423 | + | |
| 3424 | + | |
| 3425 | + | |
| 3426 | + | |
| 3427 | + | |
| 3428 | + | |
| 3429 | + | |
| 3430 | + | |
| 3431 | + | |
| 3432 | + | |
| 3433 | + | |
| 3434 | + | |
3399 | 3435 | | |
3400 | 3436 | | |
3401 | 3437 | | |
| |||
3436 | 3472 | | |
3437 | 3473 | | |
3438 | 3474 | | |
3439 | | - | |
| 3475 | + | |
| 3476 | + | |
| 3477 | + | |
| 3478 | + | |
| 3479 | + | |
| 3480 | + | |
| 3481 | + | |
| 3482 | + | |
| 3483 | + | |
| 3484 | + | |
3440 | 3485 | | |
3441 | 3486 | | |
3442 | 3487 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9452 | 9452 | | |
9453 | 9453 | | |
9454 | 9454 | | |
| 9455 | + | |
| 9456 | + | |
| 9457 | + | |
| 9458 | + | |
| 9459 | + | |
| 9460 | + | |
| 9461 | + | |
| 9462 | + | |
| 9463 | + | |
| 9464 | + | |
| 9465 | + | |
| 9466 | + | |
| 9467 | + | |
| 9468 | + | |
| 9469 | + | |
| 9470 | + | |
| 9471 | + | |
| 9472 | + | |
| 9473 | + | |
| 9474 | + | |
| 9475 | + | |
| 9476 | + | |
| 9477 | + | |
| 9478 | + | |
| 9479 | + | |
| 9480 | + | |
| 9481 | + | |
| 9482 | + | |
| 9483 | + | |
| 9484 | + | |
| 9485 | + | |
| 9486 | + | |
| 9487 | + | |
| 9488 | + | |
| 9489 | + | |
| 9490 | + | |
| 9491 | + | |
| 9492 | + | |
| 9493 | + | |
| 9494 | + | |
| 9495 | + | |
| 9496 | + | |
| 9497 | + | |
| 9498 | + | |
| 9499 | + | |
| 9500 | + | |
| 9501 | + | |
| 9502 | + | |
| 9503 | + | |
| 9504 | + | |
| 9505 | + | |
| 9506 | + | |
| 9507 | + | |
| 9508 | + | |
| 9509 | + | |
| 9510 | + | |
| 9511 | + | |
| 9512 | + | |
| 9513 | + | |
| 9514 | + | |
| 9515 | + | |
| 9516 | + | |
| 9517 | + | |
| 9518 | + | |
| 9519 | + | |
| 9520 | + | |
| 9521 | + | |
| 9522 | + | |
| 9523 | + | |
| 9524 | + | |
| 9525 | + | |
| 9526 | + | |
| 9527 | + | |
| 9528 | + | |
| 9529 | + | |
| 9530 | + | |
| 9531 | + | |
| 9532 | + | |
| 9533 | + | |
| 9534 | + | |
| 9535 | + | |
| 9536 | + | |
| 9537 | + | |
| 9538 | + | |
| 9539 | + | |
| 9540 | + | |
| 9541 | + | |
| 9542 | + | |
| 9543 | + | |
| 9544 | + | |
| 9545 | + | |
| 9546 | + | |
| 9547 | + | |
| 9548 | + | |
| 9549 | + | |
| 9550 | + | |
| 9551 | + | |
| 9552 | + | |
| 9553 | + | |
| 9554 | + | |
| 9555 | + | |
| 9556 | + | |
| 9557 | + | |
| 9558 | + | |
| 9559 | + | |
| 9560 | + | |
| 9561 | + | |
| 9562 | + | |
| 9563 | + | |
| 9564 | + | |
| 9565 | + | |
| 9566 | + | |
| 9567 | + | |
| 9568 | + | |
| 9569 | + | |
| 9570 | + | |
| 9571 | + | |
| 9572 | + | |
| 9573 | + | |
| 9574 | + | |
| 9575 | + | |
| 9576 | + | |
| 9577 | + | |
| 9578 | + | |
| 9579 | + | |
| 9580 | + | |
| 9581 | + | |
| 9582 | + | |
| 9583 | + | |
| 9584 | + | |
| 9585 | + | |
| 9586 | + | |
| 9587 | + | |
| 9588 | + | |
| 9589 | + | |
| 9590 | + | |
| 9591 | + | |
| 9592 | + | |
| 9593 | + | |
| 9594 | + | |
| 9595 | + | |
| 9596 | + | |
| 9597 | + | |
| 9598 | + | |
| 9599 | + | |
| 9600 | + | |
| 9601 | + | |
| 9602 | + | |
| 9603 | + | |
9455 | 9604 | | |
9456 | 9605 | | |
9457 | 9606 | | |
| |||
0 commit comments