Skip to content

Commit 9772946

Browse files
committed
fix: backfill agent-context extension for integrations
1 parent 7106858 commit 9772946

2 files changed

Lines changed: 108 additions & 8 deletions

File tree

src/specify_cli/integrations/_helpers.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,42 @@ def _remove_integration_json(project_root: Path) -> None:
144144
path.unlink()
145145

146146

147+
def _ensure_agent_context_extension(project_root: Path) -> None:
148+
"""Install the bundled agent-context extension if its config will be used."""
149+
from .. import (
150+
_AGENT_CTX_EXT_CONFIG,
151+
_load_agent_context_config,
152+
_save_agent_context_config,
153+
)
154+
from .._assets import _locate_bundled_extension
155+
from ..extensions import ExtensionManager
156+
157+
ext_mgr = ExtensionManager(project_root)
158+
ext_dir = project_root / ".specify" / "extensions" / "agent-context"
159+
if (
160+
ext_mgr.registry.is_installed("agent-context") and
161+
(ext_dir / "extension.yml").is_file()
162+
):
163+
return
164+
165+
existing_cfg = None
166+
if (project_root / _AGENT_CTX_EXT_CONFIG).exists():
167+
existing_cfg = _load_agent_context_config(project_root)
168+
169+
bundled_ac = _locate_bundled_extension("agent-context")
170+
if bundled_ac is None:
171+
raise ValueError("bundled agent-context extension not found")
172+
173+
ext_mgr.install_from_directory(
174+
bundled_ac,
175+
_get_speckit_version(),
176+
force=ext_mgr.registry.is_installed("agent-context"),
177+
)
178+
179+
if existing_cfg is not None:
180+
_save_agent_context_config(project_root, existing_cfg)
181+
182+
147183
# ---------------------------------------------------------------------------
148184
# Error sentinels
149185
# ---------------------------------------------------------------------------
@@ -308,20 +344,15 @@ def _update_init_options_for_integration(
308344
# Update the agent-context extension config BEFORE init-options.json
309345
# so a failure here doesn't leave init-options partially updated.
310346
ext_cfg_path = project_root / _AGENT_CTX_EXT_CONFIG
347+
if integration.context_file:
348+
_ensure_agent_context_extension(project_root)
349+
311350
if ext_cfg_path.exists():
312351
_update_agent_context_config_file(
313352
project_root,
314353
integration.context_file,
315354
preserve_markers=True,
316355
)
317-
elif integration.context_file:
318-
# Extension config doesn't exist yet (extension not installed).
319-
# Write defaults so scripts have something to read.
320-
_update_agent_context_config_file(
321-
project_root,
322-
integration.context_file,
323-
preserve_markers=False,
324-
)
325356

326357
save_init_options(project_root, opts)
327358

tests/integrations/test_integration_subcommand.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import os
5+
import shutil
56

67
from typer.testing import CliRunner
78

@@ -48,6 +49,34 @@ def _write_invalid_manifest(project, key):
4849
return manifest
4950

5051

52+
def _remove_agent_context_extension(project):
53+
ext_dir = project / ".specify" / "extensions" / "agent-context"
54+
if ext_dir.exists():
55+
shutil.rmtree(ext_dir)
56+
57+
registry = project / ".specify" / "extensions" / ".registry"
58+
if registry.exists():
59+
data = json.loads(registry.read_text(encoding="utf-8"))
60+
data.get("extensions", {}).pop("agent-context", None)
61+
registry.write_text(json.dumps(data), encoding="utf-8")
62+
63+
64+
def _assert_agent_context_installed(project, context_file):
65+
ext_dir = project / ".specify" / "extensions" / "agent-context"
66+
assert (ext_dir / "extension.yml").is_file()
67+
assert (ext_dir / "commands" / "speckit.agent-context.update.md").is_file()
68+
assert (ext_dir / "scripts" / "bash" / "update-agent-context.sh").is_file()
69+
70+
registry = project / ".specify" / "extensions" / ".registry"
71+
data = json.loads(registry.read_text(encoding="utf-8"))
72+
assert "agent-context" in data["extensions"]
73+
74+
from specify_cli import _load_agent_context_config
75+
76+
cfg = _load_agent_context_config(project)
77+
assert cfg["context_file"] == context_file
78+
79+
5180
def _integration_list_row_cells(output: str, key: str) -> list[str]:
5281
plain = strip_ansi(output)
5382
row = next(line for line in plain.splitlines() if line.startswith(f"│ {key}"))
@@ -191,6 +220,21 @@ def test_install_already_installed_non_default_guides_use(self, tmp_path):
191220
assert "specify integration upgrade codex" in normalized
192221
assert "specify integration uninstall codex" not in normalized
193222

223+
def test_install_backfills_agent_context_extension_when_missing(self, tmp_path):
224+
project = _init_project(tmp_path, "copilot")
225+
_remove_agent_context_extension(project)
226+
(project / ".specify" / "integration.json").unlink()
227+
(project / ".specify" / "integrations" / "copilot.manifest.json").unlink()
228+
shutil.rmtree(project / ".github")
229+
230+
result = _run_in_project(project, [
231+
"integration", "install", "copilot",
232+
"--script", "sh",
233+
])
234+
235+
assert result.exit_code == 0, result.output
236+
_assert_agent_context_installed(project, ".github/copilot-instructions.md")
237+
194238
def test_install_different_when_one_exists(self, tmp_path):
195239
project = _init_project(tmp_path, "copilot")
196240
old_cwd = os.getcwd()
@@ -1155,6 +1199,18 @@ def test_switch_from_nothing(self, tmp_path):
11551199
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
11561200
assert data["integration"] == "claude"
11571201

1202+
def test_switch_backfills_agent_context_extension_when_missing(self, tmp_path):
1203+
project = _init_project(tmp_path, "claude")
1204+
_remove_agent_context_extension(project)
1205+
1206+
result = _run_in_project(project, [
1207+
"integration", "switch", "copilot",
1208+
"--script", "sh",
1209+
])
1210+
1211+
assert result.exit_code == 0, result.output
1212+
_assert_agent_context_installed(project, ".github/copilot-instructions.md")
1213+
11581214
def test_failed_switch_keeps_fallback_metadata_consistent(self, tmp_path):
11591215
project = _init_project(tmp_path, "claude")
11601216
old_cwd = os.getcwd()
@@ -1308,6 +1364,19 @@ def test_upgrade_default_refreshes_shared_script_refs_for_option_separator_chang
13081364
assert "/speckit.specify" not in managed_content
13091365
assert customized_script.read_text(encoding="utf-8") == customized_before
13101366

1367+
def test_upgrade_backfills_agent_context_extension_when_missing(self, tmp_path):
1368+
project = _init_project(tmp_path, "copilot")
1369+
_remove_agent_context_extension(project)
1370+
1371+
result = _run_in_project(project, [
1372+
"integration", "upgrade", "copilot",
1373+
"--script", "sh",
1374+
"--force",
1375+
])
1376+
1377+
assert result.exit_code == 0, result.output
1378+
_assert_agent_context_installed(project, ".github/copilot-instructions.md")
1379+
13111380
def test_upgrade_non_default_keeps_default_template_invocations(self, tmp_path):
13121381
project = _init_project(tmp_path, "gemini")
13131382
template = project / ".specify" / "templates" / "plan-template.md"

0 commit comments

Comments
 (0)