From f065e27478dffe9020ea58aede21f916677f3775 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:52:14 +0100 Subject: [PATCH] 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> --- tests/integrations/test_cli.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index 377232f23..6899a5105 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -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