feat(integration): update Kimi integration for Kimi Code CLI (#2979)

* feat(integration): update Kimi integration for Kimi Code CLI

Update the Kimi integration to target the new Kimi Code CLI
(MoonshotAI/kimi-code) layout:

- Change skills directory from .kimi/skills/ to .kimi-code/skills/
- Change context file from KIMI.md to AGENTS.md
- Extend --migrate-legacy to move old .kimi/skills/ installs and
  migrate KIMI.md user content to AGENTS.md
- Clean up leftover legacy .kimi/skills/ directories on teardown
- Update devcontainer installer to @moonshot-ai/kimi-code
- Update docs and tests

Relates to #1532

* fix(integration): align Kimi dispatch and harden legacy migration

- Override build_command_invocation to emit /skill:speckit-<stem>
  so dispatched commands match Kimi Code CLI's native slash syntax.
- Skip symlinked .kimi/skills directories during legacy migration
  and teardown to avoid operating on files outside the project.
- Remove kimi from the multi-install-safe integrations table.
- Add tests for command invocation and symlink safety.

* fix(integration): resolve custom context markers in Kimi legacy migration

Use IntegrationBase._resolve_context_markers() when migrating legacy
KIMI.md content so that projects with customized context_markers in
.specify/extensions/agent-context/agent-context-config.yml have the
managed section stripped with the correct markers instead of the
hard-coded defaults.

Adds a test verifying custom markers are respected during
--migrate-legacy.

* fix(integration): harden Kimi legacy migration against symlinked paths

* fix(kimi): guard symlinked SKILL.md during migration and teardown

* docs(kimi): mention KIMI.md→AGENTS.md migration in --migrate-legacy help

The --migrate-legacy help text listed only the skills directory move and
dotted→hyphenated renaming, but the flag also migrates KIMI.md user content
into AGENTS.md. Align the help with the actual behavior, docs, and tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(kimi): validate legacy migration destination; clarify docstrings

Address Copilot review feedback on PR #2979:

- setup(): gate skills migration on _is_safe_legacy_dir(new_skills_dir)
  as well as the source. base setup() already rejects a destination that
  escapes the project root, but an in-tree symlinked .kimi-code/skills
  (e.g. -> .) could still misdirect the move; this gives the destination
  the same symlink-component protection as the source.
- _migrate_legacy_kimi_dotted_skills: rewrite docstring as a compatibility
  shim describing same-path delegation to _migrate_legacy_kimi_skills_dir.
- test_presets: clarify that the dotted-skill test exercises legacy naming
  under the current .kimi-code/ base, not the legacy .kimi/ location.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(kimi): harden legacy KIMI.md→AGENTS.md context migration

- Skip context-file migration when the agent-context extension is
  disabled, matching upsert/remove_context_section opt-out behavior so
  an opted-out project's KIMI.md/AGENTS.md are left untouched.
- Safely skip (instead of raising) on filesystem edge cases: unreadable
  or non-UTF-8 KIMI.md, and AGENTS.md existing as a non-file/unwritable.
- Refuse to migrate a corrupted managed section (single marker, or end
  before start) so a partial managed block is never copied into
  AGENTS.md; KIMI.md is preserved for manual repair.

Add regression tests for all three cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Approve fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* chore(kimi): revert CHANGELOG.md edit (auto-generated)

The CHANGELOG is generated from merged PR titles, so a hand-written entry
is redundant; it was also placed under the already-released 0.10.2 section,
which would make those release notes historically inaccurate. Revert to
match main per maintainer feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(kimi): skip symlink-safety tests when symlinks are unavailable

The Kimi legacy-migration safety tests create symlinks to assert that
migration/teardown never follow them out of the project. Symlink creation
fails on Windows without the create-symlink privilege and in some restricted
CI sandboxes, so these tests errored during setup instead of skipping.

Wrap every symlink_to() call in a shared _symlink_or_skip() helper that
pytest.skip()s on OSError/NotImplementedError, matching the guard pattern
already used by one of these tests. Verified on Windows: the 6 symlink tests
now skip cleanly (51 passed, 6 skipped) instead of erroring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(kimi): reject symlinked skills destination before install

Add a destination symlink pre-check in KimiIntegration.setup() before
super().setup() writes any SKILL.md. The base class only rejects a
destination that escapes project_root after resolve(), so an in-tree
symlinked .kimi-code/.kimi-code/skills (e.g. `-> .`) would still
misdirect writes into an unintended in-tree location (./skills/).

Extract the symlink-component walk into a shared _has_symlinked_component()
helper and reuse it from _is_safe_legacy_dir(). Add a regression test.

Also clarify that --migrate-legacy only migrates KIMI.md -> AGENTS.md when
the agent-context extension is enabled, in the CLI help text and the
integration docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refactor formatting and simplify logic in Kimi integration

* fix(kimi): reject symlinked target dir during legacy skills migration

When the migration destination already exists, guard against a symlinked
(or non-directory) target_dir before comparing SKILL.md bytes, so the
comparison never follows a link outside the project root. Also skip a
missing/non-file target SKILL.md explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
meymchen
2026-06-25 04:22:08 +08:00
committed by GitHub
parent e12beda5f9
commit dc840f07d0
8 changed files with 877 additions and 66 deletions

View File

@@ -1,18 +1,42 @@
"""Tests for KimiIntegration — skills integration with legacy migration."""
from pathlib import Path
import pytest
from specify_cli.integrations import get_integration
from specify_cli.integrations.kimi import _migrate_legacy_kimi_dotted_skills
from specify_cli.integrations.kimi import (
_migrate_legacy_kimi_context_file,
_migrate_legacy_kimi_dotted_skills,
_migrate_legacy_kimi_skills_dir,
)
from specify_cli.integrations.manifest import IntegrationManifest
from .test_integration_base_skills import SkillsIntegrationTests
def _symlink_or_skip(
link: Path, target: Path, *, target_is_directory: bool = False
) -> None:
"""Create *link* pointing at *target*, skipping the test if unsupported.
Symlink creation fails on Windows without the create-symlink privilege and
in some restricted CI sandboxes. The symlink-safety tests below assert
behavior that only matters when symlinks exist, so skip (rather than error)
when the platform cannot create them.
"""
try:
link.symlink_to(target, target_is_directory=target_is_directory)
except (OSError, NotImplementedError) as exc:
pytest.skip(f"symlinks unavailable: {exc}")
class TestKimiIntegration(SkillsIntegrationTests):
KEY = "kimi"
FOLDER = ".kimi/"
FOLDER = ".kimi-code/"
COMMANDS_SUBDIR = "skills"
REGISTRAR_DIR = ".kimi/skills"
CONTEXT_FILE = "KIMI.md"
REGISTRAR_DIR = ".kimi-code/skills"
CONTEXT_FILE = "AGENTS.md"
class TestKimiOptions:
@@ -103,12 +127,13 @@ class TestKimiLegacyMigration:
assert migrated == 0
assert removed == 0
def test_setup_with_migrate_legacy_option(self, tmp_path):
"""KimiIntegration.setup() with --migrate-legacy migrates dotted dirs."""
def test_setup_migrate_legacy_moves_old_skills_dir(self, tmp_path):
"""--migrate-legacy moves hyphenated skills from .kimi/skills to .kimi-code/skills."""
i = get_integration("kimi")
skills_dir = tmp_path / ".kimi" / "skills"
legacy = skills_dir / "speckit.oldcmd"
old_skills_dir = tmp_path / ".kimi" / "skills"
new_skills_dir = tmp_path / ".kimi-code" / "skills"
legacy = old_skills_dir / "speckit-oldcmd"
legacy.mkdir(parents=True)
(legacy / "SKILL.md").write_text("# Legacy\n")
@@ -116,9 +141,428 @@ class TestKimiLegacyMigration:
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
assert not legacy.exists()
assert (skills_dir / "speckit-oldcmd" / "SKILL.md").exists()
assert not old_skills_dir.exists()
assert (new_skills_dir / "speckit-oldcmd" / "SKILL.md").exists()
# New skills from templates should also exist
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()
assert (new_skills_dir / "speckit-specify" / "SKILL.md").exists()
def test_setup_with_migrate_legacy_option(self, tmp_path):
"""KimiIntegration.setup() with --migrate-legacy migrates dotted dirs."""
i = get_integration("kimi")
old_skills_dir = tmp_path / ".kimi" / "skills"
new_skills_dir = tmp_path / ".kimi-code" / "skills"
legacy = old_skills_dir / "speckit.oldcmd"
legacy.mkdir(parents=True)
(legacy / "SKILL.md").write_text("# Legacy\n")
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
assert not legacy.exists()
assert (new_skills_dir / "speckit-oldcmd" / "SKILL.md").exists()
# New skills from templates should also exist
assert (new_skills_dir / "speckit-specify" / "SKILL.md").exists()
class TestKimiContextFileMigration:
"""KIMI.md → AGENTS.md migration under --migrate-legacy."""
def test_setup_migrate_legacy_moves_kimi_md_user_content(self, tmp_path):
i = get_integration("kimi")
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text(
"# Project context\n\n"
"<!-- SPECKIT START -->\n"
"old managed section\n"
"<!-- SPECKIT END -->\n\n"
"Keep this user note.\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
agents_md = tmp_path / "AGENTS.md"
assert agents_md.exists()
content = agents_md.read_text(encoding="utf-8")
assert "Keep this user note." in content
assert "old managed section" not in content
assert "<!-- SPECKIT START -->" in content
assert not kimi_md.exists()
def test_setup_migrate_legacy_removes_empty_kimi_md(self, tmp_path):
i = get_integration("kimi")
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text(
"<!-- SPECKIT START -->\n"
"only managed section\n"
"<!-- SPECKIT END -->\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
assert (tmp_path / "AGENTS.md").exists()
assert not kimi_md.exists()
def test_setup_migrate_legacy_appends_to_existing_agents_md(self, tmp_path):
i = get_integration("kimi")
agents_md = tmp_path / "AGENTS.md"
agents_md.write_text("# Existing AGENTS.md\n\nExisting note.\n")
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text("# Kimi context\n\nKimi-specific note.\n")
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
content = agents_md.read_text(encoding="utf-8")
assert "Existing note." in content
assert "Kimi-specific note." in content
assert "<!-- SPECKIT START -->" in content
assert not kimi_md.exists()
def test_setup_migrate_legacy_uses_custom_context_markers(self, tmp_path):
"""Migration respects context_markers from agent-context extension config."""
i = get_integration("kimi")
config_dir = tmp_path / ".specify" / "extensions" / "agent-context"
config_dir.mkdir(parents=True)
(config_dir / "agent-context-config.yml").write_text(
"context_file: AGENTS.md\n"
"context_markers:\n"
" start: '<!-- CUSTOM START -->'\n"
" end: '<!-- CUSTOM END -->'\n"
)
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text(
"# Project context\n\n"
"<!-- CUSTOM START -->\n"
"old managed section\n"
"<!-- CUSTOM END -->\n\n"
"Keep this user note.\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
agents_md = tmp_path / "AGENTS.md"
assert agents_md.exists()
content = agents_md.read_text(encoding="utf-8")
assert "Keep this user note." in content
assert "old managed section" not in content
assert "<!-- CUSTOM START -->" in content
assert "<!-- CUSTOM END -->" in content
assert "<!-- SPECKIT START -->" not in content
assert not kimi_md.exists()
def test_setup_migrate_legacy_skipped_when_agent_context_disabled(
self, tmp_path
):
"""A disabled agent-context extension opts out of KIMI.md migration."""
i = get_integration("kimi")
registry = tmp_path / ".specify" / "extensions" / ".registry"
registry.parent.mkdir(parents=True)
registry.write_text('{"extensions": {"agent-context": {"enabled": false}}}')
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text("# Kimi context\n\nKeep this user note.\n")
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
# Opted-out project: KIMI.md is left untouched and AGENTS.md is not
# created/modified by the migration.
assert kimi_md.is_file()
assert kimi_md.read_text() == "# Kimi context\n\nKeep this user note.\n"
assert not (tmp_path / "AGENTS.md").exists()
def test_context_migration_skips_corrupted_single_marker(self, tmp_path):
"""A KIMI.md with only a start marker is left untouched (no leak)."""
project = tmp_path
kimi_md = project / "KIMI.md"
kimi_md.write_text(
"# Notes\n\n"
"<!-- SPECKIT START -->\n"
"dangling managed content\n"
)
result = _migrate_legacy_kimi_context_file(project)
assert result is False
# KIMI.md untouched; managed block never copied into AGENTS.md.
assert kimi_md.is_file()
assert "dangling managed content" in kimi_md.read_text()
assert not (project / "AGENTS.md").exists()
def test_context_migration_skips_unreadable_kimi_md(self, tmp_path):
"""Non-UTF-8 KIMI.md is skipped instead of raising during setup."""
project = tmp_path
kimi_md = project / "KIMI.md"
kimi_md.write_bytes(b"\xff\xfe invalid utf-8 \xa6\n")
result = _migrate_legacy_kimi_context_file(project)
assert result is False
assert kimi_md.is_file()
assert not (project / "AGENTS.md").exists()
def test_context_migration_skips_when_agents_md_is_directory(self, tmp_path):
"""An AGENTS.md that exists as a directory is skipped, not written to."""
project = tmp_path
(project / "AGENTS.md").mkdir()
kimi_md = project / "KIMI.md"
kimi_md.write_text("# Notes\n\nKeep this.\n")
result = _migrate_legacy_kimi_context_file(project)
assert result is False
# KIMI.md is preserved and the directory is untouched.
assert kimi_md.is_file()
assert (project / "AGENTS.md").is_dir()
class TestKimiTeardownLegacyCleanup:
"""teardown() removes leftover legacy .kimi/skills/ directories."""
def test_teardown_removes_legacy_speckit_skills(self, tmp_path):
i = get_integration("kimi")
legacy_skill = tmp_path / ".kimi" / "skills" / "speckit-plan" / "SKILL.md"
legacy_skill.parent.mkdir(parents=True)
legacy_skill.write_text(
"---\n"
"name: \"speckit-plan\"\n"
"description: \"Plan workflow\"\n"
"metadata:\n"
" author: \"github-spec-kit\"\n"
" source: \"templates/commands/plan.md\"\n"
"---\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.teardown(tmp_path, m)
assert not legacy_skill.exists()
assert not (tmp_path / ".kimi" / "skills").exists()
def test_teardown_preserves_user_skills_in_legacy_dir(self, tmp_path):
i = get_integration("kimi")
user_skill = tmp_path / ".kimi" / "skills" / "my-custom" / "SKILL.md"
user_skill.parent.mkdir(parents=True)
user_skill.write_text("# My custom skill\n")
m = IntegrationManifest("kimi", tmp_path)
i.teardown(tmp_path, m)
assert user_skill.exists()
class TestKimiCommandInvocation:
"""Kimi dispatch must use the native ``/skill:`` slash command."""
def test_build_command_invocation_uses_skill_prefix(self):
i = get_integration("kimi")
assert i.build_command_invocation("specify") == "/skill:speckit-specify"
assert i.build_command_invocation("speckit.plan") == "/skill:speckit-plan"
def test_build_command_invocation_dotted_extension(self):
i = get_integration("kimi")
assert (
i.build_command_invocation("speckit.git.commit")
== "/skill:speckit-git-commit"
)
def test_build_command_invocation_appends_args(self):
i = get_integration("kimi")
assert (
i.build_command_invocation("specify", "my feature")
== "/skill:speckit-specify my feature"
)
class TestKimiLegacySymlinkSafety:
"""Legacy migration/cleanup must not follow symlinks out of the project."""
def test_migrate_skips_symlinked_legacy_skills_dir(self, tmp_path):
# An attacker-controlled directory outside the project root. Use a
# non-template skill name so a successful migration would be visible
# (the bundled templates never create "speckit-evillegacy").
outside = tmp_path / "outside"
(outside / "speckit-evillegacy").mkdir(parents=True)
(outside / "speckit-evillegacy" / "SKILL.md").write_text("# evil\n")
project = tmp_path / "project"
(project / ".kimi").mkdir(parents=True)
# .kimi/skills is a symlink to the outside directory.
_symlink_or_skip(
project / ".kimi" / "skills", outside, target_is_directory=True
)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.setup(project, m, parsed_options={"migrate_legacy": True})
# Outside content must be untouched (not moved into .kimi-code).
assert (outside / "speckit-evillegacy" / "SKILL.md").exists()
assert not (
project / ".kimi-code" / "skills" / "speckit-evillegacy"
).exists()
def test_teardown_skips_symlinked_legacy_skills_dir(self, tmp_path):
outside = tmp_path / "outside"
outside.mkdir()
keep = outside / "keep.txt"
keep.write_text("important\n")
project = tmp_path / "project"
(project / ".kimi").mkdir(parents=True)
_symlink_or_skip(
project / ".kimi" / "skills", outside, target_is_directory=True
)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.teardown(project, m)
# The symlink target and its contents must survive teardown.
assert keep.exists()
def test_migrate_skips_symlinked_legacy_parent_dir(self, tmp_path):
# `.kimi` is itself a symlink to the project root, so `.kimi/skills`
# resolves to `./skills` — an unrelated in-tree directory. Even though
# the resolved path stays inside the project, migration must not
# operate on it because a path component is a symlink.
project = tmp_path / "project"
unrelated = project / "skills" / "speckit-evillegacy"
unrelated.mkdir(parents=True)
(unrelated / "SKILL.md").write_text("# unrelated\n")
# .kimi -> project root, so .kimi/skills == ./skills.
_symlink_or_skip(project / ".kimi", project, target_is_directory=True)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.setup(project, m, parsed_options={"migrate_legacy": True})
# The unrelated ./skills content must be untouched.
assert (unrelated / "SKILL.md").exists()
assert not (
project / ".kimi-code" / "skills" / "speckit-evillegacy"
).exists()
def test_teardown_skips_symlinked_legacy_parent_dir(self, tmp_path):
project = tmp_path / "project"
project.mkdir()
# Looks Speckit-generated, so only the symlink check protects it.
unrelated = project / "skills" / "speckit-evillegacy"
unrelated.mkdir(parents=True)
(unrelated / "SKILL.md").write_text(
"---\nmetadata:\n author: github-spec-kit\n---\n# x\n"
)
_symlink_or_skip(project / ".kimi", project, target_is_directory=True)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.teardown(project, m)
# The unrelated ./skills content must survive teardown.
assert (unrelated / "SKILL.md").exists()
def test_setup_rejects_symlinked_destination_before_writing(self, tmp_path):
# `.kimi-code` is a symlink to the project root, so the skills
# destination `.kimi-code/skills` resolves to `./skills` — an
# unintended in-tree location. base setup() only rejects a
# destination that escapes the project root, so without the
# pre-check it would write SKILL.md files into `./skills`. setup()
# must refuse before any write occurs.
project = tmp_path / "project"
project.mkdir()
_symlink_or_skip(project / ".kimi-code", project, target_is_directory=True)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
with pytest.raises(ValueError, match="symlinked"):
i.setup(project, m)
# Nothing was written into the unintended `./skills` location.
assert not (project / "skills").exists()
def test_migrate_skips_symlinked_target_dir(self, tmp_path):
# The destination `.kimi-code/skills/speckit-foo` already exists but is
# a symlink to a directory outside the project. Migration compares
# SKILL.md bytes to decide whether to drop the legacy copy; it must not
# follow the symlinked target dir to read SKILL.md from outside.
outside = tmp_path / "outside"
outside.mkdir()
(outside / "SKILL.md").write_text("# shared\n")
project = tmp_path / "project"
legacy = project / ".kimi" / "skills" / "speckit-foo"
legacy.mkdir(parents=True)
# Identical bytes: without the symlink guard the legacy dir would be
# removed after following the link out of the project.
(legacy / "SKILL.md").write_text("# shared\n")
target = project / ".kimi-code" / "skills" / "speckit-foo"
target.parent.mkdir(parents=True)
_symlink_or_skip(target, outside, target_is_directory=True)
_migrate_legacy_kimi_skills_dir(
project / ".kimi" / "skills", project / ".kimi-code" / "skills"
)
# Legacy copy is preserved (migration refused to follow the symlink),
# and the outside target is untouched.
assert (legacy / "SKILL.md").exists()
assert (outside / "SKILL.md").exists()
def test_context_migration_does_not_write_through_symlinked_agents_md(
self, tmp_path
):
# A sensitive file outside the project that a malicious AGENTS.md
# symlink points at. Migration must never overwrite it.
outside = tmp_path / "outside"
outside.mkdir()
secret = outside / "secret.txt"
secret.write_text("original secret\n")
project = tmp_path / "project"
project.mkdir()
_symlink_or_skip(project / "AGENTS.md", secret)
(project / "KIMI.md").write_text("# Notes\n\nKeep this.\n")
result = _migrate_legacy_kimi_context_file(project)
# The outside file must not be overwritten through the symlink.
assert secret.read_text() == "original secret\n"
# KIMI.md is preserved so the user can migrate manually.
assert (project / "KIMI.md").is_file()
assert result is False
def test_context_migration_does_not_follow_symlinked_kimi_md(self, tmp_path):
# A symlinked KIMI.md (source) must not be followed/consumed.
outside = tmp_path / "outside"
outside.mkdir()
external = outside / "external.md"
external.write_text("# external\n")
project = tmp_path / "project"
project.mkdir()
_symlink_or_skip(project / "KIMI.md", external)
result = _migrate_legacy_kimi_context_file(project)
assert result is False
# The external file and the symlink are left intact.
assert external.read_text() == "# external\n"
assert (project / "KIMI.md").is_symlink()
assert not (project / "AGENTS.md").exists()
class TestKimiNextSteps:

View File

@@ -1812,7 +1812,7 @@ class TestIntegrationSwitch:
assert result.exit_code == 0, f"extension add failed: {result.output}"
# Verify git extension skills exist for kimi
kimi_git_feature = project / ".kimi" / "skills" / "speckit-git-feature" / "SKILL.md"
kimi_git_feature = project / ".kimi-code" / "skills" / "speckit-git-feature" / "SKILL.md"
assert kimi_git_feature.exists(), "Git extension skill should exist for kimi"
result = _run_in_project(project, [

View File

@@ -226,17 +226,17 @@ class TestAgentConfigConsistency:
def test_kimi_in_agent_config(self):
"""AGENT_CONFIG should include kimi with correct folder and commands_subdir."""
assert "kimi" in AGENT_CONFIG
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi/"
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi-code/"
assert AGENT_CONFIG["kimi"]["commands_subdir"] == "skills"
assert AGENT_CONFIG["kimi"]["requires_cli"] is True
def test_kimi_in_extension_registrar(self):
"""Extension command registrar should include kimi using .kimi/skills and SKILL.md."""
"""Extension command registrar should include kimi using .kimi-code/skills and SKILL.md."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "kimi" in cfg
kimi_cfg = cfg["kimi"]
assert kimi_cfg["dir"] == ".kimi/skills"
assert kimi_cfg["dir"] == ".kimi-code/skills"
assert kimi_cfg["extension"] == "/SKILL.md"
def test_agent_config_includes_kimi(self):

View File

@@ -1937,7 +1937,7 @@ Agent __AGENT__
@pytest.mark.parametrize("agent_name,skills_path", [
("codex", ".agents/skills"),
("kimi", ".kimi/skills"),
("kimi", ".kimi-code/skills"),
("claude", ".claude/skills"),
("cursor-agent", ".cursor/skills"),
("trae", ".trae/skills"),

View File

@@ -3763,12 +3763,16 @@ class TestPresetSkills:
assert note_file.read_text(encoding="utf-8") == "user content"
def test_kimi_legacy_dotted_skill_override_still_applies(self, project_dir, temp_dir):
"""Preset overrides should still target legacy dotted Kimi skill directories."""
"""Preset overrides should still target legacy dotted-named skill dirs.
This exercises legacy *naming* (``speckit.specify``) under the current
``.kimi-code/`` base — distinct from the legacy ``.kimi/`` *location*.
"""
self._write_init_options(project_dir, ai="kimi")
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
self._create_skill(skills_dir, "speckit.specify", body="untouched")
(project_dir / ".kimi" / "commands").mkdir(parents=True, exist_ok=True)
(project_dir / ".kimi-code" / "commands").mkdir(parents=True, exist_ok=True)
manager = PresetManager(project_dir)
install_self_test_preset(manager)
@@ -3785,10 +3789,10 @@ class TestPresetSkills:
def test_kimi_skill_updated_even_when_ai_skills_disabled(self, project_dir, temp_dir):
"""Kimi presets should still propagate command overrides to existing skills."""
self._write_init_options(project_dir, ai="kimi", ai_skills=False)
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
self._create_skill(skills_dir, "speckit-specify", body="untouched")
(project_dir / ".kimi" / "commands").mkdir(parents=True, exist_ok=True)
(project_dir / ".kimi-code" / "commands").mkdir(parents=True, exist_ok=True)
manager = PresetManager(project_dir)
install_self_test_preset(manager)
@@ -3805,7 +3809,7 @@ class TestPresetSkills:
def test_kimi_new_skill_created_even_when_ai_skills_disabled(self, project_dir, temp_dir):
"""Kimi native skills should still receive brand-new preset commands."""
self._write_init_options(project_dir, ai="kimi", ai_skills=False)
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
preset_dir = temp_dir / "kimi-new-skill"
@@ -3854,9 +3858,9 @@ class TestPresetSkills:
def test_kimi_preset_skill_override_resolves_script_placeholders(self, project_dir, temp_dir):
"""Kimi preset skill overrides should resolve placeholders and rewrite project paths."""
self._write_init_options(project_dir, ai="kimi", ai_skills=False, script="sh")
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
self._create_skill(skills_dir, "speckit-specify", body="untouched")
(project_dir / ".kimi" / "commands").mkdir(parents=True, exist_ok=True)
(project_dir / ".kimi-code" / "commands").mkdir(parents=True, exist_ok=True)
preset_dir = temp_dir / "kimi-placeholder-override"
preset_dir.mkdir()