feat(presets): list presets in resolution/precedence order (#4086) (#4104)

`specify preset list` now sorts installed presets by (priority, id) so the
printed order matches the actual resolution/composition order used by
PresetRegistry.list_by_priority(). Lower priority number = higher precedence;
ties are broken alphabetically by preset id. Adds a header and footer note
clarifying the ordering, updates the presets reference docs, and adds tests.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Manfred Riem
2026-08-13 10:00:16 -05:00
committed by GitHub
co-authored by Copilot App
parent e79fa25f3f
commit 229022943c
3 changed files with 80 additions and 1 deletions
+2
View File
@@ -47,6 +47,8 @@ specify preset list
Lists installed presets with their versions, descriptions, template counts, and current status.
Presets are printed in **resolution/precedence order**: the highest-precedence preset (lowest priority number) is listed first, and ties on priority are broken alphabetically by preset id. This matches the order used when composing commands and resolving templates, so the top entry is the one that wins for overlapping files.
## Preset Info
```bash
+11 -1
View File
@@ -59,7 +59,15 @@ def preset_list():
console.print(" [cyan]specify preset add <pack-name>[/cyan]")
return
console.print("\n[bold cyan]Installed Presets:[/bold cyan]\n")
# Sort by actual resolution precedence: lower priority number wins, ties
# broken by preset id (matching PresetRegistry.list_by_priority()). This
# keeps the printed order aligned with how presets are composed/resolved.
installed = sorted(
installed,
key=lambda pack: (pack.get("priority", 10), str(pack.get("id", ""))),
)
console.print("\n[bold cyan]Installed Presets[/bold cyan] [dim](in resolution order — highest precedence first)[/dim]\n")
for pack in installed:
status = "[green]enabled[/green]" if pack.get("enabled", True) else "[red]disabled[/red]"
pri = pack.get('priority', 10)
@@ -75,6 +83,8 @@ def preset_list():
console.print(f" [dim]Templates: {pack['template_count']}[/dim]")
console.print()
console.print("[dim]Lower priority number = higher precedence. Ties are broken by preset id (alphabetical).[/dim]")
@preset_app.command("add")
def preset_add(
+67
View File
@@ -13776,6 +13776,73 @@ class TestInstalledPresetRichMarkup:
assert "[base]" in output, output
assert "[append]" in output, output
class TestPresetListOrdering:
"""``preset list`` must print presets in actual resolution/precedence order.
Regression coverage for #4086: the printed order was registry/insertion
order, so a preset with a *higher* priority number (lower precedence) could
appear before one with a lower number, misleading users about which preset
wins. Output must be sorted by (priority, id) to match
``PresetRegistry.list_by_priority()``.
"""
def _install(self, temp_dir, project_dir, pack_id, priority):
from specify_cli.presets import PresetManager
src = temp_dir / f"src-{pack_id}"
(src / "templates").mkdir(parents=True)
(src / "templates" / "spec-template.md").write_text("# tmpl\n")
(src / "preset.yml").write_text(yaml.dump({
"schema_version": "1.0",
"preset": {
"id": pack_id,
"name": pack_id,
"version": "1.0.0",
"description": "plain description",
},
"requires": {"speckit_version": ">=0.0.1"},
"provides": {"templates": [{
"type": "template",
"name": "spec-template",
"file": "templates/spec-template.md",
}]},
}))
PresetManager(project_dir).install_from_directory(src, "9.9.9", priority)
def _invoke(self, project_dir, args):
from typer.testing import CliRunner
from unittest.mock import patch
from specify_cli import app
with patch.object(Path, "cwd", return_value=project_dir):
return CliRunner().invoke(app, args)
def test_list_sorted_by_priority(self, temp_dir, project_dir):
"""Lower priority number is listed first regardless of install order."""
# Install in an order that does NOT match precedence.
self._install(temp_dir, project_dir, "copilot-sub-agents", priority=100)
self._install(temp_dir, project_dir, "lean", priority=10)
result = self._invoke(project_dir, ["preset", "list"])
assert result.exit_code == 0, result.output
output = strip_ansi(result.output)
# `lean` (priority 10) must appear before `copilot-sub-agents` (100).
assert output.index("(lean)") < output.index("(copilot-sub-agents)"), output
assert "resolution order" in output, output
assert "Ties are broken by preset id" in output, output
def test_list_ties_broken_by_id(self, temp_dir, project_dir):
"""Equal priority ties are broken alphabetically by preset id."""
self._install(temp_dir, project_dir, "zebra", priority=10)
self._install(temp_dir, project_dir, "alpha", priority=10)
result = self._invoke(project_dir, ["preset", "list"])
assert result.exit_code == 0, result.output
output = strip_ansi(result.output)
assert output.index("(alpha)") < output.index("(zebra)"), output
class TestConstitutionSyncPreset:
"""The bundled opt-in ``constitution-sync`` preset re-adds materialization.