mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
`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:
co-authored by
Copilot App
parent
e79fa25f3f
commit
229022943c
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user