test: cover preset constitution seeding through init CLI (#3297)

* Fix preset-constitution-not-installed: use PresetResolver in constitution setup

Apply the remediation from the bug assessment on issue #3272.

Changes:
1. Modify ensure_constitution_from_template (init.py) to resolve the
   constitution-template through the preset priority stack via
   PresetResolver, instead of hardcoding the core template path. This
   ensures a preset's replacement constitution-template is used when
   seeding .specify/memory/constitution.md.

2. Reorder init flow: move ensure_constitution_from_template to after
   the preset installation block so that 'specify init --preset' seeds
   the memory file from the already-resolved template stack, not from
   the generic template that existed before the preset arrived.

3. Add _maybe_reseed_constitution to PresetManager (presets/__init__.py):
   a post-install hook that re-seeds .specify/memory/constitution.md
   from the preset's constitution-template during 'specify preset add'
   on an existing project, but only when the memory file still contains
   generic placeholder tokens ([PROJECT_NAME] or [PRINCIPLE_1_NAME]).
   Legitimately authored constitutions (no placeholder tokens) are never
   overwritten.

4. Add regression tests covering both code paths (TestConstitutionReseedOnPresetInstall
   and TestEnsureConstitutionFromTemplate in tests/test_presets.py).

Refs #3272

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden preset constitution resolution

Use manifest-aware composed content, atomic safe writes, and conservative generic-template matching for constitution seeding and re-seeding.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb

* Limit preset CLI change to regression test

Remove accidental whole-file Ruff formatting introduced during conflict resolution so the PR contains only the intended end-to-end test.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb

* Make preset init test depend on init ordering

Disable preset-install lifecycle seeding in the regression test so it fails unless init materializes the constitution after registering the preset.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Ben Buttigieg <70525+BenBtg@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2026-07-15 17:52:14 +01:00
committed by GitHub
parent 2fb18c73cb
commit f065e27478

View File

@@ -224,6 +224,66 @@ class TestInitIntegrationFlag:
assert "Continuing without the optional preset" in normalized
assert "Project ready" in normalized
def test_init_with_local_preset_seeds_manifest_constitution(
self, tmp_path, monkeypatch
):
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.presets import PresetManager
monkeypatch.setattr(
PresetManager,
"_seed_constitution_from_preset",
lambda *_args, **_kwargs: None,
)
preset_dir = tmp_path / "constitution-preset"
(preset_dir / "organization").mkdir(parents=True)
preset_content = "# Ratified Organization Constitution\n"
(preset_dir / "organization" / "ratified.md").write_text(preset_content)
(preset_dir / "preset.yml").write_text(
yaml.safe_dump({
"schema_version": "1.0",
"preset": {
"id": "constitution-preset",
"name": "Constitution Preset",
"version": "1.0.0",
"description": "Provides a ratified constitution",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"templates": [{
"type": "template",
"name": "constitution-template",
"file": "organization/ratified.md",
"strategy": "replace",
}]
},
})
)
project = tmp_path / "init-with-preset"
result = CliRunner().invoke(
app,
[
"init",
str(project),
"--integration",
"copilot",
"--script",
"sh",
"--ignore-agent-tools",
"--preset",
str(preset_dir),
],
catch_exceptions=False,
)
assert result.exit_code == 0, result.output
assert (
project / ".specify" / "memory" / "constitution.md"
).read_text() == preset_content
def test_integration_claude_here_preserves_preexisting_commands(self, tmp_path):
from typer.testing import CliRunner
from specify_cli import app