mirror of
https://github.com/github/spec-kit.git
synced 2026-07-06 14:01:01 +08:00
fix: write Codex dev skills as files (#2988)
* fix: write Codex dev skills as files * fix: route codex dev symlink policy through metadata * fix: replace codex dev symlinks on refresh * fix: migrate codex dev skill symlinks * fix: avoid inactive shared skill dev symlinks * fix: preserve unrelated dev skill symlinks
This commit is contained in:
@@ -360,6 +360,12 @@ class TestAgentConfigConsistency:
|
||||
"expected '-' (propagated from SkillsIntegration.invoke_separator)"
|
||||
)
|
||||
|
||||
def test_codex_dev_no_symlink_policy_in_agent_config(self):
|
||||
"""Codex dev installs must expose the no-symlink policy as metadata."""
|
||||
cfg = CommandRegistrar.AGENT_CONFIGS
|
||||
|
||||
assert cfg["codex"].get("dev_no_symlink") is True
|
||||
|
||||
def test_skills_agent_command_token_resolves_with_hyphen(self, tmp_path):
|
||||
"""__SPECKIT_COMMAND_*__ tokens in extension commands resolve to /speckit-<cmd>
|
||||
when registered for a skills-based agent (e.g. claude).
|
||||
|
||||
@@ -573,6 +573,84 @@ class TestExtensionSkillRegistration:
|
||||
assert "speckit-test-ext-hello" in written
|
||||
assert "Run this updated hello." in skill_file.read_text(encoding="utf-8")
|
||||
|
||||
def test_codex_dev_skill_registration_replaces_existing_dev_symlink(
|
||||
self, project_dir, extension_dir, temp_dir
|
||||
):
|
||||
"""Codex dev skill registration should migrate prior dev symlinks to files."""
|
||||
if not _can_create_symlink(temp_dir):
|
||||
pytest.skip("Current platform/user cannot create symlinks")
|
||||
|
||||
_create_init_options(project_dir, ai="codex", ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai="codex")
|
||||
manager = ExtensionManager(project_dir)
|
||||
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||
|
||||
skill_file = skills_dir / "speckit-test-ext-hello" / "SKILL.md"
|
||||
skill_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache_file = (
|
||||
extension_dir
|
||||
/ ".specify-dev"
|
||||
/ "extension-skills"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
cache_file.parent.mkdir(parents=True)
|
||||
cache_file.write_text("old linked content", encoding="utf-8")
|
||||
os.symlink(os.path.relpath(cache_file, skill_file.parent), skill_file)
|
||||
|
||||
written = manager._register_extension_skills(
|
||||
manifest,
|
||||
extension_dir,
|
||||
link_outputs=True,
|
||||
)
|
||||
|
||||
assert "speckit-test-ext-hello" in written
|
||||
assert skill_file.exists()
|
||||
assert not skill_file.is_symlink()
|
||||
assert "Run this to say hello." in skill_file.read_text(encoding="utf-8")
|
||||
assert cache_file.read_text(encoding="utf-8") == "old linked content"
|
||||
|
||||
def test_codex_dev_skill_registration_preserves_unrelated_symlink(
|
||||
self, project_dir, extension_dir, temp_dir
|
||||
):
|
||||
"""Codex dev registration should not overwrite user-owned symlinks."""
|
||||
if not _can_create_symlink(temp_dir):
|
||||
pytest.skip("Current platform/user cannot create symlinks")
|
||||
|
||||
_create_init_options(project_dir, ai="codex", ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai="codex")
|
||||
manager = ExtensionManager(project_dir)
|
||||
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||
|
||||
skill_file = skills_dir / "speckit-test-ext-hello" / "SKILL.md"
|
||||
skill_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
unrelated_cache_file = (
|
||||
temp_dir
|
||||
/ "other-extension"
|
||||
/ ".specify-dev"
|
||||
/ "extension-skills"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
unrelated_cache_file.parent.mkdir(parents=True)
|
||||
unrelated_cache_file.write_text("user-owned linked content", encoding="utf-8")
|
||||
os.symlink(
|
||||
os.path.relpath(unrelated_cache_file, skill_file.parent), skill_file
|
||||
)
|
||||
|
||||
written = manager._register_extension_skills(
|
||||
manifest,
|
||||
extension_dir,
|
||||
link_outputs=True,
|
||||
)
|
||||
|
||||
assert "speckit-test-ext-hello" not in written
|
||||
assert skill_file.is_symlink()
|
||||
assert skill_file.resolve(strict=True) == unrelated_cache_file.resolve()
|
||||
assert unrelated_cache_file.read_text(encoding="utf-8") == (
|
||||
"user-owned linked content"
|
||||
)
|
||||
|
||||
def test_dev_skill_registration_falls_back_to_copy_when_symlink_fails(
|
||||
self, skills_project, extension_dir, monkeypatch
|
||||
):
|
||||
|
||||
@@ -2248,6 +2248,50 @@ Run {SCRIPT}
|
||||
assert target.is_file()
|
||||
assert "Extension: test-ext" in cmd_file.read_text(encoding="utf-8")
|
||||
|
||||
def test_dev_register_commands_replaces_codex_dev_symlink(
|
||||
self, extension_dir, project_dir, temp_dir
|
||||
):
|
||||
"""Codex dev registration should replace prior symlinks with real files."""
|
||||
if not can_create_symlink(temp_dir):
|
||||
pytest.skip("Current platform/user cannot create symlinks")
|
||||
|
||||
skill_file = (
|
||||
project_dir
|
||||
/ ".agents"
|
||||
/ "skills"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
skill_file.parent.mkdir(parents=True)
|
||||
cache_file = (
|
||||
extension_dir
|
||||
/ ".specify-dev"
|
||||
/ "agent-commands"
|
||||
/ "codex"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
cache_file.parent.mkdir(parents=True)
|
||||
cache_file.write_text("old linked content", encoding="utf-8")
|
||||
os.symlink(os.path.relpath(cache_file, skill_file.parent), skill_file)
|
||||
|
||||
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||
registrar = CommandRegistrar()
|
||||
registrar.register_commands_for_agent(
|
||||
"codex",
|
||||
manifest,
|
||||
extension_dir,
|
||||
project_dir,
|
||||
link_outputs=True,
|
||||
)
|
||||
|
||||
assert skill_file.exists()
|
||||
assert not skill_file.is_symlink()
|
||||
assert "name: speckit-test-ext-hello" in skill_file.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert cache_file.read_text(encoding="utf-8") == "old linked content"
|
||||
|
||||
def test_dev_register_commands_falls_back_to_copy_when_symlink_fails(
|
||||
self, extension_dir, project_dir, monkeypatch
|
||||
):
|
||||
@@ -4874,6 +4918,93 @@ class TestExtensionAddCLI:
|
||||
else:
|
||||
assert not agent_file.is_symlink()
|
||||
|
||||
def test_add_dev_writes_codex_skills_as_files(self, extension_dir, project_dir):
|
||||
"""Codex dev skills should be written as files so Codex can load them."""
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
from specify_cli import app
|
||||
|
||||
init_options = project_dir / ".specify" / "init-options.json"
|
||||
init_options.write_text(
|
||||
json.dumps({"ai": "codex", "ai_skills": True}), encoding="utf-8"
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.object(Path, "cwd", return_value=project_dir):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["extension", "add", str(extension_dir), "--dev"],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
skill_file = (
|
||||
project_dir
|
||||
/ ".agents"
|
||||
/ "skills"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
assert skill_file.exists()
|
||||
assert not skill_file.is_symlink()
|
||||
|
||||
content = skill_file.read_text(encoding="utf-8")
|
||||
assert "name: speckit-test-ext-hello" in content
|
||||
assert "metadata:" in content
|
||||
assert "source: test-ext:commands/hello.md" in content
|
||||
|
||||
def test_add_dev_replaces_existing_codex_skill_symlink(
|
||||
self, extension_dir, project_dir, temp_dir
|
||||
):
|
||||
"""Codex dev installs should migrate expected dev symlinks to files."""
|
||||
if not can_create_symlink(temp_dir):
|
||||
pytest.skip("Current platform/user cannot create symlinks")
|
||||
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
from specify_cli import app
|
||||
|
||||
init_options = project_dir / ".specify" / "init-options.json"
|
||||
init_options.write_text(
|
||||
json.dumps({"ai": "codex", "ai_skills": True}), encoding="utf-8"
|
||||
)
|
||||
|
||||
skill_file = (
|
||||
project_dir
|
||||
/ ".agents"
|
||||
/ "skills"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
skill_file.parent.mkdir(parents=True)
|
||||
cache_file = (
|
||||
extension_dir
|
||||
/ ".specify-dev"
|
||||
/ "extension-skills"
|
||||
/ "speckit-test-ext-hello"
|
||||
/ "SKILL.md"
|
||||
)
|
||||
cache_file.parent.mkdir(parents=True)
|
||||
cache_file.write_text("old linked content", encoding="utf-8")
|
||||
os.symlink(os.path.relpath(cache_file, skill_file.parent), skill_file)
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.object(Path, "cwd", return_value=project_dir):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["extension", "add", str(extension_dir), "--dev"],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert skill_file.exists()
|
||||
assert not skill_file.is_symlink()
|
||||
content = skill_file.read_text(encoding="utf-8")
|
||||
assert "name: speckit-test-ext-hello" in content
|
||||
assert "source: test-ext:commands/hello.md" in content
|
||||
assert cache_file.read_text(encoding="utf-8") == "old linked content"
|
||||
|
||||
def test_add_dev_falls_back_to_copy_when_windows_symlinks_unavailable(
|
||||
self, extension_dir, project_dir, monkeypatch
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user